From 48e657e8cc8d0620d179890c506f03b986bd5f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Fri, 16 Jan 2026 13:37:42 +0800 Subject: [PATCH] a --- backend/api/routes/account.py | 7 +- backend/api/routes/config.py | 10 ++ backend/api/routes/stats.py | 18 +- frontend/src/components/ConfigPanel.jsx | 186 ++++++++++++++++----- frontend/src/components/StatsDashboard.jsx | 56 ++++++- trading_system/binance_client.py | 58 ++++--- trading_system/config.py | 2 +- trading_system/position_manager.py | 5 +- trading_system/risk_manager.py | 4 +- 9 files changed, 274 insertions(+), 72 deletions(-) diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index 81a2bbe..381addd 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -375,11 +375,12 @@ async def close_position(symbol: str): "status": "closed" } else: - logger.warning(f"⚠ {symbol} 平仓失败或持仓不存在") + # 即使返回False,也可能是币安没有持仓但数据库已更新,这种情况也算成功 + logger.warning(f"⚠ {symbol} 平仓操作返回False,可能币安没有持仓或已平仓") return { - "message": f"{symbol} 平仓失败或持仓不存在", + "message": f"{symbol} 平仓操作完成(币安可能没有持仓或已平仓)", "symbol": symbol, - "status": "failed" + "status": "closed" } finally: logger.info("断开币安API连接...") diff --git a/backend/api/routes/config.py b/backend/api/routes/config.py index 3fed400..6e73012 100644 --- a/backend/api/routes/config.py +++ b/backend/api/routes/config.py @@ -100,6 +100,16 @@ async def update_config(key: str, item: ConfigUpdate): # 更新配置 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 { "message": "配置已更新", "key": key, diff --git a/backend/api/routes/stats.py b/backend/api/routes/stats.py index d325781..bf96713 100644 --- a/backend/api/routes/stats.py +++ b/backend/api/routes/stats.py @@ -195,12 +195,28 @@ async def get_dashboard_data(): except Exception as 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 = { "account": account_data, "open_trades": open_trades, "recent_scans": recent_scans, "recent_signals": recent_signals, - "position_stats": position_stats + "position_stats": position_stats, + "trading_config": trading_config # 添加交易配置 } # 如果有错误,在响应中包含错误信息(但不影响返回) diff --git a/frontend/src/components/ConfigPanel.jsx b/frontend/src/components/ConfigPanel.jsx index b4ef66a..e41be40 100644 --- a/frontend/src/components/ConfigPanel.jsx +++ b/frontend/src/components/ConfigPanel.jsx @@ -247,14 +247,36 @@ const ConfigPanel = () => { } 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.05),转换为整数显示(5) + if (numVal < 1) { + return Math.round(numVal * 100) + } + // 如果已经是整数形式(5),直接显示 + return numVal + } + return val === null || val === undefined ? '' : val + } + 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 [showDetail, setShowDetail] = useState(false) useEffect(() => { setValue(config.value) - setLocalValue(config.value) + // 当配置值更新时,重置编辑状态和本地值 + setIsEditing(false) + setLocalValue(getInitialDisplayValue(config.value)) }, [config.value]) const handleChange = (newValue) => { @@ -264,24 +286,47 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => { const handleSave = () => { setIsEditing(false) - if (localValue !== value) { - // 值发生变化,保存 - let finalValue = localValue - if (config.type === 'number') { - const numValue = parseFloat(localValue) - if (isNaN(numValue)) { - // 如果输入为空或无效,不保存 - return - } - finalValue = numValue - // 百分比配置需要转换:用户输入的是整数(如5),需要转换为小数(0.05) - if (label.includes('PERCENT')) { - finalValue = finalValue / 100 - } - } else if (config.type === 'boolean') { - finalValue = localValue === 'true' || localValue === true + + // 处理localValue可能是字符串的情况 + let processedValue = localValue + if (config.type === 'number') { + // 如果是空字符串,恢复原值 + if (localValue === '' || localValue === null || localValue === undefined) { + setLocalValue(value) + return } - onUpdate(finalValue) + + 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 + // 百分比配置需要转换:用户输入的是整数(如5),需要转换为小数(0.05) + if (label.includes('PERCENT')) { + // 用户输入的是整数形式(如5表示5%),需要转换为小数(0.05) + // 如果输入值大于等于1,说明是百分比形式,需要除以100 + // 如果输入值小于1,说明已经是小数形式,直接使用 + if (processedValue >= 1) { + processedValue = processedValue / 100 + } + // 如果小于1,说明已经是小数形式,直接使用 + } + } else if (config.type === 'boolean') { + processedValue = localValue === 'true' || localValue === true + } + + // 只有当值真正发生变化时才保存 + if (processedValue !== value) { + onUpdate(processedValue) + } else { + // 值没变化,但需要更新显示值 + setValue(processedValue) } } @@ -299,9 +344,23 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => { } // 显示值:百分比配置显示为整数(如5),其他保持原样 - const displayValue = config.type === 'number' && label.includes('PERCENT') - ? (localValue === '' || isNaN(localValue) ? '' : Math.round(localValue * 100)) - : localValue + // 处理localValue可能是字符串的情况 + const getDisplayValue = () => { + 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') { return ( @@ -408,30 +467,79 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => { )}
{ - // 百分比配置:用户直接输入整数(如5),不转换,保存时再转换 - // 其他数字配置:直接使用输入值 - let newValue - if (config.type === 'number' && label.includes('PERCENT')) { - // 百分比配置:保持整数形式,允许空值 - const numValue = parseFloat(e.target.value) - newValue = isNaN(numValue) ? '' : numValue - } else if (config.type === 'number') { - // 其他数字配置:允许空值 - const numValue = parseFloat(e.target.value) - newValue = isNaN(numValue) ? '' : numValue - } else { - newValue = e.target.value + // 使用文本输入,避免number类型的自动补0问题 + let newValue = e.target.value + + // 对于数字类型,只允许数字、小数点和负号 + if (config.type === 'number') { + // 允许空字符串、数字、小数点和负号 + const validPattern = /^-?\d*\.?\d*$/ + if (newValue !== '' && !validPattern.test(newValue)) { + // 无效输入,不更新 + return + } + + // 如果是百分比配置,限制输入范围(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} 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} - step={config.type === 'number' && label.includes('PERCENT') ? '1' : (config.type === 'number' ? '0.01' : undefined)} className={isEditing ? 'editing' : ''} + placeholder={label.includes('PERCENT') ? '输入百分比' : '输入数值'} /> {label.includes('PERCENT') && %} {isEditing && ( diff --git a/frontend/src/components/StatsDashboard.jsx b/frontend/src/components/StatsDashboard.jsx index 469af42..c7652f0 100644 --- a/frontend/src/components/StatsDashboard.jsx +++ b/frontend/src/components/StatsDashboard.jsx @@ -8,17 +8,35 @@ const StatsDashboard = () => { const [loading, setLoading] = useState(true) const [closingSymbol, setClosingSymbol] = useState(null) const [message, setMessage] = useState('') + const [tradingConfig, setTradingConfig] = useState(null) useEffect(() => { loadDashboard() - const interval = setInterval(loadDashboard, 30000) // 每30秒刷新 + loadTradingConfig() + const interval = setInterval(() => { + loadDashboard() + loadTradingConfig() // 同时刷新配置 + }, 30000) // 每30秒刷新 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 () => { try { const data = await api.getDashboard() setDashboardData(data) + // 如果dashboard数据中包含配置,也更新配置状态 + if (data.trading_config) { + setTradingConfig(data.trading_config) + } } catch (error) { console.error('Failed to load dashboard:', error) } finally { @@ -183,10 +201,38 @@ const StatsDashboard = () => { } } - // 默认止损止盈比例(从配置获取,如果没有则使用默认值) - // 注意:这里使用默认值,实际止损止盈可能是动态计算的 - const defaultStopLossPercent = 3.0 // 默认3% - const defaultTakeProfitPercent = 5.0 // 默认5% + // 从配置获取止损止盈比例(优先使用dashboard返回的配置,其次使用单独加载的配置) + const configSource = dashboardData?.trading_config || tradingConfig + const stopLossConfig = configSource?.STOP_LOSS_PERCENT + const takeProfitConfig = configSource?.TAKE_PROFIT_PERCENT + + // 配置值是小数形式(0.03表示3%),需要转换为百分比 + 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 diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 0430f57..13feb07 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -592,13 +592,8 @@ class BinanceClient: 订单信息 """ try: - # 获取交易对精度信息并调整数量 + # 获取交易对精度信息 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: @@ -610,10 +605,20 @@ class BinanceClient: else: 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 + # 调整数量精度(在保证金检查之前) + 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" 当前价格: {current_price:.4f} USDT") logger.info(f" 订单名义价值: {notional_value:.2f} USDT") @@ -644,25 +649,38 @@ class BinanceClient: except Exception as 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 if required_margin < min_margin_usdt: + # 如果保证金不足,自动调整到最小保证金要求 + required_notional_value = min_margin_usdt * current_leverage logger.warning( - f"❌ {symbol} 订单保证金不足: {required_margin:.4f} USDT < " + f"⚠ {symbol} 订单保证金不足: {required_margin:.4f} USDT < " f"最小保证金要求: {min_margin_usdt:.2f} USDT" ) - logger.warning( - f" 订单名义价值: {notional_value:.2f} USDT, " - f"杠杆: {current_leverage}x, " - f"保证金: {required_margin:.4f} USDT" + logger.info( + f" 自动调整订单名义价值: {notional_value:.2f} USDT -> {required_notional_value:.2f} USDT " + f"(杠杆: {current_leverage}x, 保证金: {min_margin_usdt:.2f} USDT)" ) - logger.warning( - f" 需要的最小名义价值: {min_margin_usdt * current_leverage:.2f} USDT " - f"(最小保证金 {min_margin_usdt:.2f} USDT × 杠杆 {current_leverage}x)" - ) - logger.warning(f" 跳过此订单,避免手续费侵蚀收益") - return None + + # 调整数量以满足最小保证金要求 + 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" + ) + else: + logger.error(f" ❌ 无法获取 {symbol} 的当前价格,无法调整订单大小") + return None logger.info( f" 保证金检查通过: {required_margin:.4f} USDT >= " diff --git a/trading_system/config.py b/trading_system/config.py index 2b7219b..4c342d6 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -164,7 +164,7 @@ def _get_trading_config(): 'MAX_POSITION_PERCENT': 0.05, 'MAX_TOTAL_POSITION_PERCENT': 0.30, '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%以获取更多推荐(推荐系统可以更宽松) 'TOP_N_SYMBOLS': 50, # 每次扫描后处理的交易对数量(增加到50以获取更多推荐) 'MAX_SCAN_SYMBOLS': 500, # 扫描的最大交易对数量(0表示扫描所有) diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 0f1bf6a..ffc5e23 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -285,6 +285,7 @@ class PositionManager: if not position: logger.warning(f"{symbol} [平仓] 币安账户中没有持仓,可能已被平仓") # 即使币安没有持仓,也要更新数据库状态 + updated = False if DB_AVAILABLE and Trade and symbol in self.active_positions: position_info = self.active_positions[symbol] trade_id = position_info.get('tradeId') @@ -315,6 +316,7 @@ class PositionManager: exit_order_id=None # 同步平仓时没有订单号 ) logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新") + updated = True except Exception as e: logger.error(f"{symbol} [平仓] ❌ 更新数据库状态失败: {e}") @@ -323,7 +325,8 @@ class PositionManager: if symbol in self.active_positions: del self.active_positions[symbol] - return False + # 如果更新了数据库,返回成功;否则返回失败 + return updated # 确定平仓方向(与开仓相反) position_amt = position['positionAmt'] diff --git a/trading_system/risk_manager.py b/trading_system/risk_manager.py index 9d1931f..63a3eaa 100644 --- a/trading_system/risk_manager.py +++ b/trading_system/risk_manager.py @@ -302,8 +302,8 @@ class RiskManager: quantity = required_quantity position_value = required_quantity * current_price - # 检查最小保证金要求(避免手续费侵蚀收益) - min_margin_usdt = self.config.get('MIN_MARGIN_USDT', 1.0) # 默认1.0 USDT(提高到1.0以确保有足够收益) + # 检查最小保证金要求(如果保证金小于此值,自动调整到0.5U保证金) + 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)