diff --git a/backend/app/api/system.py b/backend/app/api/system.py index 9ae1988..f584b71 100644 --- a/backend/app/api/system.py +++ b/backend/app/api/system.py @@ -91,6 +91,8 @@ def _normalize_platform_position(platform: str, position: Dict[str, Any]) -> Dic size = abs(_safe_float(position.get("size") or position.get("quantity"))) leverage = _safe_float(position.get("leverage")) margin = _safe_float(position.get("margin") or position.get("initialMargin") or position.get("initial_margin")) + if margin <= 0 and entry_price > 0 and size > 0 and leverage > 0: + margin = (entry_price * size) / leverage unrealized_pnl = _safe_float(position.get("unrealized_pnl") or position.get("pnl_amount")) pnl_percent = _safe_float(position.get("unrealized_pnl_pct") or position.get("pnl_percent") or position.get("percentage")) @@ -112,9 +114,48 @@ def _normalize_platform_position(platform: str, position: Dict[str, Any]) -> Dic "setup_basis": position.get("setup_basis"), "entry_basis": position.get("entry_basis"), "opened_at": position.get("opened_at") or position.get("created_at"), + "protection": position.get("protection"), } +def _apply_live_position_metrics(position: Dict[str, Any], latest_price: float | None) -> Dict[str, Any]: + price = _safe_float(latest_price) + if price <= 0: + return position + + entry_price = _safe_float(position.get("entry_price")) + size = abs(_safe_float(position.get("size"))) + side = str(position.get("side") or "").lower() + margin = _safe_float(position.get("margin")) + leverage = _safe_float(position.get("leverage")) + + if entry_price <= 0 or size <= 0: + position["mark_price"] = price + return position + + direction = 1 if side == "long" else -1 + pnl_percent_unlevered = ((price - entry_price) / entry_price) * 100 * direction + + if str(position.get("platform")) == "paper": + notional = _safe_float(position.get("size")) + unrealized_pnl = notional * pnl_percent_unlevered / 100 + position["unrealized_pnl"] = round(unrealized_pnl, 2) + position["pnl_percent"] = round(pnl_percent_unlevered, 4) + else: + unrealized_pnl = (price - entry_price) * size * direction + if margin <= 0 and leverage > 0: + margin = (entry_price * size) / leverage + position["margin"] = round(margin, 8) + leveraged_pnl_pct = (unrealized_pnl / margin * 100) if margin > 0 else ( + pnl_percent_unlevered * leverage if leverage > 0 else pnl_percent_unlevered + ) + position["unrealized_pnl"] = round(unrealized_pnl, 8) + position["pnl_percent"] = round(leveraged_pnl_pct, 4) + + position["mark_price"] = price + return position + + 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" @@ -449,10 +490,17 @@ async def get_console_snapshot(_: dict = Depends(require_console_access)): for symbol in configured_symbols: price_monitor.subscribe_symbol(symbol) - paper_position_items = [ - _normalize_platform_position("paper", pos) - for pos in paper_service.get_open_positions()[:12] - ] + raw_paper_positions = paper_service.get_open_positions()[:12] + for pos in raw_paper_positions: + symbol = _normalize_contract_symbol(pos.get("symbol") or pos.get("coin") or "-") + if symbol and symbol != "-": + price_monitor.subscribe_symbol(symbol) + + paper_position_items = [] + for pos in raw_paper_positions: + normalized = _normalize_platform_position("paper", pos) + live_price = price_monitor.get_latest_price(normalized.get("symbol", "")) + paper_position_items.append(_apply_live_position_metrics(normalized, live_price)) paper_order_items = [ _normalize_platform_order("paper", order) for order in paper_pending[:12] @@ -461,9 +509,20 @@ async def get_console_snapshot(_: dict = Depends(require_console_access)): bitget_position_items = [] bitget_order_items = [] for account in bitget_accounts: + protection_state_map = {} + bitget_executor = getattr(crypto_agent, "bitget_executors", {}).get(account["account_id"]) + if bitget_executor and hasattr(bitget_executor, "export_position_protection_state"): + protection_state_map = bitget_executor.export_position_protection_state() for pos in account["positions"]["items"][:12]: normalized = _normalize_platform_position("bitget", pos) + if normalized.get("symbol") and normalized.get("symbol") != "-": + price_monitor.subscribe_symbol(normalized["symbol"]) + live_price = price_monitor.get_latest_price(normalized.get("symbol", "")) + normalized = _apply_live_position_metrics(normalized, live_price) normalized["account_id"] = account["account_id"] + state_key = f"{account['account_id']}:{normalized.get('symbol', '').upper()}:{normalized.get('side', '')}:{float(normalized.get('entry_price', 0) or 0):.8f}" + if state_key in protection_state_map: + normalized["protection"] = protection_state_map[state_key] bitget_position_items.append(normalized) for order in account["orders"]["items"][:12]: enriched_order = dict(order) diff --git a/backend/app/config.py b/backend/app/config.py index 34fb249..84e603e 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -173,6 +173,13 @@ class Settings(BaseSettings): bitget_max_single_position: float = 1000 # 单笔最大持仓金额 (USDT) bitget_max_total_leverage: float = 10 # 总杠杆上限(倍数) bitget_default_leverage: int = 10 # 默认执行杠杆(启动时同步到交易对) + bitget_breakeven_threshold: float = 1.0 # Bitget 保本止损触发阈值(盈利百分比) + bitget_trailing_stop_enabled: bool = True # Bitget 是否启用移动止损 + bitget_trailing_stop_threshold_multiplier: float = 2.0 # Bitget 移动止损触发倍数(相对于保本阈值) + bitget_trailing_stop_ratio: float = 0.5 # Bitget 移动止损锁定利润比例 + bitget_trailing_min_move_step: float = 0.4 # Bitget 每次上移止损的最小利润阶梯(百分比) + bitget_dynamic_tp_enabled: bool = True # Bitget 止损抬升时是否联动止盈 + bitget_dynamic_tp_distance_ratio: float = 0.8 # Bitget 联动止盈时保留原始止盈距离的比例 # 账户级止损(所有平台通用) account_max_drawdown: float = 0.25 # 账户最大回撤(25%),超过则停止交易并平仓 diff --git a/backend/app/crypto_agent/execution_guardian.py b/backend/app/crypto_agent/execution_guardian.py index c966969..0db9379 100644 --- a/backend/app/crypto_agent/execution_guardian.py +++ b/backend/app/crypto_agent/execution_guardian.py @@ -185,14 +185,30 @@ class ExecutionGuardian: elif action == 'MOVE_SL': new_sl = action_info.get('new_sl') + new_tp = action_info.get('new_tp') pnl_pct = action_info.get('pnl_pct', 0) if new_sl: - move_result = await target.executor.move_stop_loss(symbol=symbol, new_stop_loss=new_sl) + if hasattr(target.executor, 'move_protection_levels') and target.platform == 'Bitget': + move_result = await target.executor.move_protection_levels( + symbol=symbol, + new_stop_loss=new_sl, + new_take_profit=new_tp, + ) + else: + move_result = await target.executor.move_stop_loss(symbol=symbol, new_stop_loss=new_sl) if move_result.get('success'): - self._record_action("move_sl", target.target_key, symbol, f"new_sl={new_sl}") + detail_parts = [f"new_sl={new_sl:.6f}"] + if isinstance(new_tp, (int, float)): + detail_parts.append(f"new_tp={new_tp:.6f}") + if isinstance(pnl_pct, (int, float)): + detail_parts.append(f"pnl={pnl_pct:.2f}%") + if reason: + detail_parts.append(f"reason={reason}") + self._record_action("move_sl", target.target_key, symbol, " | ".join(detail_parts)) + tp_line = f"\n新止盈: ${new_tp:.2f}" if isinstance(new_tp, (int, float)) else "" await self.agent._send_alert_notification( f"🔒 [{target.target_key}] 移动止损", - f"交易对: {symbol}\n新止损: ${new_sl:.2f}\n原因: {reason}" + f"交易对: {symbol}\n新止损: ${new_sl:.2f}{tp_line}\n原因: {reason}" ) await target.executor.send_execution_notification( operation='POSITION_MANAGEMENT', @@ -200,6 +216,7 @@ class ExecutionGuardian: result={'success': True, 'action': 'MOVE_SL', 'reason': reason}, details={ 'new_sl': new_sl, + 'new_tp': new_tp, 'pnl_percent': pnl_pct, 'account_id': target.account_id, 'target_key': target.target_key, diff --git a/backend/app/crypto_agent/executor/bitget_executor.py b/backend/app/crypto_agent/executor/bitget_executor.py index 3ec6230..d12178e 100644 --- a/backend/app/crypto_agent/executor/bitget_executor.py +++ b/backend/app/crypto_agent/executor/bitget_executor.py @@ -15,6 +15,7 @@ class BitgetExecutor(BaseExecutor): super().__init__("Bitget") self.account_id = (account_id or "default").strip() or "default" self.bitget = service or get_bitget_live_service(self.account_id) + self._position_protection_state: Dict[str, Dict[str, Any]] = {} def _notification_context(self) -> Dict[str, str]: account_id = getattr(self, 'account_id', 'default') or 'default' @@ -293,8 +294,8 @@ class BitgetExecutor(BaseExecutor): return 24.0 def get_position_exit_rules(self) -> tuple: - """持仓退出规则:(目标盈利 3%, 无最大持仓时间限制)""" - return (3.0, float('inf')) + """Bitget 依赖保护单和移动止损管理利润,不做固定盈利自动平仓。""" + return (float('inf'), float('inf')) def get_fee_rate(self) -> float: """手续费率: 0.06% (taker)""" @@ -380,6 +381,181 @@ class BitgetExecutor(BaseExecutor): logger.error(f"Bitget 移动止损失败: {e}") return {'success': False, 'message': str(e)} + async def move_protection_levels(self, + symbol: str, + new_stop_loss: float, + new_take_profit: Optional[float] = None, + current_stop_loss: Optional[float] = None) -> Dict[str, Any]: + """同时更新 Bitget 的止损/止盈保护单。""" + try: + position = self.bitget.get_position_for_symbol(symbol) + if not position: + return {'success': False, 'message': f'找不到 {symbol} 的持仓'} + + current_tp_sl = self.bitget.get_tp_sl_prices(symbol.replace('USDT', '')) + target_tp = new_take_profit if new_take_profit is not None else current_tp_sl.get('take_profit') + result = self.bitget.set_tp_sl( + symbol=symbol.replace('USDT', ''), + is_long=position['size'] > 0, + size=abs(position['size']), + tp_price=target_tp, + sl_price=new_stop_loss + ) + + if result.get('success'): + logger.info( + f" ✅ Bitget 保护单更新成功: {symbol} " + f"SL→${new_stop_loss:.2f} TP→{f'${target_tp:.2f}' if isinstance(target_tp, (int, float)) else '保持'}" + ) + return { + 'success': True, + 'message': f'保护单更新成功: SL={new_stop_loss:.2f}', + 'take_profit': target_tp, + } + + errors = result.get('errors', []) + return {'success': False, 'message': '; '.join(errors) if errors else '保护单更新失败'} + except Exception as e: + logger.error(f"Bitget 更新保护单失败: {e}") + return {'success': False, 'message': str(e)} + + def _protection_state_key(self, position: Dict[str, Any]) -> str: + symbol = str(position.get('symbol') or '').upper() + side = str(position.get('side') or '') + entry_price = float(position.get('entry_price', 0) or 0) + return f"{self.account_id}:{symbol}:{side}:{entry_price:.8f}" + + def export_position_protection_state(self) -> Dict[str, Dict[str, Any]]: + return {key: dict(value) for key, value in self._position_protection_state.items()} + + def _compute_trailing_take_profit(self, + position: Dict[str, Any], + current_price: float, + new_stop_loss: float, + current_take_profit: Optional[float]) -> Optional[float]: + if not self.bitget.settings.bitget_dynamic_tp_enabled: + return current_take_profit + if not isinstance(current_take_profit, (int, float)) or current_take_profit <= 0: + return current_take_profit + + side = position.get('side') + entry_price = float(position.get('entry_price', 0) or 0) + distance_ratio = float(self.bitget.settings.bitget_dynamic_tp_distance_ratio or 0.8) + + if side == 'buy': + original_tp_distance = float(current_take_profit) - entry_price + candidate = new_stop_loss + (original_tp_distance * distance_ratio) + if candidate <= current_price: + return current_take_profit + return max(float(current_take_profit), candidate) + + original_tp_distance = entry_price - float(current_take_profit) + candidate = new_stop_loss - (original_tp_distance * distance_ratio) + if candidate >= current_price: + return current_take_profit + return min(float(current_take_profit), candidate) + + def check_position_management(self, + positions: List[Dict], + current_prices: Dict[str, float], + volatility_data: Optional[Dict[str, float]] = None) -> List[Dict[str, Any]]: + """Bitget 仓位保护:先保本,再基于最大浮盈分段上移止损。""" + actions: List[Dict[str, Any]] = [] + active_keys = set() + + breakeven_threshold = float(self.bitget.settings.bitget_breakeven_threshold or 1.0) + trailing_enabled = bool(self.bitget.settings.bitget_trailing_stop_enabled) + trailing_threshold = breakeven_threshold * float(self.bitget.settings.bitget_trailing_stop_threshold_multiplier or 2.0) + trailing_ratio = float(self.bitget.settings.bitget_trailing_stop_ratio or 0.5) + min_move_step = float(self.bitget.settings.bitget_trailing_min_move_step or 0.4) + + for pos in positions: + symbol = pos.get('symbol') + current_price = float(current_prices.get(symbol, pos.get('entry_price', 0)) or 0) + entry_price = float(pos.get('entry_price', 0) or 0) + side = pos.get('side') + current_sl = pos.get('stop_loss') + current_tp = pos.get('take_profit') + + if current_price <= 0 or entry_price <= 0 or side not in {'buy', 'sell'} or not isinstance(current_sl, (int, float)): + continue + + pnl_pct = ((current_price - entry_price) / entry_price * 100) if side == 'buy' else ((entry_price - current_price) / entry_price * 100) + state_key = self._protection_state_key(pos) + active_keys.add(state_key) + state = self._position_protection_state.setdefault(state_key, { + 'max_pnl_pct': pnl_pct, + 'breakeven_done': False, + 'trailing_active': False, + }) + state['max_pnl_pct'] = max(float(state.get('max_pnl_pct', pnl_pct) or pnl_pct), pnl_pct) + max_pnl_pct = float(state['max_pnl_pct']) + + if trailing_enabled and pnl_pct >= trailing_threshold: + locked_profit_pct = max_pnl_pct * trailing_ratio + if side == 'buy': + new_sl = entry_price * (1 + locked_profit_pct / 100) + current_locked_pct = (float(current_sl) - entry_price) / entry_price * 100 + can_move = new_sl > float(current_sl) and (locked_profit_pct - current_locked_pct) >= min_move_step + else: + new_sl = entry_price * (1 - locked_profit_pct / 100) + current_locked_pct = (entry_price - float(current_sl)) / entry_price * 100 + can_move = new_sl < float(current_sl) and (locked_profit_pct - current_locked_pct) >= min_move_step + + if can_move: + new_tp = self._compute_trailing_take_profit(pos, current_price, new_sl, current_tp) + state['trailing_active'] = True + state['last_move_reason'] = 'trailing' + state['last_new_sl'] = new_sl + state['last_new_tp'] = new_tp + actions.append({ + 'symbol': symbol, + 'action': 'MOVE_SL', + 'new_sl': new_sl, + 'new_tp': new_tp, + 'pnl_pct': pnl_pct, + 'reason': f"最高盈利 {max_pnl_pct:.1f}% ,锁定利润 {locked_profit_pct:.1f}% ,上移止损", + 'priority': 3, + }) + continue + + if pnl_pct >= breakeven_threshold: + if side == 'buy' and float(current_sl) < entry_price: + state['breakeven_done'] = True + state['last_move_reason'] = 'breakeven' + state['last_new_sl'] = entry_price + state['last_new_tp'] = current_tp + actions.append({ + 'symbol': symbol, + 'action': 'MOVE_SL', + 'new_sl': entry_price, + 'new_tp': current_tp, + 'pnl_pct': pnl_pct, + 'reason': f"盈利 {pnl_pct:.1f}% >= {breakeven_threshold:.1f}% ,止损移到保本", + 'priority': 3, + }) + elif side == 'sell' and float(current_sl) > entry_price: + state['breakeven_done'] = True + state['last_move_reason'] = 'breakeven' + state['last_new_sl'] = entry_price + state['last_new_tp'] = current_tp + actions.append({ + 'symbol': symbol, + 'action': 'MOVE_SL', + 'new_sl': entry_price, + 'new_tp': current_tp, + 'pnl_pct': pnl_pct, + 'reason': f"盈利 {pnl_pct:.1f}% >= {breakeven_threshold:.1f}% ,止损移到保本", + 'priority': 3, + }) + + stale_keys = [key for key in self._position_protection_state.keys() if key not in active_keys] + for key in stale_keys: + self._position_protection_state.pop(key, None) + + actions.sort(key=lambda x: x.get('priority', 99)) + return actions + # ==================== 辅助方法 ==================== def _calculate_contracts(self, symbol: str, margin: float, price: float, leverage: int) -> int: diff --git a/frontend/console.html b/frontend/console.html index 5d004dc..ac98848 100644 --- a/frontend/console.html +++ b/frontend/console.html @@ -2271,6 +2271,97 @@ line-height: 1.6; } + .protection-action-list { + display: grid; + gap: 10px; + } + + .protection-action-item { + padding: 14px 16px; + border-radius: 14px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(126, 200, 255, 0.1); + } + + .protection-action-item.move { + border-color: rgba(255, 184, 77, 0.18); + background: rgba(255, 184, 77, 0.06); + } + + .protection-action-item.repair { + border-color: rgba(126, 200, 255, 0.18); + background: rgba(126, 200, 255, 0.06); + } + + .protection-summary-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 12px; + } + + .protection-summary-card { + padding: 12px 14px; + border-radius: 14px; + border: 1px solid rgba(255,255,255,0.06); + background: rgba(255,255,255,0.025); + } + + .protection-summary-card .label { + display: block; + color: var(--muted); + font-size: 10px; + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-family: "IBM Plex Mono", monospace; + } + + .protection-summary-card .value { + display: block; + color: var(--text); + font-size: 18px; + font-weight: 700; + font-family: "IBM Plex Mono", monospace; + } + + .protection-summary-card .sub { + display: block; + color: var(--muted); + font-size: 11px; + margin-top: 4px; + line-height: 1.5; + } + + .protection-action-top { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: flex-start; + margin-bottom: 8px; + } + + .protection-action-main { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + } + + .protection-action-symbol { + font-size: 14px; + font-weight: 700; + color: var(--text); + } + + .protection-action-detail { + color: var(--muted); + font-size: 12px; + line-height: 1.7; + font-family: "IBM Plex Mono", monospace; + word-break: break-word; + } + .table-toolbar { display: flex; flex-wrap: wrap; @@ -2506,6 +2597,7 @@ .signal-grid, .ops-grid, .coord-grid, + .protection-summary-grid, .position-card-grid, .pane-grid.two-col, .pane-grid.equal, @@ -2772,6 +2864,19 @@
正在整理跨平台挂单...
+ +
+
+
+ +

保护动作

+
最近 10 条保本、移动止损和 TP/SL 修复动作,方便确认 Bitget 执行保护是否正常
+
+
+
+
正在整理保护动作...
+
+
@@ -2909,6 +3014,26 @@ return String(value || '-').toUpperCase(); } + function formatProtectionSummary(protection) { + if (!protection || typeof protection !== 'object') return '-'; + const lastSl = Number(protection.last_new_sl || 0); + const lastTp = Number(protection.last_new_tp || 0); + const latestLine = lastSl > 0 + ? `
最近 SL ${formatMoney(lastSl)}${lastTp > 0 ? ` / TP ${formatMoney(lastTp)}` : ''}` + : ''; + if (protection.trailing_active) { + const locked = Number(protection.max_pnl_pct || 0) * 0.5; + return `移动止损中 / 最大浮盈 ${formatPercent(protection.max_pnl_pct || 0, 1)} / 参考锁盈 ${formatPercent(locked, 1)}${latestLine}`; + } + if (protection.breakeven_done) { + return `已保本 / 最大浮盈 ${formatPercent(protection.max_pnl_pct || 0, 1)}${latestLine}`; + } + if (Number.isFinite(Number(protection.max_pnl_pct))) { + return `监控中 / 最大浮盈 ${formatPercent(protection.max_pnl_pct || 0, 1)}${latestLine}`; + } + return '-'; + } + function setFeedback(message, isError = false) { const el = document.getElementById('feedback'); if (!message) { @@ -4026,6 +4151,103 @@ `).join(''); } + function protectionActionLabel(actionType) { + const value = String(actionType || '').toLowerCase(); + if (value === 'move_sl') return '移动保护'; + if (value === 'repair_tpsl') return '修复保护单'; + if (value === 'fallback_tpsl') return '回补保护单'; + if (value === 'cleanup_pending_tpsl') return '清理保护残单'; + return value || '-'; + } + + function protectionActionClass(actionType) { + const value = String(actionType || '').toLowerCase(); + if (value === 'move_sl') return 'move'; + return 'repair'; + } + + function classifyProtectionAction(actionType) { + const value = String(actionType || '').toLowerCase(); + if (value === 'move_sl') return 'move'; + if (value === 'repair_tpsl' || value === 'fallback_tpsl') return 'repair'; + if (value === 'cleanup_pending_tpsl') return 'cleanup'; + return 'other'; + } + + function formatProtectionActionDetail(item) { + const lines = []; + const platform = item.platform || '-'; + lines.push(`目标: ${platform}`); + if (item.detail) { + const detail = String(item.detail) + .replaceAll(' | ', '\n') + .replace(/^reason=/gm, 'reason: ') + .replace(/^new_sl=/gm, 'new_sl: ') + .replace(/^new_tp=/gm, 'new_tp: ') + .replace(/^pnl=/gm, 'pnl: '); + lines.push(detail); + } + return lines.join('\n'); + } + + function renderProtectionActionSummary(actions) { + const moveItems = actions.filter((item) => classifyProtectionAction(item.action_type) === 'move'); + const repairItems = actions.filter((item) => classifyProtectionAction(item.action_type) === 'repair'); + const cleanupItems = actions.filter((item) => classifyProtectionAction(item.action_type) === 'cleanup'); + const latestMove = moveItems[0]; + const latestRepair = repairItems[0]; + const latestCleanup = cleanupItems[0]; + + return ` +
+
+ 锁盈推进 + ${moveItems.length} + ${latestMove ? `${latestMove.symbol || '-'} · ${relativeTime(latestMove.timestamp)}` : '最近没有移动保护'} +
+
+ 保护修复 + ${repairItems.length} + ${latestRepair ? `${latestRepair.symbol || '-'} · ${relativeTime(latestRepair.timestamp)}` : '最近没有补修保护单'} +
+
+ 残单清理 + ${cleanupItems.length} + ${latestCleanup ? `${latestCleanup.symbol || '-'} · ${relativeTime(latestCleanup.timestamp)}` : '最近没有清理动作'} +
+
+ `; + } + + function renderProtectionActions(guardianState) { + const container = document.getElementById('protectionActionList'); + const actions = (guardianState?.last_actions || []) + .filter((item) => ['move_sl', 'repair_tpsl', 'fallback_tpsl', 'cleanup_pending_tpsl'].includes(String(item.action_type || '').toLowerCase())) + .slice(0, 10); + + if (!actions.length) { + container.innerHTML = compactEmpty('最近没有保护动作', '没有移动止损或补设保护单时,这里会保持紧凑显示。'); + return; + } + + container.innerHTML = ` + ${renderProtectionActionSummary(actions)} + ${actions.map((item) => ` +
+
+
+ ${platformDisplayName(item.platform)} + ${protectionActionLabel(item.action_type)} + ${item.symbol || '-'} +
+
${item.timestamp ? `${relativeTime(item.timestamp)} / ${formatTime(item.timestamp)}` : '-'}
+
+
${formatProtectionActionDetail(item).replaceAll('\n', '
')}
+
+ `).join('')} + `; + } + function renderUnifiedPositions(positions) { const toolbar = document.getElementById('positionsToolbar'); const container = document.getElementById('positionsTable'); @@ -4092,8 +4314,10 @@ ${item.take_profit ? formatMoney(item.take_profit) : '-'} / ${item.stop_loss ? formatMoney(item.stop_loss) : '-'}
- Setup / Entry - ${item.setup_type || '-'}${item.entry_basis || item.setup_basis ? `
${item.entry_basis || item.setup_basis}` : ''}
+ ${normalizePlatformKey(item.platform) === 'bitget' ? '保护状态' : 'Setup / Entry'} + ${normalizePlatformKey(item.platform) === 'bitget' + ? formatProtectionSummary(item.protection) + : `${item.setup_type || '-'}${item.entry_basis || item.setup_basis ? `
${item.entry_basis || item.setup_basis}` : ''}`}
@@ -4291,6 +4515,7 @@ renderAttentionItems(data.management?.attention_items || []); renderUnifiedPositions(data.management?.positions || []); renderUnifiedOrders(data.management?.orders || []); + renderProtectionActions(data.crypto_agent?.execution_guardian || {}); } catch (error) { console.error(error); setFeedback(`总控台加载失败: ${error.message}`, true);