Enhance dashboard with contract symbols and risk control overview.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+86
-2
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+124
-6
@@ -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
|
||||
? ' <span class="badge planned dash-main-badge">主力</span>' : '';
|
||||
var titleInner = escHtml(name) + mainBadge;
|
||||
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
|
||||
titleInner += ' <span class="text-accent">' + escHtml(code) + '</span>';
|
||||
} else if (!name && code) {
|
||||
titleInner = '<span class="text-accent">' + escHtml(code) + '</span>';
|
||||
}
|
||||
var sub = row.symbol_exchange || code || '';
|
||||
return (
|
||||
'<div class="dash-symbol-cell">' +
|
||||
'<div class="dash-symbol-title">' + titleInner + '</div>' +
|
||||
(sub ? '<div class="dash-symbol-sub text-muted">' + escHtml(sub) + '</div>' : '') +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function directionBadgeHtml(row) {
|
||||
var label = row.direction_label || (row.direction === 'short' ? '做空' : '做多');
|
||||
return '<span class="badge dir">' + escHtml(label) + '</span>';
|
||||
}
|
||||
|
||||
function fmtHours(h) {
|
||||
if (h == null) return '—';
|
||||
var n = Number(h);
|
||||
if (isNaN(n)) return '—';
|
||||
if (Math.abs(n - Math.round(n)) < 0.01) return String(Math.round(n)) + 'h';
|
||||
return n.toFixed(1) + 'h';
|
||||
}
|
||||
|
||||
function fmtRemainSec(sec) {
|
||||
if (sec == null || sec <= 0) return '—';
|
||||
var s = Math.max(0, Math.floor(sec));
|
||||
var h = Math.floor(s / 3600);
|
||||
var m = Math.floor((s % 3600) / 60);
|
||||
if (h > 0) return h + 'h ' + m + 'm';
|
||||
return m + 'm';
|
||||
}
|
||||
|
||||
function applyRisk(risk, account) {
|
||||
if (!riskGridEl || !risk) return;
|
||||
if (risk.limits && Object.keys(risk.limits).length) {
|
||||
lastRiskPayload = risk;
|
||||
} else if (lastRiskPayload) {
|
||||
risk = {
|
||||
enabled: lastRiskPayload.enabled,
|
||||
limits: lastRiskPayload.limits,
|
||||
manual_close_count_today: lastRiskPayload.manual_close_count_today,
|
||||
margin_pct_used: lastRiskPayload.margin_pct_used,
|
||||
status: risk.status || lastRiskPayload.status,
|
||||
};
|
||||
}
|
||||
if (account && account.equity > 0 && account.margin_used != null) {
|
||||
risk.margin_pct_used = Math.round(account.margin_used / account.equity * 10000) / 100;
|
||||
}
|
||||
var lim = risk.limits || {};
|
||||
var st = risk.status || {};
|
||||
var enabled = risk.enabled !== false;
|
||||
var active = st.active_count != null ? st.active_count : '—';
|
||||
var maxPos = lim.max_active_positions != null ? lim.max_active_positions : st.max_active_positions;
|
||||
var manualCnt = risk.manual_close_count_today != null ? risk.manual_close_count_today : 0;
|
||||
var manualLim = lim.manual_close_daily_limit;
|
||||
var marginPct = risk.margin_pct_used;
|
||||
var maxMarginPct = lim.max_margin_pct;
|
||||
var marginPctText = marginPct != null ? fmtNum(marginPct) + '%' : '—';
|
||||
if (maxMarginPct != null && marginPct != null) {
|
||||
marginPctText += ' / ' + fmtNum(maxMarginPct) + '%';
|
||||
}
|
||||
|
||||
if (riskReasonEl) {
|
||||
var reason = st.reason || (enabled ? '可新开仓' : '风控已关闭');
|
||||
riskReasonEl.textContent = (st.status_label ? st.status_label + ' · ' : '') + reason;
|
||||
riskReasonEl.className = 'dashboard-risk-reason';
|
||||
if (st.can_trade === false) riskReasonEl.classList.add('is-blocked');
|
||||
else if (st.can_trade) riskReasonEl.classList.add('is-ok');
|
||||
}
|
||||
|
||||
var sizingDetail = lim.sizing_label || '—';
|
||||
if (lim.sizing_mode === 'amount') {
|
||||
sizingDetail += ' · ' + fmtNum(lim.fixed_amount, 0) + ' 元';
|
||||
} else if (lim.fixed_lots != null) {
|
||||
sizingDetail += ' · ' + lim.fixed_lots + ' 手';
|
||||
}
|
||||
|
||||
var items = [
|
||||
{ label: '风控开关', value: enabled ? '开启' : '关闭' },
|
||||
{ label: '持仓限制', value: active + ' / ' + (maxPos != null ? maxPos : '—') },
|
||||
{ label: '日手动平仓', value: manualCnt + ' / ' + (manualLim != null ? manualLim : '—') },
|
||||
{ label: '冷静期(默认)', value: fmtHours(lim.cooling_hours_manual) },
|
||||
{ label: '复盘后冷静', value: fmtHours(lim.cooling_hours_manual_journal) },
|
||||
{ label: '冷静剩余', value: fmtRemainSec(st.freeze_remaining_sec) },
|
||||
{ label: '保证金占比', value: marginPctText },
|
||||
{ label: '保证金上限', value: maxMarginPct != null ? fmtNum(maxMarginPct) + '%' : '—' },
|
||||
{ label: '滚仓保证金上限', value: lim.roll_max_margin_pct != null ? fmtNum(lim.roll_max_margin_pct) + '%' : '—' },
|
||||
{ label: '计仓模式', value: sizingDetail },
|
||||
{ label: '单笔风险', value: lim.risk_percent != null ? fmtNum(lim.risk_percent) + '%' : '—' },
|
||||
{ label: '交易日切', value: lim.trading_day_reset_hour != null ? lim.trading_day_reset_hour + ':00' : '—' }
|
||||
];
|
||||
|
||||
riskGridEl.innerHTML = items.map(function (it) {
|
||||
return (
|
||||
'<div class="stat-item">' +
|
||||
'<div class="label">' + escHtml(it.label) + '</div>' +
|
||||
'<div class="value">' + escHtml(it.value) + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateCtpBadge(st) {
|
||||
if (!ctpBadge || !st) return;
|
||||
var connected = !!st.connected;
|
||||
@@ -116,7 +231,7 @@
|
||||
keysBody.innerHTML = keys.map(function (k) {
|
||||
return (
|
||||
'<tr data-key-id="' + k.id + '">' +
|
||||
'<td>' + escHtml(k.symbol_name || k.symbol) + '</td>' +
|
||||
'<td>' + symbolCellHtml(k) + '</td>' +
|
||||
'<td>' + escHtml(k.monitor_type || '—') + '</td>' +
|
||||
'<td>' + escHtml(k.bar_period || '—') + '</td>' +
|
||||
'<td>' + fmtNum(k.upper) + '</td>' +
|
||||
@@ -153,8 +268,8 @@
|
||||
var pc = pnlClass(c.pnl_net != null ? c.pnl_net : c.pnl);
|
||||
return (
|
||||
'<tr data-close-id="' + c.id + '">' +
|
||||
'<td>' + escHtml(c.symbol) + '</td>' +
|
||||
'<td>' + escHtml(c.direction_label || c.direction) + '</td>' +
|
||||
'<td>' + symbolCellHtml(c) + '</td>' +
|
||||
'<td>' + directionBadgeHtml(c) + '</td>' +
|
||||
'<td>' + fmtNum(c.lots) + '</td>' +
|
||||
'<td>' + fmtNum(c.entry_price) + '</td>' +
|
||||
'<td>' + fmtNum(c.close_price) + '</td>' +
|
||||
@@ -213,11 +328,10 @@
|
||||
posBody.innerHTML = rows.map(function (r) {
|
||||
var key = r.key || r.position_key || ((r.symbol_code || '') + ':' + (r.direction || ''));
|
||||
posRowCache[key] = true;
|
||||
var name = r.symbol || r.symbol_name || r.symbol_code || '—';
|
||||
return (
|
||||
'<tr data-pos-key="' + escHtml(key) + '">' +
|
||||
'<td>' + escHtml(name) + '</td>' +
|
||||
'<td>' + escHtml(r.direction_label || r.direction) + '</td>' +
|
||||
'<td>' + symbolCellHtml(r) + '</td>' +
|
||||
'<td>' + directionBadgeHtml(r) + '</td>' +
|
||||
'<td class="dash-p-lots">' + escHtml(String(r.lots)) + '</td>' +
|
||||
'<td class="dash-p-entry">' + fmtNum(r.entry_price) + '</td>' +
|
||||
'<td class="dash-p-mark">' + (r.current_price != null ? fmtNum(r.current_price) : '—') + '</td>' +
|
||||
@@ -256,6 +370,9 @@
|
||||
function applyPositionsData(data) {
|
||||
if (!data) return;
|
||||
if (data.ctp_status) updateCtpBadge(data.ctp_status);
|
||||
if (data.risk_status) {
|
||||
applyRisk({ status: data.risk_status });
|
||||
}
|
||||
if (data.trading_mode_label && modeBadge) {
|
||||
modeBadge.textContent = data.trading_mode_label;
|
||||
}
|
||||
@@ -302,6 +419,7 @@
|
||||
.then(function (data) {
|
||||
if (!data.ok) return;
|
||||
applyAccount(data);
|
||||
applyRisk(data.risk, data.account);
|
||||
applyKeys(data.keys || []);
|
||||
applyCloses(data.closes || []);
|
||||
})
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card dashboard-section dashboard-risk-card">
|
||||
<h2>风控说明</h2>
|
||||
<p class="dashboard-risk-reason" id="dash-risk-reason">加载中…</p>
|
||||
<div class="stat-grid stat-grid-summary dashboard-risk-grid" id="dash-risk-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="card dashboard-section">
|
||||
<h2>持仓信息</h2>
|
||||
<div class="card-scroll">
|
||||
|
||||
Reference in New Issue
Block a user