diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 0cd117f..4005de7 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Query, HTTPException, Header, Depends from typing import Optional from datetime import datetime, timedelta from collections import Counter +import json import sys from pathlib import Path import logging @@ -146,6 +147,13 @@ async def get_trades( else: formatted_trade['exit_reason_display'] = '' + # 入场思路 entry_context 可能从 DB 以 JSON 字符串返回,解析为对象便于前端/分析使用 + if formatted_trade.get('entry_context') is not None and isinstance(formatted_trade['entry_context'], str): + try: + formatted_trade['entry_context'] = json.loads(formatted_trade['entry_context']) + except Exception: + pass + formatted_trades.append(formatted_trade) result = { diff --git a/backend/database/models.py b/backend/database/models.py index 25290f1..416c084 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -393,6 +393,7 @@ class Trade: notional_usdt=None, margin_usdt=None, account_id: int = None, + entry_context=None, ): """创建交易记录(使用北京时间) @@ -402,7 +403,7 @@ class Trade: quantity: 数量 entry_price: 入场价 leverage: 杠杆 - entry_reason: 入场原因 + entry_reason: 入场原因(简短文本) entry_order_id: 币安开仓订单号(可选,用于对账) stop_loss_price: 实际使用的止损价格(考虑了ATR等动态计算) take_profit_price: 实际使用的止盈价格(考虑了ATR等动态计算) @@ -411,6 +412,7 @@ class Trade: atr: 开仓时使用的ATR值(可选) notional_usdt: 名义下单量(USDT,可选) margin_usdt: 保证金(USDT,可选) + entry_context: 入场思路/过程(dict,将存为 JSON):信号强度、市场状态、趋势、过滤通过情况等,便于事后分析策略执行效果 """ entry_time = get_beijing_time() @@ -474,6 +476,15 @@ class Trade: columns.append("take_profit_2") values.append(take_profit_2) + if _has_column("entry_context") and entry_context is not None: + try: + entry_context_str = json.dumps(entry_context, ensure_ascii=False) if isinstance(entry_context, dict) else str(entry_context) + except Exception: + entry_context_str = None + if entry_context_str is not None: + columns.append("entry_context") + values.append(entry_context_str) + placeholders = ", ".join(["%s"] * len(columns)) sql = f"INSERT INTO trades ({', '.join(columns)}) VALUES ({placeholders})" db.execute_update(sql, tuple(values)) diff --git a/frontend/src/components/TradeList.css b/frontend/src/components/TradeList.css index 5699796..64b9bae 100644 --- a/frontend/src/components/TradeList.css +++ b/frontend/src/components/TradeList.css @@ -282,11 +282,15 @@ color: #e74c3c; } +/* 表格横向滚动:避免整页过宽,内容区域可左右滑动 */ .table-wrapper { width: 100%; + max-width: 100%; overflow-x: auto; margin-top: 1rem; -webkit-overflow-scrolling: touch; + border-radius: 8px; + border: 1px solid #e9ecef; } .table-wrapper::-webkit-scrollbar { @@ -311,7 +315,7 @@ width: 100%; border-collapse: collapse; display: none; - min-width: 1200px; /* 确保表格有最小宽度,避免列被压缩 */ + min-width: 1100px; /* 表格最小宽度,超出时由 table-wrapper 横向滚动 */ } @media (min-width: 768px) { @@ -323,10 +327,10 @@ .trades-table th { background-color: #34495e; color: white; - padding: 0.75rem 0.5rem; + padding: 0.5rem 0.4rem; text-align: left; font-weight: 500; - font-size: 0.85rem; + font-size: 0.8rem; white-space: nowrap; position: sticky; top: 0; @@ -334,89 +338,88 @@ } .trades-table td { - padding: 0.6rem 0.5rem; + padding: 0.5rem 0.4rem; border-bottom: 1px solid #eee; - font-size: 0.85rem; + font-size: 0.8rem; white-space: nowrap; } -/* 优化特定列的宽度 */ +/* 列宽适度收紧,减少横向占用,仍保证可读 */ .trades-table th:nth-child(1), .trades-table td:nth-child(1) { - min-width: 60px; - max-width: 80px; + min-width: 52px; } .trades-table th:nth-child(2), .trades-table td:nth-child(2) { - min-width: 90px; + min-width: 88px; } .trades-table th:nth-child(3), .trades-table td:nth-child(3) { - min-width: 60px; + min-width: 52px; } .trades-table th:nth-child(4), .trades-table td:nth-child(4) { - min-width: 90px; + min-width: 78px; } .trades-table th:nth-child(5), .trades-table td:nth-child(5) { - min-width: 90px; + min-width: 82px; } .trades-table th:nth-child(6), .trades-table td:nth-child(6) { - min-width: 90px; + min-width: 82px; } .trades-table th:nth-child(7), .trades-table td:nth-child(7) { - min-width: 90px; + min-width: 78px; } .trades-table th:nth-child(8), .trades-table td:nth-child(8) { - min-width: 90px; + min-width: 78px; } .trades-table th:nth-child(9), .trades-table td:nth-child(9) { - min-width: 100px; + min-width: 88px; } .trades-table th:nth-child(10), .trades-table td:nth-child(10) { - min-width: 100px; + min-width: 88px; } .trades-table th:nth-child(11), .trades-table td:nth-child(11) { - min-width: 80px; + min-width: 72px; } .trades-table th:nth-child(12), .trades-table td:nth-child(12) { - min-width: 100px; + min-width: 88px; } .trades-table th:nth-child(13), .trades-table td:nth-child(13) { - min-width: 200px; + min-width: 160px; white-space: normal; word-break: break-all; } .trades-table th:nth-child(14), .trades-table td:nth-child(14) { - min-width: 140px; + min-width: 120px; } .trades-table th:nth-child(15), .trades-table td:nth-child(15) { - min-width: 140px; + min-width: 120px; } .trades-table tr:hover { diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 0b5eda2..d24246e 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -81,14 +81,13 @@ const TradeList = () => { setUseCustomDate(false) } - // 导出当前订单数据 + // 导出当前订单数据(含入场/离场原因、入场思路等完整字段,便于后续分析) const handleExport = () => { if (trades.length === 0) { alert('暂无数据可导出') return } - // 准备导出数据 const exportData = trades.map(trade => { const notional = trade.notional_usdt !== undefined && trade.notional_usdt !== null ? parseFloat(trade.notional_usdt) @@ -102,7 +101,7 @@ const TradeList = () => { const pnl = parseFloat(trade.pnl || 0) const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0 - return { + const row = { 交易ID: trade.id, 交易对: trade.symbol, 方向: trade.side, @@ -120,7 +119,21 @@ const TradeList = () => { 平仓订单号: trade.exit_order_id || '-', 入场时间: trade.entry_time, 平仓时间: trade.exit_time || null, + // 以下为分析用完整字段 + 入场原因: trade.entry_reason ?? null, + 离场原因: trade.exit_reason ?? null, + 持仓时长分钟: trade.duration_minutes ?? null, + 止损价: trade.stop_loss_price != null ? parseFloat(trade.stop_loss_price) : null, + 止盈价: trade.take_profit_price != null ? parseFloat(trade.take_profit_price) : null, + 第一目标止盈价: trade.take_profit_1 != null ? parseFloat(trade.take_profit_1) : null, + 第二目标止盈价: trade.take_profit_2 != null ? parseFloat(trade.take_profit_2) : null, + ATR: trade.atr != null ? parseFloat(trade.atr) : null, + 策略类型: trade.strategy_type ?? null, } + if (trade.entry_context != null) { + row.入场思路 = trade.entry_context + } + return row }) // 生成文件名 @@ -288,7 +301,7 @@ const TradeList = () => { 重置 {trades.length > 0 && ( - )} @@ -430,7 +443,8 @@ const TradeList = () => {
暂无交易记录
) : ( <> - {/* 桌面端表格 */} + {/* 桌面端表格:用横向滚动包裹,避免整页过宽 */} +
@@ -535,6 +549,7 @@ const TradeList = () => { })}
+
{/* 移动端卡片 */}
diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 1bc63b6..80af03f 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -160,7 +160,8 @@ class PositionManager: trend_4h: Optional[str] = None, atr: Optional[float] = None, klines: Optional[List] = None, - bollinger: Optional[Dict] = None + bollinger: Optional[Dict] = None, + entry_context: Optional[Dict] = None, ) -> Optional[Dict]: """ 开仓 @@ -630,6 +631,7 @@ class PositionManager: atr=atr, notional_usdt=notional_usdt, margin_usdt=margin_usdt, + entry_context=entry_context, # 入场思路与过程(便于事后分析策略执行效果) ) logger.info(f"✓ {symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})") except Exception as e: diff --git a/trading_system/strategy.py b/trading_system/strategy.py index 3337d20..064c10e 100644 --- a/trading_system/strategy.py +++ b/trading_system/strategy.py @@ -192,6 +192,24 @@ class TradingStrategy: f"(信号强度: {signal_strength}/10)" ) + # 构建「入场思路/过程」并写入订单,便于事后综合分析策略执行效果 + entry_context = { + 'signal_strength': signal_strength, + 'market_regime': market_regime, + 'trend_4h': trade_signal.get('trend_4h'), + 'change_percent': change_percent, + 'direction': trade_direction, + 'reason': entry_reason, + 'rsi': symbol_info.get('rsi'), + 'volume_confirmed': True, # 已通过 _check_volume_confirmation + 'filters_passed': ['only_trending', 'should_trade', 'volume_ok', 'signal_ok'], + } + macd_hist = symbol_info.get('macd', {}).get('histogram') if isinstance(symbol_info.get('macd'), dict) else None + if macd_hist is not None: + entry_context['macd_histogram'] = macd_hist + if symbol_info.get('atr') is not None: + entry_context['atr'] = symbol_info.get('atr') + # 开仓(使用改进的仓位管理) position = await self.position_manager.open_position( symbol=symbol, @@ -204,7 +222,8 @@ class TradingStrategy: trend_4h=trade_signal.get('trend_4h'), atr=symbol_info.get('atr'), klines=symbol_info.get('klines'), # 传递K线数据用于动态止损 - bollinger=symbol_info.get('bollinger') # 传递布林带数据用于动态止损 + bollinger=symbol_info.get('bollinger'), # 传递布林带数据用于动态止损 + entry_context=entry_context, ) if position: