From 90f3d019eddd49ae2fa454a3e68e1c6c251283ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Sat, 17 Jan 2026 00:58:39 +0800 Subject: [PATCH] a --- backend/api/routes/account.py | 142 +++++++++++++++++ backend/config_manager.py | 6 +- backend/database/init.sql | 6 +- frontend/src/components/ConfigGuide.jsx | 24 +-- frontend/src/components/ConfigPanel.jsx | 65 ++++++-- frontend/src/components/StatsDashboard.jsx | 65 +++++++- frontend/src/services/api.js | 15 ++ trading_system/config.py | 8 +- trading_system/position_manager.py | 168 +++++++++++++-------- trading_system/trade_recommender.py | 4 +- 10 files changed, 406 insertions(+), 97 deletions(-) diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index 302fd1c..253b878 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -544,3 +544,145 @@ async def close_position(symbol: str): logger.error(f"错误类型: {type(e).__name__}") logger.error("=" * 60, exc_info=True) raise HTTPException(status_code=500, detail=error_msg) + + +@router.post("/positions/sync") +async def sync_positions(): + """同步币安实际持仓状态与数据库状态""" + try: + logger.info("=" * 60) + logger.info("收到持仓状态同步请求") + logger.info("=" * 60) + + # 从数据库读取API密钥 + api_key = TradingConfig.get_value('BINANCE_API_KEY') + api_secret = TradingConfig.get_value('BINANCE_API_SECRET') + use_testnet = TradingConfig.get_value('USE_TESTNET', False) + + if not api_key or not api_secret: + error_msg = "API密钥未配置" + logger.warning(error_msg) + raise HTTPException(status_code=400, detail=error_msg) + + # 导入必要的模块 + try: + from binance_client import BinanceClient + except ImportError: + trading_system_path = project_root / 'trading_system' + sys.path.insert(0, str(trading_system_path)) + from binance_client import BinanceClient + + # 导入数据库模型 + from database.models import Trade + + # 创建客户端 + client = BinanceClient( + api_key=api_key, + api_secret=api_secret, + testnet=use_testnet + ) + + logger.info("连接币安API...") + await client.connect() + + try: + # 1. 获取币安实际持仓 + binance_positions = await client.get_open_positions() + binance_symbols = {p['symbol'] for p in binance_positions if float(p.get('positionAmt', 0)) != 0} + logger.info(f"币安实际持仓: {len(binance_symbols)} 个") + if binance_symbols: + logger.info(f" 持仓列表: {', '.join(binance_symbols)}") + + # 2. 获取数据库中状态为open的交易记录 + db_open_trades = Trade.get_all(status='open') + db_open_symbols = {t['symbol'] for t in db_open_trades} + logger.info(f"数据库open状态: {len(db_open_symbols)} 个") + if db_open_symbols: + logger.info(f" 持仓列表: {', '.join(db_open_symbols)}") + + # 3. 找出在数据库中open但在币安已不存在的持仓(需要更新为closed) + missing_in_binance = db_open_symbols - binance_symbols + updated_count = 0 + + if missing_in_binance: + logger.info(f"发现 {len(missing_in_binance)} 个持仓在数据库中是open但币安已不存在: {', '.join(missing_in_binance)}") + + for symbol in missing_in_binance: + try: + # 获取该交易对的所有open记录 + open_trades = Trade.get_by_symbol(symbol, status='open') + + for trade in open_trades: + trade_id = trade['id'] + entry_price = float(trade['entry_price']) + quantity = float(trade['quantity']) + + # 获取当前价格作为平仓价格 + ticker = await client.get_ticker_24h(symbol) + exit_price = float(ticker['price']) if ticker else entry_price + + # 计算盈亏 + if trade['side'] == 'BUY': + pnl = (exit_price - entry_price) * quantity + else: + pnl = (entry_price - exit_price) * quantity + + # 计算基于保证金的盈亏百分比 + leverage = float(trade.get('leverage', 10)) + entry_value = entry_price * quantity + margin = entry_value / leverage if leverage > 0 else entry_value + pnl_percent_margin = (pnl / margin * 100) if margin > 0 else 0 + + # 更新数据库记录 + Trade.update_exit( + trade_id=trade_id, + exit_price=exit_price, + exit_reason='sync', # 标记为同步平仓 + pnl=pnl, + pnl_percent=pnl_percent_margin, # 使用基于保证金的盈亏百分比 + exit_order_id=None + ) + updated_count += 1 + logger.info( + f"✓ {symbol} 已更新为closed (ID: {trade_id}, " + f"盈亏: {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()}") + else: + logger.info("✓ 数据库与币安状态一致,无需更新") + + # 4. 检查币安有但数据库没有记录的持仓 + missing_in_db = binance_symbols - db_open_symbols + if missing_in_db: + logger.info(f"发现 {len(missing_in_db)} 个持仓在币安存在但数据库中没有记录: {', '.join(missing_in_db)}") + logger.info(" 这些持仓可能是手动开仓的,建议手动处理") + + result = { + "message": "持仓状态同步完成", + "binance_positions": len(binance_symbols), + "db_open_positions": len(db_open_symbols), + "updated_to_closed": updated_count, + "missing_in_binance": list(missing_in_binance), + "missing_in_db": list(missing_in_db) + } + + logger.info("=" * 60) + logger.info("持仓状态同步完成!") + logger.info(f"结果: {result}") + logger.info("=" * 60) + + return result + + finally: + await client.disconnect() + logger.info("✓ 已断开币安API连接") + + except HTTPException: + raise + except Exception as e: + error_msg = f"同步持仓状态失败: {str(e)}" + logger.error(error_msg, exc_info=True) + raise HTTPException(status_code=500, detail=error_msg) diff --git a/backend/config_manager.py b/backend/config_manager.py index 54e391e..4acdbc7 100644 --- a/backend/config_manager.py +++ b/backend/config_manager.py @@ -119,8 +119,10 @@ class ConfigManager: 'TOP_N_SYMBOLS': self.get('TOP_N_SYMBOLS', 10), # 风险控制 - 'STOP_LOSS_PERCENT': self.get('STOP_LOSS_PERCENT', 0.03), - 'TAKE_PROFIT_PERCENT': self.get('TAKE_PROFIT_PERCENT', 0.05), + 'STOP_LOSS_PERCENT': self.get('STOP_LOSS_PERCENT', 0.08), # 默认8%(更宽松,避免被正常波动触发) + 'TAKE_PROFIT_PERCENT': self.get('TAKE_PROFIT_PERCENT', 0.15), # 默认15%(给趋势更多空间) + 'MIN_STOP_LOSS_PRICE_PCT': self.get('MIN_STOP_LOSS_PRICE_PCT', 0.02), # 默认2% + 'MIN_TAKE_PROFIT_PRICE_PCT': self.get('MIN_TAKE_PROFIT_PRICE_PCT', 0.03), # 默认3% # 市场扫描(1小时主周期) 'SCAN_INTERVAL': self.get('SCAN_INTERVAL', 3600), # 1小时 diff --git a/backend/database/init.sql b/backend/database/init.sql index 76a4df3..c26b765 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql @@ -141,8 +141,10 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate ('MAX_SCAN_SYMBOLS', '500', 'number', 'scan', '扫描的最大交易对数量(0表示扫描所有,建议100-500)'), -- 风险控制 -('STOP_LOSS_PERCENT', '0.03', 'number', 'risk', '止损:3%'), -('TAKE_PROFIT_PERCENT', '0.05', 'number', 'risk', '止盈:5%'), +('STOP_LOSS_PERCENT', '0.08', 'number', 'risk', '止损:8%(相对于保证金,更宽松避免被正常波动触发)'), +('TAKE_PROFIT_PERCENT', '0.15', 'number', 'risk', '止盈:15%(相对于保证金,给趋势更多空间)'), +('MIN_STOP_LOSS_PRICE_PCT', '0.02', 'number', 'risk', '最小止损价格变动:2%(防止止损过紧)'), +('MIN_TAKE_PROFIT_PRICE_PCT', '0.03', 'number', 'risk', '最小止盈价格变动:3%(防止止盈过紧)'), -- 市场扫描(1小时主周期) ('SCAN_INTERVAL', '3600', 'number', 'scan', '扫描间隔:1小时(秒)'), diff --git a/frontend/src/components/ConfigGuide.jsx b/frontend/src/components/ConfigGuide.jsx index 825f9e3..3d93037 100644 --- a/frontend/src/components/ConfigGuide.jsx +++ b/frontend/src/components/ConfigGuide.jsx @@ -15,53 +15,59 @@ const ConfigGuide = () => {

