Improve dashboard risk card styling, colors, and add risk control docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-29 22:14:11 +08:00
parent d8c6428eb5
commit 9a10ac8a51
6 changed files with 243 additions and 27 deletions
+3
View File
@@ -10,6 +10,7 @@
|------|------| |------|------|
| [FEATURES.md](./FEATURES.md) | 功能总览与导航结构 | | [FEATURES.md](./FEATURES.md) | 功能总览与导航结构 |
| [RISK.md](./RISK.md) | **全局账户风控**(冷静期、仓位上限、保证金、品种范围) | | [RISK.md](./RISK.md) | **全局账户风控**(冷静期、仓位上限、保证金、品种范围) |
| [风控说明.md](./风控说明.md) | **数据看板风控卡片**(各指标含义与颜色规则) |
| [WECHAT.md](./WECHAT.md) | **企业微信推送**(全部消息类型与完整模板) | | [WECHAT.md](./WECHAT.md) | **企业微信推送**(全部消息类型与完整模板) |
| [AI.md](./AI.md) | **AI 分析**(配置、触发、输出、推送) | | [AI.md](./AI.md) | **AI 分析**(配置、触发、输出、推送) |
@@ -19,6 +20,7 @@
| 板块 | 路径 | 文档 | | 板块 | 路径 | 文档 |
|------|------|------| |------|------|------|
| 数据看板 | `/dashboard` | [风控说明.md](./风控说明.md) |
| 下单监控 | `/positions` | [ORDER_MONITOR.md](./ORDER_MONITOR.md) | | 下单监控 | `/positions` | [ORDER_MONITOR.md](./ORDER_MONITOR.md) |
| 策略交易 | `/strategy` | [STRATEGY.md](./STRATEGY.md) | | 策略交易 | `/strategy` | [STRATEGY.md](./STRATEGY.md) |
| 开单计划 | `/plans` | [PLANS.md](./PLANS.md) | | 开单计划 | `/plans` | [PLANS.md](./PLANS.md) |
@@ -53,5 +55,6 @@
| 开仓/平仓微信长文格式 | [WECHAT.md#结构化推送](./WECHAT.md) | | 开仓/平仓微信长文格式 | [WECHAT.md#结构化推送](./WECHAT.md) |
| AI 何时分析、推什么 | [AI.md](./AI.md) | | AI 何时分析、推什么 | [AI.md](./AI.md) |
| 手动平仓后为何冻结 | [RISK.md#冷静期与日冻结](./RISK.md) | | 手动平仓后为何冻结 | [RISK.md#冷静期与日冻结](./RISK.md) |
| 看板风控各指标什么意思 | [风控说明.md](./风控说明.md) |
| 移动保本怎么抬止损 | [ORDER_MONITOR.md#移动保本](./ORDER_MONITOR.md) | | 移动保本怎么抬止损 | [ORDER_MONITOR.md#移动保本](./ORDER_MONITOR.md) |
| 实盘 CTP 怎么接、和 SimNow 开平是否一样 | [CTP_LIVE.md](./CTP_LIVE.md) | | 实盘 CTP 怎么接、和 SimNow 开平是否一样 | [CTP_LIVE.md](./CTP_LIVE.md) |
+1 -1
View File
@@ -105,7 +105,7 @@
## 风险状态展示 ## 风险状态展示
下单监控顶栏显示当前状态: 下单监控顶栏、**数据看板 → 风控说明** 均展示当前状态。看板各指标释义与颜色见 [风控说明.md](./风控说明.md)。
| 状态 | 含义 | | 状态 | 含义 |
|------|------| |------|------|
+97
View File
@@ -0,0 +1,97 @@
# 数据看板 · 风控说明
**路径**`/dashboard`(数据看板)· 风控说明卡片
本文说明看板 **风控说明** 区域各指标含义、颜色规则及对应配置。全局风控逻辑详见 [RISK.md](./RISK.md)。
---
## 状态行(卡片顶部)
顶栏红色/绿色一行文字为 **当前风控结论**,例如:
| 显示 | 含义 |
|------|------|
| 正常 · 可新开仓 | 未触发冻结,可新开仓 |
| 仓位上限冻结 · 已达仓位上限 1/1 | 同时 active 持仓数已达上限,禁止新开仓,**滚仓/加仓仍允许** |
| 1h / 4h 冻结 | 手动平仓触发冷静期 |
| 日冻结 | 复盘勾选情绪问题或当日规则触发,禁止新开仓 |
- **绿色**:当前可交易(`can_trade=true`
- **红色**:当前禁止新开仓(`can_trade=false`
---
## 指标一览
| 指标 | 说明 | 配置来源 |
|------|------|----------|
| **风控开关** | 是否启用账户冷静期等风控 | `.env``RISK_CONTROL_ENABLED` |
| **持仓限制** | 当前 active 持仓数 / 同时持仓上限 | `.env``MAX_ACTIVE_POSITIONS` |
| **日持仓限制** | 当日已开仓次数(含已平)/ 日开仓上限 | `.env``RISK_DAILY_POSITION_LIMIT`(默认 5 |
| **日交易风险** | 当日累计止损风险占权益 / 上限 | `.env``RISK_DAILY_TRADING_RISK_PCT`(默认 2% |
| **手动平仓(冷静期触发)** | 当日手动平仓次数 / 上限 | `.env``RISK_MANUAL_CLOSE_DAILY_LIMIT` |
| **冷静期(默认)** | 超限后默认冻结时长 | `.env``RISK_COOLING_HOURS_MANUAL`(默认 4h |
| **复盘后冷静** | 填写复盘情绪日记后缩短的冷静期 | `.env``RISK_COOLING_HOURS_MANUAL_JOURNAL`(默认 1h |
| **冷静剩余** | 当前冷静期剩余时间 | 运行时计算 |
| **综合保证金占比** | 占用保证金占权益比例 / 单仓上限 | 系统设置 `max_margin_pct` |
| **单仓保证金上限** | 新开仓允许的保证金占权益上限 | 系统设置 `max_margin_pct`(默认 30% |
| **综合保证金上限** | 滚仓/加仓时允许的更高保证金占比 | 系统设置 `roll_max_margin_pct` |
| **计仓模式** | 固定金额(以损定仓)或固定手数 | 系统设置 |
| **交易日切** | 统计日重置时刻 | `.env``TRADING_DAY_RESET_HOUR`(默认 8:00 |
---
## 颜色规则(看板 UI
### 风控开关
| 状态 | 颜色 |
|------|------|
| 开启 | **绿色** |
| 关闭 | **红色** |
### 综合保证金占比
显示格式:`已用% / 单仓上限%`
| 已用占上限比例 | 已用部分颜色 |
|----------------|--------------|
| &lt; 85% | **绿色**(安全) |
| 85% ~ 100% | **琥珀色**(接近上限) |
| ≥ 100% | **红色**(已达或超过单仓上限) |
斜杠后的 **上限数值****蓝色**,与「单仓保证金上限」一致。
### 单仓保证金上限 / 综合保证金上限
| 指标 | 数值颜色 |
|------|----------|
| 单仓保证金上限 | **蓝色**(新开仓保证金天花板) |
| 综合保证金上限 | **琥珀色**(滚仓/加仓专用,通常高于单仓上限) |
### 持仓方向(持仓信息、平仓记录)
| 方向 | 颜色 |
|------|------|
| 做多 | **绿色** |
| 做空 | **红色** |
---
## 与全局风控的关系
- 看板 **实时展示** 账户风控状态;下单前各板块仍调用 `assert_can_open()` 做相同校验。
- **日持仓限制**、**日交易风险** 为新增维度,与「同时持仓上限」「冷静期」并列生效,任一超限即禁止新开仓。
- **综合保证金占比** 使用 CTP 柜台权益与占用保证金实时计算;断线时可能短暂显示 `—`
---
## 相关文档
| 文档 | 内容 |
|------|------|
| [RISK.md](./RISK.md) | 全局账户风控规则与 env 变量 |
| [SETTINGS.md](./SETTINGS.md) | 保证金上限、计仓模式等系统设置 |
| [ORDER_MONITOR.md](./ORDER_MONITOR.md) | 下单监控顶栏风控状态 |
| [INDEX.md](./INDEX.md) | 文档总索引 |
+84 -7
View File
@@ -75,10 +75,26 @@
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
.dashboard-risk-heading {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.35rem;
}
.dash-risk-doc-ref {
font-size: 0.72rem;
font-weight: 400;
}
.dash-risk-doc-ref code {
font-size: 0.68rem;
}
.dashboard-risk-item { .dashboard-risk-item {
flex: 1 1 0; flex: 1 1 0;
min-width: 5.8rem; min-width: 6.4rem;
padding: 0.45rem 0.55rem; padding: 0.5rem 0.6rem;
text-align: center; text-align: center;
border-right: 1px solid var(--table-border); border-right: 1px solid var(--table-border);
} }
@@ -88,21 +104,71 @@
} }
.dashboard-risk-label { .dashboard-risk-label {
font-size: 0.62rem; font-size: 0.74rem;
line-height: 1.3; line-height: 1.35;
color: var(--text-muted); color: var(--text-muted);
white-space: nowrap; white-space: nowrap;
} }
.dashboard-risk-value { .dashboard-risk-value {
font-size: 0.8rem; font-size: 0.92rem;
font-weight: 600; font-weight: 600;
color: var(--text-title); color: var(--text-title);
margin-top: 0.18rem; margin-top: 0.22rem;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
white-space: nowrap; white-space: nowrap;
} }
.dashboard-risk-value.risk-switch-on {
color: var(--profit);
}
.dashboard-risk-value.risk-switch-off {
color: var(--loss);
}
.dashboard-risk-value.risk-cap-single {
color: #5eb8ff;
}
[data-theme="light"] .dashboard-risk-value.risk-cap-single {
color: #1d4ed8;
}
.dashboard-risk-value.risk-cap-roll {
color: #c4a035;
}
[data-theme="light"] .dashboard-risk-value.risk-cap-roll {
color: #b45309;
}
.dashboard-risk-value .risk-margin-safe {
color: var(--profit);
}
.dashboard-risk-value .risk-margin-warn {
color: var(--planned-text);
}
.dashboard-risk-value .risk-margin-over {
color: var(--loss);
}
.dashboard-risk-value .risk-margin-sep {
color: var(--text-muted);
font-weight: 400;
}
.dashboard-risk-value .risk-margin-cap-inline {
color: #5eb8ff;
font-weight: 600;
}
[data-theme="light"] .dashboard-risk-value .risk-margin-cap-inline {
color: #1d4ed8;
}
.dashboard-risk-grid .stat-item { .dashboard-risk-grid .stat-item {
min-width: 5.5rem; min-width: 5.5rem;
} }
@@ -131,10 +197,21 @@
vertical-align: middle; vertical-align: middle;
} }
.dashboard-table .badge.dir { .dashboard-table .badge.dir-long,
.dashboard-table .badge.dir-short {
font-size: 0.72rem; font-size: 0.72rem;
} }
.dashboard-table .badge.dir-long {
background: var(--profit-bg);
color: var(--profit);
}
.dashboard-table .badge.dir-short {
background: var(--loss-bg);
color: var(--loss);
}
.dashboard-table { .dashboard-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
+54 -18
View File
@@ -100,8 +100,40 @@
} }
function directionBadgeHtml(row) { function directionBadgeHtml(row) {
var label = row.direction_label || (row.direction === 'short' ? '做空' : '做多'); var dir = (row.direction || 'long').toString().toLowerCase();
return '<span class="badge dir">' + escHtml(label) + '</span>'; var label = row.direction_label || (dir === 'short' ? '做空' : '做多');
var cls = dir === 'short' ? 'dir dir-short' : 'dir dir-long';
return '<span class="badge ' + cls + '">' + escHtml(label) + '</span>';
}
function riskMarginPctHtml(used, limit) {
if (used == null) return escHtml('—');
var usedCls = 'risk-margin-safe';
if (limit != null && limit > 0) {
var ratio = Number(used) / Number(limit);
if (ratio >= 1) usedCls = 'risk-margin-over';
else if (ratio >= 0.85) usedCls = 'risk-margin-warn';
}
var html = '<span class="' + usedCls + '">' + escHtml(fmtNum(used) + '%') + '</span>';
if (limit != null) {
html += ' <span class="risk-margin-sep">/</span> ' +
'<span class="risk-margin-cap-inline">' + escHtml(fmtNum(limit) + '%') + '</span>';
}
return html;
}
function renderRiskGrid(items) {
if (!riskGridEl) return;
riskGridEl.innerHTML = items.map(function (it) {
var val = it.valueHtml != null ? it.valueHtml : escHtml(it.value);
var cls = 'dashboard-risk-value' + (it.valueClass ? ' ' + it.valueClass : '');
return (
'<div class="dashboard-risk-item">' +
'<div class="dashboard-risk-label">' + escHtml(it.label) + '</div>' +
'<div class="' + cls + '">' + val + '</div>' +
'</div>'
);
}).join('');
} }
function fmtHours(h) { function fmtHours(h) {
@@ -164,10 +196,6 @@
} }
var marginPct = risk.margin_pct_used; var marginPct = risk.margin_pct_used;
var maxMarginPct = lim.max_margin_pct; var maxMarginPct = lim.max_margin_pct;
var marginPctText = marginPct != null ? fmtNum(marginPct) + '%' : '—';
if (maxMarginPct != null && marginPct != null) {
marginPctText += ' / ' + fmtNum(maxMarginPct) + '%';
}
if (riskReasonEl) { if (riskReasonEl) {
var reason = st.reason || (enabled ? '可新开仓' : '风控已关闭'); var reason = st.reason || (enabled ? '可新开仓' : '风控已关闭');
@@ -185,7 +213,11 @@
} }
var items = [ var items = [
{ label: '风控开关', value: enabled ? '开启' : '关闭' }, {
label: '风控开关',
value: enabled ? '开启' : '关闭',
valueClass: enabled ? 'risk-switch-on' : 'risk-switch-off',
},
{ label: '持仓限制', value: active + ' / ' + (maxPos != null ? maxPos : '—') }, { label: '持仓限制', value: active + ' / ' + (maxPos != null ? maxPos : '—') },
{ label: '日持仓限制', value: dailyOpens + ' / ' + (dailyPosLim != null ? dailyPosLim : '—') }, { label: '日持仓限制', value: dailyOpens + ' / ' + (dailyPosLim != null ? dailyPosLim : '—') },
{ label: '日交易风险', value: dailyRiskText }, { label: '日交易风险', value: dailyRiskText },
@@ -193,21 +225,25 @@
{ label: '冷静期(默认)', value: fmtHours(lim.cooling_hours_manual) }, { label: '冷静期(默认)', value: fmtHours(lim.cooling_hours_manual) },
{ label: '复盘后冷静', value: fmtHours(lim.cooling_hours_manual_journal) }, { label: '复盘后冷静', value: fmtHours(lim.cooling_hours_manual_journal) },
{ label: '冷静剩余', value: fmtRemainSec(st.freeze_remaining_sec) }, { label: '冷静剩余', value: fmtRemainSec(st.freeze_remaining_sec) },
{ label: '保证金占比', value: marginPctText }, {
{ label: '保证金上限', value: maxMarginPct != null ? fmtNum(maxMarginPct) + '%' : '—' }, label: '综合保证金占比',
{ label: '综合保证金上限', value: lim.roll_max_margin_pct != null ? fmtNum(lim.roll_max_margin_pct) + '%' : '—' }, valueHtml: riskMarginPctHtml(marginPct, maxMarginPct),
},
{
label: '单仓保证金上限',
value: maxMarginPct != null ? fmtNum(maxMarginPct) + '%' : '—',
valueClass: 'risk-cap-single',
},
{
label: '综合保证金上限',
value: lim.roll_max_margin_pct != null ? fmtNum(lim.roll_max_margin_pct) + '%' : '—',
valueClass: 'risk-cap-roll',
},
{ label: '计仓模式', value: sizingDetail }, { label: '计仓模式', value: sizingDetail },
{ label: '交易日切', value: lim.trading_day_reset_hour != null ? lim.trading_day_reset_hour + ':00' : '—' } { label: '交易日切', value: lim.trading_day_reset_hour != null ? lim.trading_day_reset_hour + ':00' : '—' }
]; ];
riskGridEl.innerHTML = items.map(function (it) { renderRiskGrid(items);
return (
'<div class="dashboard-risk-item">' +
'<div class="dashboard-risk-label">' + escHtml(it.label) + '</div>' +
'<div class="dashboard-risk-value">' + escHtml(it.value) + '</div>' +
'</div>'
);
}).join('');
} }
function updateCtpBadge(st) { function updateCtpBadge(st) {
+4 -1
View File
@@ -33,7 +33,10 @@
</div> </div>
<div class="card dashboard-section dashboard-risk-card"> <div class="card dashboard-section dashboard-risk-card">
<h2>风控说明</h2> <h2 class="dashboard-risk-heading">
风控说明
<span class="text-muted dash-risk-doc-ref">· 详见 <code>docs/风控说明.md</code></span>
</h2>
<p class="dashboard-risk-reason" id="dash-risk-reason">加载中…</p> <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 class="stat-grid stat-grid-summary dashboard-risk-grid" id="dash-risk-grid"></div>
</div> </div>