diff --git a/account_risk_lib.py b/account_risk_lib.py index 5807a0f..f474c3c 100644 --- a/account_risk_lib.py +++ b/account_risk_lib.py @@ -9,12 +9,14 @@ STATUS_NORMAL = "normal" STATUS_FREEZE_1H = "freeze_1h" STATUS_FREEZE_4H = "freeze_4h" STATUS_DAILY = "freeze_daily" +STATUS_FREEZE_POSITION = "freeze_position" STATUS_LABELS = { STATUS_NORMAL: "正常", STATUS_FREEZE_1H: "1h冻结", STATUS_FREEZE_4H: "4h冻结", STATUS_DAILY: "日冻结", + STATUS_FREEZE_POSITION: "仓位上限冻结", } MOOD_ISSUE_OPTIONS = ( @@ -84,6 +86,13 @@ def manual_close_daily_limit() -> int: return 2 +def max_active_positions_from_env(default: int = 1) -> int: + try: + return max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", str(default)))) + except (TypeError, ValueError): + return max(1, default) + + def mood_issues_daily_freeze_enabled() -> bool: return _env_bool("RISK_MOOD_ISSUES_DAILY_FREEZE", True) @@ -688,6 +697,39 @@ def enrich_risk_status_countdown( return st +def apply_position_limit_risk( + st: dict[str, Any], + active_count: int, + *, + max_active_positions: Optional[int] = None, +) -> dict[str, Any]: + """持仓达 env MAX_ACTIVE_POSITIONS 时叠加「仓位上限冻结」(时间冻结优先展示)。""" + out = dict(st or {}) + try: + mx = max(1, int(max_active_positions if max_active_positions is not None else max_active_positions_from_env())) + except (TypeError, ValueError): + mx = max_active_positions_from_env() + try: + ac = max(0, int(active_count)) + except (TypeError, ValueError): + ac = 0 + out["max_active_positions"] = mx + out["active_count"] = ac + if out.get("status") != STATUS_NORMAL: + return out + if ac >= mx: + out["status"] = STATUS_FREEZE_POSITION + out["status_label"] = STATUS_LABELS[STATUS_FREEZE_POSITION] + out["can_trade"] = False + out["reason"] = f"已达最大持仓数({ac}/{mx}),平仓前不可新开" + out["position_limit_frozen"] = True + out["freeze_until_ms"] = None + out["freeze_remaining_sec"] = 0 + else: + out["position_limit_frozen"] = False + return out + + def compute_account_risk_status( conn, *, diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 99c59c4..1f1c47a 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -1542,6 +1542,7 @@ def get_db(): def hub_account_risk_status(conn): from account_risk_lib import ( + apply_position_limit_risk, compute_account_risk_status, enrich_risk_status_countdown, ensure_account_risk_schema, @@ -1555,7 +1556,12 @@ def hub_account_risk_status(conn): now=now, fmt_local_ms=ms_to_app_local_str, ) - return enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + return apply_position_limit_risk( + st, + get_active_position_count(conn), + max_active_positions=MAX_ACTIVE_POSITIONS, + ) def hub_user_initiated_close( diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index eadea96..68bb524 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -5,8 +5,8 @@ - - + + diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index cbab36a..abf3b1f 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -1533,6 +1533,7 @@ def get_db(): def hub_account_risk_status(conn): from account_risk_lib import ( + apply_position_limit_risk, compute_account_risk_status, enrich_risk_status_countdown, ensure_account_risk_schema, @@ -1546,7 +1547,12 @@ def hub_account_risk_status(conn): now=now, fmt_local_ms=ms_to_app_local_str, ) - return enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + return apply_position_limit_risk( + st, + get_active_position_count(conn), + max_active_positions=MAX_ACTIVE_POSITIONS, + ) def hub_user_initiated_close( diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index a10f789..33067cc 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -5,8 +5,8 @@ - - + + diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 1543cdc..706a19c 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -1533,6 +1533,7 @@ def get_db(): def hub_account_risk_status(conn): from account_risk_lib import ( + apply_position_limit_risk, compute_account_risk_status, enrich_risk_status_countdown, ensure_account_risk_schema, @@ -1546,7 +1547,12 @@ def hub_account_risk_status(conn): now=now, fmt_local_ms=ms_to_app_local_str, ) - return enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + return apply_position_limit_risk( + st, + get_active_position_count(conn), + max_active_positions=MAX_ACTIVE_POSITIONS, + ) def hub_user_initiated_close( diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index a10f789..33067cc 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -5,8 +5,8 @@ - - + + diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 95edbcf..7d9c277 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -1520,6 +1520,7 @@ def get_db(): def hub_account_risk_status(conn): from account_risk_lib import ( + apply_position_limit_risk, compute_account_risk_status, enrich_risk_status_countdown, ensure_account_risk_schema, @@ -1533,7 +1534,12 @@ def hub_account_risk_status(conn): now=now, fmt_local_ms=ms_to_app_local_str, ) - return enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + return apply_position_limit_risk( + st, + get_active_position_count(conn), + max_active_positions=MAX_ACTIVE_POSITIONS, + ) def hub_user_initiated_close( diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index de222d1..c57f964 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -5,8 +5,8 @@ - - + + diff --git a/docs/account-risk-cooldown.md b/docs/account-risk-cooldown.md index 2c47e7c..4f0e93b 100644 --- a/docs/account-risk-cooldown.md +++ b/docs/account-risk-cooldown.md @@ -97,17 +97,20 @@ APP_TIMEZONE=Asia/Shanghai | 字段 | 说明 | |------|------| -| `status` | `normal` / `freeze_1h` / `freeze_4h` / `freeze_daily` | +| `status` | `normal` / `freeze_1h` / `freeze_4h` / `freeze_daily` / `freeze_position` | | `status_label` | 中文标签 | | `can_trade` | 是否允许新开仓(仅风控维度) | | `reason` | 悬停提示文案 | +| `active_count` / `max_active_positions` | 当前活跃持仓与 `.env` 中 `MAX_ACTIVE_POSITIONS` | | `cooloff_until_ms` | 1h/4h 冷静期结束时间戳(毫秒) | | `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) | | `freeze_remaining_sec` | 服务端计算的剩余秒数(供调试) | +**仓位上限冻结**:当活跃持仓数 ≥ 实例 `.env` 的 `MAX_ACTIVE_POSITIONS`(默认 1)且账户无时间类冻结时,徽章显示 **仓位上限冻结**;平仓后自动恢复 **正常**。时间冻结(1h/4h/日)优先展示。 + ## 前端倒计时 -- 共用脚本:`static/account_risk_badge.js?v=3` +- 共用脚本:`static/account_risk_badge.js?v=4` - 样式:`static/account_risk_badge.css` - 展示格式:`4h冻结 · 3h 12m`;日冻结为距下一交易日切点剩余时间 - 倒计时优先用服务端 `freeze_remaining_sec` 推算结束时刻,避免绝对时间戳与时区/脏数据偏差 @@ -118,7 +121,7 @@ APP_TIMEZONE=Asia/Shanghai ## 相关代码 -- `account_risk_lib.py` — 状态机、`enrich_risk_status_countdown`、`on_user_initiated_close` +- `account_risk_lib.py` — 状态机、`enrich_risk_status_countdown`、`apply_position_limit_risk`、`on_user_initiated_close` - `hub_bridge.py` — `/api/hub/account-risk/user-close` - `manual_trading_hub/hub.py` — 中控平仓成功后调用 user-close - `strategy_trend_register.py` — `stop_trend_pullback` 结束计划时登记风控 diff --git a/embed_templates/embed_shell.html b/embed_templates/embed_shell.html index 8fe8a50..bc2c254 100644 --- a/embed_templates/embed_shell.html +++ b/embed_templates/embed_shell.html @@ -5,10 +5,10 @@ - + - +