Show position-limit freeze on hub and instance risk badges.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,12 +9,14 @@ STATUS_NORMAL = "normal"
|
|||||||
STATUS_FREEZE_1H = "freeze_1h"
|
STATUS_FREEZE_1H = "freeze_1h"
|
||||||
STATUS_FREEZE_4H = "freeze_4h"
|
STATUS_FREEZE_4H = "freeze_4h"
|
||||||
STATUS_DAILY = "freeze_daily"
|
STATUS_DAILY = "freeze_daily"
|
||||||
|
STATUS_FREEZE_POSITION = "freeze_position"
|
||||||
|
|
||||||
STATUS_LABELS = {
|
STATUS_LABELS = {
|
||||||
STATUS_NORMAL: "正常",
|
STATUS_NORMAL: "正常",
|
||||||
STATUS_FREEZE_1H: "1h冻结",
|
STATUS_FREEZE_1H: "1h冻结",
|
||||||
STATUS_FREEZE_4H: "4h冻结",
|
STATUS_FREEZE_4H: "4h冻结",
|
||||||
STATUS_DAILY: "日冻结",
|
STATUS_DAILY: "日冻结",
|
||||||
|
STATUS_FREEZE_POSITION: "仓位上限冻结",
|
||||||
}
|
}
|
||||||
|
|
||||||
MOOD_ISSUE_OPTIONS = (
|
MOOD_ISSUE_OPTIONS = (
|
||||||
@@ -84,6 +86,13 @@ def manual_close_daily_limit() -> int:
|
|||||||
return 2
|
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:
|
def mood_issues_daily_freeze_enabled() -> bool:
|
||||||
return _env_bool("RISK_MOOD_ISSUES_DAILY_FREEZE", True)
|
return _env_bool("RISK_MOOD_ISSUES_DAILY_FREEZE", True)
|
||||||
|
|
||||||
@@ -688,6 +697,39 @@ def enrich_risk_status_countdown(
|
|||||||
return st
|
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(
|
def compute_account_risk_status(
|
||||||
conn,
|
conn,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -1542,6 +1542,7 @@ def get_db():
|
|||||||
|
|
||||||
def hub_account_risk_status(conn):
|
def hub_account_risk_status(conn):
|
||||||
from account_risk_lib import (
|
from account_risk_lib import (
|
||||||
|
apply_position_limit_risk,
|
||||||
compute_account_risk_status,
|
compute_account_risk_status,
|
||||||
enrich_risk_status_countdown,
|
enrich_risk_status_countdown,
|
||||||
ensure_account_risk_schema,
|
ensure_account_risk_schema,
|
||||||
@@ -1555,7 +1556,12 @@ def hub_account_risk_status(conn):
|
|||||||
now=now,
|
now=now,
|
||||||
fmt_local_ms=ms_to_app_local_str,
|
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(
|
def hub_user_initiated_close(
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<script src="/static/instance_theme.js?v=16"></script>
|
<script src="/static/instance_theme.js?v=16"></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=4">
|
||||||
<script src="/static/account_risk_badge.js?v=3"></script>
|
<script src="/static/account_risk_badge.js?v=4"></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="监控">
|
||||||
|
|||||||
@@ -1533,6 +1533,7 @@ def get_db():
|
|||||||
|
|
||||||
def hub_account_risk_status(conn):
|
def hub_account_risk_status(conn):
|
||||||
from account_risk_lib import (
|
from account_risk_lib import (
|
||||||
|
apply_position_limit_risk,
|
||||||
compute_account_risk_status,
|
compute_account_risk_status,
|
||||||
enrich_risk_status_countdown,
|
enrich_risk_status_countdown,
|
||||||
ensure_account_risk_schema,
|
ensure_account_risk_schema,
|
||||||
@@ -1546,7 +1547,12 @@ def hub_account_risk_status(conn):
|
|||||||
now=now,
|
now=now,
|
||||||
fmt_local_ms=ms_to_app_local_str,
|
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(
|
def hub_user_initiated_close(
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<script src="/static/instance_theme.js?v=16"></script>
|
<script src="/static/instance_theme.js?v=16"></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=4">
|
||||||
<script src="/static/account_risk_badge.js?v=3"></script>
|
<script src="/static/account_risk_badge.js?v=4"></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="监控">
|
||||||
|
|||||||
@@ -1533,6 +1533,7 @@ def get_db():
|
|||||||
|
|
||||||
def hub_account_risk_status(conn):
|
def hub_account_risk_status(conn):
|
||||||
from account_risk_lib import (
|
from account_risk_lib import (
|
||||||
|
apply_position_limit_risk,
|
||||||
compute_account_risk_status,
|
compute_account_risk_status,
|
||||||
enrich_risk_status_countdown,
|
enrich_risk_status_countdown,
|
||||||
ensure_account_risk_schema,
|
ensure_account_risk_schema,
|
||||||
@@ -1546,7 +1547,12 @@ def hub_account_risk_status(conn):
|
|||||||
now=now,
|
now=now,
|
||||||
fmt_local_ms=ms_to_app_local_str,
|
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(
|
def hub_user_initiated_close(
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<script src="/static/instance_theme.js?v=16"></script>
|
<script src="/static/instance_theme.js?v=16"></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=4">
|
||||||
<script src="/static/account_risk_badge.js?v=3"></script>
|
<script src="/static/account_risk_badge.js?v=4"></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="监控">
|
||||||
|
|||||||
@@ -1520,6 +1520,7 @@ def get_db():
|
|||||||
|
|
||||||
def hub_account_risk_status(conn):
|
def hub_account_risk_status(conn):
|
||||||
from account_risk_lib import (
|
from account_risk_lib import (
|
||||||
|
apply_position_limit_risk,
|
||||||
compute_account_risk_status,
|
compute_account_risk_status,
|
||||||
enrich_risk_status_countdown,
|
enrich_risk_status_countdown,
|
||||||
ensure_account_risk_schema,
|
ensure_account_risk_schema,
|
||||||
@@ -1533,7 +1534,12 @@ def hub_account_risk_status(conn):
|
|||||||
now=now,
|
now=now,
|
||||||
fmt_local_ms=ms_to_app_local_str,
|
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(
|
def hub_user_initiated_close(
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<script src="/static/instance_theme.js?v=16"></script>
|
<script src="/static/instance_theme.js?v=16"></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=4">
|
||||||
<script src="/static/account_risk_badge.js?v=3"></script>
|
<script src="/static/account_risk_badge.js?v=4"></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="监控">
|
||||||
|
|||||||
@@ -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` | 中文标签 |
|
| `status_label` | 中文标签 |
|
||||||
| `can_trade` | 是否允许新开仓(仅风控维度) |
|
| `can_trade` | 是否允许新开仓(仅风控维度) |
|
||||||
| `reason` | 悬停提示文案 |
|
| `reason` | 悬停提示文案 |
|
||||||
|
| `active_count` / `max_active_positions` | 当前活跃持仓与 `.env` 中 `MAX_ACTIVE_POSITIONS` |
|
||||||
| `cooloff_until_ms` | 1h/4h 冷静期结束时间戳(毫秒) |
|
| `cooloff_until_ms` | 1h/4h 冷静期结束时间戳(毫秒) |
|
||||||
| `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) |
|
| `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) |
|
||||||
| `freeze_remaining_sec` | 服务端计算的剩余秒数(供调试) |
|
| `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`
|
- 样式:`static/account_risk_badge.css`
|
||||||
- 展示格式:`4h冻结 · 3h 12m`;日冻结为距下一交易日切点剩余时间
|
- 展示格式:`4h冻结 · 3h 12m`;日冻结为距下一交易日切点剩余时间
|
||||||
- 倒计时优先用服务端 `freeze_remaining_sec` 推算结束时刻,避免绝对时间戳与时区/脏数据偏差
|
- 倒计时优先用服务端 `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`
|
- `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` 结束计划时登记风控
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<script src="/static/instance_theme.js?v=17"></script>
|
<script src="/static/instance_theme.js?v=17"></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=4">
|
||||||
<link rel="stylesheet" href="/static/instance_page.css?v=2">
|
<link rel="stylesheet" href="/static/instance_page.css?v=2">
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=18">
|
<link rel="stylesheet" href="/static/instance_theme.css?v=18">
|
||||||
<script src="/static/account_risk_badge.js?v=3"></script>
|
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||||
<meta name="theme-color" content="#0b0d14">
|
<meta name="theme-color" content="#0b0d14">
|
||||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||||
<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=20260614-calculator" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260614-calculator" />
|
||||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=3" />
|
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||||
<script src="/assets/account_risk_badge.js?v=3"></script>
|
<script src="/assets/account_risk_badge.js?v=4"></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>
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ html[data-theme="dark"] {
|
|||||||
--risk-daily-border: rgba(210, 75, 120, 0.5);
|
--risk-daily-border: rgba(210, 75, 120, 0.5);
|
||||||
--risk-daily-glow: rgba(210, 75, 120, 0.36);
|
--risk-daily-glow: rgba(210, 75, 120, 0.36);
|
||||||
|
|
||||||
|
--risk-position-fg: #8ec8ff;
|
||||||
|
--risk-position-bg: rgba(55, 120, 210, 0.18);
|
||||||
|
--risk-position-border: rgba(75, 145, 230, 0.48);
|
||||||
|
--risk-position-glow: rgba(75, 145, 230, 0.34);
|
||||||
|
|
||||||
--risk-badge-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
|
--risk-badge-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +51,11 @@ html[data-theme="light"] {
|
|||||||
--risk-daily-border: rgba(155, 28, 68, 0.34);
|
--risk-daily-border: rgba(155, 28, 68, 0.34);
|
||||||
--risk-daily-glow: rgba(180, 35, 80, 0.18);
|
--risk-daily-glow: rgba(180, 35, 80, 0.18);
|
||||||
|
|
||||||
|
--risk-position-fg: #0b5cab;
|
||||||
|
--risk-position-bg: rgba(20, 100, 190, 0.12);
|
||||||
|
--risk-position-border: rgba(15, 85, 165, 0.36);
|
||||||
|
--risk-position-glow: rgba(20, 100, 190, 0.2);
|
||||||
|
|
||||||
--risk-badge-shadow: 0 1px 2px rgba(20, 50, 80, 0.1);
|
--risk-badge-shadow: 0 1px 2px rgba(20, 50, 80, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +123,13 @@ html[data-hub-linked="1"] .header-row .risk-status-badge {
|
|||||||
--risk-glow: var(--risk-daily-glow);
|
--risk-glow: var(--risk-daily-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.risk-status-freeze_position {
|
||||||
|
--risk-fg: var(--risk-position-fg);
|
||||||
|
--risk-bg: var(--risk-position-bg);
|
||||||
|
--risk-border: var(--risk-position-border);
|
||||||
|
--risk-glow: var(--risk-position-glow);
|
||||||
|
}
|
||||||
|
|
||||||
/* 实例页:与交易所标签并排 */
|
/* 实例页:与交易所标签并排 */
|
||||||
.header-row .risk-status-badge {
|
.header-row .risk-status-badge {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ from account_risk_lib import (
|
|||||||
STATUS_DAILY,
|
STATUS_DAILY,
|
||||||
STATUS_FREEZE_1H,
|
STATUS_FREEZE_1H,
|
||||||
STATUS_FREEZE_4H,
|
STATUS_FREEZE_4H,
|
||||||
|
STATUS_FREEZE_POSITION,
|
||||||
STATUS_NORMAL,
|
STATUS_NORMAL,
|
||||||
account_risk_blocks_trading,
|
account_risk_blocks_trading,
|
||||||
|
apply_position_limit_risk,
|
||||||
compute_account_risk_status,
|
compute_account_risk_status,
|
||||||
enrich_risk_status_countdown,
|
enrich_risk_status_countdown,
|
||||||
ensure_account_risk_schema,
|
ensure_account_risk_schema,
|
||||||
|
max_active_positions_from_env,
|
||||||
on_journal_saved,
|
on_journal_saved,
|
||||||
on_manual_close,
|
on_manual_close,
|
||||||
on_user_initiated_close,
|
on_user_initiated_close,
|
||||||
@@ -489,6 +492,33 @@ class AccountRiskLibTests(unittest.TestCase):
|
|||||||
ok, _ = account_risk_blocks_trading(conn, trading_day="2026-06-14", now=now)
|
ok, _ = account_risk_blocks_trading(conn, trading_day="2026-06-14", now=now)
|
||||||
self.assertTrue(ok)
|
self.assertTrue(ok)
|
||||||
|
|
||||||
|
def test_position_limit_freeze_from_env(self):
|
||||||
|
os.environ["MAX_ACTIVE_POSITIONS"] = "2"
|
||||||
|
st = apply_position_limit_risk({"status": STATUS_NORMAL, "can_trade": True}, 2)
|
||||||
|
self.assertEqual(st["status"], STATUS_FREEZE_POSITION)
|
||||||
|
self.assertEqual(st["status_label"], "仓位上限冻结")
|
||||||
|
self.assertFalse(st["can_trade"])
|
||||||
|
self.assertIn("2/2", st["reason"])
|
||||||
|
self.assertEqual(st["max_active_positions"], 2)
|
||||||
|
|
||||||
|
def test_position_limit_normal_when_under_cap(self):
|
||||||
|
st = apply_position_limit_risk({"status": STATUS_NORMAL, "can_trade": True}, 0, max_active_positions=1)
|
||||||
|
self.assertEqual(st["status"], STATUS_NORMAL)
|
||||||
|
self.assertTrue(st["can_trade"])
|
||||||
|
|
||||||
|
def test_time_freeze_takes_priority_over_position_limit(self):
|
||||||
|
st = apply_position_limit_risk(
|
||||||
|
{"status": STATUS_FREEZE_4H, "status_label": "4h冻结", "can_trade": False},
|
||||||
|
5,
|
||||||
|
max_active_positions=1,
|
||||||
|
)
|
||||||
|
self.assertEqual(st["status"], STATUS_FREEZE_4H)
|
||||||
|
self.assertEqual(st["active_count"], 5)
|
||||||
|
|
||||||
|
def test_max_active_positions_from_env(self):
|
||||||
|
os.environ["MAX_ACTIVE_POSITIONS"] = "3"
|
||||||
|
self.assertEqual(max_active_positions_from_env(), 3)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user