From 55ae7b5b084b02b33f07a304b7d69f3e57862d5e 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 22:27:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(trade=5Flist,=20api):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E5=90=8C=E6=AD=A5=E5=8A=9F=E8=83=BD=E4=BB=A5?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E7=BC=BA=E5=A4=B1=E7=9A=84=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `TradeList.jsx` 中新增订单同步功能,允许用户从币安同步最近的历史订单并补全缺失的订单号。引入 `syncTradesFromBinance` 方法于 `api.js`,实现与后端的交互,处理同步请求并返回结果。更新前端界面以显示同步状态和结果,提升用户体验和数据完整性。 --- frontend/src/components/TradeList.jsx | 100 ++++++++++++++++++++++++++ frontend/src/services/api.js | 30 ++++++++ trading_system/user_data_stream.py | 10 +++ 3 files changed, 140 insertions(+) diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index e40b709..8c9165a 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -22,6 +22,9 @@ const TradeList = () => { const [exitReason, setExitReason] = useState('') const [reconciledOnly, setReconciledOnly] = useState(true) // 默认仅可对账,与币安一致 const [timeFilter, setTimeFilter] = useState('exit') // 'exit' 按平仓时间(今天=今天平掉的单), 'entry' 按开仓时间 + const [syncing, setSyncing] = useState(false) // 同步订单状态 + const [syncResult, setSyncResult] = useState(null) // 同步结果 + const [syncDays, setSyncDays] = useState(7) // 同步天数 useEffect(() => { loadData() @@ -87,6 +90,41 @@ const TradeList = () => { setReconciledOnly(true) } + // 同步订单:从币安同步历史订单,补全缺失的订单号 + const handleSyncOrders = async () => { + if (syncing) { + return // 防止重复点击 + } + + if (!window.confirm(`确定要同步最近 ${syncDays} 天的订单吗?\n这将从币安拉取历史订单并补全缺失的订单号。`)) { + return + } + + setSyncing(true) + setSyncResult(null) + + try { + const result = await api.syncTradesFromBinance(syncDays) + setSyncResult({ + success: true, + message: `同步完成:共处理 ${result.total_orders || 0} 个订单,更新 ${result.updated_trades || 0} 条记录`, + details: result + }) + // 同步成功后自动刷新数据 + setTimeout(() => { + loadData() + }, 1000) + } catch (error) { + setSyncResult({ + success: false, + message: `同步失败:${error.message || '未知错误'}`, + details: null + }) + } finally { + setSyncing(false) + } + } + // 导出当前订单数据(含入场/离场原因、入场思路等完整字段,便于后续分析) // type: 'csv' | 'json' const handleExport = (type = 'csv') => { @@ -484,6 +522,35 @@ const TradeList = () => { +
+ + + +
{trades.length > 0 && ( <> + + )} { diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 41a257a..d3389fd 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -405,6 +405,36 @@ export const api = { return response.json(); }, + // 同步订单:从币安同步历史订单,补全缺失的订单号 + syncTradesFromBinance: async (days = 7) => { + const response = await fetch(buildUrl(`/api/trades/sync-binance?days=${days}`), { + method: 'POST', + headers: { + ...withAccountHeaders({ 'Content-Type': 'application/json' }), + }, + }); + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: '同步订单失败' })); + throw new Error(error.detail || '同步订单失败'); + } + return response.json(); + }, + + // 验证订单一致性:校验订单记录是否与币安一致 + verifyTradesAgainstBinance: async (days = 30, limit = 100) => { + const response = await fetch(buildUrl(`/api/trades/verify-binance?days=${days}&limit=${limit}`), { + method: 'GET', + headers: { + ...withAccountHeaders({ 'Content-Type': 'application/json' }), + }, + }); + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: '验证订单失败' })); + throw new Error(error.detail || '验证订单失败'); + } + return response.json(); + }, + // 统计 getPerformance: async (days = 7) => { const response = await fetch(buildUrl(`/api/stats/performance?days=${days}`), { headers: withAccountHeaders() }); diff --git a/trading_system/user_data_stream.py b/trading_system/user_data_stream.py index 1745d3b..521bc48 100644 --- a/trading_system/user_data_stream.py +++ b/trading_system/user_data_stream.py @@ -249,17 +249,23 @@ class UserDataStream: try: data = json.loads(raw) except Exception: + logger.debug(f"UserDataStream: 无法解析推送消息: {raw[:200]}") return False e = data.get("e") if e == "listenKeyExpired": logger.warning("UserDataStream: 收到 listenKeyExpired,将换新 key 重连") return True if e == "ORDER_TRADE_UPDATE": + logger.debug(f"UserDataStream: 收到 ORDER_TRADE_UPDATE 推送") self._on_order_trade_update(data.get("o") or {}) elif e == "ACCOUNT_UPDATE": + logger.debug(f"UserDataStream: 收到 ACCOUNT_UPDATE 推送") self._on_account_update(data.get("a") or {}) elif e == "ALGO_UPDATE": + logger.debug(f"UserDataStream: 收到 ALGO_UPDATE 推送") self._on_algo_update(data.get("o") or {}) + else: + logger.debug(f"UserDataStream: 收到未知事件类型: {e}") return False def _on_order_trade_update(self, o: Dict): @@ -267,7 +273,11 @@ class UserDataStream: # ap=均价, z=累计成交量, R=只减仓, rp=该交易实现盈亏, s=交易对 event_type = (o.get("x") or "").strip().upper() status = (o.get("X") or "").strip().upper() + symbol = (o.get("s") or "").strip() + order_id = o.get("i") + logger.debug(f"UserDataStream: ORDER_TRADE_UPDATE symbol={symbol!r} orderId={order_id} event={event_type} status={status}") if status != "FILLED": + logger.debug(f"UserDataStream: 订单状态非 FILLED,跳过 symbol={symbol!r} orderId={order_id} status={status}") return client_order_id = (o.get("c") or "").strip() or None order_id = o.get("i")