diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 1f62b2f..c25d87f 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -94,18 +94,16 @@ async def get_trades( 默认按平仓时间(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}") + logger.debug(f"获取交易记录: period={period}, symbol={symbol}, status={status}, limit={limit}, reconciled_only={reconciled_only}") start_timestamp = None end_timestamp = None - # 如果提供了 period,使用快速时间段筛选 if period: period_start, period_end = get_timestamp_range(period) if period_start is not None and period_end is not None: start_timestamp = period_start end_timestamp = period_end - logger.info(f"使用快速时间段筛选: {period} -> {start_timestamp} ({datetime.fromtimestamp(start_timestamp)}) 至 {end_timestamp} ({datetime.fromtimestamp(end_timestamp)})") elif start_date or end_date: # 自定义时间段:将日期字符串转换为Unix时间戳 beijing_tz = timezone(timedelta(hours=8)) @@ -128,33 +126,14 @@ 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, time_filter=time_filter or "exit") - if not include_sync: - trades = [ - t for t in trades - if (t.get("entry_reason") or "") != "sync_recovered" - and (t.get("exit_reason") or "") != "sync" - ] - # 仅可对账:有开仓订单号,已平仓的还须有平仓订单号,保证与币安可一一对应、统计真实 - def _has_entry_order_id(t): - eid = t.get("entry_order_id") - return eid is not None and eid != "" and (eid != 0 if isinstance(eid, (int, float)) else True) - def _has_exit_order_id(t): - xid = t.get("exit_order_id") - return xid is not None and xid != "" and (xid != 0 if isinstance(xid, (int, float)) else True) - if reconciled_only: - before = len(trades) - trades = [ - t for t in trades - if _has_entry_order_id(t) - and (t.get("status") != "closed" or _has_exit_order_id(t)) - ] - logger.info(f"可对账过滤: {before} -> {len(trades)} 条(reconciled_only=True)") - logger.info(f"查询到 {len(trades)} 条交易记录(include_sync={include_sync}, reconciled_only={reconciled_only})") + trades = Trade.get_all( + start_timestamp, end_timestamp, symbol, status, trade_type, exit_reason, + account_id=account_id, time_filter=time_filter or "exit", + limit=limit, reconciled_only=reconciled_only, include_sync=include_sync, + ) - # 格式化交易记录,添加平仓类型的中文显示 formatted_trades = [] - for trade in trades[:limit]: + for trade in trades: formatted_trade = dict(trade) # 将 exit_reason 转换为中文显示 @@ -181,7 +160,7 @@ async def get_trades( formatted_trades.append(formatted_trade) result = { - "total": len(trades), + "total": len(formatted_trades), "trades": formatted_trades, "filters": { "start_timestamp": start_timestamp, @@ -195,7 +174,7 @@ async def get_trades( } } - logger.debug(f"返回交易记录: {len(result['trades'])} 条 (限制: {limit})") + logger.debug(f"返回交易记录: {len(result['trades'])} 条") return result except Exception as e: logger.error(f"获取交易记录失败: {e}", exc_info=True) @@ -215,12 +194,11 @@ async def get_trade_stats( ): """获取交易统计(默认按平仓时间统计:今日=今日平仓的盈亏,与订单记录筛选一致)""" try: - logger.info(f"获取交易统计请求: start_date={start_date}, end_date={end_date}, period={period}, symbol={symbol}") + logger.debug(f"获取交易统计: period={period}, symbol={symbol}, reconciled_only={reconciled_only}") start_timestamp = None end_timestamp = None - # 如果提供了 period,使用快速时间段筛选 if period: period_start, period_end = get_timestamp_range(period) if period_start is not None and period_end is not None: @@ -248,27 +226,12 @@ 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, time_filter=time_filter or "exit") - if not include_sync: - trades = [ - t for t in trades - if (t.get("entry_reason") or "") != "sync_recovered" - and (t.get("exit_reason") or "") != "sync" - ] - if reconciled_only: - before = len(trades) - def _has_eid(t): - eid = t.get("entry_order_id") - return eid is not None and eid != "" and (eid != 0 if isinstance(eid, (int, float)) else True) - def _has_xid(t): - xid = t.get("exit_order_id") - return xid is not None and xid != "" and (xid != 0 if isinstance(xid, (int, float)) else True) - trades = [ - t for t in trades - if _has_eid(t) and (t.get("status") != "closed" or _has_xid(t)) - ] - logger.info(f"统计可对账过滤: {before} -> {len(trades)} 条(reconciled_only=True)") - closed_trades = [t for t in trades if t['status'] == 'closed'] + trades = Trade.get_all( + start_timestamp, end_timestamp, symbol, None, + account_id=account_id, time_filter=time_filter or "exit", + limit=None, reconciled_only=reconciled_only, include_sync=include_sync, + ) + closed_trades = [t for t in trades if t.get("status") == "closed"] # 辅助函数:计算净盈亏(优先使用 realized_pnl - commission) def get_net_pnl(t): @@ -379,12 +342,10 @@ async def get_trade_stats( } } - logger.info( - f"交易统计: 总交易数={stats['total_trades']}, 已平仓={stats['closed_trades']}, " - f"有意义交易={stats['meaningful_trades']}, 0盈亏交易={stats['zero_pnl_trades']}, " - f"胜率={stats['win_rate']:.2f}%, 总盈亏={stats['total_pnl']:.2f} USDT" + logger.debug( + f"交易统计: total={stats['total_trades']}, closed={stats['closed_trades']}, " + f"win_rate={stats['win_rate']:.2f}%, total_pnl={stats['total_pnl']:.2f}" ) - return stats except Exception as e: logger.error(f"获取交易统计失败: {e}", exc_info=True) diff --git a/backend/database/models.py b/backend/database/models.py index 0be9e77..575a2c8 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -988,13 +988,16 @@ 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, time_filter: str = "exit"): + 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", limit: int = None, reconciled_only: bool = False, include_sync: bool = True): """ 获取交易记录。 time_filter: 时间范围按哪种时间筛选 - "exit": 按平仓时间(已平仓用 exit_time,未平仓用 entry_time)。选「今天」= 今天平掉的单 + 今天开的未平仓,更符合直觉。 - "entry": 按开仓时间。 - "both": 原逻辑,COALESCE(exit_time, entry_time)。 + limit: 最多返回条数,None 表示不限制。 + reconciled_only: 仅可对账(有 entry_order_id,已平仓的还有 exit_order_id),在 SQL 中过滤以减轻负载。 + include_sync: 是否包含 entry_reason=sync_recovered / exit_reason=sync 的记录,在 SQL 中过滤。 """ query = "SELECT * FROM trades WHERE 1=1" params = [] @@ -1006,10 +1009,16 @@ class Trade: params.append(int(account_id or DEFAULT_ACCOUNT_ID)) except Exception: pass + + if reconciled_only and _table_has_column("trades", "entry_order_id"): + query += " AND entry_order_id IS NOT NULL AND entry_order_id != 0" + query += " AND (status != 'closed' OR (exit_order_id IS NOT NULL AND exit_order_id != 0))" + if include_sync is False: + query += " AND (entry_reason IS NULL OR entry_reason != 'sync_recovered')" + query += " AND (exit_reason IS NULL OR exit_reason != 'sync')" 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": @@ -1052,9 +1061,11 @@ class Trade: query += " AND exit_reason = %s" params.append(exit_reason) - # 按平仓时间倒序(已平仓的按 exit_time,未平仓的按 entry_time) query += " ORDER BY COALESCE(exit_time, entry_time) DESC, id DESC" - logger.info(f"查询交易记录: time_filter={time_filter}, {query}, {params}") + if limit is not None and limit > 0: + query += " LIMIT %s" + params.append(int(limit)) + logger.debug(f"查询交易记录: time_filter={time_filter}, limit={limit}, reconciled_only={reconciled_only}, include_sync={include_sync}") result = db.execute_query(query, params) return result