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:
+56
-25
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user