fix(risk): trigger cooldown only on user-initiated closes

Remove external-close risk hooks; register user_instance, user_hub, and user_trend_stop via hub API and trend stop; update docs and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-17 19:14:05 +08:00
parent 850ffcd7d2
commit b6acbf4b2c
14 changed files with 423 additions and 131 deletions
+56 -25
View File
@@ -26,7 +26,18 @@ MOOD_ISSUE_OPTIONS = (
"重仓违规",
)
EXTERNAL_CLOSE_RESULTS = frozenset({"外部平仓"})
# 仅以下来源计入「手动平仓」风控(用户主动点平仓/结束计划)
CLOSE_SOURCE_USER_INSTANCE = "user_instance"
CLOSE_SOURCE_USER_HUB = "user_hub"
CLOSE_SOURCE_USER_TREND_STOP = "user_trend_stop"
USER_INITIATED_CLOSE_SOURCES = frozenset(
{
CLOSE_SOURCE_USER_INSTANCE,
CLOSE_SOURCE_USER_HUB,
CLOSE_SOURCE_USER_TREND_STOP,
}
)
def _env_bool(key: str, default: bool = True) -> bool:
@@ -52,10 +63,6 @@ def cooling_hours_manual() -> float:
return _env_hours("RISK_COOLING_HOURS_MANUAL", 4.0)
def cooling_hours_external() -> float:
return _env_hours("RISK_COOLING_HOURS_EXTERNAL", 4.0)
def cooling_hours_manual_journal() -> float:
return _env_hours("RISK_COOLING_HOURS_MANUAL_JOURNAL", 1.0)
@@ -185,26 +192,26 @@ def parse_mood_issues(raw: Any) -> list[str]:
return [p for p in parts if p in MOOD_ISSUE_OPTIONS]
def on_manual_close(
def _record_one_user_initiated_close(
conn,
*,
trade_record_id: int,
source: str,
trade_record_id: Optional[int],
closed_at_ms: Optional[int],
trading_day: str,
now: Optional[datetime] = None,
) -> None:
if not risk_control_enabled():
return
row = _sync_trading_day(conn, trading_day, now=now)
count = int(_row_get(row, "manual_close_count") or 0) + 1
close_ms = int(closed_at_ms) if closed_at_ms else _now_ms(now)
pending = int(trade_record_id) if trade_record_id else None
conn.execute(
"""UPDATE account_risk_state SET
manual_close_count=?,
pending_journal_trade_id=?,
updated_at=?
WHERE id=1""",
(count, int(trade_record_id), (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")),
(count, pending, (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")),
)
if count >= manual_close_daily_limit():
_set_daily_frozen(conn, trading_day=trading_day, now=now)
@@ -218,26 +225,54 @@ def on_manual_close(
)
def on_external_close(
def on_user_initiated_close(
conn,
*,
source: str,
trade_record_id: Optional[int] = None,
closed_at_ms: Optional[int] = None,
trading_day: str,
now: Optional[datetime] = None,
count: int = 1,
) -> None:
"""用户主动平仓/结束趋势计划:计入手动平仓次数与冷静期。"""
if not risk_control_enabled():
return
src = (source or "").strip()
if src not in USER_INITIATED_CLOSE_SOURCES:
return
n = max(1, int(count or 1))
for i in range(n):
_record_one_user_initiated_close(
conn,
source=src,
trade_record_id=trade_record_id if i == 0 else None,
closed_at_ms=closed_at_ms,
trading_day=trading_day,
now=now,
)
row = _load_state(conn)
if int(_row_get(row, "daily_frozen") or 0) == 1:
break
def on_manual_close(
conn,
*,
trade_record_id: int,
closed_at_ms: Optional[int],
trading_day: str,
now: Optional[datetime] = None,
) -> None:
if not risk_control_enabled():
return
close_ms = int(closed_at_ms) if closed_at_ms else _now_ms(now)
_set_cooloff(
"""兼容旧调用:等同实例页用户平仓。"""
on_user_initiated_close(
conn,
source=CLOSE_SOURCE_USER_INSTANCE,
trade_record_id=trade_record_id,
closed_at_ms=closed_at_ms,
trading_day=trading_day,
close_at_ms=close_ms,
hours=cooling_hours_external(),
now=now,
)
conn.execute(
"UPDATE account_risk_state SET pending_journal_trade_id=NULL, updated_at=? WHERE id=1",
((now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),),
count=1,
)
@@ -357,10 +392,6 @@ def account_risk_blocks_trading(
return False, str(st.get("reason") or STATUS_LABELS.get(st.get("status"), "账户冻结"))
def should_apply_external_close_risk(result: str) -> bool:
return (result or "").strip() in EXTERNAL_CLOSE_RESULTS
def insert_trade_record_id(conn) -> int:
row = conn.execute("SELECT last_insert_rowid()").fetchone()
return int(row[0] if row else 0)