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:
@@ -240,6 +240,7 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s
|
|||||||
"underlying": row.get("underlying"),
|
"underlying": row.get("underlying"),
|
||||||
"opt_type": opt_type,
|
"opt_type": opt_type,
|
||||||
"target_index": target_f,
|
"target_index": target_f,
|
||||||
|
"plan_type": "options_options",
|
||||||
"managed_by": "hedge_plan",
|
"managed_by": "hedge_plan",
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ def build_hub_monitor_payload(
|
|||||||
orders,
|
orders,
|
||||||
trends,
|
trends,
|
||||||
rolls,
|
rolls,
|
||||||
|
hedges=None,
|
||||||
enrich=None,
|
enrich=None,
|
||||||
risk_status=None,
|
risk_status=None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -185,6 +186,7 @@ def build_hub_monitor_payload(
|
|||||||
"orders": orders,
|
"orders": orders,
|
||||||
"trends": trends,
|
"trends": trends,
|
||||||
"rolls": rolls,
|
"rolls": rolls,
|
||||||
|
"hedges": hedges if isinstance(hedges, list) else [],
|
||||||
"key_prices": [],
|
"key_prices": [],
|
||||||
}
|
}
|
||||||
if isinstance(risk_status, dict):
|
if isinstance(risk_status, dict):
|
||||||
@@ -193,6 +195,9 @@ def build_hub_monitor_payload(
|
|||||||
extra = enrich(keys=keys, orders=orders, trends=trends, rolls=rolls)
|
extra = enrich(keys=keys, orders=orders, trends=trends, rolls=rolls)
|
||||||
if isinstance(extra, dict):
|
if isinstance(extra, dict):
|
||||||
payload.update(extra)
|
payload.update(extra)
|
||||||
|
# enrich 可能不返回 hedges,保留本地组装的对冲列表.
|
||||||
|
if "hedges" not in extra:
|
||||||
|
payload["hedges"] = hedges if isinstance(hedges, list) else []
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
@@ -572,6 +577,17 @@ def register_hub_routes(app):
|
|||||||
rolls.append(_row_to_dict(row))
|
rolls.append(_row_to_dict(row))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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_status = None
|
||||||
risk_fn = c.get("risk_status_fn")
|
risk_fn = c.get("risk_status_fn")
|
||||||
if callable(risk_fn):
|
if callable(risk_fn):
|
||||||
@@ -589,6 +605,7 @@ def register_hub_routes(app):
|
|||||||
orders=orders,
|
orders=orders,
|
||||||
trends=trends,
|
trends=trends,
|
||||||
rolls=rolls,
|
rolls=rolls,
|
||||||
|
hedges=hedges,
|
||||||
enrich=enrich,
|
enrich=enrich,
|
||||||
risk_status=risk_status,
|
risk_status=risk_status,
|
||||||
)
|
)
|
||||||
@@ -601,6 +618,7 @@ def register_hub_routes(app):
|
|||||||
orders=orders,
|
orders=orders,
|
||||||
trends=trends,
|
trends=trends,
|
||||||
rolls=rolls,
|
rolls=rolls,
|
||||||
|
hedges=hedges,
|
||||||
risk_status=risk_status,
|
risk_status=risk_status,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,6 +53,20 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
|||||||
if not mon:
|
if not mon:
|
||||||
# 中控卡片共用 target_index 只读展示;实际平仓仍由对冲计划监控处理。
|
# 中控卡片共用 target_index 只读展示;实际平仓仍由对冲计划监控处理。
|
||||||
p["target_index"] = hedge_target.get("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:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -330,6 +330,8 @@ async def _run_board_aggregate() -> dict:
|
|||||||
await asyncio.to_thread(record_fund_snapshot_from_board, body.get("rows") or [])
|
await asyncio.to_thread(record_fund_snapshot_from_board, body.get("rows") or [])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# 监控聚合完成即唤醒数据看板,持仓来源与监控 5s 同步.
|
||||||
|
dashboard_store.request_refresh()
|
||||||
return {"ok": True, **body}
|
return {"ok": True, **body}
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -973,9 +973,119 @@ def format_account_remark(ac: dict) -> str:
|
|||||||
return ";".join(parts)
|
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]:
|
def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
|
||||||
"""数据看板分户卡片:监控仅数量,持仓逐行(含浮盈亏与来源)."""
|
"""数据看板分户卡片:监控数量 + 持仓表(来源=监控匹配)."""
|
||||||
mon = ac.get("monitor_lines") or {}
|
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]] = []
|
position_lines: list[dict[str, Any]] = []
|
||||||
for p in _filter_open_positions(ac.get("positions") or []):
|
for p in _filter_open_positions(ac.get("positions") or []):
|
||||||
sym = p.get("symbol") or "?"
|
sym = p.get("symbol") or "?"
|
||||||
@@ -984,10 +1094,11 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
|
|||||||
if contracts is None:
|
if contracts is None:
|
||||||
contracts = p.get("size")
|
contracts = p.get("size")
|
||||||
upnl = _position_float_pnl(p)
|
upnl = _position_float_pnl(p)
|
||||||
|
source = resolve_position_monitor_source(p, hub_mon)
|
||||||
position_lines.append(
|
position_lines.append(
|
||||||
{
|
{
|
||||||
"kind": "position",
|
"kind": "position",
|
||||||
"source": "永续",
|
"source": source,
|
||||||
"symbol": sym,
|
"symbol": sym,
|
||||||
"side": side,
|
"side": side,
|
||||||
"contracts": contracts,
|
"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 {}
|
opt_snap = ac.get("options_snapshot") if isinstance(ac.get("options_snapshot"), dict) else {}
|
||||||
options_positions: list[dict[str, Any]] = []
|
options_positions: list[dict[str, Any]] = []
|
||||||
if opt_snap.get("ok") is not False and opt_snap.get("enabled") is not False:
|
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 []:
|
for p in opt_snap.get("positions") or []:
|
||||||
if not isinstance(p, dict):
|
if not isinstance(p, dict):
|
||||||
continue
|
continue
|
||||||
options_positions.append(p)
|
row = dict(p)
|
||||||
inst = p.get("inst_id") or "?"
|
row["source_label"] = _options_source_label(p)
|
||||||
opt_type = (p.get("opt_type") or "").upper()
|
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"
|
label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else opt_type or "OPT"
|
||||||
upl = p.get("upl")
|
upl = row.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}"
|
|
||||||
line: dict[str, Any] = {
|
line: dict[str, Any] = {
|
||||||
"kind": "options",
|
"kind": "options",
|
||||||
"source": "期权",
|
"source": row.get("source_label") or "—",
|
||||||
"text": text,
|
"text": f"期权 {inst} {label}",
|
||||||
}
|
}
|
||||||
if upl is not None:
|
if upl is not None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""中控数据看板:三户当日总览(无 AI,纯数据聚合)."""
|
"""中控数据看板:三户当日总览(无 AI,纯数据聚合)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Optional
|
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
|
from lib.hub.hub_trades_lib import current_trading_day
|
||||||
|
|
||||||
LOSS_ALERT_PCT = 5.0
|
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]:
|
def _safe_float(v: Any) -> Optional[float]:
|
||||||
|
|||||||
@@ -373,6 +373,51 @@ body.hub-page-dashboard .page#page-dashboard {
|
|||||||
letter-spacing: 0.02em;
|
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 {
|
.dash-pos-source.is-perp {
|
||||||
color: #93c5fd;
|
color: #93c5fd;
|
||||||
background: rgba(59, 130, 246, 0.18);
|
background: rgba(59, 130, 246, 0.18);
|
||||||
|
|||||||
@@ -144,19 +144,29 @@
|
|||||||
return perp.length > 0 || (ac && ac.options_layout && optionsPositions.length > 0);
|
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) {
|
function renderDashboardPerpTable(lines) {
|
||||||
const rows = Array.isArray(lines) ? lines : [];
|
const rows = Array.isArray(lines) ? lines : [];
|
||||||
if (!rows.length) return "";
|
if (!rows.length) return "";
|
||||||
const body = rows
|
const body = rows
|
||||||
.map((ln) => {
|
.map((ln) => {
|
||||||
const source = esc((ln && ln.source) || "永续");
|
const source = String((ln && ln.source) || "—");
|
||||||
const symbol = esc((ln && (ln.symbol || ln.text)) || "—");
|
const symbol = esc((ln && (ln.symbol || ln.text)) || "—");
|
||||||
const side = esc((ln && ln.side) || "—");
|
const side = esc((ln && ln.side) || "—");
|
||||||
const contracts =
|
const contracts =
|
||||||
ln && ln.contracts != null && ln.contracts !== "" ? esc(String(ln.contracts)) : "—";
|
ln && ln.contracts != null && ln.contracts !== "" ? esc(String(ln.contracts)) : "—";
|
||||||
const pnl = ln && ln.pnl != null ? Number(ln.pnl) : NaN;
|
const pnl = ln && ln.pnl != null ? Number(ln.pnl) : NaN;
|
||||||
return `<tr>
|
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>${symbol}</td>
|
||||||
<td>${side}</td>
|
<td>${side}</td>
|
||||||
<td>${contracts}</td>
|
<td>${contracts}</td>
|
||||||
@@ -188,14 +198,16 @@
|
|||||||
: (p.opt_type || "").toUpperCase() === "P"
|
: (p.opt_type || "").toUpperCase() === "P"
|
||||||
? "Put"
|
? "Put"
|
||||||
: p.opt_type || "—";
|
: 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>
|
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 title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
|
||||||
<td>${esc(optType)}</td>
|
<td>${esc(optType)}</td>
|
||||||
<td>${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</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.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
||||||
<td>${p.expiry_be_px != null ? fmt(p.expiry_be_px, 0) : "—"}</td>
|
<td><span class="${targetCls}">${esc(target)}</span></td>
|
||||||
<td>${p.close_be_px != null ? fmt(p.close_be_px, 0) : "—"}</td>
|
|
||||||
<td class="${pnlClass(p.upl)}">${p.upl != null ? pnlSigned(p.upl, 2) : "—"}</td>
|
<td class="${pnlClass(p.upl)}">${p.upl != null ? pnlSigned(p.upl, 2) : "—"}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
})
|
})
|
||||||
@@ -205,7 +217,7 @@
|
|||||||
<div class="dash-table-wrap dash-options-table-wrap">
|
<div class="dash-table-wrap dash-options-table-wrap">
|
||||||
<table class="dash-table dash-options-table">
|
<table class="dash-table dash-options-table">
|
||||||
<thead><tr>
|
<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>
|
</tr></thead>
|
||||||
<tbody>${rows}</tbody>
|
<tbody>${rows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -360,7 +372,7 @@
|
|||||||
const ver = Number(data.dashboard_version) || 0;
|
const ver = Number(data.dashboard_version) || 0;
|
||||||
if (ver) localDashVersion = ver;
|
if (ver) localDashVersion = ver;
|
||||||
renderPayload(data);
|
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 聚合`);
|
setStatus(options.silent ? `SSE 已连接 · 后台每 ${sec}s 聚合` : `已更新 · 后台每 ${sec}s 聚合`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus(String(e.message || e), true);
|
setStatus(String(e.message || e), true);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
||||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||||
<script src="/assets/account_risk_badge.js?v=4"></script>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<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/archive.js?v=20260717-archive-cal-chart"></script>
|
||||||
<script src="/assets/quotes.js?v=20260717-quotes-feed"></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/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/strategy.js?v=3"></script>
|
||||||
<script src="/assets/help.js?v=1"></script>
|
<script src="/assets/help.js?v=1"></script>
|
||||||
<script src="/assets/logs.js?v=1"></script>
|
<script src="/assets/logs.js?v=1"></script>
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -32,6 +32,7 @@ class TestHubMonitorPayload(unittest.TestCase):
|
|||||||
self.assertEqual(out["keys"], keys)
|
self.assertEqual(out["keys"], keys)
|
||||||
self.assertEqual(out["orders"], orders)
|
self.assertEqual(out["orders"], orders)
|
||||||
self.assertEqual(out["rolls"], rolls)
|
self.assertEqual(out["rolls"], rolls)
|
||||||
|
self.assertEqual(out["hedges"], [])
|
||||||
self.assertEqual(out["trends"][0]["add_count"], 2)
|
self.assertEqual(out["trends"][0]["add_count"], 2)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user