Add daily loss-count freeze for account risk cooldown.
RISK_DAILY_LOSS_LIMIT (default 2, 0 disables) freezes new opens after N losing closes in the trading day. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+1
@@ -58,6 +58,7 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"RISK_COOLING_HOURS_MANUAL",
|
||||
"RISK_COOLING_HOURS_MANUAL_JOURNAL",
|
||||
"RISK_MANUAL_CLOSE_DAILY_LIMIT",
|
||||
"RISK_DAILY_LOSS_LIMIT",
|
||||
"RISK_MOOD_ISSUES_DAILY_FREEZE",
|
||||
"KEY_AUTO_ORDER_ENABLED",
|
||||
"TRADE_DIRECTION_RESTRICT_ENABLED",
|
||||
|
||||
Vendored
+2
@@ -94,6 +94,7 @@ _SHARED_SECTIONS: list[dict[str, Any]] = [
|
||||
("RISK_COOLING_HOURS_MANUAL", "手动平仓冷静(小时)", ""),
|
||||
("RISK_COOLING_HOURS_MANUAL_JOURNAL", "复盘情绪冷静(小时)", ""),
|
||||
("RISK_MANUAL_CLOSE_DAILY_LIMIT", "日手动平仓次数上限", ""),
|
||||
("RISK_DAILY_LOSS_LIMIT", "日亏损次数上限", "默认2;达限当日冻结开仓;0=不因亏损次数冻结"),
|
||||
("RISK_MOOD_ISSUES_DAILY_FREEZE", "情绪标签日冻结", ""),
|
||||
],
|
||||
},
|
||||
@@ -197,6 +198,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
"RISK_COOLING_HOURS_MANUAL": "4",
|
||||
"RISK_COOLING_HOURS_MANUAL_JOURNAL": "1",
|
||||
"RISK_MANUAL_CLOSE_DAILY_LIMIT": "2",
|
||||
"RISK_DAILY_LOSS_LIMIT": "2",
|
||||
"RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
|
||||
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
|
||||
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
||||
|
||||
@@ -8,6 +8,7 @@ from lib.key_monitor.key_auto_order_lib import load_key_auto_order_enabled
|
||||
from lib.trade.account_risk_lib import (
|
||||
cooling_hours_manual,
|
||||
cooling_hours_manual_journal,
|
||||
daily_loss_limit,
|
||||
manual_close_daily_limit,
|
||||
max_active_positions_from_env,
|
||||
mood_issues_daily_freeze_enabled,
|
||||
@@ -113,6 +114,15 @@ def build_instance_settings_view(
|
||||
_row("手动平仓冷静", f"{cooling_hours_manual():g} 小时"),
|
||||
_row("复盘后冷静", f"{cooling_hours_manual_journal():g} 小时", "手动平仓且填写说明后可缩短"),
|
||||
_row("日手动平仓上限", f"{manual_close_daily_limit()} 次", "超限当日冻结"),
|
||||
_row(
|
||||
"日亏损次数上限",
|
||||
(
|
||||
f"{daily_loss_limit()} 次"
|
||||
if daily_loss_limit() > 0
|
||||
else "未启用"
|
||||
),
|
||||
"平仓亏损达限后当日冻结开仓;0=不启用" if daily_loss_limit() > 0 else "RISK_DAILY_LOSS_LIMIT=0",
|
||||
),
|
||||
_row(
|
||||
"复盘情绪日冻结",
|
||||
_on_off(mood_issues_daily_freeze_enabled()),
|
||||
|
||||
@@ -86,6 +86,14 @@ def manual_close_daily_limit() -> int:
|
||||
return 2
|
||||
|
||||
|
||||
def daily_loss_limit() -> int:
|
||||
"""日亏损次数上限:达限当日冻结开仓;0=不因亏损次数冻结."""
|
||||
try:
|
||||
return max(0, int(os.getenv("RISK_DAILY_LOSS_LIMIT", "2")))
|
||||
except (TypeError, ValueError):
|
||||
return 2
|
||||
|
||||
|
||||
def max_active_positions_from_env(default: int = 1) -> int:
|
||||
try:
|
||||
return max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", str(default))))
|
||||
@@ -116,6 +124,7 @@ def ensure_account_risk_schema(conn) -> None:
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
trading_day TEXT,
|
||||
manual_close_count INTEGER DEFAULT 0,
|
||||
daily_loss_count INTEGER DEFAULT 0,
|
||||
cooloff_until_ms INTEGER,
|
||||
cooloff_hours INTEGER,
|
||||
daily_frozen INTEGER DEFAULT 0,
|
||||
@@ -124,10 +133,18 @@ def ensure_account_risk_schema(conn) -> None:
|
||||
updated_at TEXT
|
||||
)"""
|
||||
)
|
||||
cols = {
|
||||
str(r[1])
|
||||
for r in conn.execute("PRAGMA table_info(account_risk_state)").fetchall()
|
||||
}
|
||||
if "daily_loss_count" not in cols:
|
||||
conn.execute(
|
||||
"ALTER TABLE account_risk_state ADD COLUMN daily_loss_count INTEGER DEFAULT 0"
|
||||
)
|
||||
row = conn.execute("SELECT id FROM account_risk_state WHERE id=1").fetchone()
|
||||
if not row:
|
||||
conn.execute(
|
||||
"INSERT INTO account_risk_state (id, trading_day, manual_close_count, daily_frozen) VALUES (1, '', 0, 0)"
|
||||
"INSERT INTO account_risk_state (id, trading_day, manual_close_count, daily_loss_count, daily_frozen) VALUES (1, '', 0, 0, 0)"
|
||||
)
|
||||
|
||||
|
||||
@@ -268,6 +285,7 @@ def _sync_trading_day(conn, trading_day: str, now: Optional[datetime] = None) ->
|
||||
"""UPDATE account_risk_state SET
|
||||
trading_day=?,
|
||||
manual_close_count=0,
|
||||
daily_loss_count=0,
|
||||
daily_frozen=0,
|
||||
cooloff_until_ms=?,
|
||||
cooloff_hours=?,
|
||||
@@ -600,6 +618,43 @@ def on_manual_close(
|
||||
)
|
||||
|
||||
|
||||
def on_closed_trade_pnl(
|
||||
conn,
|
||||
*,
|
||||
pnl_amount: Any,
|
||||
trading_day: str,
|
||||
now: Optional[datetime] = None,
|
||||
) -> None:
|
||||
"""
|
||||
已平仓交易记盈亏后调用:亏损笔数达 RISK_DAILY_LOSS_LIMIT 则当日冻结开仓.
|
||||
上限为 0 时不启用本规则.
|
||||
"""
|
||||
if not risk_control_enabled():
|
||||
return
|
||||
limit = daily_loss_limit()
|
||||
if limit <= 0:
|
||||
return
|
||||
try:
|
||||
pnl = float(pnl_amount)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
if pnl >= 0:
|
||||
return
|
||||
row = _sync_trading_day(conn, trading_day, now=now)
|
||||
if int(_row_get(row, "daily_frozen") or 0) == 1:
|
||||
return
|
||||
count = int(_row_get(row, "daily_loss_count") or 0) + 1
|
||||
conn.execute(
|
||||
"""UPDATE account_risk_state SET
|
||||
daily_loss_count=?,
|
||||
updated_at=?
|
||||
WHERE id=1""",
|
||||
(count, (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")),
|
||||
)
|
||||
if count >= limit:
|
||||
_set_daily_frozen(conn, trading_day=trading_day, now=now)
|
||||
|
||||
|
||||
def on_journal_saved(
|
||||
conn,
|
||||
*,
|
||||
@@ -762,6 +817,7 @@ def compute_account_risk_status(
|
||||
"cooloff_until_ms": None,
|
||||
"cooloff_until": None,
|
||||
"manual_close_count": 0,
|
||||
"daily_loss_count": 0,
|
||||
"daily_frozen": False,
|
||||
}
|
||||
row = _sync_trading_day(conn, trading_day, now=now)
|
||||
@@ -784,12 +840,21 @@ def compute_account_risk_status(
|
||||
row = _load_state(conn)
|
||||
cooloff_until_ms = _resolved_cooloff_until_ms(row, now_ms)
|
||||
manual_close_count = int(_row_get(row, "manual_close_count") or 0)
|
||||
daily_loss_count = int(_row_get(row, "daily_loss_count") or 0)
|
||||
loss_limit = daily_loss_limit()
|
||||
|
||||
status = STATUS_NORMAL
|
||||
reason = ""
|
||||
if daily_frozen:
|
||||
status = STATUS_DAILY
|
||||
reason = f"账户今日已冻结(手动平仓 {manual_close_count} 次或复盘情绪标签)"
|
||||
parts = []
|
||||
if loss_limit > 0 and daily_loss_count >= loss_limit:
|
||||
parts.append(f"日亏损 {daily_loss_count}/{loss_limit} 次")
|
||||
if manual_close_count >= manual_close_daily_limit():
|
||||
parts.append(f"手动平仓 {manual_close_count} 次")
|
||||
if not parts:
|
||||
parts.append("手动平仓/日亏损达限或复盘情绪标签")
|
||||
reason = "账户今日已冻结(" + "、".join(parts) + ")"
|
||||
elif cooloff_until_ms is not None:
|
||||
remaining_ms = cooloff_until_ms - now_ms
|
||||
hours = _cooloff_hours_value(row)
|
||||
@@ -818,6 +883,8 @@ def compute_account_risk_status(
|
||||
if fmt_local_ms and cooloff_until_ms
|
||||
else None,
|
||||
"manual_close_count": manual_close_count,
|
||||
"daily_loss_count": daily_loss_count,
|
||||
"daily_loss_limit": loss_limit,
|
||||
"daily_frozen": daily_frozen,
|
||||
"pending_journal_trade_id": pending,
|
||||
"freeze_remaining_sec": freeze_remaining_sec if not can_trade else 0,
|
||||
|
||||
Reference in New Issue
Block a user