Enhance dashboard with contract symbols and risk control overview.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 || []);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user