fix(risk): reset badge to normal when freeze countdown expires

Also expand account-risk-cooldown docs with countdown format, API fields, and frontend assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-18 17:47:47 +08:00
parent 97370926d6
commit f8e760961e
7 changed files with 68 additions and 26 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
<script src="/static/instance_theme.js?v=9"></script> <script src="/static/instance_theme.js?v=9"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=3"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=3">
<script src="/static/account_risk_badge.js?v=1"></script> <script src="/static/account_risk_badge.js?v=2"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+1 -1
View File
@@ -6,7 +6,7 @@
<script src="/static/instance_theme.js?v=9"></script> <script src="/static/instance_theme.js?v=9"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=3"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=3">
<script src="/static/account_risk_badge.js?v=1"></script> <script src="/static/account_risk_badge.js?v=2"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+1 -1
View File
@@ -6,7 +6,7 @@
<script src="/static/instance_theme.js?v=9"></script> <script src="/static/instance_theme.js?v=9"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=3"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=3">
<script src="/static/account_risk_badge.js?v=1"></script> <script src="/static/account_risk_badge.js?v=2"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+1 -1
View File
@@ -6,7 +6,7 @@
<script src="/static/instance_theme.js?v=9"></script> <script src="/static/instance_theme.js?v=9"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=3"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=3">
<script src="/static/account_risk_badge.js?v=1"></script> <script src="/static/account_risk_badge.js?v=2"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+49 -18
View File
@@ -5,14 +5,17 @@
## 状态展示 ## 状态展示
实例页顶、中控监控卡片账户名旁: 实例页顶、中控监控卡片账户名旁显示风控徽章
| 状态 | 含义 | | 状态 | 含义 | 倒计时 |
|------|------| |------|------|--------|
| 正常 | 可新开仓 | | 正常 | 可新开仓 | 无 |
| 1h冻结 | 冷静期中(通常为复盘后缩短的 1 小时);徽章显示剩余倒计时 | | 1h冻结 | 冷静期中(通常为复盘后缩短的 1 小时) | 剩余时间,如 `1h冻结 · 52m 08s` |
| 4h冻结 | 冷静期中(默认 4 小时);徽章显示剩余倒计时 | | 4h冻结 | 冷静期中(默认 4 小时) | 剩余时间,如 `4h冻结 · 3h 12m` |
| 日冻结 | 当日禁止一切新开仓;徽章显示至下一交易日切点的倒计时 | | 日冻结 | 当日禁止一切新开仓 | 至下一 **交易日切点**`TRADING_DAY_RESET_HOUR` |
- 倒计时每秒刷新;到期后徽章自动恢复为 **正常**(下次轮询/API 刷新会再次对齐服务端状态)。
- 鼠标悬停徽章可见完整说明(含解除时刻,如有)。
## 什么算「手动平仓」(计入风控) ## 什么算「手动平仓」(计入风控)
@@ -39,16 +42,24 @@
| 第 1 次用户主动平仓 | 默认 **4h** 冷静期 | | 第 1 次用户主动平仓 | 默认 **4h** 冷静期 |
| 第 2 次用户主动平仓(同一交易日) | **日冻结** | | 第 2 次用户主动平仓(同一交易日) | **日冻结** |
| 复盘勾选任意情绪标签 | **日冻结** | | 复盘勾选任意情绪标签 | **日冻结** |
| 复盘:离场=手动平仓 且说明非空 | 将当前冷静期降为 **1h**(须处于手动平仓冷静期中;有 pending 关联交易记录时同样生效 | | 复盘:离场=手动平仓 且说明非空 | 将当前冷静期降为 **1h**(须处于 4h 档冷静期中 |
情绪标签:怕踏空、报复开仓、盈利飘了、拿不住单、扛单、重仓违规。 情绪标签:怕踏空、报复开仓、盈利飘了、拿不住单、扛单、重仓违规。
**复盘缩短 1h 说明** ### 复盘缩短 1h
- 复盘表单须选 **离场触发 = 手动平仓**,并在 **离场补充** 填写说明(不是下方「备注」栏)。
- 或在交易记录 **核对修改** 中:结果填 **手动平仓**,**备注** 填写说明(勾选「核对修改」后点按钮)。 任选一种方式,并填写说明:
- 中控全平/实例手动平仓后,只要账户仍在 4h 冷静期内,完成上述任一种即可降为 1h。
- 若在平仓后超过 1h 才复盘,则从复盘保存时刻起再计 1h(不会延长原 4h 窗口)。 | 方式 | 必填 |
- 保存后需 **重启对应实例**`pm2 restart crypto_gate` 等)并刷新页面,状态才会更新。 |------|------|
| **复盘表单**提交 | 离场触发 = **手动平仓**;**离场补充** 非空(不是下方「备注」) |
| **核对修改**保存 | 结果 = **手动平仓****备注** 非空 |
说明:
- 中控全平 / 实例手动平仓后,只要在 4h 窗口内完成上述操作即可降为 1h。
- 若超过「平仓 + 1h」才复盘,则从 **保存复盘时刻** 起再计 1h(不延长原 4h)。
- 代码更新后需 **重启对应实例** 并硬刷新页面。
## 环境变量 ## 环境变量
@@ -58,21 +69,41 @@ RISK_COOLING_HOURS_MANUAL=4
RISK_COOLING_HOURS_MANUAL_JOURNAL=1 RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2 RISK_MANUAL_CLOSE_DAILY_LIMIT=2
RISK_MOOD_ISSUES_DAILY_FREEZE=true RISK_MOOD_ISSUES_DAILY_FREEZE=true
TRADING_DAY_RESET_HOUR=8
``` ```
`RISK_COOLING_HOURS_EXTERNAL` 已废弃(外部平仓不再触发风控)。 `RISK_COOLING_HOURS_EXTERNAL` 已废弃(外部平仓不再触发风控)。
## API ## API`risk_status` 字段
| 接口 | 说明 | | 接口 | 说明 |
|------|------| |------|------|
| `GET /api/account_snapshot` | 返回 `risk_status` | | `GET /api/account_snapshot` | 实例页轮询,含 `risk_status` |
| `GET /api/account_risk_status` | hub_bridge,供中控拉取 | | `GET /api/account_risk_status` | hub_bridge 专用 |
| `GET /api/hub/monitor` | 中控监控板,每账户含 `risk_status` |
| `POST /api/hub/account-risk/user-close` | 中控登记用户平仓,`body: { source, count }` | | `POST /api/hub/account-risk/user-close` | 中控登记用户平仓,`body: { source, count }` |
`risk_status` 主要字段:
| 字段 | 说明 |
|------|------|
| `status` | `normal` / `freeze_1h` / `freeze_4h` / `freeze_daily` |
| `status_label` | 中文标签 |
| `can_trade` | 是否允许新开仓(仅风控维度) |
| `reason` | 悬停提示文案 |
| `cooloff_until_ms` | 1h/4h 冷静期结束时间戳(毫秒) |
| `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) |
| `freeze_remaining_sec` | 服务端计算的剩余秒数(供调试) |
## 前端倒计时
- 共用脚本:`static/account_risk_badge.js`(实例 `/static/…`,中控 `/assets/account_risk_badge.js`
- 样式:`static/account_risk_badge.css`
- 中控 `app.js``formatRiskStatusBadge()` 与实例 `refreshAccountSnapshot()` 均调用 `AccountRiskBadge`
## 相关代码 ## 相关代码
- `account_risk_lib.py` — 状态机`on_user_initiated_close` - `account_risk_lib.py` — 状态机`enrich_risk_status_countdown``on_user_initiated_close`
- `hub_bridge.py``/api/hub/account-risk/user-close` - `hub_bridge.py``/api/hub/account-risk/user-close`
- `manual_trading_hub/hub.py` — 中控平仓成功后调用 user-close - `manual_trading_hub/hub.py` — 中控平仓成功后调用 user-close
- `strategy_trend_register.py``stop_trend_pullback` 结束计划时登记风控 - `strategy_trend_register.py``stop_trend_pullback` 结束计划时登记风控
+1 -1
View File
@@ -17,7 +17,7 @@
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript> <noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260618-macro-calendar" /> <link rel="stylesheet" href="/assets/app.css?v=20260618-macro-calendar" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=3" /> <link rel="stylesheet" href="/assets/account_risk_badge.css?v=3" />
<script src="/assets/account_risk_badge.js?v=1"></script> <script src="/assets/account_risk_badge.js?v=2"></script>
<link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" /> <link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" />
</head> </head>
<body> <body>
+14 -3
View File
@@ -29,13 +29,24 @@
return cd ? `${label} · ${cd}` : label; return cd ? `${label} · ${cd}` : label;
} }
function setNormalBadge(el) {
el.className = "risk-status-badge risk-status-normal";
el.dataset.statusLabel = "正常";
el.textContent = "正常";
el.title = "";
if (el.dataset) delete el.dataset.freezeUntilMs;
}
function refreshElement(el) { function refreshElement(el) {
if (!el) return; if (!el) return;
const label = baseLabel(null, el); const label = baseLabel(null, el);
const until = Number(el.dataset && el.dataset.freezeUntilMs); const until = Number(el.dataset && el.dataset.freezeUntilMs);
if (!Number.isFinite(until) || until <= Date.now()) { if (!Number.isFinite(until) || until <= Date.now()) {
el.textContent = label; if (el.dataset && el.dataset.freezeUntilMs) {
if (el.dataset) delete el.dataset.freezeUntilMs; setNormalBadge(el);
} else {
el.textContent = label;
}
return; return;
} }
const cd = formatRemaining((until - Date.now()) / 1000); const cd = formatRemaining((until - Date.now()) / 1000);
@@ -47,7 +58,7 @@
const st = riskStatus.status || "normal"; const st = riskStatus.status || "normal";
el.className = "risk-status-badge risk-status-" + st; el.className = "risk-status-badge risk-status-" + st;
el.dataset.statusLabel = baseLabel(riskStatus, el); el.dataset.statusLabel = baseLabel(riskStatus, el);
if (riskStatus.freeze_until_ms) { if (riskStatus.freeze_until_ms != null && riskStatus.freeze_until_ms !== "") {
el.dataset.freezeUntilMs = String(riskStatus.freeze_until_ms); el.dataset.freezeUntilMs = String(riskStatus.freeze_until_ms);
} else if (el.dataset) { } else if (el.dataset) {
delete el.dataset.freezeUntilMs; delete el.dataset.freezeUntilMs;