diff --git a/backend/api/routes/config.py b/backend/api/routes/config.py index 6e73012..26d83b4 100644 --- a/backend/api/routes/config.py +++ b/backend/api/routes/config.py @@ -5,14 +5,17 @@ from fastapi import APIRouter, HTTPException from api.models.config import ConfigItem, ConfigUpdate import sys from pathlib import Path +import logging # 添加项目根目录到路径 project_root = Path(__file__).parent.parent.parent.parent sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root / 'backend')) +sys.path.insert(0, str(project_root / 'trading_system')) from database.models import TradingConfig +logger = logging.getLogger(__name__) router = APIRouter() @@ -171,3 +174,112 @@ async def update_configs_batch(configs: list[ConfigItem]): } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/feasibility-check") +async def check_config_feasibility(): + """ + 检查配置可行性,基于当前账户余额和杠杆倍数计算可行的配置建议 + """ + try: + # 获取账户余额 + try: + from api.routes.account import get_realtime_account_data + account_data = await get_realtime_account_data() + available_balance = account_data.get('available_balance', 0) + total_balance = account_data.get('total_balance', 0) + except Exception as e: + logger.warning(f"获取账户余额失败: {e},使用默认值") + available_balance = 0 + total_balance = 0 + + if available_balance <= 0: + return { + "feasible": False, + "error": "无法获取账户余额,请检查API配置", + "suggestions": [] + } + + # 获取当前配置 + min_margin_usdt = TradingConfig.get_value('MIN_MARGIN_USDT', 5.0) + min_position_percent = TradingConfig.get_value('MIN_POSITION_PERCENT', 0.02) + max_position_percent = TradingConfig.get_value('MAX_POSITION_PERCENT', 0.08) + leverage = TradingConfig.get_value('LEVERAGE', 10) + + # 计算最小保证金要求对应的最小仓位价值 + required_position_value = min_margin_usdt * leverage + required_position_percent = required_position_value / available_balance if available_balance > 0 else 0 + + # 检查是否可行 + is_feasible = required_position_percent <= max_position_percent + + suggestions = [] + + if not is_feasible: + # 不可行,给出建议 + # 方案1:降低最小保证金 + suggested_min_margin = (available_balance * max_position_percent) / leverage + suggestions.append({ + "type": "reduce_min_margin", + "title": "降低最小保证金", + "description": f"将 MIN_MARGIN_USDT 调整为 {suggested_min_margin:.2f} USDT(当前: {min_margin_usdt:.2f} USDT)", + "config_key": "MIN_MARGIN_USDT", + "suggested_value": round(suggested_min_margin, 2), + "reason": f"当前配置需要 {required_position_percent*100:.1f}% 的仓位价值,但最大允许 {max_position_percent*100:.1f}%" + }) + + # 方案2:增加账户余额 + required_balance = required_position_value / max_position_percent + suggestions.append({ + "type": "increase_balance", + "title": "增加账户余额", + "description": f"将账户余额增加到至少 {required_balance:.2f} USDT(当前: {available_balance:.2f} USDT)", + "config_key": None, + "suggested_value": round(required_balance, 2), + "reason": f"当前余额不足以满足最小保证金 {min_margin_usdt:.2f} USDT 的要求" + }) + + # 方案3:降低杠杆倍数 + suggested_leverage = int((available_balance * max_position_percent) / min_margin_usdt) + if suggested_leverage >= 1: + suggestions.append({ + "type": "reduce_leverage", + "title": "降低杠杆倍数", + "description": f"将 LEVERAGE 调整为 {suggested_leverage}x(当前: {leverage}x)", + "config_key": "LEVERAGE", + "suggested_value": suggested_leverage, + "reason": f"降低杠杆可以减少所需仓位价值" + }) + else: + # 可行,显示当前配置信息 + actual_min_position_value = available_balance * min_position_percent + actual_min_margin = actual_min_position_value / leverage if leverage > 0 else actual_min_position_value + + suggestions.append({ + "type": "info", + "title": "配置可行", + "description": f"当前配置可以正常下单。最小仓位价值: {actual_min_position_value:.2f} USDT,对应保证金: {actual_min_margin:.2f} USDT", + "config_key": None, + "suggested_value": None, + "reason": None + }) + + return { + "feasible": is_feasible, + "account_balance": available_balance, + "leverage": leverage, + "current_config": { + "min_margin_usdt": min_margin_usdt, + "min_position_percent": min_position_percent, + "max_position_percent": max_position_percent + }, + "calculated_values": { + "required_position_value": required_position_value, + "required_position_percent": required_position_percent * 100, + "max_allowed_position_percent": max_position_percent * 100 + }, + "suggestions": suggestions + } + except Exception as e: + logger.error(f"检查配置可行性失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"检查配置可行性失败: {str(e)}") diff --git a/backend/database/add_trade_statistics.sql b/backend/database/add_trade_statistics.sql index edaeb60..02d3f4e 100644 --- a/backend/database/add_trade_statistics.sql +++ b/backend/database/add_trade_statistics.sql @@ -1,11 +1,78 @@ -- 添加交易统计字段 -- 用于记录策略类型和持仓持续时间 +-- 兼容MySQL 5.7+,使用动态SQL检查列和索引是否存在 -ALTER TABLE `trades` -ADD COLUMN IF NOT EXISTS `strategy_type` VARCHAR(50) COMMENT '策略类型: trend_following, mean_reversion' AFTER `exit_reason`, -ADD COLUMN IF NOT EXISTS `duration_minutes` INT COMMENT '持仓持续时间(分钟)' AFTER `strategy_type`; +USE `auto_trade_sys`; --- 添加索引以便统计查询 -ALTER TABLE `trades` -ADD INDEX IF NOT EXISTS `idx_strategy_type` (`strategy_type`), -ADD INDEX IF NOT EXISTS `idx_exit_reason` (`exit_reason`); +-- 1. 添加 strategy_type 列(如果不存在) +SET @column_exists = ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = 'auto_trade_sys' + AND table_name = 'trades' + AND column_name = 'strategy_type' +); + +SET @sql = IF(@column_exists = 0, + 'ALTER TABLE `trades` ADD COLUMN `strategy_type` VARCHAR(50) COMMENT ''策略类型: trend_following, mean_reversion'' AFTER `exit_reason`', + 'SELECT "strategy_type 列已存在,跳过添加" as message' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 2. 添加 duration_minutes 列(如果不存在) +SET @column_exists2 = ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = 'auto_trade_sys' + AND table_name = 'trades' + AND column_name = 'duration_minutes' +); + +SET @sql2 = IF(@column_exists2 = 0, + 'ALTER TABLE `trades` ADD COLUMN `duration_minutes` INT COMMENT ''持仓持续时间(分钟)'' AFTER `strategy_type`', + 'SELECT "duration_minutes 列已存在,跳过添加" as message' +); +PREPARE stmt2 FROM @sql2; +EXECUTE stmt2; +DEALLOCATE PREPARE stmt2; + +-- 3. 添加 idx_strategy_type 索引(如果不存在) +SET @index_exists = ( + SELECT COUNT(*) + FROM information_schema.statistics + WHERE table_schema = 'auto_trade_sys' + AND table_name = 'trades' + AND index_name = 'idx_strategy_type' +); + +SET @sql3 = IF(@index_exists = 0, + 'ALTER TABLE `trades` ADD INDEX `idx_strategy_type` (`strategy_type`)', + 'SELECT "idx_strategy_type 索引已存在,跳过添加" as message' +); +PREPARE stmt3 FROM @sql3; +EXECUTE stmt3; +DEALLOCATE PREPARE stmt3; + +-- 4. 添加 idx_exit_reason 索引(如果不存在) +SET @index_exists2 = ( + SELECT COUNT(*) + FROM information_schema.statistics + WHERE table_schema = 'auto_trade_sys' + AND table_name = 'trades' + AND index_name = 'idx_exit_reason' +); + +SET @sql4 = IF(@index_exists2 = 0, + 'ALTER TABLE `trades` ADD INDEX `idx_exit_reason` (`exit_reason`)', + 'SELECT "idx_exit_reason 索引已存在,跳过添加" as message' +); +PREPARE stmt4 FROM @sql4; +EXECUTE stmt4; +DEALLOCATE PREPARE stmt4; + +-- 验证:检查表结构 +-- SHOW CREATE TABLE `trades`; +-- 或者 +-- DESCRIBE `trades`; diff --git a/backend/database/init.sql b/backend/database/init.sql index eb0b7e9..3697397 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql @@ -170,7 +170,11 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate ('TRAILING_STOP_PROTECT', '0.05', 'number', 'strategy', '移动止损保护利润(保护5%利润,更合理)'), -- 持仓同步 +<<<<<<< Current (Your changes) ('POSITION_SYNC_INTERVAL', '300', 'number', 'scan', '持仓状态同步间隔(秒),默认5分钟,用于同步币安实际持仓与数据库状态'), +======= +('POSITION_SYNC_INTERVAL', '60', 'number', 'scan', '持仓状态同步间隔(秒),默认1分钟,用于同步币安实际持仓与数据库状态'), +>>>>>>> Incoming (Background Agent changes) -- API配置 diff --git a/frontend/src/components/ConfigPanel.css b/frontend/src/components/ConfigPanel.css index f1cafee..3090c5a 100644 --- a/frontend/src/components/ConfigPanel.css +++ b/frontend/src/components/ConfigPanel.css @@ -464,3 +464,139 @@ min-width: 44px; } } + +/* 配置可行性检查样式 */ +.feasibility-check { + margin-bottom: 1.5rem; + padding: 1rem; + border-radius: 8px; + border: 2px solid; + animation: slideIn 0.3s ease-out; +} + +.feasibility-check.feasible { + background: #e8f5e9; + border-color: #4CAF50; +} + +.feasibility-check.infeasible { + background: #fff3e0; + border-color: #FF9800; +} + +.feasibility-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.feasibility-header h4 { + margin: 0; + font-size: 1.1rem; + color: #2c3e50; +} + +.refresh-btn { + padding: 0.4rem 0.8rem; + background: white; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + transition: all 0.3s; +} + +.refresh-btn:hover:not(:disabled) { + background: #f5f5f5; + border-color: #999; +} + +.refresh-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.feasibility-info { + margin-bottom: 0.75rem; + font-size: 0.9rem; + color: #555; + line-height: 1.6; +} + +.feasibility-info p { + margin: 0.25rem 0; +} + +.feasibility-info strong { + color: #2c3e50; + font-weight: 600; +} + +.warning-text { + color: #d32f2f !important; + font-weight: 500; +} + +.feasibility-suggestions { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid rgba(0,0,0,0.1); +} + +.feasibility-suggestions h5 { + margin: 0 0 0.75rem 0; + font-size: 1rem; + color: #2c3e50; +} + +.suggestion-item { + background: white; + padding: 0.75rem; + border-radius: 6px; + margin-bottom: 0.75rem; + border: 1px solid #e0e0e0; +} + +.suggestion-item:last-child { + margin-bottom: 0; +} + +.suggestion-header { + margin-bottom: 0.5rem; +} + +.suggestion-header strong { + color: #1976D2; + font-size: 0.95rem; +} + +.suggestion-desc { + margin: 0.5rem 0; + font-size: 0.9rem; + color: #666; + line-height: 1.5; +} + +.apply-suggestion-btn { + margin-top: 0.5rem; + padding: 0.5rem 1rem; + background: #2196F3; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + font-weight: 500; + transition: all 0.3s; +} + +.apply-suggestion-btn:hover { + background: #1976D2; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(33, 150, 243, 0.3); +} + +.apply-suggestion-btn:active { + transform: translateY(0); +} diff --git a/frontend/src/components/ConfigPanel.jsx b/frontend/src/components/ConfigPanel.jsx index 4d914a2..e7e1e56 100644 --- a/frontend/src/components/ConfigPanel.jsx +++ b/frontend/src/components/ConfigPanel.jsx @@ -8,6 +8,8 @@ const ConfigPanel = () => { const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [message, setMessage] = useState('') + const [feasibilityCheck, setFeasibilityCheck] = useState(null) + const [checkingFeasibility, setCheckingFeasibility] = useState(false) // 预设方案配置 // 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08) @@ -64,7 +66,21 @@ const ConfigPanel = () => { useEffect(() => { loadConfigs() + checkFeasibility() }, []) + + const checkFeasibility = async () => { + setCheckingFeasibility(true) + try { + const result = await api.checkConfigFeasibility() + setFeasibilityCheck(result) + } catch (error) { + console.error('检查配置可行性失败:', error) + // 静默失败,不显示错误 + } finally { + setCheckingFeasibility(false) + } + } const loadConfigs = async () => { try { @@ -257,6 +273,73 @@ const ConfigPanel = () => { + + {/* 配置可行性检查提示 */} + {feasibilityCheck && ( +
+
+

