diff --git a/dashboard_lib.py b/dashboard_lib.py index 2c98ae3..4b09254 100644 --- a/dashboard_lib.py +++ b/dashboard_lib.py @@ -16,6 +16,83 @@ def _direction_label(direction: str) -> str: return "做多" if (direction or "").strip().lower() == "long" else "做空" +def _symbol_fields(ths_code: str) -> dict[str, Any]: + from symbols import position_symbol_meta + + sym = (ths_code or "").strip() + meta = position_symbol_meta(sym) + return { + "symbol_code": sym, + "symbol_name": meta.get("name") or sym, + "symbol_exchange": meta.get("exchange") or "", + "symbol_is_main": bool(meta.get("is_main")), + } + + +def build_risk_overview( + conn, + get_setting: Callable[[str, str], str], + *, + equity: Optional[float] = None, + margin_used: Optional[float] = None, +) -> dict[str, Any]: + from risk.account_risk_lib import ( + ensure_account_risk_schema, + get_risk_status, + manual_close_daily_limit, + max_active_positions, + risk_control_enabled, + cooling_hours_manual, + cooling_hours_manual_journal, + trading_day_label, + trading_day_reset_hour, + ) + from trading_context import ( + get_fixed_amount, + get_fixed_lots, + get_max_margin_pct, + get_roll_max_margin_pct, + get_risk_percent, + get_sizing_mode, + ) + + ensure_account_risk_schema(conn) + risk = dict(get_risk_status(conn) or {}) + row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone() + td = trading_day_label() + stored_td = str(row["trading_day"] or "") if row else "" + manual_count = int(row["manual_close_count"] or 0) if row and stored_td == td else 0 + + margin_pct_used: Optional[float] = None + if equity and equity > 0 and margin_used is not None and margin_used >= 0: + margin_pct_used = round(float(margin_used) / float(equity) * 100, 2) + + max_margin = get_max_margin_pct(get_setting) + sizing = get_sizing_mode(get_setting) + sizing_label = "固定金额" if sizing == "amount" else "固定手数" + + return { + "enabled": risk_control_enabled(), + "status": risk, + "manual_close_count_today": manual_count, + "margin_pct_used": margin_pct_used, + "limits": { + "max_active_positions": max_active_positions(), + "manual_close_daily_limit": manual_close_daily_limit(), + "cooling_hours_manual": cooling_hours_manual(), + "cooling_hours_manual_journal": cooling_hours_manual_journal(), + "trading_day_reset_hour": trading_day_reset_hour(), + "max_margin_pct": max_margin, + "roll_max_margin_pct": get_roll_max_margin_pct(get_setting), + "risk_percent": get_risk_percent(get_setting), + "sizing_mode": sizing, + "sizing_label": sizing_label, + "fixed_lots": get_fixed_lots(get_setting), + "fixed_amount": get_fixed_amount(get_setting), + }, + } + + def build_dashboard_payload( *, get_db: Callable, @@ -94,6 +171,7 @@ def build_dashboard_payload( "id": r["id"], "symbol": sym, "symbol_name": r["symbol_name"] or sym, + **_symbol_fields(sym), "monitor_type": mtype, "direction": r["direction"] or "", "direction_label": _direction_label(r["direction"] or "long") @@ -121,10 +199,12 @@ def build_dashboard_payload( ).fetchall() closes: list[dict[str, Any]] = [] for r in close_rows: + sym_code = r["symbol"] or "" closes.append({ "id": r["id"], - "symbol": r["symbol_name"] or r["symbol"], - "symbol_code": r["symbol"], + "symbol": r["symbol_name"] or sym_code, + "symbol_code": sym_code, + **_symbol_fields(sym_code), "direction": r["direction"] or "long", "direction_label": _direction_label(r["direction"] or "long"), "lots": float(r["lots"] or 0), @@ -139,6 +219,9 @@ def build_dashboard_payload( }) now_iso = datetime.now(_TZ).strftime("%Y-%m-%d %H:%M:%S") + risk = build_risk_overview( + conn, get_setting, equity=equity, margin_used=margin_used, + ) return { "ok": True, "updated_at": now_iso, @@ -150,6 +233,7 @@ def build_dashboard_payload( "available": available, "capital_fallback": round(capital, 2), }, + "risk": risk, "keys": keys, "closes": closes, } diff --git a/static/css/dashboard.css b/static/css/dashboard.css index b5dff39..c4a4fe3 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -46,6 +46,62 @@ margin-bottom: 0.65rem; } +.dashboard-risk-card h2 { + margin-bottom: 0.45rem; +} + +.dashboard-risk-reason { + margin: 0 0 0.65rem; + font-size: 0.82rem; + line-height: 1.5; + color: var(--text-muted); +} + +.dashboard-risk-reason.is-blocked { + color: var(--loss); +} + +.dashboard-risk-reason.is-ok { + color: var(--profit); +} + +.dashboard-risk-grid { + margin-bottom: 0; +} + +.dashboard-risk-grid .stat-item { + min-width: 5.5rem; +} + +.dashboard-risk-grid .stat-item .value { + font-size: 0.82rem; +} + +.dash-symbol-cell { + min-width: 7.5rem; + white-space: normal; +} + +.dash-symbol-title { + font-weight: 600; + line-height: 1.35; +} + +.dash-symbol-sub { + font-size: 0.72rem; + line-height: 1.35; + margin-top: 0.12rem; +} + +.dash-main-badge { + font-size: 0.68rem; + vertical-align: middle; +} + +.dashboard-table .badge.dir { + font-size: 0.72rem; +} + .dashboard-table { width: 100%; border-collapse: collapse; diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 2b2b3b0..e8743ed 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -13,12 +13,15 @@ var ctpBadge = document.getElementById('dash-ctp-badge'); var modeBadge = document.getElementById('dash-mode-badge'); var updatedEl = document.getElementById('dash-updated'); + var riskReasonEl = document.getElementById('dash-risk-reason'); + var riskGridEl = document.getElementById('dash-risk-grid'); var pollTimer = null; var positionSource = null; var posRowCache = {}; var lastKeyIds = ''; var lastCloseHeadId = null; + var lastRiskPayload = null; function fmtNum(v, digits) { if (v === null || v === undefined || v === '') return '—'; @@ -64,6 +67,118 @@ return parts.length ? parts.join(' · ') : '—'; } + function symbolCellHtml(row) { + var name = row.symbol_name || row.symbol || ''; + var code = row.symbol_code || ''; + var mainBadge = row.symbol_is_main + ? ' 主力' : ''; + var titleInner = escHtml(name) + mainBadge; + if (code && String(name).toLowerCase() !== String(code).toLowerCase()) { + titleInner += ' ' + escHtml(code) + ''; + } else if (!name && code) { + titleInner = '' + escHtml(code) + ''; + } + var sub = row.symbol_exchange || code || ''; + return ( + '
加载中…
+ +