Label dashboard positions from monitor sources with priority.

Match hedge/roll/trend/order/key monitors for source badges, show option target monitors in green, and refresh the dashboard with the board 5s cycle.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 14:25:36 +08:00
parent 0bda44248f
commit c4c8ad172a
11 changed files with 290 additions and 28 deletions
+1
View File
@@ -240,6 +240,7 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s
"underlying": row.get("underlying"),
"opt_type": opt_type,
"target_index": target_f,
"plan_type": "options_options",
"managed_by": "hedge_plan",
}
return out
+18
View File
@@ -175,6 +175,7 @@ def build_hub_monitor_payload(
orders,
trends,
rolls,
hedges=None,
enrich=None,
risk_status=None,
) -> dict:
@@ -185,6 +186,7 @@ def build_hub_monitor_payload(
"orders": orders,
"trends": trends,
"rolls": rolls,
"hedges": hedges if isinstance(hedges, list) else [],
"key_prices": [],
}
if isinstance(risk_status, dict):
@@ -193,6 +195,9 @@ def build_hub_monitor_payload(
extra = enrich(keys=keys, orders=orders, trends=trends, rolls=rolls)
if isinstance(extra, dict):
payload.update(extra)
# enrich 可能不返回 hedges,保留本地组装的对冲列表.
if "hedges" not in extra:
payload["hedges"] = hedges if isinstance(hedges, list) else []
return payload
@@ -572,6 +577,17 @@ def register_hub_routes(app):
rolls.append(_row_to_dict(row))
except Exception:
pass
hedges = []
try:
from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans
hedge_rows: list = []
for st in ("opening", "active", "partial"):
hedge_rows.extend(list_plans(conn, status=st, limit=80))
hedge_rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
hedges = attach_legs_to_plans(conn, hedge_rows)
except Exception:
hedges = []
risk_status = None
risk_fn = c.get("risk_status_fn")
if callable(risk_fn):
@@ -589,6 +605,7 @@ def register_hub_routes(app):
orders=orders,
trends=trends,
rolls=rolls,
hedges=hedges,
enrich=enrich,
risk_status=risk_status,
)
@@ -601,6 +618,7 @@ def register_hub_routes(app):
orders=orders,
trends=trends,
rolls=rolls,
hedges=hedges,
risk_status=risk_status,
)
)
+14
View File
@@ -53,6 +53,20 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
if not mon:
# 中控卡片共用 target_index 只读展示;实际平仓仍由对冲计划监控处理。
p["target_index"] = hedge_target.get("target_index")
try:
from lib.instance.instance_dashboard_lib import (
_format_options_target,
_resolve_options_source,
)
inst = str(p.get("inst_id") or "")
source_key, source_label = _resolve_options_source(conn, inst)
p["source"] = source_key
p["source_label"] = source_label
p["target_monitor_text"] = _format_options_target(p)
except Exception:
p.setdefault("source_label", "")
p.setdefault("target_monitor_text", "")
finally:
conn.close()
except Exception:
+2
View File
@@ -330,6 +330,8 @@ async def _run_board_aggregate() -> dict:
await asyncio.to_thread(record_fund_snapshot_from_board, body.get("rows") or [])
except Exception:
pass
# 监控聚合完成即唤醒数据看板,持仓来源与监控 5s 同步.
dashboard_store.request_refresh()
return {"ok": True, **body}
except asyncio.TimeoutError:
return {
+122 -18
View File
@@ -973,9 +973,119 @@ def format_account_remark(ac: dict) -> str:
return ";".join(parts)
def _monitor_item_matches_position(item: dict, symbol: str, side: str) -> bool:
o_sym = item.get("exchange_symbol") or item.get("symbol") or ""
if not _symbols_match(symbol, o_sym):
return False
return (str(item.get("direction") or "").lower() == str(side or "").lower())
def _order_monitor_source_label(order: dict) -> tuple[int, str]:
"""返回 (优先级, 来源标签). 对冲=1 … 关键位=5."""
mt = str(
order.get("monitor_type_display")
or order.get("monitor_type_label")
or order.get("monitor_type")
or ""
).strip()
if "顺势" in mt:
return 2, "顺势加仓"
if "趋势" in mt:
return 3, "趋势回调"
if "关键位" in mt:
return 5, "关键位"
return 4, "下单监控"
def _hedge_matches_position(plan: dict, symbol: str, side: str) -> bool:
"""进行中对冲计划是否覆盖该永续仓(方向 + 永续腿/标的)."""
direction = str(plan.get("direction") or "").lower()
if direction and direction != str(side or "").lower():
return False
for leg in plan.get("legs") or []:
if not isinstance(leg, dict):
continue
if str(leg.get("leg_role") or "") != "perp":
continue
if str(leg.get("status") or "open") not in ("", "open"):
continue
if _symbols_match(symbol, str(leg.get("symbol") or "")):
return True
und = str(plan.get("underlying") or "").strip()
if und and _symbols_match(symbol, und):
return True
return False
def _hedge_source_label(plan: dict) -> str:
pt = str(plan.get("plan_type") or "").strip()
if pt == "perp_options" or str(plan.get("plan_type_label") or "") == "永期对冲":
return "永期对冲"
if pt == "options_options" or str(plan.get("plan_type_label") or "") == "期期对冲":
return "期期对冲"
return "对冲"
def resolve_position_monitor_source(pos: dict, hub_mon: Optional[dict]) -> str:
"""仓位来源:对冲 > 顺势加仓 > 趋势回调 > 下单监控 > 关键位;对不上为 —."""
if not isinstance(hub_mon, dict) or hub_mon.get("ok") is False:
return ""
sym = str(pos.get("symbol") or "")
side = str(pos.get("side") or "")
if not sym:
return ""
candidates: list[tuple[int, str]] = []
for h in hub_mon.get("hedges") or []:
if isinstance(h, dict) and _hedge_matches_position(h, sym, side):
candidates.append((1, _hedge_source_label(h)))
for r in hub_mon.get("rolls") or []:
if isinstance(r, dict) and _monitor_item_matches_position(r, sym, side):
candidates.append((2, "顺势加仓"))
for t in hub_mon.get("trends") or []:
if isinstance(t, dict) and _monitor_item_matches_position(t, sym, side):
candidates.append((3, "趋势回调"))
for o in hub_mon.get("orders") or []:
if isinstance(o, dict) and _monitor_item_matches_position(o, sym, side):
candidates.append(_order_monitor_source_label(o))
for k in hub_mon.get("keys") or []:
if isinstance(k, dict) and _monitor_item_matches_position(k, sym, side):
candidates.append((5, "关键位"))
if not candidates:
return ""
candidates.sort(key=lambda x: x[0])
return candidates[0][1]
def _options_source_label(p: dict) -> str:
"""看板期权来源:仅对冲标期期/永期;纯期权或对不上监控显示 —."""
source = str(p.get("source") or "").strip()
label = str(p.get("source_label") or "").strip()
if source == "perp_options" or label == "永期对冲":
return "永期对冲"
if source == "options_options" or label == "期期对冲":
return "期期对冲"
hedge = p.get("hedge_plan_target") if isinstance(p.get("hedge_plan_target"), dict) else None
if hedge:
return _hedge_source_label(hedge)
return ""
def _options_target_monitor_text(p: dict) -> str:
raw = p.get("target_monitor_text")
if raw not in (None, ""):
return str(raw)
try:
from lib.instance.instance_dashboard_lib import _format_options_target
return _format_options_target(p)
except Exception:
return ""
def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
"""数据看板分户卡片:监控数量,持仓逐行(含浮盈亏与来源)."""
"""数据看板分户卡片:监控数量 + 持仓表(来源=监控匹配)."""
mon = ac.get("monitor_lines") or {}
hub_mon = ac.get("hub_monitor") if isinstance(ac.get("hub_monitor"), dict) else None
position_lines: list[dict[str, Any]] = []
for p in _filter_open_positions(ac.get("positions") or []):
sym = p.get("symbol") or "?"
@@ -984,10 +1094,11 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
if contracts is None:
contracts = p.get("size")
upnl = _position_float_pnl(p)
source = resolve_position_monitor_source(p, hub_mon)
position_lines.append(
{
"kind": "position",
"source": "永续",
"source": source,
"symbol": sym,
"side": side,
"contracts": contracts,
@@ -998,28 +1109,21 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
opt_snap = ac.get("options_snapshot") if isinstance(ac.get("options_snapshot"), dict) else {}
options_positions: list[dict[str, Any]] = []
if opt_snap.get("ok") is not False and opt_snap.get("enabled") is not False:
from lib.options.options_pricing_lib import format_options_breakeven_line
for p in opt_snap.get("positions") or []:
if not isinstance(p, dict):
continue
options_positions.append(p)
inst = p.get("inst_id") or "?"
opt_type = (p.get("opt_type") or "").upper()
row = dict(p)
row["source_label"] = _options_source_label(p)
row["target_monitor_text"] = _options_target_monitor_text(p)
options_positions.append(row)
inst = row.get("inst_id") or "?"
opt_type = (row.get("opt_type") or "").upper()
label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else opt_type or "OPT"
upl = p.get("upl")
be_line = format_options_breakeven_line(
expiry_be_px=p.get("expiry_be_px"),
close_be_px=p.get("close_be_px"),
idx_px=p.get("idx_px"),
)
text = f"期权 {inst} {label}"
if be_line:
text = f"{text} {be_line}"
upl = row.get("upl")
line: dict[str, Any] = {
"kind": "options",
"source": "期权",
"text": text,
"source": row.get("source_label") or "",
"text": f"期权 {inst} {label}",
}
if upl is not None:
try:
+3 -1
View File
@@ -1,6 +1,7 @@
"""中控数据看板:三户当日总览(无 AI,纯数据聚合)."""
from __future__ import annotations
import os
from datetime import datetime, timezone
from typing import Any, Optional
@@ -14,7 +15,8 @@ from hub_ai.config import trading_day_reset_hour
from lib.hub.hub_trades_lib import current_trading_day
LOSS_ALERT_PCT = 5.0
DASHBOARD_POLL_INTERVAL_SEC = 60
# 与监控区 board 默认 5s 对齐,看板持仓来源跟监控同步.
DASHBOARD_POLL_INTERVAL_SEC = float(os.getenv("DASHBOARD_POLL_INTERVAL_SEC", "5"))
def _safe_float(v: Any) -> Optional[float]:
+45
View File
@@ -373,6 +373,51 @@ body.hub-page-dashboard .page#page-dashboard {
letter-spacing: 0.02em;
}
.dash-pos-source.is-hedge {
color: #fbbf24;
background: rgba(245, 158, 11, 0.16);
border: 1px solid rgba(245, 158, 11, 0.4);
}
.dash-pos-source.is-roll {
color: #6ee7b7;
background: rgba(16, 185, 129, 0.16);
border: 1px solid rgba(16, 185, 129, 0.4);
}
.dash-pos-source.is-trend {
color: #93c5fd;
background: rgba(59, 130, 246, 0.18);
border: 1px solid rgba(59, 130, 246, 0.35);
}
.dash-pos-source.is-order {
color: #c4b5fd;
background: rgba(139, 92, 246, 0.18);
border: 1px solid rgba(139, 92, 246, 0.35);
}
.dash-pos-source.is-key {
color: #fdba74;
background: rgba(249, 115, 22, 0.16);
border: 1px solid rgba(249, 115, 22, 0.4);
}
.dash-pos-source.is-none {
color: var(--dash-muted);
background: rgba(148, 163, 184, 0.12);
border: 1px solid rgba(148, 163, 184, 0.28);
}
.dash-target-monitor {
color: var(--dash-muted);
}
.dash-target-monitor.is-on {
color: #4ade80;
font-weight: 600;
}
.dash-pos-source.is-perp {
color: #93c5fd;
background: rgba(59, 130, 246, 0.18);
+19 -7
View File
@@ -144,19 +144,29 @@
return perp.length > 0 || (ac && ac.options_layout && optionsPositions.length > 0);
}
function sourceBadgeClass(source) {
const s = String(source || "");
if (s.indexOf("对冲") >= 0) return "is-hedge";
if (s.indexOf("顺势") >= 0) return "is-roll";
if (s.indexOf("趋势") >= 0) return "is-trend";
if (s.indexOf("关键位") >= 0) return "is-key";
if (s.indexOf("下单") >= 0) return "is-order";
return "is-none";
}
function renderDashboardPerpTable(lines) {
const rows = Array.isArray(lines) ? lines : [];
if (!rows.length) return "";
const body = rows
.map((ln) => {
const source = esc((ln && ln.source) || "永续");
const source = String((ln && ln.source) || "");
const symbol = esc((ln && (ln.symbol || ln.text)) || "—");
const side = esc((ln && ln.side) || "—");
const contracts =
ln && ln.contracts != null && ln.contracts !== "" ? esc(String(ln.contracts)) : "—";
const pnl = ln && ln.pnl != null ? Number(ln.pnl) : NaN;
return `<tr>
<td><span class="dash-pos-source is-perp">${source}</span></td>
<td><span class="dash-pos-source ${sourceBadgeClass(source)}">${esc(source)}</span></td>
<td>${symbol}</td>
<td>${side}</td>
<td>${contracts}</td>
@@ -188,14 +198,16 @@
: (p.opt_type || "").toUpperCase() === "P"
? "Put"
: p.opt_type || "—";
const source = String(p.source_label || p.source || "—");
const target = String(p.target_monitor_text || "—");
const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor";
return `<tr>
<td><span class="dash-pos-source is-opt">期权</span></td>
<td><span class="dash-pos-source ${sourceBadgeClass(source)}">${esc(source)}</span></td>
<td title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
<td>${esc(optType)}</td>
<td>${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
<td>${p.expiry_be_px != null ? fmt(p.expiry_be_px, 0) : "—"}</td>
<td>${p.close_be_px != null ? fmt(p.close_be_px, 0) : "—"}</td>
<td><span class="${targetCls}">${esc(target)}</span></td>
<td class="${pnlClass(p.upl)}">${p.upl != null ? pnlSigned(p.upl, 2) : "—"}</td>
</tr>`;
})
@@ -205,7 +217,7 @@
<div class="dash-table-wrap dash-options-table-wrap">
<table class="dash-table dash-options-table">
<thead><tr>
<th>来源</th><th></th><th></th><th></th><th></th><th></th><th></th><th></th>
<th>来源</th><th></th><th></th><th></th><th></th><th></th><th></th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
@@ -360,7 +372,7 @@
const ver = Number(data.dashboard_version) || 0;
if (ver) localDashVersion = ver;
renderPayload(data);
const sec = Number(data.poll_interval_sec) || 60;
const sec = Number(data.poll_interval_sec) || 5;
setStatus(options.silent ? `SSE 已连接 · 后台每 ${sec}s 聚合` : `已更新 · 后台每 ${sec}s 聚合`);
} catch (e) {
setStatus(String(e.message || e), true);
+2 -2
View File
@@ -19,7 +19,7 @@
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
<script src="/assets/account_risk_badge.js?v=4"></script>
<link rel="stylesheet" href="/assets/dashboard.css?v=20260717-dash-pos-only" />
<link rel="stylesheet" href="/assets/dashboard.css?v=20260717-dash-source" />
</head>
<body>
<div class="app-bg" aria-hidden="true"></div>
@@ -1373,7 +1373,7 @@
<script src="/assets/archive.js?v=20260717-archive-cal-chart"></script>
<script src="/assets/quotes.js?v=20260717-quotes-feed"></script>
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
<script src="/assets/dashboard.js?v=20260717-dash-pos-only"></script>
<script src="/assets/dashboard.js?v=20260717-dash-source"></script>
<script src="/assets/strategy.js?v=3"></script>
<script src="/assets/help.js?v=1"></script>
<script src="/assets/logs.js?v=1"></script>
+63
View File
@@ -0,0 +1,63 @@
"""数据看板仓位来源:监控匹配优先级."""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "manual_trading_hub"))
sys.path.insert(0, str(ROOT))
from hub_ai.context import ( # noqa: E402
_options_source_label,
resolve_position_monitor_source,
)
class TestDashboardPositionSource(unittest.TestCase):
def test_priority_hedge_over_roll(self):
hub = {
"ok": True,
"hedges": [
{
"plan_type": "perp_options",
"direction": "long",
"legs": [{"leg_role": "perp", "symbol": "ETH/USDT:USDT", "status": "open"}],
}
],
"rolls": [{"symbol": "ETH/USDT:USDT", "direction": "long"}],
}
self.assertEqual(
resolve_position_monitor_source({"symbol": "ETH/USDT:USDT", "side": "long"}, hub),
"永期对冲",
)
def test_unmatched_is_dash(self):
hub = {"ok": True, "orders": [], "trends": [], "rolls": [], "keys": [], "hedges": []}
self.assertEqual(
resolve_position_monitor_source({"symbol": "BTC/USDT:USDT", "side": "short"}, hub),
"",
)
def test_roll_beats_order(self):
hub = {
"ok": True,
"rolls": [{"symbol": "BTC/USDT:USDT", "direction": "short"}],
"orders": [{"symbol": "BTC/USDT:USDT", "direction": "short", "monitor_type": "下单监控"}],
}
self.assertEqual(
resolve_position_monitor_source({"symbol": "BTC/USDT:USDT", "side": "short"}, hub),
"顺势加仓",
)
def test_options_plain_is_dash(self):
self.assertEqual(_options_source_label({"source": "option", "source_label": "纯期权"}), "")
self.assertEqual(
_options_source_label({"source": "options_options", "source_label": "期期对冲"}),
"期期对冲",
)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -32,6 +32,7 @@ class TestHubMonitorPayload(unittest.TestCase):
self.assertEqual(out["keys"], keys)
self.assertEqual(out["orders"], orders)
self.assertEqual(out["rolls"], rolls)
self.assertEqual(out["hedges"], [])
self.assertEqual(out["trends"][0]["add_count"], 2)