Add instance data dashboard (default off).
Read-only overview of live orders, key monitors, and strategy; show options/hedge only when present. Toggle via system settings nav prefs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
"""实例数据看板:本户活跃监控 / 持仓只读聚合."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
|
||||
def _row_dict(row: Any) -> dict[str, Any]:
|
||||
if row is None:
|
||||
return {}
|
||||
if isinstance(row, dict):
|
||||
return dict(row)
|
||||
try:
|
||||
return dict(row)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _safe_float(v: Any) -> Optional[float]:
|
||||
try:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _dir_label(direction: Any) -> str:
|
||||
d = str(direction or "").strip().lower()
|
||||
if d == "short":
|
||||
return "做空"
|
||||
if d == "long":
|
||||
return "做多"
|
||||
return str(direction or "-")
|
||||
|
||||
|
||||
def _format_order_item(od: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
from lib.strategy.strategy_trade_labels import apply_order_monitor_source_labels
|
||||
|
||||
od = apply_order_monitor_source_labels(od)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from lib.trade.entry_model_lib import enrich_entry_model_display
|
||||
|
||||
enrich_entry_model_display(od)
|
||||
except Exception:
|
||||
pass
|
||||
sym = od.get("exchange_symbol") or od.get("symbol") or "-"
|
||||
direction = str(od.get("direction") or "long").lower()
|
||||
mt = od.get("monitor_type_display") or od.get("monitor_type") or ""
|
||||
kst = od.get("key_signal_type") or ""
|
||||
title = f"{sym} {_dir_label(direction)}"
|
||||
bits = [x for x in (mt, kst) if x]
|
||||
subtitle = " · ".join(bits) if bits else ""
|
||||
entry = _safe_float(od.get("trigger_price"))
|
||||
sl = _safe_float(od.get("stop_loss"))
|
||||
tp = _safe_float(od.get("take_profit"))
|
||||
return {
|
||||
"id": od.get("id"),
|
||||
"kind": "order",
|
||||
"tab": "trade",
|
||||
"title": title,
|
||||
"subtitle": subtitle,
|
||||
"symbol": sym,
|
||||
"direction": direction,
|
||||
"direction_label": _dir_label(direction),
|
||||
"entry": entry,
|
||||
"stop_loss": sl,
|
||||
"take_profit": tp,
|
||||
"status": od.get("status") or "active",
|
||||
}
|
||||
|
||||
|
||||
def _format_key_item(kd: dict[str, Any]) -> dict[str, Any]:
|
||||
sym = kd.get("exchange_symbol") or kd.get("symbol") or "-"
|
||||
direction = str(kd.get("direction") or "long").lower()
|
||||
signal = kd.get("signal_type") or kd.get("key_signal_type") or kd.get("monitor_type") or ""
|
||||
upper = _safe_float(kd.get("upper"))
|
||||
lower = _safe_float(kd.get("lower"))
|
||||
subtitle_parts = []
|
||||
if signal:
|
||||
subtitle_parts.append(str(signal))
|
||||
if upper is not None or lower is not None:
|
||||
subtitle_parts.append(
|
||||
f"上{upper if upper is not None else '-'} / 下{lower if lower is not None else '-'}"
|
||||
)
|
||||
return {
|
||||
"id": kd.get("id"),
|
||||
"kind": "key",
|
||||
"tab": "key_monitor",
|
||||
"title": f"{sym} {_dir_label(direction)}",
|
||||
"subtitle": " · ".join(subtitle_parts),
|
||||
"symbol": sym,
|
||||
"direction": direction,
|
||||
"direction_label": _dir_label(direction),
|
||||
"upper": upper,
|
||||
"lower": lower,
|
||||
"status": kd.get("status") or "active",
|
||||
}
|
||||
|
||||
|
||||
def _format_trend_item(td: dict[str, Any]) -> dict[str, Any]:
|
||||
sym = td.get("exchange_symbol") or td.get("symbol") or "-"
|
||||
direction = str(td.get("direction") or "long").lower()
|
||||
status = td.get("status") or "active"
|
||||
entry = _safe_float(td.get("entry_price") or td.get("trigger_price"))
|
||||
return {
|
||||
"id": td.get("id"),
|
||||
"kind": "trend",
|
||||
"tab": "strategy",
|
||||
"title": f"趋势回调 {sym} {_dir_label(direction)}",
|
||||
"subtitle": f"状态 {status}",
|
||||
"symbol": sym,
|
||||
"direction": direction,
|
||||
"direction_label": _dir_label(direction),
|
||||
"entry": entry,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def _format_roll_item(rd: dict[str, Any]) -> dict[str, Any]:
|
||||
sym = rd.get("exchange_symbol") or rd.get("symbol") or "-"
|
||||
direction = str(rd.get("direction") or "long").lower()
|
||||
status = rd.get("status") or "active"
|
||||
return {
|
||||
"id": rd.get("id"),
|
||||
"kind": "roll",
|
||||
"tab": "strategy",
|
||||
"title": f"顺势加仓 {sym} {_dir_label(direction)}",
|
||||
"subtitle": f"状态 {status}",
|
||||
"symbol": sym,
|
||||
"direction": direction,
|
||||
"direction_label": _dir_label(direction),
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def _format_options_item(p: dict[str, Any]) -> dict[str, Any]:
|
||||
inst = p.get("inst_id") or p.get("instId") or "-"
|
||||
opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
|
||||
label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else (opt_type or "OPT")
|
||||
upl = _safe_float(p.get("upl"))
|
||||
net = None
|
||||
try:
|
||||
from lib.options.options_positions_lib import net_pnl_from_display_row
|
||||
|
||||
net = net_pnl_from_display_row(p)
|
||||
except Exception:
|
||||
net = None
|
||||
pnl = net if net is not None else upl
|
||||
pos = _safe_float(p.get("pos"))
|
||||
return {
|
||||
"id": inst,
|
||||
"kind": "options",
|
||||
"tab": "options",
|
||||
"title": f"{inst} {label}",
|
||||
"subtitle": f"张数 {pos if pos is not None else '-'}",
|
||||
"inst_id": inst,
|
||||
"opt_type": opt_type,
|
||||
"pnl": round(pnl, 4) if pnl is not None else None,
|
||||
"pos": pos,
|
||||
}
|
||||
|
||||
|
||||
def _format_hedge_item(plan: dict[str, Any]) -> dict[str, Any]:
|
||||
pid = plan.get("id")
|
||||
underlying = plan.get("underlying") or "-"
|
||||
plan_type = plan.get("plan_type") or ""
|
||||
status = plan.get("status") or ""
|
||||
summary = plan.get("contracts_summary") or ""
|
||||
return {
|
||||
"id": pid,
|
||||
"kind": "hedge_plan",
|
||||
"tab": "hedge_plan",
|
||||
"title": f"对冲 #{pid} {underlying}",
|
||||
"subtitle": " · ".join(x for x in (plan_type, status, summary) if x),
|
||||
"underlying": underlying,
|
||||
"plan_type": plan_type,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def _table_exists(conn, name: str) -> bool:
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
|
||||
(name,),
|
||||
).fetchone()
|
||||
return bool(row)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def collect_orders(conn) -> list[dict[str, Any]]:
|
||||
if not _table_exists(conn, "order_monitors"):
|
||||
return []
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
|
||||
).fetchall()
|
||||
return [_format_order_item(_row_dict(r)) for r in rows]
|
||||
|
||||
|
||||
def collect_keys(conn) -> list[dict[str, Any]]:
|
||||
if not _table_exists(conn, "key_monitors"):
|
||||
return []
|
||||
rows = conn.execute("SELECT * FROM key_monitors ORDER BY id DESC").fetchall()
|
||||
return [_format_key_item(_row_dict(r)) for r in rows]
|
||||
|
||||
|
||||
def collect_trends(conn) -> list[dict[str, Any]]:
|
||||
if not _table_exists(conn, "trend_pullback_plans"):
|
||||
return []
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC"
|
||||
).fetchall()
|
||||
except Exception:
|
||||
return []
|
||||
return [_format_trend_item(_row_dict(r)) for r in rows]
|
||||
|
||||
|
||||
def collect_rolls(conn) -> list[dict[str, Any]]:
|
||||
if not _table_exists(conn, "roll_groups") or not _table_exists(conn, "order_monitors"):
|
||||
return []
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"""SELECT g.* FROM roll_groups g
|
||||
INNER JOIN order_monitors m ON m.id = g.order_monitor_id AND m.status='active'
|
||||
WHERE g.status='active' ORDER BY g.id DESC"""
|
||||
).fetchall()
|
||||
except Exception:
|
||||
return []
|
||||
return [_format_roll_item(_row_dict(r)) for r in rows]
|
||||
|
||||
|
||||
def collect_hedge_plans(conn) -> list[dict[str, Any]]:
|
||||
if not _table_exists(conn, "hedge_plans"):
|
||||
return []
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
for status in ("opening", "active", "partial"):
|
||||
rows.extend(list_plans(conn, status=status, limit=80))
|
||||
rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
|
||||
plans = attach_legs_to_plans(conn, rows)
|
||||
return [_format_hedge_item(p) for p in plans]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def collect_options_items(
|
||||
fetch_options_positions: Optional[Callable[[], list[dict[str, Any]]]] = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
if not callable(fetch_options_positions):
|
||||
return []
|
||||
try:
|
||||
raw = fetch_options_positions() or []
|
||||
except Exception:
|
||||
return []
|
||||
out: list[dict[str, Any]] = []
|
||||
for p in raw:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
out.append(_format_options_item(p))
|
||||
return out
|
||||
|
||||
|
||||
def build_instance_dashboard_payload(
|
||||
conn,
|
||||
*,
|
||||
fetch_options_positions: Optional[Callable[[], list[dict[str, Any]]]] = None,
|
||||
hedge_enabled: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
orders = collect_orders(conn)
|
||||
keys = collect_keys(conn)
|
||||
trends = collect_trends(conn)
|
||||
rolls = collect_rolls(conn)
|
||||
strategy_items = trends + rolls
|
||||
options_items = collect_options_items(fetch_options_positions)
|
||||
hedge_items = collect_hedge_plans(conn) if hedge_enabled else []
|
||||
now = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
||||
return {
|
||||
"ok": True,
|
||||
"updated_at": now,
|
||||
"orders": {"title": "实盘下单", "count": len(orders), "items": orders, "tab": "trade"},
|
||||
"keys": {"title": "关键位监控", "count": len(keys), "items": keys, "tab": "key_monitor"},
|
||||
"strategy": {
|
||||
"title": "策略交易",
|
||||
"count": len(strategy_items),
|
||||
"items": strategy_items,
|
||||
"trends": trends,
|
||||
"rolls": rolls,
|
||||
"tab": "strategy",
|
||||
},
|
||||
"options": {
|
||||
"title": "期权持仓",
|
||||
"count": len(options_items),
|
||||
"items": options_items,
|
||||
"visible": len(options_items) > 0,
|
||||
"tab": "options",
|
||||
},
|
||||
"hedge_plan": {
|
||||
"title": "对冲计划",
|
||||
"count": len(hedge_items),
|
||||
"items": hedge_items,
|
||||
"visible": len(hedge_items) > 0,
|
||||
"tab": "hedge_plan",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"""注册 GET /api/instance/dashboard(三所共用)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from flask import Flask, jsonify
|
||||
|
||||
|
||||
def register_instance_dashboard_routes(
|
||||
app: Flask,
|
||||
*,
|
||||
login_required: Callable,
|
||||
get_db: Callable,
|
||||
fetch_options_positions: Optional[Callable[[], list[dict[str, Any]]]] = None,
|
||||
hedge_enabled: bool = False,
|
||||
) -> None:
|
||||
from lib.instance.instance_dashboard_lib import build_instance_dashboard_payload
|
||||
|
||||
@app.route("/api/instance/dashboard")
|
||||
@login_required
|
||||
def api_instance_dashboard():
|
||||
conn = get_db()
|
||||
try:
|
||||
payload = build_instance_dashboard_payload(
|
||||
conn,
|
||||
fetch_options_positions=fetch_options_positions,
|
||||
hedge_enabled=bool(hedge_enabled),
|
||||
)
|
||||
return jsonify(payload)
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -8,6 +8,7 @@ from lib.instance.runtime_settings_lib import runtime_get_prefix, runtime_set_ma
|
||||
DISPLAY_RUNTIME_PREFIX = "display."
|
||||
|
||||
DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
||||
"show_nav_dashboard": False,
|
||||
"show_nav_strategy": True,
|
||||
"show_nav_strategy_records": True,
|
||||
"show_nav_records": True,
|
||||
@@ -25,6 +26,7 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
||||
}
|
||||
|
||||
DISPLAY_LABELS: dict[str, str] = {
|
||||
"show_nav_dashboard": "数据看板",
|
||||
"show_nav_strategy": "策略交易",
|
||||
"show_nav_strategy_records": "策略交易记录",
|
||||
"show_nav_records": "交易记录与复盘",
|
||||
@@ -42,6 +44,7 @@ DISPLAY_LABELS: dict[str, str] = {
|
||||
}
|
||||
|
||||
NAV_TAB_ALLOWED: dict[str, str] = {
|
||||
"dashboard": "show_nav_dashboard",
|
||||
"strategy": "show_nav_strategy",
|
||||
"strategy_records": "show_nav_strategy_records",
|
||||
"records": "show_nav_records",
|
||||
@@ -103,6 +106,7 @@ def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool:
|
||||
|
||||
def display_meta_for_ui() -> list[dict[str, Any]]:
|
||||
nav_keys = [
|
||||
"show_nav_dashboard",
|
||||
"show_nav_strategy",
|
||||
"show_nav_strategy_records",
|
||||
"show_nav_records",
|
||||
|
||||
@@ -11,6 +11,7 @@ from flask import Flask, Response, jsonify, redirect, request, session
|
||||
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||
|
||||
EMBED_TABS: tuple[str, ...] = (
|
||||
"dashboard",
|
||||
"key_monitor",
|
||||
"trade",
|
||||
"strategy",
|
||||
@@ -28,6 +29,7 @@ EMBED_TABS: tuple[str, ...] = (
|
||||
PATH_TO_EMBED_TAB: dict[str, str] = {
|
||||
"/": "trade",
|
||||
"/trade": "trade",
|
||||
"/dashboard": "dashboard",
|
||||
"/key_monitor": "key_monitor",
|
||||
"/strategy": "strategy",
|
||||
"/strategy/trend": "strategy",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{# 实例数据看板:只读活跃监控总览 #}
|
||||
<div class="card full inst-dash-card" id="instance-dashboard" data-inst-dashboard="1">
|
||||
<div class="inst-dash-head">
|
||||
<div>
|
||||
<h2 style="margin-bottom:4px">数据看板</h2>
|
||||
<p class="muted inst-dash-desc">本户活跃监控总览 · 只读 · 期权/对冲有数据才显示</p>
|
||||
</div>
|
||||
<div class="inst-dash-head-actions">
|
||||
<span class="muted inst-dash-updated" id="inst-dash-updated">—</span>
|
||||
<button type="button" class="btn-sm" id="inst-dash-refresh">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="inst-dash-status muted" id="inst-dash-status"></p>
|
||||
<div class="inst-dash-sections" id="inst-dash-sections"></div>
|
||||
</div>
|
||||
@@ -81,7 +81,9 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<div class="grid">
|
||||
{% if page == 'key_monitor' %}
|
||||
{% if page == 'dashboard' %}
|
||||
{% include 'dashboard_panel.html' %}
|
||||
{% elif page == 'key_monitor' %}
|
||||
{% include 'key_monitor_panel.html' %}
|
||||
{% elif page == 'trade' %}
|
||||
<div class="dual-panel-grid" style="grid-column:1/-1">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<script src="/static/instance_theme.js?v=50"></script>
|
||||
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
||||
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=6">
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=7">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=95">
|
||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
@@ -29,6 +29,7 @@
|
||||
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
||||
</div>
|
||||
<nav class="top-nav embed-top-nav" aria-label="实例导航">
|
||||
<a href="/dashboard" data-embed-tab="dashboard" class="{% if initial_tab == 'dashboard' %}active{% endif %}"{% if not display.show_nav_dashboard %} style="display:none"{% endif %}>数据看板</a>
|
||||
<a href="/key_monitor" data-embed-tab="key_monitor" class="{% if initial_tab == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" data-embed-tab="trade" class="{% if initial_tab == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||
@@ -109,11 +110,12 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
||||
<script src="/static/instance_stats.js?v=4"></script>
|
||||
{% include 'embed_boot_scripts.html' %}
|
||||
<script src="/static/records_review_page.js?v=2"></script>
|
||||
<script src="/static/instance_dashboard.js?v=1"></script>
|
||||
<script>
|
||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=13"></script>
|
||||
<script src="/static/instance_live.js?v=5"></script>
|
||||
<script src="/static/instance_embed.js?v=24"></script>
|
||||
<script src="/static/instance_settings_prefs.js?v=14"></script>
|
||||
<script src="/static/instance_live.js?v=6"></script>
|
||||
<script src="/static/instance_embed.js?v=25"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
||||
<title>{{ pwa_app_name }}</title>
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=3">
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=7">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=95">
|
||||
|
||||
</head>
|
||||
@@ -116,6 +116,7 @@
|
||||
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
||||
</div>
|
||||
<div class="top-nav">
|
||||
<a href="/dashboard" data-embed-tab="dashboard" class="{% if page == 'dashboard' %}active{% endif %}"{% if not display.show_nav_dashboard %} style="display:none"{% endif %}>数据看板</a>
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||
@@ -155,7 +156,9 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="grid">
|
||||
{% if page == 'key_monitor' %}
|
||||
{% if page == 'dashboard' %}
|
||||
{% include 'dashboard_panel.html' %}
|
||||
{% elif page == 'key_monitor' %}
|
||||
{% include 'key_monitor_panel.html' %}
|
||||
{% elif page == 'trade' %}
|
||||
<div class="dual-panel-grid" style="grid-column:1/-1">
|
||||
@@ -1989,9 +1992,15 @@ tickOrderHoldDurations();
|
||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||
</script>
|
||||
<script src="/static/records_review_page.js?v=2"></script>
|
||||
<script src="/static/instance_dashboard.js?v=1"></script>
|
||||
<script>
|
||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||
{% if page == 'dashboard' %}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
if (window.InstanceDashboard) InstanceDashboard.init(true);
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=13"></script>
|
||||
<script src="/static/instance_settings_prefs.js?v=14"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user