diff --git a/backend/app/api/system.py b/backend/app/api/system.py index ddd92f2..9ae1988 100644 --- a/backend/app/api/system.py +++ b/backend/app/api/system.py @@ -63,12 +63,25 @@ def _safe_float(value: Any, default: float = 0.0) -> float: return default +def _normalize_contract_symbol(symbol: Any) -> str: + text = str(symbol or "").strip().upper() + if not text: + return "-" + if text.endswith("USDTUSDT"): + return text[:-4] + if text.endswith("/USDT:USDT"): + return f"{text.split('/')[0]}USDT" + if text.endswith("USDT"): + return text + if "/" in text: + return f"{text.split('/')[0]}USDT" + return f"{text}USDT" + + def _normalize_platform_position(platform: str, position: Dict[str, Any]) -> Dict[str, Any]: side_raw = str(position.get("side") or "").lower() side = "long" if side_raw in {"buy", "long"} else "short" - symbol = position.get("symbol") or position.get("coin") or "-" - if isinstance(symbol, str) and symbol.endswith("USDTUSDT"): - symbol = symbol.replace("USDTUSDT", "USDT") + symbol = _normalize_contract_symbol(position.get("symbol") or position.get("coin") or "-") entry_price = _safe_float(position.get("entry_price") or position.get("filled_price")) mark_price = _safe_float(position.get("mark_price") or position.get("current_price")) @@ -105,9 +118,7 @@ def _normalize_platform_position(platform: str, position: Dict[str, Any]) -> Dic def _normalize_platform_order(platform: str, order: Dict[str, Any]) -> Dict[str, Any]: side_raw = str(order.get("side") or "").lower() side = "long" if side_raw in {"buy", "long", "b"} else "short" - symbol = order.get("symbol") or order.get("coin") or "-" - if isinstance(symbol, str) and symbol.endswith("USDTUSDT"): - symbol = symbol.replace("USDTUSDT", "USDT") + symbol = _normalize_contract_symbol(order.get("symbol") or order.get("coin") or "-") price = _safe_float(order.get("price") or order.get("entry_price")) size = abs(_safe_float(order.get("size") or order.get("quantity"))) diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 1b49d90..7a2d06d 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -3580,7 +3580,7 @@ class CryptoAgent: for order in all_orders: pending_orders.append({ 'order_id': order.get('order_id'), - 'symbol': f"{order['symbol']}USDT", + 'symbol': order.get('symbol'), 'side': order.get('side', ''), 'entry_price': order.get('price'), 'quantity': order.get('size'), diff --git a/backend/app/services/bitget_live_trading_service.py b/backend/app/services/bitget_live_trading_service.py index 579d305..d6798d8 100644 --- a/backend/app/services/bitget_live_trading_service.py +++ b/backend/app/services/bitget_live_trading_service.py @@ -544,7 +544,7 @@ class BitgetLiveTradingService: result.append({ "order_id": str(order.get('id', '')), - "symbol": coin, + "symbol": f"{coin}USDT", "side": order.get('side', ''), "size": size_in_coins, "price": display_price, diff --git a/frontend/console.html b/frontend/console.html index 2103c80..5d004dc 100644 --- a/frontend/console.html +++ b/frontend/console.html @@ -283,6 +283,55 @@ line-height: 1.6; } + .toolbar-group { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + } + + .filter-chip { + appearance: none; + border: 1px solid rgba(126, 200, 255, 0.16); + background: rgba(255,255,255,0.04); + color: var(--muted); + border-radius: 999px; + padding: 8px 14px; + cursor: pointer; + font-family: "IBM Plex Mono", monospace; + font-size: 12px; + transition: all 0.18s ease; + } + + .filter-chip.active { + color: var(--text); + border-color: rgba(126, 200, 255, 0.34); + background: rgba(126, 200, 255, 0.14); + box-shadow: inset 0 0 0 1px rgba(126, 200, 255, 0.08); + } + + .position-card.platform-paper { + border-color: rgba(126, 200, 255, 0.22); + box-shadow: inset 0 0 0 1px rgba(126, 200, 255, 0.06); + } + + .position-card.platform-bitget { + border-color: rgba(156, 255, 87, 0.18); + box-shadow: inset 0 0 0 1px rgba(156, 255, 87, 0.06); + } + + .platform-pill.paper { + border-color: rgba(126, 200, 255, 0.32); + color: var(--cold); + background: rgba(126, 200, 255, 0.10); + } + + .platform-pill.bitget { + border-color: rgba(156, 255, 87, 0.28); + color: var(--accent); + background: rgba(156, 255, 87, 0.10); + } + .hero { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(360px, 0.9fr); @@ -2769,6 +2818,8 @@ let cachedConsoleData = null; let revealSensitiveData = false; let consoleAuthenticated = false; + let positionsPlatformFilter = 'all'; + let ordersPlatformFilter = 'all'; const SENSITIVE_VISIBILITY_KEY = 'console_sensitive_visible_v2'; const HUB_PREFERENCE_KEY = 'console_hub_active_v1'; @@ -2847,6 +2898,17 @@ return Object.values(platformHalts || {}).filter((item) => item && item.halted).length; } + function normalizePlatformKey(value) { + return String(value || '').trim().toLowerCase(); + } + + function platformDisplayName(value) { + const key = normalizePlatformKey(value); + if (key === 'paper') return 'PAPER'; + if (key === 'bitget') return 'BITGET'; + return String(value || '-').toUpperCase(); + } + function setFeedback(message, isError = false) { const el = document.getElementById('feedback'); if (!message) { @@ -3968,21 +4030,39 @@ const toolbar = document.getElementById('positionsToolbar'); const container = document.getElementById('positionsTable'); const total = positions || []; - const platformCounts = ['paper', 'bitget'].map((platform) => { - const count = total.filter((item) => item.platform === platform).length; - return `${platform}: ${count}`; - }).join(''); - toolbar.innerHTML = `${platformCounts}total: ${total.length}`; + const paperCount = total.filter((item) => normalizePlatformKey(item.platform) === 'paper').length; + const bitgetCount = total.filter((item) => normalizePlatformKey(item.platform) === 'bitget').length; + const filtered = total.filter((item) => positionsPlatformFilter === 'all' || normalizePlatformKey(item.platform) === positionsPlatformFilter); - if (!total.length) { + toolbar.innerHTML = ` +
+ paper: ${paperCount} + bitget: ${bitgetCount} + total: ${total.length} +
+
+ + + +
+ `; + + toolbar.querySelectorAll('[data-position-filter]').forEach((button) => { + button.addEventListener('click', () => { + positionsPlatformFilter = button.getAttribute('data-position-filter') || 'all'; + renderUnifiedPositions(total); + }); + }); + + if (!filtered.length) { container.innerHTML = compactEmpty('当前没有跨平台持仓', '没有活动持仓时,这里会保持紧凑,不再占用大块留白。'); return; } container.innerHTML = `
- ${total.map((item) => ` -
+ ${filtered.map((item) => ` +
${item.symbol || '-'}
@@ -3994,7 +4074,7 @@
- ${item.platform} + ${platformDisplayName(item.platform)} ${item.side === 'long' ? 'long' : 'short'} ${item.setup_type ? `${item.setup_type}` : ''}
@@ -4028,21 +4108,40 @@ const total = orders || []; const entryCount = total.filter((item) => item.category === 'entry').length; const protectionCount = total.filter((item) => item.category === 'tp_sl').length; + const paperCount = total.filter((item) => normalizePlatformKey(item.platform) === 'paper').length; + const bitgetCount = total.filter((item) => normalizePlatformKey(item.platform) === 'bitget').length; + const filtered = total.filter((item) => ordersPlatformFilter === 'all' || normalizePlatformKey(item.platform) === ordersPlatformFilter); toolbar.innerHTML = ` - entry: ${entryCount} - tp/sl: ${protectionCount} - total: ${total.length} +
+ entry: ${entryCount} + tp/sl: ${protectionCount} + paper: ${paperCount} + bitget: ${bitgetCount} + total: ${total.length} +
+
+ + + +
`; - if (!total.length) { + toolbar.querySelectorAll('[data-order-filter]').forEach((button) => { + button.addEventListener('click', () => { + ordersPlatformFilter = button.getAttribute('data-order-filter') || 'all'; + renderUnifiedOrders(total); + }); + }); + + if (!filtered.length) { container.innerHTML = compactEmpty('当前没有跨平台挂单', '没有入场单或保护单时,这里会保持紧凑展示。'); return; } container.innerHTML = `
- ${total.map((item) => ` -
+ ${filtered.map((item) => ` +
${item.symbol || '-'}
@@ -4054,7 +4153,7 @@
- ${item.platform} + ${platformDisplayName(item.platform)} ${item.side === 'long' ? 'long' : 'short'} ${item.signal_grade ? `${item.signal_grade}` : ''} ${item.signal_type ? `${item.signal_type}` : ''}