From 9b81832af24c6a935171730ed2ba800fb613b881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Fri, 20 Feb 2026 12:17:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(trades,=20database,=20frontend):=20?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E4=BA=A4=E6=98=93=E8=AE=B0=E5=BD=95=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E4=B8=8E=E5=B1=95=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `trades.py` 中更新了 `include_sync` 参数的默认值为 `True`,以便于订单记录与币安一致,并添加了提示信息以指导用户如何补全缺失的订单号。在 `models.py` 中新增了 `get_trades_missing_entry_order_id` 方法,用于获取缺少 `entry_order_id` 的记录,确保在同步时能够补全数据。前端组件 `StatsDashboard.jsx` 和 `TradeList.jsx` 中相应调整了开仓时间的展示逻辑和无交易记录时的提示信息,提升了用户体验与数据准确性。 --- backend/api/routes/trades.py | 23 +++++++++++----- backend/database/models.py | 32 ++++++++++++++++++++++ frontend/src/components/StatsDashboard.jsx | 6 ++-- frontend/src/components/TradeList.jsx | 14 ++++++++-- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index c25d87f..5caee2c 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -80,7 +80,7 @@ async def get_trades( 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 的历史同步单"), + include_sync: bool = Query(True, 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="返回记录数限制"), ): @@ -173,6 +173,9 @@ async def get_trades( "reconciled_only": reconciled_only, } } + # 仅可对账且无记录时提示:可先同步币安订单补全订单号,或取消「仅可对账」查看全部 + if reconciled_only and len(formatted_trades) == 0: + result["hint"] = "当前没有可对账记录。可尝试:1) 调用「同步币安订单」接口补全开仓/平仓订单号;2) 使用 reconciled_only=false 查看全部记录(含无订单号的补建单)。" logger.debug(f"返回交易记录: {len(result['trades'])} 条") return result @@ -189,7 +192,7 @@ async def get_trade_stats( 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 的历史同步单"), + include_sync: bool = Query(True, description="是否包含 entry_reason 为 sync_recovered 的补建/同步单(默认与订单记录一致)"), reconciled_only: bool = Query(True, description="仅统计可对账记录,与币安一致,避免系统盈利/币安亏损偏差"), ): """获取交易统计(默认按平仓时间统计:今日=今日平仓的盈亏,与订单记录筛选一致)""" @@ -759,19 +762,25 @@ async def sync_trades_from_binance( time_window_start = order_time_sec - 3600 time_window_end = order_time_sec + 3600 - trades_no_entry_id = Trade.get_all( + trades_in_window = Trade.get_all( account_id=aid, symbol=symbol, start_timestamp=time_window_start, end_timestamp=time_window_end, - time_filter="entry", # 使用 entry_time 过滤 + time_filter="entry", + reconciled_only=False, ) - - # 过滤出没有 entry_order_id 的记录 trades_no_entry_id = [ - t for t in trades_no_entry_id + t for t in trades_in_window if not t.get("entry_order_id") or str(t.get("entry_order_id")).strip() in ("", "0") ] + # 并入缺少 entry_order_id 的其它记录(含 entry_time 为 NULL 的旧记录),便于补全后「仅可对账」能显示 + extra = Trade.get_trades_missing_entry_order_id(symbol, aid, limit=50) + seen_ids = {t["id"] for t in trades_no_entry_id} + for t in extra: + if t.get("id") not in seen_ids: + trades_no_entry_id.append(t) + seen_ids.add(t["id"]) # 按价格和数量匹配(允许 5% 误差) matched_trade = None diff --git a/backend/database/models.py b/backend/database/models.py index 76e1855..2455b22 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -906,6 +906,38 @@ class Trade: (exit_order_id,) ) + @staticmethod + def get_trades_missing_entry_order_id(symbol: str, account_id: int, limit: int = 50): + """ + 获取该 symbol+account 下缺少 entry_order_id 的记录(用于从币安同步订单时补全)。 + 包含 entry_time 为 NULL 的旧记录,避免「仅可对账」下看不到今日有单却未补全的情况。 + """ + if not symbol or account_id is None: + return [] + try: + if not _table_has_column("trades", "entry_order_id"): + return [] + if _table_has_column("trades", "account_id"): + return db.execute_query( + """SELECT * FROM trades + WHERE account_id = %s AND symbol = %s + AND (entry_order_id IS NULL OR entry_order_id = 0 OR entry_order_id = '') + ORDER BY id DESC + LIMIT %s""", + (int(account_id), symbol.strip(), int(limit)), + ) + return db.execute_query( + """SELECT * FROM trades + WHERE symbol = %s + AND (entry_order_id IS NULL OR entry_order_id = 0 OR entry_order_id = '') + ORDER BY id DESC + LIMIT %s""", + (symbol.strip(), int(limit)), + ) + except Exception as e: + logger.debug(f"get_trades_missing_entry_order_id 失败: {e}") + return [] + @staticmethod def get_by_client_order_id(client_order_id, account_id: int = None): """根据 clientOrderId 获取交易记录(可选按 account_id 隔离)""" diff --git a/frontend/src/components/StatsDashboard.jsx b/frontend/src/components/StatsDashboard.jsx index 6606668..7443114 100644 --- a/frontend/src/components/StatsDashboard.jsx +++ b/frontend/src/components/StatsDashboard.jsx @@ -738,9 +738,9 @@ const StatsDashboard = () => { {/* 止损止盈比例 */} - {trade.entry_time && ( -
开仓时间: {formatEntryTime(trade.entry_time)}
- )} +
+ 开仓时间: {trade.entry_time ? formatEntryTime(trade.entry_time) : '—'} +
diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index f78c30c..9ecebf7 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -110,9 +110,12 @@ const TradeList = () => { try { const result = await api.syncTradesFromBinance(syncDays, syncAllSymbols) + const backfillHint = (result.entry_order_id_filled || result.exit_order_id_filled) + ? `,补全开仓订单号 ${result.entry_order_id_filled || 0} 个、平仓订单号 ${result.exit_order_id_filled || 0} 个(勾选「仅可对账」后可见)` + : '' setSyncResult({ success: true, - message: `同步完成:共处理 ${result.total_orders || 0} 个订单,更新 ${result.updated_trades || 0} 条记录${result.created_trades ? `,创建 ${result.created_trades} 条新记录` : ''}`, + message: `同步完成:共处理 ${result.total_orders || 0} 个订单,更新 ${result.updated_trades || 0} 条记录${result.created_trades ? `,创建 ${result.created_trades} 条新记录` : ''}${backfillHint}`, details: result }) // 同步成功后自动刷新数据 @@ -810,7 +813,14 @@ const TradeList = () => { { trades.length === 0 ? ( -
暂无交易记录
+
+
暂无交易记录
+ {reconciledOnly && ( +

+ 若币安今日有订单但此处为空,可先点击右上角「同步订单」补全开仓/平仓订单号,或取消勾选「仅可对账」查看全部记录。 +

+ )} +
) : ( <> {/* 桌面端表格:用横向滚动包裹,避免整页过宽 */}