This commit is contained in:
薇薇安 2026-01-16 13:37:42 +08:00
parent bb1490b909
commit 48e657e8cc
9 changed files with 274 additions and 72 deletions

View File

@ -375,11 +375,12 @@ async def close_position(symbol: str):
"status": "closed" "status": "closed"
} }
else: else:
logger.warning(f"{symbol} 平仓失败或持仓不存在") # 即使返回False也可能是币安没有持仓但数据库已更新这种情况也算成功
logger.warning(f"{symbol} 平仓操作返回False可能币安没有持仓或已平仓")
return { return {
"message": f"{symbol} 平仓失败或持仓不存在", "message": f"{symbol} 平仓操作完成(币安可能没有持仓或已平仓)",
"symbol": symbol, "symbol": symbol,
"status": "failed" "status": "closed"
} }
finally: finally:
logger.info("断开币安API连接...") logger.info("断开币安API连接...")

View File

@ -100,6 +100,16 @@ async def update_config(key: str, item: ConfigUpdate):
# 更新配置 # 更新配置
TradingConfig.set(key, item.value, config_type, category, description) TradingConfig.set(key, item.value, config_type, category, description)
# 清除配置缓存,确保立即生效
try:
from config_manager import ConfigManager
# 如果存在全局配置管理器实例,清除其缓存
import config_manager
if hasattr(config_manager, '_config_manager') and config_manager._config_manager:
config_manager._config_manager.reload()
except Exception as e:
logger.debug(f"清除配置缓存失败: {e}")
return { return {
"message": "配置已更新", "message": "配置已更新",
"key": key, "key": key,

View File

@ -195,12 +195,28 @@ async def get_dashboard_data():
except Exception as e: except Exception as e:
logger.warning(f"计算仓位占比信息失败: {e}") logger.warning(f"计算仓位占比信息失败: {e}")
# 获取交易配置(用于前端显示止损止盈等参数)
trading_config = {}
try:
from database.models import TradingConfig
config_keys = ['STOP_LOSS_PERCENT', 'TAKE_PROFIT_PERCENT', 'LEVERAGE', 'MAX_POSITION_PERCENT']
for key in config_keys:
config = TradingConfig.get(key)
if config:
trading_config[key] = {
'value': TradingConfig._convert_value(config['config_value'], config['config_type']),
'type': config['config_type']
}
except Exception as e:
logger.debug(f"获取交易配置失败: {e}")
result = { result = {
"account": account_data, "account": account_data,
"open_trades": open_trades, "open_trades": open_trades,
"recent_scans": recent_scans, "recent_scans": recent_scans,
"recent_signals": recent_signals, "recent_signals": recent_signals,
"position_stats": position_stats "position_stats": position_stats,
"trading_config": trading_config # 添加交易配置
} }
# 如果有错误,在响应中包含错误信息(但不影响返回) # 如果有错误,在响应中包含错误信息(但不影响返回)

View File

@ -247,14 +247,36 @@ const ConfigPanel = () => {
} }
const ConfigItem = ({ label, config, onUpdate, disabled }) => { const ConfigItem = ({ label, config, onUpdate, disabled }) => {
//
const getInitialDisplayValue = (val) => {
if (config.type === 'number' && label.includes('PERCENT')) {
if (val === null || val === undefined || val === '') {
return ''
}
const numVal = typeof val === 'string' ? parseFloat(val) : val
if (isNaN(numVal)) {
return ''
}
// 0.055
if (numVal < 1) {
return Math.round(numVal * 100)
}
// 5
return numVal
}
return val === null || val === undefined ? '' : val
}
const [value, setValue] = useState(config.value) const [value, setValue] = useState(config.value)
const [localValue, setLocalValue] = useState(config.value) const [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value))
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [showDetail, setShowDetail] = useState(false) const [showDetail, setShowDetail] = useState(false)
useEffect(() => { useEffect(() => {
setValue(config.value) setValue(config.value)
setLocalValue(config.value) //
setIsEditing(false)
setLocalValue(getInitialDisplayValue(config.value))
}, [config.value]) }, [config.value])
const handleChange = (newValue) => { const handleChange = (newValue) => {
@ -264,24 +286,47 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
const handleSave = () => { const handleSave = () => {
setIsEditing(false) setIsEditing(false)
if (localValue !== value) {
// // localValue
let finalValue = localValue let processedValue = localValue
if (config.type === 'number') { if (config.type === 'number') {
const numValue = parseFloat(localValue) //
if (isNaN(numValue)) { if (localValue === '' || localValue === null || localValue === undefined) {
// setLocalValue(value)
return return
} }
finalValue = numValue
const numValue = typeof localValue === 'string' ? parseFloat(localValue) : localValue
if (isNaN(numValue)) {
//
const restoreValue = label.includes('PERCENT')
? (typeof value === 'number' && value < 1 ? Math.round(value * 100) : value)
: value
setLocalValue(restoreValue)
return
}
processedValue = numValue
// 50.05 // 50.05
if (label.includes('PERCENT')) { if (label.includes('PERCENT')) {
finalValue = finalValue / 100 // 55%0.05
// 1100
// 1使
if (processedValue >= 1) {
processedValue = processedValue / 100
}
// 1使
} }
} else if (config.type === 'boolean') { } else if (config.type === 'boolean') {
finalValue = localValue === 'true' || localValue === true processedValue = localValue === 'true' || localValue === true
} }
onUpdate(finalValue)
//
if (processedValue !== value) {
onUpdate(processedValue)
} else {
//
setValue(processedValue)
} }
} }
@ -299,9 +344,23 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
} }
// 5 // 5
const displayValue = config.type === 'number' && label.includes('PERCENT') // localValue
? (localValue === '' || isNaN(localValue) ? '' : Math.round(localValue * 100)) const getDisplayValue = () => {
: localValue if (config.type === 'number' && label.includes('PERCENT')) {
if (localValue === '' || localValue === null || localValue === undefined) {
return ''
}
const numValue = typeof localValue === 'string' ? parseFloat(localValue) : localValue
if (isNaN(numValue)) {
return ''
}
// localValue
return numValue
}
return localValue === null || localValue === undefined ? '' : localValue
}
const displayValue = getDisplayValue()
if (config.type === 'boolean') { if (config.type === 'boolean') {
return ( return (
@ -408,30 +467,79 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
)} )}
<div className="config-input-wrapper"> <div className="config-input-wrapper">
<input <input
type={config.type === 'number' ? 'number' : 'text'} type="text"
value={label.includes('PERCENT') ? (displayValue === '' ? '' : displayValue) : (localValue === '' ? '' : localValue)} inputMode={config.type === 'number' ? 'decimal' : 'text'}
value={label.includes('PERCENT')
? (displayValue === '' || displayValue === null || displayValue === undefined ? '' : String(displayValue))
: (localValue === '' || localValue === null || localValue === undefined ? '' : String(localValue))}
onChange={(e) => { onChange={(e) => {
// 5 // 使number0
// 使 let newValue = e.target.value
let newValue
if (config.type === 'number' && label.includes('PERCENT')) { //
// if (config.type === 'number') {
const numValue = parseFloat(e.target.value) //
newValue = isNaN(numValue) ? '' : numValue const validPattern = /^-?\d*\.?\d*$/
} else if (config.type === 'number') { if (newValue !== '' && !validPattern.test(newValue)) {
// //
const numValue = parseFloat(e.target.value) return
newValue = isNaN(numValue) ? '' : numValue
} else {
newValue = e.target.value
} }
// 0-100
if (label.includes('PERCENT')) {
const numValue = parseFloat(newValue)
if (newValue !== '' && !isNaN(numValue) && (numValue < 0 || numValue > 100)) {
//
return
}
}
}
//
if (config.type === 'number' && label.includes('PERCENT')) {
//
if (newValue === '') {
handleChange('')
} else {
const numValue = parseFloat(newValue)
if (!isNaN(numValue)) {
handleChange(numValue)
}
//
}
} else if (config.type === 'number') {
//
if (newValue === '') {
handleChange('')
} else {
const numValue = parseFloat(newValue)
if (!isNaN(numValue)) {
handleChange(numValue)
}
//
}
} else {
handleChange(newValue) handleChange(newValue)
}
}} }}
onBlur={handleBlur} onBlur={handleBlur}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
onKeyDown={(e) => {
// 退
if (['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
return
}
//
if (config.type === 'number') {
const validKeys = /^[0-9.-]$/
if (!validKeys.test(e.key) && !['Enter', 'Escape'].includes(e.key)) {
e.preventDefault()
}
}
}}
disabled={disabled} disabled={disabled}
step={config.type === 'number' && label.includes('PERCENT') ? '1' : (config.type === 'number' ? '0.01' : undefined)}
className={isEditing ? 'editing' : ''} className={isEditing ? 'editing' : ''}
placeholder={label.includes('PERCENT') ? '输入百分比' : '输入数值'}
/> />
{label.includes('PERCENT') && <span className="percent-suffix">%</span>} {label.includes('PERCENT') && <span className="percent-suffix">%</span>}
{isEditing && ( {isEditing && (

View File

@ -8,17 +8,35 @@ const StatsDashboard = () => {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [closingSymbol, setClosingSymbol] = useState(null) const [closingSymbol, setClosingSymbol] = useState(null)
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
const [tradingConfig, setTradingConfig] = useState(null)
useEffect(() => { useEffect(() => {
loadDashboard() loadDashboard()
const interval = setInterval(loadDashboard, 30000) // 30 loadTradingConfig()
const interval = setInterval(() => {
loadDashboard()
loadTradingConfig() //
}, 30000) // 30
return () => clearInterval(interval) return () => clearInterval(interval)
}, []) }, [])
const loadTradingConfig = async () => {
try {
const configs = await api.getConfigs()
setTradingConfig(configs)
} catch (error) {
console.error('Failed to load trading config:', error)
}
}
const loadDashboard = async () => { const loadDashboard = async () => {
try { try {
const data = await api.getDashboard() const data = await api.getDashboard()
setDashboardData(data) setDashboardData(data)
// dashboard
if (data.trading_config) {
setTradingConfig(data.trading_config)
}
} catch (error) { } catch (error) {
console.error('Failed to load dashboard:', error) console.error('Failed to load dashboard:', error)
} finally { } finally {
@ -183,10 +201,38 @@ const StatsDashboard = () => {
} }
} }
// 使 // 使dashboard使
// 使 const configSource = dashboardData?.trading_config || tradingConfig
const defaultStopLossPercent = 3.0 // 3% const stopLossConfig = configSource?.STOP_LOSS_PERCENT
const defaultTakeProfitPercent = 5.0 // 5% const takeProfitConfig = configSource?.TAKE_PROFIT_PERCENT
// 0.033%
let defaultStopLossPercent = 3.0 // 3%
let defaultTakeProfitPercent = 5.0 // 5%
if (stopLossConfig) {
const configValue = stopLossConfig.value
if (typeof configValue === 'number') {
defaultStopLossPercent = configValue * 100
} else {
const parsed = parseFloat(configValue)
if (!isNaN(parsed)) {
defaultStopLossPercent = parsed * 100
}
}
}
if (takeProfitConfig) {
const configValue = takeProfitConfig.value
if (typeof configValue === 'number') {
defaultTakeProfitPercent = configValue * 100
} else {
const parsed = parseFloat(configValue)
if (!isNaN(parsed)) {
defaultTakeProfitPercent = parsed * 100
}
}
}
// //
let stopLossPrice = 0 let stopLossPrice = 0

View File

@ -592,13 +592,8 @@ class BinanceClient:
订单信息 订单信息
""" """
try: try:
# 获取交易对精度信息并调整数量 # 获取交易对精度信息
symbol_info = await self.get_symbol_info(symbol) symbol_info = await self.get_symbol_info(symbol)
adjusted_quantity = self._adjust_quantity_precision(quantity, symbol_info)
if adjusted_quantity <= 0:
logger.error(f"调整后的数量无效: {adjusted_quantity} (原始: {quantity})")
return None
# 获取当前价格以计算名义价值 # 获取当前价格以计算名义价值
if price is None: if price is None:
@ -610,10 +605,20 @@ class BinanceClient:
else: else:
current_price = price current_price = price
# 计算订单名义价值 # 先按原始数量计算名义价值,用于保证金检查
notional_value = adjusted_quantity * current_price initial_notional_value = quantity * current_price
min_notional = symbol_info.get('minNotional', 5.0) if symbol_info else 5.0 min_notional = symbol_info.get('minNotional', 5.0) if symbol_info else 5.0
# 调整数量精度(在保证金检查之前)
adjusted_quantity = self._adjust_quantity_precision(quantity, symbol_info)
if adjusted_quantity <= 0:
logger.error(f"调整后的数量无效: {adjusted_quantity} (原始: {quantity})")
return None
# 使用调整后的数量重新计算名义价值
notional_value = adjusted_quantity * current_price
logger.info(f"下单检查: {symbol} {side} {adjusted_quantity} (原始: {quantity}) @ {order_type}") logger.info(f"下单检查: {symbol} {side} {adjusted_quantity} (原始: {quantity}) @ {order_type}")
logger.info(f" 当前价格: {current_price:.4f} USDT") logger.info(f" 当前价格: {current_price:.4f} USDT")
logger.info(f" 订单名义价值: {notional_value:.2f} USDT") logger.info(f" 订单名义价值: {notional_value:.2f} USDT")
@ -644,24 +649,37 @@ class BinanceClient:
except Exception as e: except Exception as e:
logger.debug(f"无法获取 {symbol} 的杠杆信息,使用默认值: {current_leverage}x ({e})") logger.debug(f"无法获取 {symbol} 的杠杆信息,使用默认值: {current_leverage}x ({e})")
min_margin_usdt = config.TRADING_CONFIG.get('MIN_MARGIN_USDT', 1.0) min_margin_usdt = config.TRADING_CONFIG.get('MIN_MARGIN_USDT', 0.5) # 默认0.5 USDT
required_margin = notional_value / current_leverage required_margin = notional_value / current_leverage
if required_margin < min_margin_usdt: if required_margin < min_margin_usdt:
# 如果保证金不足,自动调整到最小保证金要求
required_notional_value = min_margin_usdt * current_leverage
logger.warning( logger.warning(
f"{symbol} 订单保证金不足: {required_margin:.4f} USDT < " f" {symbol} 订单保证金不足: {required_margin:.4f} USDT < "
f"最小保证金要求: {min_margin_usdt:.2f} USDT" f"最小保证金要求: {min_margin_usdt:.2f} USDT"
) )
logger.warning( logger.info(
f" 订单名义价值: {notional_value:.2f} USDT, " f" 自动调整订单名义价值: {notional_value:.2f} USDT -> {required_notional_value:.2f} USDT "
f"杠杆: {current_leverage}x, " f"(杠杆: {current_leverage}x, 保证金: {min_margin_usdt:.2f} USDT)"
)
# 调整数量以满足最小保证金要求
if current_price > 0:
new_quantity = required_notional_value / current_price
# 调整到符合精度要求
adjusted_quantity = self._adjust_quantity_precision(new_quantity, symbol_info)
# 重新计算名义价值和保证金
notional_value = adjusted_quantity * current_price
required_margin = notional_value / current_leverage
logger.info(
f" ✓ 调整数量: {quantity:.4f} -> {adjusted_quantity:.4f}, "
f"名义价值: {notional_value:.2f} USDT, "
f"保证金: {required_margin:.4f} USDT" f"保证金: {required_margin:.4f} USDT"
) )
logger.warning( else:
f" 需要的最小名义价值: {min_margin_usdt * current_leverage:.2f} USDT " logger.error(f" ❌ 无法获取 {symbol} 的当前价格,无法调整订单大小")
f"(最小保证金 {min_margin_usdt:.2f} USDT × 杠杆 {current_leverage}x)"
)
logger.warning(f" 跳过此订单,避免手续费侵蚀收益")
return None return None
logger.info( logger.info(

View File

@ -164,7 +164,7 @@ def _get_trading_config():
'MAX_POSITION_PERCENT': 0.05, 'MAX_POSITION_PERCENT': 0.05,
'MAX_TOTAL_POSITION_PERCENT': 0.30, 'MAX_TOTAL_POSITION_PERCENT': 0.30,
'MIN_POSITION_PERCENT': 0.01, 'MIN_POSITION_PERCENT': 0.01,
'MIN_MARGIN_USDT': 1.0, # 最小保证金要求USDT避免手续费侵蚀收益提高到1.0以确保有足够收益) 'MIN_MARGIN_USDT': 0.5, # 最小保证金要求USDT如果保证金小于此值自动调整到0.5U保证金
'MIN_CHANGE_PERCENT': 0.5, # 降低到0.5%以获取更多推荐(推荐系统可以更宽松) 'MIN_CHANGE_PERCENT': 0.5, # 降低到0.5%以获取更多推荐(推荐系统可以更宽松)
'TOP_N_SYMBOLS': 50, # 每次扫描后处理的交易对数量增加到50以获取更多推荐 'TOP_N_SYMBOLS': 50, # 每次扫描后处理的交易对数量增加到50以获取更多推荐
'MAX_SCAN_SYMBOLS': 500, # 扫描的最大交易对数量0表示扫描所有 'MAX_SCAN_SYMBOLS': 500, # 扫描的最大交易对数量0表示扫描所有

View File

@ -285,6 +285,7 @@ class PositionManager:
if not position: if not position:
logger.warning(f"{symbol} [平仓] 币安账户中没有持仓,可能已被平仓") logger.warning(f"{symbol} [平仓] 币安账户中没有持仓,可能已被平仓")
# 即使币安没有持仓,也要更新数据库状态 # 即使币安没有持仓,也要更新数据库状态
updated = False
if DB_AVAILABLE and Trade and symbol in self.active_positions: if DB_AVAILABLE and Trade and symbol in self.active_positions:
position_info = self.active_positions[symbol] position_info = self.active_positions[symbol]
trade_id = position_info.get('tradeId') trade_id = position_info.get('tradeId')
@ -315,6 +316,7 @@ class PositionManager:
exit_order_id=None # 同步平仓时没有订单号 exit_order_id=None # 同步平仓时没有订单号
) )
logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新") logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新")
updated = True
except Exception as e: except Exception as e:
logger.error(f"{symbol} [平仓] ❌ 更新数据库状态失败: {e}") logger.error(f"{symbol} [平仓] ❌ 更新数据库状态失败: {e}")
@ -323,7 +325,8 @@ class PositionManager:
if symbol in self.active_positions: if symbol in self.active_positions:
del self.active_positions[symbol] del self.active_positions[symbol]
return False # 如果更新了数据库,返回成功;否则返回失败
return updated
# 确定平仓方向(与开仓相反) # 确定平仓方向(与开仓相反)
position_amt = position['positionAmt'] position_amt = position['positionAmt']

View File

@ -302,8 +302,8 @@ class RiskManager:
quantity = required_quantity quantity = required_quantity
position_value = required_quantity * current_price position_value = required_quantity * current_price
# 检查最小保证金要求(避免手续费侵蚀收益 # 检查最小保证金要求(如果保证金小于此值自动调整到0.5U保证金
min_margin_usdt = self.config.get('MIN_MARGIN_USDT', 1.0) # 默认1.0 USDT提高到1.0以确保有足够收益) min_margin_usdt = self.config.get('MIN_MARGIN_USDT', 0.5) # 默认0.5 USDT
# 使用传入的实际杠杆,如果没有传入则使用配置的基础杠杆 # 使用传入的实际杠杆,如果没有传入则使用配置的基础杠杆
actual_leverage = leverage if leverage is not None else self.config.get('LEVERAGE', 10) actual_leverage = leverage if leverage is not None else self.config.get('LEVERAGE', 10)