feat(risk): add account cooldown and daily freeze after manual/external close

Implements shared account_risk_lib with 4h/1h cooloff and daily freeze rules, wires hooks into all four exchange apps and hub monitor UI, with tests and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-17 17:05:19 +08:00
parent b77741ee21
commit e307eef690
18 changed files with 1015 additions and 5 deletions
+62 -1
View File
@@ -1491,6 +1491,9 @@ def init_db():
from strategy_db import init_strategy_tables
init_strategy_tables(conn)
from account_risk_lib import ensure_account_risk_schema
ensure_account_risk_schema(conn)
backfill_missing_key_signal_types(conn, monitor_type=ORDER_MONITOR_TYPE_KEY_AUTO)
conn.commit()
conn.close()
@@ -1524,6 +1527,18 @@ def get_db():
return conn
def hub_account_risk_status(conn):
from account_risk_lib import compute_account_risk_status, ensure_account_risk_schema
ensure_account_risk_schema(conn)
return compute_account_risk_status(
conn,
trading_day=get_trading_day(),
now=app_now(),
fmt_local_ms=ms_to_app_local_str,
)
def app_now():
"""应用本地时区当前墙钟时间(无时区的 datetime,便于与库中字符串直接比较)。"""
return datetime.now(APP_TZ).replace(tzinfo=None)
@@ -2795,6 +2810,16 @@ def resolve_capital_base_for_key_open(conn, trading_day, live_capital):
def precheck_risk(conn, symbol, direction):
now = app_now()
from account_risk_lib import account_risk_blocks_trading
ok_risk, risk_reason = account_risk_blocks_trading(
conn,
trading_day=get_trading_day(now),
now=now,
fmt_local_ms=ms_to_app_local_str,
)
if not ok_risk:
return False, risk_reason
if not trading_day_reset_allows_new_open(now):
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
active_count = get_active_position_count(conn)
@@ -4137,6 +4162,16 @@ def reconcile_external_closes(conn, days=None):
opened_at=opened_at,
closed_at=closed_at,
)
from account_risk_lib import on_external_close, should_apply_external_close_risk
if should_apply_external_close_risk(result):
close_ms = _to_ms_with_fallback(None, closed_at)
on_external_close(
conn,
closed_at_ms=close_ms,
trading_day=session_date,
now=app_now(),
)
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],))
clear_key_sizing_snapshot_if_flat(conn, r["session_date"] or get_trading_day())
if result in ("止盈", "止损", "保本止盈", "移动止盈", "手动平仓", "强制清仓"):
@@ -6740,12 +6775,14 @@ def render_main_page(page="trade"):
rate = round(win/total*100,2) if total else 0
active_count = len(order_list)
opens_today = count_opens_for_trading_day(conn, trading_day)
risk_status = hub_account_risk_status(conn)
can_trade = can_trade_new_open(
time_allows=trading_day_reset_allows_new_open(now),
active_count=active_count,
max_active_positions=MAX_ACTIVE_POSITIONS,
opens_today=opens_today,
hard_limit=DAILY_OPEN_HARD_LIMIT,
extra_blocks=not risk_status.get("can_trade", True),
)
key_rule_ctx = key_monitor_rule_template_context(
kline_timeframe=KLINE_TIMEFRAME,
@@ -6840,6 +6877,7 @@ def render_main_page(page="trade"):
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR,
exchange_display=EXCHANGE_DISPLAY_NAME,
risk_status=risk_status,
max_active_positions=MAX_ACTIVE_POSITIONS,
manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,
key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR,
@@ -6907,6 +6945,7 @@ def api_account_snapshot():
recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
active_count = get_active_position_count(conn)
opens_today = count_opens_for_trading_day(conn, trading_day)
risk_status = hub_account_risk_status(conn)
conn.close()
can_trade = can_trade_new_open(
time_allows=trading_day_reset_allows_new_open(now),
@@ -6914,6 +6953,7 @@ def api_account_snapshot():
max_active_positions=MAX_ACTIVE_POSITIONS,
opens_today=opens_today,
hard_limit=DAILY_OPEN_HARD_LIMIT,
extra_blocks=not risk_status.get("can_trade", True),
)
available_trading_usdt = get_available_trading_usdt()
return jsonify({
@@ -6928,7 +6968,8 @@ def api_account_snapshot():
"daily_open_hard_limit": DAILY_OPEN_HARD_LIMIT,
"daily_open_alert_threshold": DAILY_OPEN_ALERT_THRESHOLD,
"manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,
"trading_day": trading_day
"trading_day": trading_day,
"risk_status": risk_status,
})
@@ -8522,6 +8563,15 @@ def del_order(id):
opened_at=opened_at,
closed_at=closed_at,
)
from account_risk_lib import insert_trade_record_id, on_manual_close
on_manual_close(
conn,
trade_record_id=insert_trade_record_id(conn),
closed_at_ms=_to_ms_with_fallback(None, closed_at),
trading_day=session_date,
now=app_now(),
)
conn.execute("UPDATE order_monitors SET status='stopped', exchange_close_order_id=? WHERE id=?", (close_order_id, id))
clear_key_sizing_snapshot_if_flat(conn, session_date)
conn.commit()
@@ -8750,6 +8800,16 @@ def add_journal():
d.get("post_breakeven_stare"), d.get("new_trade_while_occupied"), d.get("note"), image_filename
)
)
from account_risk_lib import on_journal_saved
on_journal_saved(
conn,
early_exit_trigger=early_exit_trigger,
early_exit_note=early_exit_note,
mood_issues_raw=mood_issues,
trading_day=get_trading_day(),
now=app_now(),
)
conn.commit()
conn.close()
if chart_msg:
@@ -9250,6 +9310,7 @@ try:
ohlcv_fn=_hub_fetch_ohlcv,
volume_rank_fn=_hub_fetch_volume_rank,
reconcile_hub_flat_fn=reconcile_hub_external_close,
risk_status_fn=hub_account_risk_status,
)
except Exception as _hub_err:
print(f"[hub_bridge] gate: {_hub_err}")