一、预设方案说明

-

方案1:保守配置(默认)

-

适合新手或稳健型交易者,风险较低,交易频率适中

+

方案1:保守配置

+

适合新手或稳健型交易者,风险较低,止损止盈较宽松,避免被正常波动触发

  • 扫描间隔: 3600秒(1小时)
  • 最小涨跌幅: 2.0%
  • 信号强度: 5/10
  • 处理交易对: 10个
  • +
  • 止损: 10% of margin(最小2%价格变动)
  • +
  • 止盈: 20% of margin(最小3%价格变动)
- 效果:每小时扫描一次,只捕捉2%以上的波动,信号质量高,胜率较高但交易机会较少 + 效果:每小时扫描一次,只捕捉2%以上的波动,止损止盈宽松,避免被正常波动触发,适合稳健交易

方案2:平衡配置(推荐)

-

平衡交易频率和信号质量,适合大多数交易者

+

平衡交易频率和信号质量,止损止盈适中,适合大多数交易者

  • 扫描间隔: 600秒(10分钟)
  • 最小涨跌幅: 1.5%
  • 信号强度: 4/10
  • 处理交易对: 12个
  • +
  • 止损: 8% of margin(最小2%价格变动)
  • +
  • 止盈: 15% of margin(最小3%价格变动)
