diff --git a/docs/INDEX.md b/docs/INDEX.md index 485e4a0..f6e7156 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -10,6 +10,7 @@ |------|------| | [FEATURES.md](./FEATURES.md) | 功能总览与导航结构 | | [RISK.md](./RISK.md) | **全局账户风控**(冷静期、仓位上限、保证金、品种范围) | +| [风控说明.md](./风控说明.md) | **数据看板风控卡片**(各指标含义与颜色规则) | | [WECHAT.md](./WECHAT.md) | **企业微信推送**(全部消息类型与完整模板) | | [AI.md](./AI.md) | **AI 分析**(配置、触发、输出、推送) | @@ -19,6 +20,7 @@ | 板块 | 路径 | 文档 | |------|------|------| +| 数据看板 | `/dashboard` | [风控说明.md](./风控说明.md) | | 下单监控 | `/positions` | [ORDER_MONITOR.md](./ORDER_MONITOR.md) | | 策略交易 | `/strategy` | [STRATEGY.md](./STRATEGY.md) | | 开单计划 | `/plans` | [PLANS.md](./PLANS.md) | @@ -53,5 +55,6 @@ | 开仓/平仓微信长文格式 | [WECHAT.md#结构化推送](./WECHAT.md) | | AI 何时分析、推什么 | [AI.md](./AI.md) | | 手动平仓后为何冻结 | [RISK.md#冷静期与日冻结](./RISK.md) | +| 看板风控各指标什么意思 | [风控说明.md](./风控说明.md) | | 移动保本怎么抬止损 | [ORDER_MONITOR.md#移动保本](./ORDER_MONITOR.md) | | 实盘 CTP 怎么接、和 SimNow 开平是否一样 | [CTP_LIVE.md](./CTP_LIVE.md) | diff --git a/docs/RISK.md b/docs/RISK.md index b5142b3..bc6a4a6 100644 --- a/docs/RISK.md +++ b/docs/RISK.md @@ -105,7 +105,7 @@ ## 风险状态展示 -下单监控顶栏显示当前状态: +下单监控顶栏、**数据看板 → 风控说明** 均展示当前状态。看板各指标释义与颜色见 [风控说明.md](./风控说明.md)。 | 状态 | 含义 | |------|------| diff --git a/docs/风控说明.md b/docs/风控说明.md new file mode 100644 index 0000000..8251297 --- /dev/null +++ b/docs/风控说明.md @@ -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) + +### 风控开关 + +| 状态 | 颜色 | +|------|------| +| 开启 | **绿色** | +| 关闭 | **红色** | + +### 综合保证金占比 + +显示格式:`已用% / 单仓上限%` + +| 已用占上限比例 | 已用部分颜色 | +|----------------|--------------| +| < 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) | 文档总索引 | diff --git a/static/css/dashboard.css b/static/css/dashboard.css index e3c561e..09cb1b5 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -75,10 +75,26 @@ -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 { flex: 1 1 0; - min-width: 5.8rem; - padding: 0.45rem 0.55rem; + min-width: 6.4rem; + padding: 0.5rem 0.6rem; text-align: center; border-right: 1px solid var(--table-border); } @@ -88,21 +104,71 @@ } .dashboard-risk-label { - font-size: 0.62rem; - line-height: 1.3; + font-size: 0.74rem; + line-height: 1.35; color: var(--text-muted); white-space: nowrap; } .dashboard-risk-value { - font-size: 0.8rem; + font-size: 0.92rem; font-weight: 600; color: var(--text-title); - margin-top: 0.18rem; + margin-top: 0.22rem; font-variant-numeric: tabular-nums; 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 { min-width: 5.5rem; } @@ -131,10 +197,21 @@ vertical-align: middle; } -.dashboard-table .badge.dir { +.dashboard-table .badge.dir-long, +.dashboard-table .badge.dir-short { 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 { width: 100%; border-collapse: collapse; diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 510af80..4590c26 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -100,8 +100,40 @@ } function directionBadgeHtml(row) { - var label = row.direction_label || (row.direction === 'short' ? '做空' : '做多'); - return '' + escHtml(label) + ''; + var dir = (row.direction || 'long').toString().toLowerCase(); + var label = row.direction_label || (dir === 'short' ? '做空' : '做多'); + var cls = dir === 'short' ? 'dir dir-short' : 'dir dir-long'; + return '' + escHtml(label) + ''; + } + + 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 = '' + escHtml(fmtNum(used) + '%') + ''; + if (limit != null) { + html += ' / ' + + '' + escHtml(fmtNum(limit) + '%') + ''; + } + 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 ( + '
' + + '
' + escHtml(it.label) + '
' + + '
' + val + '
' + + '
' + ); + }).join(''); } function fmtHours(h) { @@ -164,10 +196,6 @@ } 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 ? '可新开仓' : '风控已关闭'); @@ -185,7 +213,11 @@ } var items = [ - { label: '风控开关', value: enabled ? '开启' : '关闭' }, + { + label: '风控开关', + value: enabled ? '开启' : '关闭', + valueClass: enabled ? 'risk-switch-on' : 'risk-switch-off', + }, { label: '持仓限制', value: active + ' / ' + (maxPos != null ? maxPos : '—') }, { label: '日持仓限制', value: dailyOpens + ' / ' + (dailyPosLim != null ? dailyPosLim : '—') }, { label: '日交易风险', value: dailyRiskText }, @@ -193,21 +225,25 @@ { 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: '综合保证金占比', + 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: lim.trading_day_reset_hour != null ? lim.trading_day_reset_hour + ':00' : '—' } ]; - riskGridEl.innerHTML = items.map(function (it) { - return ( - '
' + - '
' + escHtml(it.label) + '
' + - '
' + escHtml(it.value) + '
' + - '
' - ); - }).join(''); + renderRiskGrid(items); } function updateCtpBadge(st) { diff --git a/templates/dashboard.html b/templates/dashboard.html index 179ccb4..f690f6a 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -33,7 +33,10 @@
-

风控说明

+

+ 风控说明 + · 详见 docs/风控说明.md +

加载中…