From 6e922d792147d5479932ffa9c5a00858da1d2aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Mon, 19 Jan 2026 22:07:48 +0800 Subject: [PATCH] a --- backend/api/routes/account.py | 33 +++++++++++++++++++ frontend/src/components/StatsDashboard.css | 38 ++++++++++++++++++++++ frontend/src/components/StatsDashboard.jsx | 36 ++++++++++++++++++-- 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index b402136..b600e6d 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -174,6 +174,37 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str): sl_order = await client.client.futures_create_order(**sl_params) tp_order = await client.client.futures_create_order(**tp_params) + + # 再查一次未成交委托,确认是否真的挂上(并用于前端展示/排查) + open_orders = [] + try: + oo = await client.client.futures_get_open_orders(symbol=symbol) + if isinstance(oo, list): + for o in oo: + try: + if not isinstance(o, dict): + continue + otype2 = str(o.get("type") or "").upper() + if otype2 in {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"}: + open_orders.append( + { + "orderId": o.get("orderId"), + "type": otype2, + "side": o.get("side"), + "stopPrice": o.get("stopPrice"), + "price": o.get("price"), + "workingType": o.get("workingType"), + "positionSide": o.get("positionSide"), + "closePosition": o.get("closePosition"), + "status": o.get("status"), + "updateTime": o.get("updateTime"), + } + ) + except Exception: + continue + except Exception: + open_orders = [] + return { "symbol": symbol, "position_side": side, @@ -184,6 +215,8 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str): "stop_market": sl_order, "take_profit_market": tp_order, }, + "open_protection_orders": open_orders, + "ui_hint": "在币安【U本位合约】里,这类 STOP/TP 通常显示在【条件单/止盈止损/计划委托】而不一定在普通【当前委托(限价)】列表。", } finally: await client.disconnect() diff --git a/frontend/src/components/StatsDashboard.css b/frontend/src/components/StatsDashboard.css index 4784ed2..6671fbd 100644 --- a/frontend/src/components/StatsDashboard.css +++ b/frontend/src/components/StatsDashboard.css @@ -446,6 +446,44 @@ font-style: normal; } +.positions-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.positions-header h3 { + margin: 0; +} + +.sltp-all-btn { + padding: 0.5rem 0.9rem; + background: #1f7aec; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + font-weight: 600; + transition: all 0.3s; + white-space: nowrap; +} + +.sltp-all-btn:hover:not(:disabled) { + background: #175fb8; +} + +.sltp-all-btn:active:not(:disabled) { + transform: scale(0.98); +} + +.sltp-all-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + background: #95a5a6; +} + .close-btn { padding: 0.5rem 1rem; background: #e74c3c; diff --git a/frontend/src/components/StatsDashboard.jsx b/frontend/src/components/StatsDashboard.jsx index 4aef04d..ca1b53f 100644 --- a/frontend/src/components/StatsDashboard.jsx +++ b/frontend/src/components/StatsDashboard.jsx @@ -8,6 +8,7 @@ const StatsDashboard = () => { const [loading, setLoading] = useState(true) const [closingSymbol, setClosingSymbol] = useState(null) const [sltpSymbol, setSltpSymbol] = useState(null) + const [sltpAllBusy, setSltpAllBusy] = useState(false) const [message, setMessage] = useState('') const [tradingConfig, setTradingConfig] = useState(null) @@ -116,7 +117,8 @@ const StatsDashboard = () => { const res = await api.ensurePositionSLTP(symbol) const slId = res?.orders?.stop_market?.orderId const tpId = res?.orders?.take_profit_market?.orderId - setMessage(`${symbol} 已补挂保护单:SL=${slId || '-'} / TP=${tpId || '-'}`) + const cnt = Array.isArray(res?.open_protection_orders) ? res.open_protection_orders.length : 0 + setMessage(`${symbol} 已补挂保护单:SL=${slId || '-'} / TP=${tpId || '-'}(币安条件单可见,当前检测到保护单 ${cnt} 条)`) await loadDashboard() } catch (error) { setMessage(`补挂失败 ${symbol}: ${error.message || '未知错误'}`) @@ -126,6 +128,26 @@ const StatsDashboard = () => { } } + const handleEnsureAllSLTP = async () => { + if (!window.confirm('确定要为【所有当前持仓】一键补挂“币安止损/止盈保护单”吗?\n\n说明:会对每个持仓 symbol 自动取消旧的 STOP/TP 保护单并重新挂单(避免重复)。')) { + return + } + setSltpAllBusy(true) + setMessage('') + try { + const res = await api.ensureAllPositionsSLTP(50) + const ok = res?.ok ?? 0 + const failed = res?.failed ?? 0 + setMessage(`一键补挂完成:成功 ${ok} / 失败 ${failed}(请在币安【条件单/止盈止损】里查看)`) + await loadDashboard() + } catch (error) { + setMessage(`一键补挂失败: ${error.message || '未知错误'}`) + } finally { + setSltpAllBusy(false) + setTimeout(() => setMessage(''), 5000) + } + } + const handleSyncPositions = async () => { if (!window.confirm('确定要同步持仓状态吗?这将检查币安实际持仓并更新数据库状态。')) { return @@ -337,7 +359,17 @@ const StatsDashboard = () => { )}
-

当前持仓

+
+

当前持仓

+ +
限价入场: {entryTypeCounts.limit} 市价入场: {entryTypeCounts.market}