+ {feasibilityCheck.feasible ? '✓ 配置可行' : '⚠ 配置冲突'} +

+ +
+
+

+ 账户余额: {feasibilityCheck.account_balance?.toFixed(2) || 'N/A'} USDT | + 杠杆: {feasibilityCheck.leverage}x | + 最小保证金: {feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2) || 'N/A'} USDT +

+ {!feasibilityCheck.feasible && ( +

+ 需要仓位价值: {feasibilityCheck.calculated_values?.required_position_percent?.toFixed(1)}% | + 最大允许: {feasibilityCheck.calculated_values?.max_allowed_position_percent?.toFixed(1)}% +

+ )} +
+ {feasibilityCheck.suggestions && feasibilityCheck.suggestions.length > 0 && ( +
+
建议方案:
+ {feasibilityCheck.suggestions.map((suggestion, index) => ( +
+
+ {suggestion.title} +
+

{suggestion.description}

+ {suggestion.config_key && suggestion.suggested_value !== null && ( + + )} +
+ ))} +
+ )} +
+ )} + {message && (
{message} diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 8104d01..df6a843 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -19,6 +19,12 @@ const TradeList = () => { useEffect(() => { loadData() + // 每30秒自动刷新一次,确保订单状态及时更新 + const interval = setInterval(() => { + loadData() + }, 30000) // 30秒刷新一次 + + return () => clearInterval(interval) }, []) const loadData = async () => { diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 13a78ea..7e6671a 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -54,6 +54,16 @@ export const api = { return response.json(); }, + // 检查配置可行性 + checkConfigFeasibility: async () => { + const response = await fetch(buildUrl('/api/config/feasibility-check')); + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: '检查配置可行性失败' })); + throw new Error(error.detail || '检查配置可行性失败'); + } + return response.json(); + }, + // 交易记录 getTrades: async (params = {}) => { const query = new URLSearchParams(params).toString(); diff --git a/trading_system/config.py b/trading_system/config.py index abebb6f..aeca233 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -189,7 +189,11 @@ def _get_trading_config(): 'USE_TRAILING_STOP': True, 'TRAILING_STOP_ACTIVATION': 0.10, # 移动止损激活提高到10%(盈利10%后激活,给趋势更多空间) 'TRAILING_STOP_PROTECT': 0.05, # 保护利润提高到5%(保护5%利润,更合理) +<<<<<<< Current (Your changes) 'POSITION_SYNC_INTERVAL': 300, # 持仓状态同步间隔(秒),默认5分钟 +======= + 'POSITION_SYNC_INTERVAL': 60, # 持仓状态同步间隔(秒),缩短到1分钟,确保状态及时同步 +>>>>>>> Incoming (Background Agent changes) } # 币安API配置(优先从数据库,回退到环境变量和默认值) diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 5a9212d..f950564 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -1894,57 +1894,25 @@ class PositionManager: f"数量: {quantity:.4f}" ) - # 更新数据库 - if DB_AVAILABLE and Trade: - trade_id = position_info.get('tradeId') - if trade_id: - try: - if position_info['side'] == 'BUY': - pnl = (current_price_float - entry_price) * quantity - else: # SELL - pnl = (entry_price - current_price_float) * quantity - - logger.info(f"{symbol} [自动平仓] 更新数据库记录 (ID: {trade_id})...") - # 计算持仓持续时间和策略类型 - entry_time = position_info.get('entryTime') - duration_minutes = None - if entry_time: - try: - from datetime import datetime - if isinstance(entry_time, str): - entry_dt = datetime.strptime(entry_time, '%Y-%m-%d %H:%M:%S') - else: - entry_dt = entry_time - exit_dt = get_beijing_time() # 使用北京时间计算持续时间 - duration = exit_dt - entry_dt - duration_minutes = int(duration.total_seconds() / 60) - except Exception as e: - logger.debug(f"计算持仓持续时间失败: {e}") - - strategy_type = position_info.get('strategyType', 'trend_following') - - Trade.update_exit( - trade_id=trade_id, - exit_price=current_price_float, - exit_reason=exit_reason, - pnl=pnl, - pnl_percent=pnl_percent_margin, # 修复:使用 pnl_percent_margin 而不是 pnl_percent - strategy_type=strategy_type, - duration_minutes=duration_minutes - ) - logger.info(f"{symbol} [自动平仓] ✓ 数据库记录已更新 (盈亏: {pnl:.2f} USDT, {pnl_percent_margin:.2f}% of margin)") - except Exception as e: - logger.error(f"{symbol} [自动平仓] ❌ 更新数据库记录失败: {e}") - import traceback - logger.error(f" 错误详情:\n{traceback.format_exc()}") - - # 执行平仓(注意:这里会停止监控,所以先更新数据库) + # 执行平仓(让 close_position 统一处理数据库更新,避免重复更新和状态不一致) logger.info(f"{symbol} [自动平仓] 正在执行平仓订单...") success = await self.close_position(symbol, reason=exit_reason) if success: logger.info(f"{symbol} [自动平仓] ✓ 平仓成功完成") + # 平仓成功后,立即触发一次状态同步,确保数据库状态与币安一致 + try: + await asyncio.sleep(2) # 等待2秒让币安订单完全成交 + await self.sync_positions_with_binance() + logger.debug(f"{symbol} [自动平仓] 已触发状态同步") + except Exception as sync_error: + logger.warning(f"{symbol} [自动平仓] 状态同步失败: {sync_error}") else: logger.error(f"{symbol} [自动平仓] ❌ 平仓失败") + # 即使平仓失败,也尝试同步状态(可能币安已经平仓了) + try: + await self.sync_positions_with_binance() + except Exception as sync_error: + logger.warning(f"{symbol} [自动平仓] 状态同步失败: {sync_error}") async def diagnose_position(self, symbol: str): """