- 效果:10分钟扫描一次,捕捉1.5%以上的波动,交易机会增加,信号质量仍然较高 + 效果:10分钟扫描一次,捕捉1.5%以上的波动,止损止盈适中,平衡风险与收益,推荐使用

方案3:激进高频配置

-

适合晚间波动大时使用,交易频率高,需要密切监控

+

适合晚间波动大时使用,交易频率高,止损止盈较紧,快速止盈止损

  • 扫描间隔: 300秒(5分钟)
  • 最小涨跌幅: 1.0%
  • 信号强度: 3/10
  • -
  • 处理交易对: 20个
  • +
  • 处理交易对: 18个
  • +
  • 止损: 5% of margin(最小1.5%价格变动)
  • +
  • 止盈: 10% of margin(最小2%价格变动)
- 效果:5分钟扫描一次,捕捉1%以上的波动,交易机会大幅增加,但需要监控胜率和手续费 + 效果:5分钟扫描一次,捕捉1%以上的波动,止损止盈较紧,快速锁定利润或止损,适合高频交易
- ⚠️ 风险提示:高频交易会增加手续费成本,建议在波动大的时段使用,并密切监控胜率 + ⚠️ 风险提示:高频交易会增加手续费成本,止损止盈较紧可能被正常波动触发,建议在波动大的时段使用,并密切监控胜率
diff --git a/frontend/src/components/ConfigPanel.jsx b/frontend/src/components/ConfigPanel.jsx index e41be40..a7411a2 100644 --- a/frontend/src/components/ConfigPanel.jsx +++ b/frontend/src/components/ConfigPanel.jsx @@ -10,42 +10,54 @@ const ConfigPanel = () => { const [message, setMessage] = useState('') // 预设方案配置 - // 注意:百分比配置使用整数形式(如2.0表示2%),在应用时会转换为小数(0.02) + // 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08) const presets = { conservative: { name: '保守配置', - desc: '适合新手,风险较低,交易频率适中', + desc: '适合新手,风险较低,止损止盈较宽松,避免被正常波动触发', configs: { SCAN_INTERVAL: 3600, MIN_CHANGE_PERCENT: 2.0, // 2% MIN_SIGNAL_STRENGTH: 5, TOP_N_SYMBOLS: 10, MAX_SCAN_SYMBOLS: 150, - MIN_VOLATILITY: 0.02 // 保持小数形式(波动率) + MIN_VOLATILITY: 0.02, // 保持小数形式(波动率) + STOP_LOSS_PERCENT: 10.0, // 10%(相对于保证金,更宽松) + TAKE_PROFIT_PERCENT: 20.0, // 20%(相对于保证金,给趋势更多空间) + MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护 + MIN_TAKE_PROFIT_PRICE_PCT: 3.0 // 3%最小价格变动保护 } }, balanced: { name: '平衡配置', - desc: '推荐使用,平衡频率和质量', + desc: '推荐使用,平衡频率和质量,止损止盈适中', configs: { SCAN_INTERVAL: 600, MIN_CHANGE_PERCENT: 1.5, // 1.5% MIN_SIGNAL_STRENGTH: 4, TOP_N_SYMBOLS: 12, MAX_SCAN_SYMBOLS: 250, - MIN_VOLATILITY: 0.018 // 保持小数形式(波动率) + MIN_VOLATILITY: 0.018, // 保持小数形式(波动率) + STOP_LOSS_PERCENT: 8.0, // 8%(相对于保证金,默认值) + TAKE_PROFIT_PERCENT: 15.0, // 15%(相对于保证金,默认值) + MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护 + MIN_TAKE_PROFIT_PRICE_PCT: 3.0 // 3%最小价格变动保护 } }, aggressive: { name: '激进高频', - desc: '晚间波动大时使用,交易频率高', + desc: '晚间波动大时使用,交易频率高,止损止盈较紧', configs: { SCAN_INTERVAL: 300, MIN_CHANGE_PERCENT: 1.0, // 1% MIN_SIGNAL_STRENGTH: 3, TOP_N_SYMBOLS: 18, MAX_SCAN_SYMBOLS: 350, - MIN_VOLATILITY: 0.015 // 保持小数形式(波动率) + MIN_VOLATILITY: 0.015, // 保持小数形式(波动率) + STOP_LOSS_PERCENT: 5.0, // 5%(相对于保证金,较紧) + TAKE_PROFIT_PERCENT: 10.0, // 10%(相对于保证金,快速止盈) + MIN_STOP_LOSS_PRICE_PCT: 1.5, // 1.5%最小价格变动保护 + MIN_TAKE_PROFIT_PRICE_PCT: 2.0 // 2%最小价格变动保护 } } } @@ -107,7 +119,7 @@ const ConfigPanel = () => { // 获取当前值(处理百分比转换) let currentValue = currentConfig.value - if (key.includes('PERCENT')) { + if (key.includes('PERCENT') || key.includes('PCT')) { currentValue = currentValue * 100 } @@ -140,10 +152,37 @@ const ConfigPanel = () => { try { const configItems = Object.entries(preset.configs).map(([key, value]) => { const config = configs[key] - if (!config) return null + if (!config) { + // 如果配置项不存在,尝试创建(用于新增的配置项) + // 根据key判断类型和分类 + let type = 'number' + let category = 'risk' + if (key.includes('PERCENT') || key.includes('PCT')) { + type = 'number' + if (key.includes('STOP_LOSS') || key.includes('TAKE_PROFIT')) { + category = 'risk' + } else { + category = 'scan' + } + } else if (key === 'MIN_VOLATILITY') { + type = 'number' + category = 'scan' + } else if (typeof value === 'number') { + type = 'number' + category = 'scan' + } + + return { + key, + value: (key.includes('PERCENT') || key.includes('PCT')) ? value / 100 : value, + type, + category, + description: `预设方案配置项:${key}` + } + } return { key, - value: key.includes('PERCENT') ? value / 100 : value, + value: (key.includes('PERCENT') || key.includes('PCT')) ? value / 100 : value, type: config.type, category: config.category, description: config.description @@ -585,8 +624,10 @@ const getConfigDetail = (key) => { 'MIN_POSITION_PERCENT': '单笔最小仓位(账户余额的百分比,如0.01表示1%)。单笔交易允许的最小仓位大小,避免交易过小的仓位,减少手续费影响。建议:1-2%。', // 风险控制参数 - 'STOP_LOSS_PERCENT': '止损百分比(如0.03表示3%)。当亏损达到此百分比时自动平仓止损,限制单笔交易的最大亏损。值越小止损更严格,单笔损失更小但可能被正常波动触发。值越大允许更大的回撤,但单笔损失可能较大。建议:保守策略3-5%,平衡策略2-3%,激进策略2-3%。注意:止损应该小于止盈,建议盈亏比至少1:1.5。', - 'TAKE_PROFIT_PERCENT': '止盈百分比(如0.05表示5%)。当盈利达到此百分比时自动平仓止盈,锁定利润。值越大目标利润更高,但可能错过及时止盈的机会,持仓时间更长。值越小能更快锁定利润,但可能错过更大的趋势。建议:保守策略5-8%,平衡策略5-6%,激进策略3-5%。注意:应该大于止损,建议盈亏比至少1:1.5。', + 'STOP_LOSS_PERCENT': '止损百分比(如0.08表示8%,相对于保证金)。当亏损达到此百分比时自动平仓止损,限制单笔交易的最大亏损。值越小止损更严格,单笔损失更小但可能被正常波动触发。值越大允许更大的回撤,但单笔损失可能较大。建议:保守策略10-15%,平衡策略8-10%,激进策略5-8%。注意:止损应该小于止盈,建议盈亏比至少1:1.5。系统会结合最小价格变动保护,取更宽松的一个。', + 'TAKE_PROFIT_PERCENT': '止盈百分比(如0.15表示15%,相对于保证金)。当盈利达到此百分比时自动平仓止盈,锁定利润。值越大目标利润更高,但可能错过及时止盈的机会,持仓时间更长。值越小能更快锁定利润,但可能错过更大的趋势。建议:保守策略20-30%,平衡策略15-20%,激进策略10-15%。注意:应该大于止损,建议盈亏比至少1:1.5。系统会结合最小价格变动保护,取更宽松的一个。', + 'MIN_STOP_LOSS_PRICE_PCT': '最小止损价格变动百分比(如0.02表示2%)。防止止损过紧,即使基于保证金的止损更紧,也会使用至少此百分比的价格变动。建议:保守策略2-3%,平衡策略2%,激进策略1.5-2%。', + 'MIN_TAKE_PROFIT_PRICE_PCT': '最小止盈价格变动百分比(如0.03表示3%)。防止止盈过紧,即使基于保证金的止盈更紧,也会使用至少此百分比的价格变动。建议:保守策略3-4%,平衡策略3%,激进策略2-3%。', // 策略参数 'LEVERAGE': '交易杠杆倍数。放大资金利用率,同时放大收益和风险。杠杆越高,相同仓位下需要的保证金越少,但风险越大。建议:保守策略5-10倍,平衡策略10倍,激进策略10-15倍。注意:高杠杆会增加爆仓风险,请谨慎使用。', diff --git a/frontend/src/components/StatsDashboard.jsx b/frontend/src/components/StatsDashboard.jsx index af826b4..a9bb05a 100644 --- a/frontend/src/components/StatsDashboard.jsx +++ b/frontend/src/components/StatsDashboard.jsx @@ -79,6 +79,46 @@ const StatsDashboard = () => { } } + const handleSyncPositions = async () => { + if (!window.confirm('确定要同步持仓状态吗?这将检查币安实际持仓并更新数据库状态。')) { + return + } + + setMessage('正在同步持仓状态...') + + try { + const result = await api.syncPositions() + console.log('同步结果:', result) + + let message = result.message || '同步完成' + if (result.updated_to_closed > 0) { + message += `,已更新 ${result.updated_to_closed} 条记录为已平仓` + } + if (result.missing_in_db && result.missing_in_db.length > 0) { + message += `,发现 ${result.missing_in_db.length} 个币安持仓在数据库中没有记录` + } + + setMessage(message) + + // 立即刷新数据 + await loadDashboard() + + // 5秒后清除消息 + setTimeout(() => { + setMessage('') + }, 5000) + } catch (error) { + console.error('Sync positions error:', error) + const errorMessage = error.message || error.toString() || '同步失败,请检查网络连接或后端服务' + setMessage(`同步失败: ${errorMessage}`) + + // 错误消息5秒后清除 + setTimeout(() => { + setMessage('') + }, 5000) + } + } + if (loading) return
加载中...
const account = dashboardData?.account @@ -86,7 +126,24 @@ const StatsDashboard = () => { return (
-

仪表板

+
+

仪表板

+ +
{message && (
@@ -205,9 +262,9 @@ const StatsDashboard = () => { const stopLossConfig = configSource?.STOP_LOSS_PERCENT const takeProfitConfig = configSource?.TAKE_PROFIT_PERCENT - // 配置值是小数形式(0.03表示3%),相对于保证金 - let stopLossPercentMargin = 0.03 // 默认3%(相对于保证金) - let takeProfitPercentMargin = 0.05 // 默认5%(相对于保证金) + // 配置值是小数形式(0.08表示8%),相对于保证金 + let stopLossPercentMargin = 0.08 // 默认8%(相对于保证金,更宽松) + let takeProfitPercentMargin = 0.15 // 默认15%(相对于保证金,给趋势更多空间) if (stopLossConfig) { const configValue = stopLossConfig.value diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index df47364..13a78ea 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -121,6 +121,21 @@ export const api = { return response.json(); }, + // 同步持仓状态 + syncPositions: async () => { + const response = await fetch(buildUrl('/api/account/positions/sync'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: '同步失败' })); + throw new Error(error.detail || '同步失败'); + } + return response.json(); + }, + // 交易推荐 getRecommendations: async (params = {}) => { // 默认使用实时推荐 diff --git a/trading_system/config.py b/trading_system/config.py index a3b9e94..4cad959 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -168,10 +168,10 @@ def _get_trading_config(): 'MIN_CHANGE_PERCENT': 0.5, # 降低到0.5%以获取更多推荐(推荐系统可以更宽松) 'TOP_N_SYMBOLS': 50, # 每次扫描后处理的交易对数量(增加到50以获取更多推荐) 'MAX_SCAN_SYMBOLS': 500, # 扫描的最大交易对数量(0表示扫描所有) - 'STOP_LOSS_PERCENT': 0.03, # 止损百分比(相对于保证金),默认3% - 'TAKE_PROFIT_PERCENT': 0.05, # 止盈百分比(相对于保证金),默认5% - 'MIN_STOP_LOSS_PRICE_PCT': 0.01, # 最小止损价格变动百分比(如0.01表示1%),防止止损过紧,默认1% - 'MIN_TAKE_PROFIT_PRICE_PCT': 0.015, # 最小止盈价格变动百分比(如0.015表示1.5%),防止止盈过紧,默认1.5% + 'STOP_LOSS_PERCENT': 0.08, # 止损百分比(相对于保证金),默认8%(更宽松,避免被正常波动触发) + 'TAKE_PROFIT_PERCENT': 0.15, # 止盈百分比(相对于保证金),默认15%(更宽松,给趋势更多空间) + 'MIN_STOP_LOSS_PRICE_PCT': 0.02, # 最小止损价格变动百分比(如0.02表示2%),防止止损过紧,默认2% + 'MIN_TAKE_PROFIT_PRICE_PCT': 0.03, # 最小止盈价格变动百分比(如0.03表示3%),防止止盈过紧,默认3% 'SCAN_INTERVAL': 3600, 'KLINE_INTERVAL': '1h', 'PRIMARY_INTERVAL': '1h', diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 387933e..02808e1 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -163,9 +163,12 @@ class PositionManager: atr=atr ) - # 计算止盈(基于保证金,为止损的倍数) - # 如果止损是保证金的3%,止盈可以是保证金的7.5%(2.5倍) - take_profit_pct_margin = stop_loss_pct_margin * 2.5 + # 计算止盈(基于保证金) + # 优先使用配置的止盈百分比,如果没有配置则使用止损的2倍 + take_profit_pct_margin = self.risk_manager.config.get('TAKE_PROFIT_PERCENT', 0.15) + # 如果配置中没有设置止盈,则使用止损的2倍作为默认 + if take_profit_pct_margin is None or take_profit_pct_margin == 0: + take_profit_pct_margin = stop_loss_pct_margin * 2.0 take_profit_price = self.risk_manager.get_take_profit_price( entry_price, side, quantity, leverage, take_profit_pct=take_profit_pct_margin @@ -185,61 +188,97 @@ class PositionManager: if entry_order_id: logger.info(f"{symbol} [开仓] 币安订单号: {entry_order_id}") - # 等待订单成交,然后从币安获取实际成交价格 + # 等待订单成交,检查订单状态并获取实际成交价格 + # 只有在订单真正成交(FILLED)后才保存到数据库 actual_entry_price = None - try: - # 等待一小段时间让订单成交 - await asyncio.sleep(1) - - # 从币安获取订单详情,获取实际成交价格 + order_status = None + filled_quantity = 0 + max_retries = 5 # 最多重试5次,每次等待1秒 + retry_count = 0 + + while retry_count < max_retries: try: - order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id) - if order_info: - # 优先使用平均成交价格(avgPrice),如果没有则使用价格字段 - actual_entry_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0)) - if actual_entry_price > 0: - logger.info(f"{symbol} [开仓] 从币安订单获取实际成交价格: {actual_entry_price:.4f} USDT") - else: - # 如果订单还没有完全成交,尝试从成交记录获取 - if order_info.get('status') == 'FILLED' and order_info.get('fills'): - # 计算加权平均成交价格 - total_qty = 0 - total_value = 0 - for fill in order_info.get('fills', []): - qty = float(fill.get('qty', 0)) - price = float(fill.get('price', 0)) - total_qty += qty - total_value += qty * price - if total_qty > 0: - actual_entry_price = total_value / total_qty - logger.info(f"{symbol} [开仓] 从成交记录计算平均成交价格: {actual_entry_price:.4f} USDT") - except Exception as order_error: - logger.warning(f"{symbol} [开仓] 获取订单详情失败: {order_error},使用备用方法") - - # 如果无法从订单获取价格,使用当前价格作为备用 - if not actual_entry_price or actual_entry_price <= 0: - ticker = await self.client.get_ticker_24h(symbol) - if ticker: - actual_entry_price = float(ticker['price']) - logger.warning(f"{symbol} [开仓] 使用当前价格作为入场价格: {actual_entry_price:.4f} USDT") - else: - actual_entry_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0)) - if actual_entry_price <= 0: - logger.error(f"{symbol} [开仓] 无法获取入场价格,使用订单价格字段") - actual_entry_price = float(order.get('price', 0)) or entry_price - except Exception as price_error: - logger.warning(f"{symbol} [开仓] 获取成交价格时出错: {price_error},使用当前价格") - ticker = await self.client.get_ticker_24h(symbol) - actual_entry_price = float(ticker['price']) if ticker else entry_price + # 等待一小段时间让订单成交 + await asyncio.sleep(1) + + # 从币安获取订单详情,检查订单状态 + try: + order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id) + if order_info: + order_status = order_info.get('status') + logger.info(f"{symbol} [开仓] 订单状态: {order_status} (重试 {retry_count + 1}/{max_retries})") + + # 检查订单是否已成交 + if order_status == 'FILLED': + # 订单已完全成交,获取实际成交价格和数量 + actual_entry_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0)) + filled_quantity = float(order_info.get('executedQty', 0)) + + if actual_entry_price > 0 and filled_quantity > 0: + logger.info(f"{symbol} [开仓] ✓ 订单已成交,成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}") + break + elif order_info.get('fills'): + # 从成交记录计算加权平均成交价格和总成交数量 + total_qty = 0 + total_value = 0 + for fill in order_info.get('fills', []): + qty = float(fill.get('qty', 0)) + price = float(fill.get('price', 0)) + total_qty += qty + total_value += qty * price + if total_qty > 0: + actual_entry_price = total_value / total_qty + filled_quantity = total_qty + logger.info(f"{symbol} [开仓] ✓ 订单已成交,从成交记录计算平均成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}") + break + elif order_status == 'PARTIALLY_FILLED': + # 部分成交,继续等待 + filled_quantity = float(order_info.get('executedQty', 0)) + logger.info(f"{symbol} [开仓] ⏳ 订单部分成交 ({filled_quantity:.4f}/{quantity:.4f}),继续等待...") + retry_count += 1 + continue + elif order_status in ['NEW', 'PENDING_NEW']: + # 订单已提交但未成交,继续等待 + logger.info(f"{symbol} [开仓] ⏳ 订单已提交但未成交,继续等待...") + retry_count += 1 + continue + elif order_status in ['CANCELED', 'REJECTED', 'EXPIRED']: + # 订单被取消、拒绝或过期 + logger.error(f"{symbol} [开仓] ❌ 订单状态异常: {order_status},订单未成交") + return None + else: + logger.warning(f"{symbol} [开仓] ⚠️ 未知订单状态: {order_status},继续等待...") + retry_count += 1 + continue + except Exception as order_error: + logger.warning(f"{symbol} [开仓] 获取订单详情失败: {order_error},重试中...") + retry_count += 1 + continue + except Exception as price_error: + logger.warning(f"{symbol} [开仓] 检查订单状态时出错: {price_error},重试中...") + retry_count += 1 + continue - # 使用实际成交价格(如果获取成功) - if actual_entry_price and actual_entry_price > 0: - # 记录下单时的价格(用于对比) - original_entry_price = entry_price - entry_price = actual_entry_price - logger.info(f"{symbol} [开仓] 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f})") + # 检查订单是否最终成交 + if order_status != 'FILLED': + logger.error(f"{symbol} [开仓] ❌ 订单未成交,状态: {order_status},不保存到数据库") + return None - # 记录到数据库(使用实际成交价格) + if not actual_entry_price or actual_entry_price <= 0: + logger.error(f"{symbol} [开仓] ❌ 无法获取实际成交价格,不保存到数据库") + return None + + if filled_quantity <= 0: + logger.error(f"{symbol} [开仓] ❌ 成交数量为0,不保存到数据库") + return None + + # 使用实际成交价格和数量 + original_entry_price = entry_price + entry_price = actual_entry_price + quantity = filled_quantity # 使用实际成交数量 + logger.info(f"{symbol} [开仓] ✓ 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f}), 成交数量: {quantity:.4f}") + + # 记录到数据库(只有在订单真正成交后才保存) trade_id = None if DB_AVAILABLE and Trade: try: @@ -247,18 +286,19 @@ class PositionManager: trade_id = Trade.create( symbol=symbol, side=side, - quantity=quantity, + quantity=quantity, # 使用实际成交数量 entry_price=entry_price, # 使用实际成交价格 leverage=leverage, entry_reason=entry_reason, entry_order_id=entry_order_id # 保存币安订单号 ) - logger.info(f"✓ {symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f})") + logger.info(f"✓ {symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})") except Exception as e: logger.error(f"❌ 保存交易记录到数据库失败: {e}") logger.error(f" 错误类型: {type(e).__name__}") import traceback logger.error(f" 错误详情:\n{traceback.format_exc()}") + return None elif not DB_AVAILABLE: logger.debug(f"数据库不可用,跳过保存 {symbol} 交易记录") elif not Trade: @@ -1059,10 +1099,11 @@ class PositionManager: # 计算止损止盈(基于保证金) leverage = binance_position.get('leverage', 10) - stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.03) - take_profit_pct_margin = self.risk_manager.config.get('TAKE_PROFIT_PERCENT', 0.05) - # 止盈为止损的2.5倍 - take_profit_pct_margin = stop_loss_pct_margin * 2.5 + stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.08) + take_profit_pct_margin = self.risk_manager.config.get('TAKE_PROFIT_PERCENT', 0.15) + # 如果配置中没有设置止盈,则使用止损的2倍作为默认 + if take_profit_pct_margin is None or take_profit_pct_margin == 0: + take_profit_pct_margin = stop_loss_pct_margin * 2.0 stop_loss_price = self.risk_manager.get_stop_loss_price( entry_price, side, quantity, leverage, @@ -1153,8 +1194,11 @@ class PositionManager: # 计算止损止盈(基于保证金) leverage = position.get('leverage', 10) - stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.03) - take_profit_pct_margin = stop_loss_pct_margin * 2.5 + stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.08) + take_profit_pct_margin = self.risk_manager.config.get('TAKE_PROFIT_PERCENT', 0.15) + # 如果配置中没有设置止盈,则使用止损的2倍作为默认 + if take_profit_pct_margin is None or take_profit_pct_margin == 0: + take_profit_pct_margin = stop_loss_pct_margin * 2.0 stop_loss_price = self.risk_manager.get_stop_loss_price( entry_price, side, quantity, leverage, diff --git a/trading_system/trade_recommender.py b/trading_system/trade_recommender.py index 49e8c76..aa36509 100644 --- a/trading_system/trade_recommender.py +++ b/trading_system/trade_recommender.py @@ -372,8 +372,8 @@ class TradeRecommender: estimated_quantity = estimated_position_value / entry_price if entry_price > 0 else 0 # 计算基于保证金的止损止盈 - stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.03) - take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.05) + stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08) + take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15) stop_loss_price = self.risk_manager.get_stop_loss_price( entry_price,