diff --git a/manual_trading_hub/hub_ai/context.py b/manual_trading_hub/hub_ai/context.py index f444d85..d86b88c 100644 --- a/manual_trading_hub/hub_ai/context.py +++ b/manual_trading_hub/hub_ai/context.py @@ -1060,27 +1060,73 @@ def resolve_position_monitor_source(pos: dict, hub_mon: Optional[dict]) -> str: def resolve_position_reward_at_tp(pos: dict, hub_mon: Optional[dict]) -> Optional[float]: - """与监控区「盈利金额」一致:优先匹配下单监控 reward_at_tp_usdt,否则仓位自身字段.""" + """与监控区「盈利金额」一致:有字段用字段,否则按止盈价×张数推算.""" sym = str(pos.get("symbol") or "") - side = str(pos.get("side") or "") + side = str(pos.get("side") or "").lower() + if side in ("buy",): + side = "long" + elif side in ("sell",): + side = "short" + matched: Optional[dict] = None if isinstance(hub_mon, dict) and hub_mon.get("ok") is not False and sym: - # 与前端 findMonitorOrder 一致:先扫 orders - for o in hub_mon.get("orders") or []: - if isinstance(o, dict) and _monitor_item_matches_position(o, sym, side): + for bucket in ("orders", "rolls", "trends"): + for o in hub_mon.get(bucket) or []: + if not isinstance(o, dict): + continue + o_sym = o.get("exchange_symbol") or o.get("symbol") or "" + if not _symbols_match(sym, str(o_sym)): + continue + o_side = str(o.get("direction") or "").lower() + # 与前端 findMonitorOrder 一致:方向为空也可匹配 + if o_side and o_side != side: + continue + matched = o v = _safe_float(o.get("reward_at_tp_usdt")) if v is not None: return v - for r in hub_mon.get("rolls") or []: - if isinstance(r, dict) and _monitor_item_matches_position(r, sym, side): - v = _safe_float(r.get("reward_at_tp_usdt")) - if v is not None: - return v - for t in hub_mon.get("trends") or []: - if isinstance(t, dict) and _monitor_item_matches_position(t, sym, side): - v = _safe_float(t.get("reward_at_tp_usdt")) - if v is not None: - return v - return _safe_float(pos.get("reward_at_tp_usdt")) + break + if matched is not None: + break + + v = _safe_float(pos.get("reward_at_tp_usdt")) + if v is not None: + return v + + entry = _safe_float(pos.get("entry_price")) + if entry is None and matched is not None: + entry = _safe_float( + matched.get("avg_entry_price") + or matched.get("entry_price") + or matched.get("avg_px") + ) + tp = None + if matched is not None: + tp = _safe_float(matched.get("take_profit")) + if tp is None: + tp = _safe_float(matched.get("take_profit_display")) + if tp is None: + tpsl = _resolve_position_tpsl(pos, hub_mon) + tp = tpsl.get("tp") + contracts = pos.get("contracts") + if contracts is None: + contracts = pos.get("size") + if contracts is None and matched is not None: + contracts = matched.get("contracts") + try: + qty = abs(float(contracts)) if contracts is not None else None + except (TypeError, ValueError): + qty = None + cs = _safe_float(pos.get("contract_size")) + if cs is None or cs <= 0: + cs = 1.0 + if entry is None or tp is None or not qty: + return None + try: + from lib.strategy.strategy_roll_ui_lib import reward_at_tp_usdt + + return reward_at_tp_usdt(side or "long", float(entry), float(tp), float(qty), contract_size=float(cs)) + except Exception: + return None def _options_source_label(p: dict) -> str: diff --git a/manual_trading_hub/static/dashboard.js b/manual_trading_hub/static/dashboard.js index 2878dff..aaac17d 100644 --- a/manual_trading_hub/static/dashboard.js +++ b/manual_trading_hub/static/dashboard.js @@ -259,6 +259,20 @@ return "solo:" + ex + ":" + String((p && p.inst_id) || Math.random()); } + function optionsRoiPct(p) { + if (!p || typeof p !== "object") return null; + const preview = p.close_preview || {}; + if (preview.estimated_pnl_ratio_pct != null && Number.isFinite(Number(preview.estimated_pnl_ratio_pct))) { + return Number(preview.estimated_pnl_ratio_pct); + } + const net = optionsNetPnl(p); + const paid = Number(p.premium_paid); + if (net != null && Number.isFinite(paid) && paid > 0) { + return (net / paid) * 100; + } + return null; + } + function renderOptionsLegRow(ac, p, groupCls) { const optType = (p.opt_type || "").toUpperCase() === "C" @@ -270,6 +284,7 @@ const target = String(p.target_monitor_text || "—"); const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor"; const net = optionsNetPnl(p); + const roi = optionsRoiPct(p); return ` ${exchangeLinkCell(ac)} ${sourceTypeCell(source)} @@ -279,6 +294,7 @@ ${p.idx_px != null ? fmt(p.idx_px, 0) : "—"} ${esc(target)} ${net != null ? pnlSigned(net, 2) : "—"} + ${roi != null ? esc(Number(roi).toFixed(2)) + "%" : "—"} `; } @@ -322,14 +338,22 @@ groups.forEach((g) => { if (g.isHedge && g.items.length >= 1) { let sum = 0; + let paidSum = 0; let hasSum = false; + let hasPaid = false; g.items.forEach(({ p }) => { const n = optionsNetPnl(p); if (n != null) { sum += n; hasSum = true; } + const paid = Number(p && p.premium_paid); + if (Number.isFinite(paid) && paid > 0) { + paidSum += paid; + hasPaid = true; + } }); + const groupRoi = hasSum && hasPaid && paidSum > 0 ? (sum / paidSum) * 100 : null; const title = (g.source && g.source !== "—" ? g.source : "对冲") + (g.planId ? " #" + g.planId : "") + @@ -339,6 +363,9 @@ body += ` ${esc(title)} ${hasSum ? pnlSigned(sum, 2) : "—"} + ${ + groupRoi != null ? esc(Number(groupRoi).toFixed(2)) + "%" : "—" + } `; g.items.forEach(({ ac, p }, idx) => { const cls = @@ -359,7 +386,7 @@
- + ${body}
交易所类型合约Call/Put到期倒计时指数目标监控净盈亏交易所类型合约Call/Put到期倒计时指数目标监控净盈亏收益率
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 577423d..ab1a462 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -20,7 +20,7 @@ - + @@ -1385,7 +1385,7 @@ - +