From 42480ef886660ff5cfea124df70bb3e862210ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Tue, 17 Feb 2026 08:01:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(trades):=20=E6=B7=BB=E5=8A=A0=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD=E4=BB=A5=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BA=A4=E6=98=93=E8=AE=B0=E5=BD=95=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `trades.py` 中新增 `time_filter` 参数,允许用户按平仓时间或开仓时间筛选交易记录。更新 `Trade.get_all` 方法以支持该功能,并调整查询逻辑以符合新的时间筛选需求。同时,前端组件 `TradeList.jsx` 也进行了相应更新,增加了时间筛选按钮,提升了用户体验和数据查询的灵活性。 --- backend/api/routes/trades.py | 10 +++-- backend/database/models.py | 55 +++++++++++++++++++++------ frontend/src/components/TradeList.jsx | 25 +++++++++++- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 777dafc..05ab56c 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -79,6 +79,7 @@ async def get_trades( trade_type: Optional[str] = Query(None, description="交易类型筛选: 'buy', 'sell'"), exit_reason: Optional[str] = Query(None, description="平仓原因筛选: 'stop_loss', 'take_profit', 'trailing_stop', 'manual', 'sync'"), status: Optional[str] = Query(None, description="状态筛选: 'open', 'closed', 'cancelled'"), + time_filter: str = Query("exit", description="时间范围按哪种时间筛选: 'exit'(按平仓时间,今日=今天平掉的单+今天开的未平仓), 'entry'(按开仓时间), 'both'(原逻辑)"), include_sync: bool = Query(False, description="是否包含 entry_reason 为 sync_recovered 的历史同步单"), reconciled_only: bool = Query(True, description="仅返回可对账记录(有 entry_order_id,已平仓的还有 exit_order_id),与币安一致,统计真实"), limit: int = Query(100, ge=1, le=1000, description="返回记录数限制"), @@ -90,7 +91,7 @@ async def get_trades( 1. 快速时间段筛选:使用 period 参数 ('1d', '7d', '30d', 'today', 'week', 'month') 2. 自定义时间段筛选:使用 start_date 和 end_date 参数(会转换为Unix时间戳) - 如果同时提供了 period 和 start_date/end_date,period 优先级更高 + 默认按平仓时间(time_filter=exit):选「今天」= 今天平掉的单 + 今天开的未平仓,更符合直觉。 """ try: logger.info(f"获取交易记录请求: start_date={start_date}, end_date={end_date}, period={period}, symbol={symbol}, status={status}, limit={limit}, trade_type={trade_type}, exit_reason={exit_reason}") @@ -127,7 +128,7 @@ async def get_trades( except ValueError: logger.warning(f"无效的结束日期格式: {end_date}") - trades = Trade.get_all(start_timestamp, end_timestamp, symbol, status, trade_type, exit_reason, account_id=account_id) + trades = Trade.get_all(start_timestamp, end_timestamp, symbol, status, trade_type, exit_reason, account_id=account_id, time_filter=time_filter or "exit") if not include_sync: trades = [ t for t in trades @@ -208,10 +209,11 @@ async def get_trade_stats( end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS)"), period: Optional[str] = Query(None, description="快速时间段筛选: '1d', '7d', '30d', 'today', 'week', 'month'"), symbol: Optional[str] = Query(None, description="交易对筛选"), + time_filter: str = Query("exit", description="时间范围按哪种时间: 'exit'(按平仓时间,今日统计=今日平仓的盈亏), 'entry'(按开仓时间), 'both'"), include_sync: bool = Query(False, description="是否包含 entry_reason 为 sync_recovered 的历史同步单"), reconciled_only: bool = Query(True, description="仅统计可对账记录,与币安一致,避免系统盈利/币安亏损偏差"), ): - """获取交易统计(默认仅统计可对账记录,保证与币安一致)""" + """获取交易统计(默认按平仓时间统计:今日=今日平仓的盈亏,与订单记录筛选一致)""" try: logger.info(f"获取交易统计请求: start_date={start_date}, end_date={end_date}, period={period}, symbol={symbol}") @@ -246,7 +248,7 @@ async def get_trade_stats( except ValueError: logger.warning(f"无效的结束日期格式: {end_date}") - trades = Trade.get_all(start_timestamp, end_timestamp, symbol, None, account_id=account_id) + trades = Trade.get_all(start_timestamp, end_timestamp, symbol, None, account_id=account_id, time_filter=time_filter or "exit") if not include_sync: trades = [ t for t in trades diff --git a/backend/database/models.py b/backend/database/models.py index 410ae8d..dac70a0 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -936,8 +936,14 @@ class Trade: return False @staticmethod - def get_all(start_timestamp=None, end_timestamp=None, symbol=None, status=None, trade_type=None, exit_reason=None, account_id: int = None): - """获取交易记录(仅包含本系统开仓的记录已通过清理脚本维护,不再在查询里筛选)""" + def get_all(start_timestamp=None, end_timestamp=None, symbol=None, status=None, trade_type=None, exit_reason=None, account_id: int = None, time_filter: str = "exit"): + """ + 获取交易记录。 + time_filter: 时间范围按哪种时间筛选 + - "exit": 按平仓时间(已平仓用 exit_time,未平仓用 entry_time)。选「今天」= 今天平掉的单 + 今天开的未平仓,更符合直觉。 + - "entry": 按开仓时间。 + - "both": 原逻辑,COALESCE(exit_time, entry_time)。 + """ query = "SELECT * FROM trades WHERE 1=1" params = [] @@ -949,12 +955,38 @@ class Trade: except Exception: pass - if start_timestamp is not None: - query += " AND COALESCE(exit_time, entry_time) >= %s" - params.append(start_timestamp) - if end_timestamp is not None: - query += " AND COALESCE(exit_time, entry_time) <= %s" - params.append(end_timestamp) + if start_timestamp is not None and end_timestamp is not None: + if time_filter == "exit": + # 按平仓时间:已平仓看 exit_time,未平仓看 entry_time + query += " AND ((status = 'closed' AND exit_time >= %s AND exit_time <= %s) OR (status != 'closed' AND entry_time >= %s AND entry_time <= %s))" + params.extend([start_timestamp, end_timestamp, start_timestamp, end_timestamp]) + elif time_filter == "entry": + query += " AND entry_time >= %s AND entry_time <= %s" + params.extend([start_timestamp, end_timestamp]) + else: + query += " AND COALESCE(exit_time, entry_time) >= %s AND COALESCE(exit_time, entry_time) <= %s" + params.extend([start_timestamp, end_timestamp]) + elif start_timestamp is not None: + if time_filter == "exit": + query += " AND ((status = 'closed' AND exit_time >= %s) OR (status != 'closed' AND entry_time >= %s))" + params.extend([start_timestamp, start_timestamp]) + elif time_filter == "entry": + query += " AND entry_time >= %s" + params.append(start_timestamp) + else: + query += " AND COALESCE(exit_time, entry_time) >= %s" + params.append(start_timestamp) + elif end_timestamp is not None: + if time_filter == "exit": + query += " AND ((status = 'closed' AND exit_time <= %s) OR (status != 'closed' AND entry_time <= %s))" + params.extend([end_timestamp, end_timestamp]) + elif time_filter == "entry": + query += " AND entry_time <= %s" + params.append(end_timestamp) + else: + query += " AND COALESCE(exit_time, entry_time) <= %s" + params.append(end_timestamp) + if symbol: query += " AND symbol = %s" params.append(symbol) @@ -968,10 +1000,9 @@ class Trade: query += " AND exit_reason = %s" params.append(exit_reason) - # 按平仓时间倒序排序,如果没有平仓时间则按入场时间倒序 - # query += " ORDER BY COALESCE(exit_time, entry_time) DESC, entry_time DESC" - query += " ORDER BY id DESC" - logger.info(f"查询交易记录: {query}, {params}") + # 按平仓时间倒序(已平仓的按 exit_time,未平仓的按 entry_time) + query += " ORDER BY COALESCE(exit_time, entry_time) DESC, id DESC" + logger.info(f"查询交易记录: time_filter={time_filter}, {query}, {params}") result = db.execute_query(query, params) return result diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 39bf080..e40b709 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -21,10 +21,11 @@ const TradeList = () => { const [tradeType, setTradeType] = useState('') const [exitReason, setExitReason] = useState('') const [reconciledOnly, setReconciledOnly] = useState(true) // 默认仅可对账,与币安一致 + const [timeFilter, setTimeFilter] = useState('exit') // 'exit' 按平仓时间(今天=今天平掉的单), 'entry' 按开仓时间 useEffect(() => { loadData() - }, [accountId, reconciledOnly]) // accountId 或「仅可对账」变化时重新加载 + }, [accountId, reconciledOnly, timeFilter]) // accountId / 仅可对账 / 时间筛选方式 变化时重新加载 const loadData = async () => { setLoading(true) @@ -47,6 +48,7 @@ const TradeList = () => { if (tradeType) params.trade_type = tradeType if (exitReason) params.exit_reason = exitReason params.reconciled_only = reconciledOnly + params.time_filter = timeFilter || 'exit' const [tradesData, statsData] = await Promise.all([ api.getTrades(params), @@ -311,11 +313,30 @@ const TradeList = () => {

交易记录

- 说明:每条记录代表一笔完整的交易(开仓+平仓),统计总盈亏时每条记录只计算一次。默认「仅可对账」:只显示有开仓/平仓订单号的记录,统计与币安一致。 + 说明:每条记录代表一笔完整的交易(开仓+平仓),统计总盈亏时每条记录只计算一次。默认「仅可对账」:只显示有开仓/平仓订单号的记录,统计与币安一致。默认「按平仓时间」:选「今天」= 今天平掉的单 + 今天开的未平仓。

{/* 筛选面板 */}
+
+ +
+ + +
+