From ddacdbc1e8a17756747f052b54e6b5d59944e88f Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 7 Oct 2026 13:11:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A1=B5=E9=9D=A2=E6=8F=A1?= =?UTF-8?q?=E5=BA=93=E6=8B=89=E4=BD=99=E9=A2=9D=E5=AF=BC=E8=87=B4=20databa?= =?UTF-8?q?se=20is=20locked=20=E6=95=B4=E9=A1=B5=20500=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app.py | 58 ++++++--- lib/trade/account_risk_lib.py | 215 ++++++++++++++++++---------------- 2 files changed, 160 insertions(+), 113 deletions(-) diff --git a/app.py b/app.py index a1db786..c77899b 100644 --- a/app.py +++ b/app.py @@ -1619,22 +1619,38 @@ def account_risk_status(conn): ensure_account_risk_schema, ) - ensure_account_risk_schema(conn) now = app_now() - st = compute_account_risk_status( - conn, - trading_day=get_trading_day(), - now=now, - fmt_local_ms=ms_to_app_local_str, - ) - st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) - from lib.trade.trade_labels_lib import count_position_limit_active_monitors + try: + ensure_account_risk_schema(conn) + st = compute_account_risk_status( + conn, + trading_day=get_trading_day(), + now=now, + fmt_local_ms=ms_to_app_local_str, + ) + st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) + from lib.trade.trade_labels_lib import count_position_limit_active_monitors - return apply_position_limit_risk( - st, - count_position_limit_active_monitors(conn), - max_active_positions=MAX_ACTIVE_POSITIONS, - ) + return apply_position_limit_risk( + st, + count_position_limit_active_monitors(conn), + max_active_positions=MAX_ACTIVE_POSITIONS, + ) + except Exception as e: + print(f"[account_risk_status] {e}", flush=True) + return { + "enabled": True, + "status": "normal", + "status_label": "正常", + "can_trade": True, + "reason": "", + "cooloff_until_ms": None, + "cooloff_until": None, + "manual_close_count": 0, + "daily_loss_count": 0, + "daily_frozen": False, + "freeze_remaining_sec": 0, + } def hub_user_initiated_close( @@ -3863,6 +3879,11 @@ def render_main_page(page="options", embed_mode=None): conn = get_db() session_row = ensure_session(conn, trading_day) local_current_capital = float(session_row["current_capital"]) + try: + conn.commit() + except Exception: + pass + conn.close() from lib.instance.instance_embed_context_lib import ( embed_render_plan, minimal_stats_bundle, @@ -3875,6 +3896,7 @@ def render_main_page(page="options", embed_mode=None): ) plan = embed_render_plan(page, embed_mode) + # 拉交易所余额前不持库连接,避免 database is locked → 页面 500 if plan.exchange_capitals: funding_capital, trading_capital = get_exchange_capitals() else: @@ -3938,6 +3960,7 @@ def render_main_page(page="options", embed_mode=None): options_margin_mode = "coin" options_underly = "ETH" recommended_capital = get_recommended_capital(capital_for_sizing) + conn = get_db() key_list = ( conn.execute("SELECT * FROM key_monitors").fetchall() if plan.key_list else [] ) @@ -4328,7 +4351,13 @@ def api_account_snapshot(): conn = get_db() session_row = ensure_session(conn, trading_day) local_current_capital = float(session_row["current_capital"]) + try: + conn.commit() + except Exception: + pass + conn.close() force_refresh = (request.args.get("force") or "").strip().lower() in ("1", "true", "yes") + # 拉交易所余额前不持库连接 funding_capital, trading_capital = get_exchange_capitals(force=force_refresh) funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None # 顶栏只展示交易所余额;勿用会话资金回退(避免模拟盘 30U 残留到实盘) @@ -4372,6 +4401,7 @@ def api_account_snapshot(): options_margin_mode = "coin" options_underly = "ETH" recommended_capital = get_recommended_capital(capital_for_sizing) + conn = get_db() from lib.trade.trade_labels_lib import count_position_limit_active_monitors position_limit_count = count_position_limit_active_monitors(conn) diff --git a/lib/trade/account_risk_lib.py b/lib/trade/account_risk_lib.py index dd24098..f104c7b 100644 --- a/lib/trade/account_risk_lib.py +++ b/lib/trade/account_risk_lib.py @@ -275,33 +275,39 @@ def _load_state(conn): def _sync_trading_day(conn, trading_day: str, now: Optional[datetime] = None) -> Any: + import sqlite3 + row = _load_state(conn) td = (trading_day or "").strip() stored = str(_row_get(row, "trading_day") or "").strip() if stored != td: now_ms = _now_ms(now) cooloff_active = _resolved_cooloff_until_ms(row, now_ms) - conn.execute( - """UPDATE account_risk_state SET - trading_day=?, - manual_close_count=0, - daily_loss_count=0, - daily_frozen=0, - cooloff_until_ms=?, - cooloff_hours=?, - last_close_at_ms=?, - pending_journal_trade_id=NULL, - updated_at=? - WHERE id=1""", - ( - td, - cooloff_active, - _row_get(row, "cooloff_hours") if cooloff_active else None, - _row_get(row, "last_close_at_ms") if cooloff_active else None, - (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"), - ), - ) - row = _load_state(conn) + try: + conn.execute( + """UPDATE account_risk_state SET + trading_day=?, + manual_close_count=0, + daily_loss_count=0, + daily_frozen=0, + cooloff_until_ms=?, + cooloff_hours=?, + last_close_at_ms=?, + pending_journal_trade_id=NULL, + updated_at=? + WHERE id=1""", + ( + td, + cooloff_active, + _row_get(row, "cooloff_hours") if cooloff_active else None, + _row_get(row, "last_close_at_ms") if cooloff_active else None, + (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"), + ), + ) + row = _load_state(conn) + except sqlite3.OperationalError: + # 页面渲染勿因跨日写库锁死而 500;下次再同步 + pass return row @@ -800,6 +806,24 @@ def apply_position_limit_risk( return out +def _disabled_or_fallback_risk_status(*, enabled: bool = False) -> dict[str, Any]: + return { + "enabled": bool(enabled), + "status": STATUS_NORMAL, + "status_label": STATUS_LABELS[STATUS_NORMAL], + "can_trade": True, + "reason": "", + "cooloff_until_ms": None, + "cooloff_until": None, + "manual_close_count": 0, + "daily_loss_count": 0, + "daily_frozen": False, + "daily_loss_limit": daily_loss_limit() if enabled else 0, + "pending_journal_trade_id": None, + "freeze_remaining_sec": 0, + } + + def compute_account_risk_status( conn, *, @@ -807,88 +831,81 @@ def compute_account_risk_status( now: Optional[datetime] = None, fmt_local_ms: Optional[Callable[[int], str]] = None, ) -> dict[str, Any]: + import sqlite3 + if not risk_control_enabled(): - return { - "enabled": False, - "status": STATUS_NORMAL, - "status_label": STATUS_LABELS[STATUS_NORMAL], - "can_trade": True, - "reason": "", - "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) - now_ms = _now_ms(now) - daily_frozen = int(_row_get(row, "daily_frozen") or 0) == 1 - pending = _row_get(row, "pending_journal_trade_id") - cooloff_until_ms = _resolved_cooloff_until_ms(row, now_ms) - if ( - not daily_frozen - and cooloff_until_ms is not None - and _journaled_manual_cooloff_expired( - conn, trading_day=trading_day, now_ms=now_ms, pending=pending - ) - ): - cooloff_until_ms = None - if not daily_frozen: - _repair_stale_cooloff_row( - conn, row, now_ms=now_ms, resolved_until_ms=cooloff_until_ms, now=now - ) - row = _load_state(conn) + return _disabled_or_fallback_risk_status(enabled=False) + try: + row = _sync_trading_day(conn, trading_day, now=now) + now_ms = _now_ms(now) + daily_frozen = int(_row_get(row, "daily_frozen") or 0) == 1 + pending = _row_get(row, "pending_journal_trade_id") 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() + if ( + not daily_frozen + and cooloff_until_ms is not None + and _journaled_manual_cooloff_expired( + conn, trading_day=trading_day, now_ms=now_ms, pending=pending + ) + ): + cooloff_until_ms = None + if not daily_frozen: + _repair_stale_cooloff_row( + conn, row, now_ms=now_ms, resolved_until_ms=cooloff_until_ms, now=now + ) + 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 - 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) - status = _freeze_tier_from_remaining_ms(remaining_ms, hours) - status_label = _freeze_status_label(hours, status) - until_str = _ms_to_local_str(cooloff_until_ms, fmt_local_ms) if fmt_local_ms else None - label = status_label - reason = f"账户{label}中" - if until_str: - reason += f",至 {until_str}" + status = STATUS_NORMAL + reason = "" + if daily_frozen: + status = STATUS_DAILY + 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) + status = _freeze_tier_from_remaining_ms(remaining_ms, hours) + until_str = _ms_to_local_str(cooloff_until_ms, fmt_local_ms) if fmt_local_ms else None + reason = f"账户{_freeze_status_label(hours, status)}中" + if until_str: + reason += f",至 {until_str}" - can_trade = status == STATUS_NORMAL - freeze_remaining_sec = ( - max(0, (cooloff_until_ms - now_ms) // 1000) if cooloff_until_ms is not None else 0 - ) - return { - "enabled": True, - "status": status, - "status_label": _freeze_status_label(_cooloff_hours_value(row), status) - if status in (STATUS_FREEZE_1H, STATUS_FREEZE_4H) - else STATUS_LABELS[status], - "can_trade": can_trade, - "reason": reason, - "cooloff_until_ms": cooloff_until_ms, - "cooloff_until": _ms_to_local_str(cooloff_until_ms, fmt_local_ms) - 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, - } + can_trade = status == STATUS_NORMAL + freeze_remaining_sec = ( + max(0, (cooloff_until_ms - now_ms) // 1000) if cooloff_until_ms is not None else 0 + ) + return { + "enabled": True, + "status": status, + "status_label": _freeze_status_label(_cooloff_hours_value(row), status) + if status in (STATUS_FREEZE_1H, STATUS_FREEZE_4H) + else STATUS_LABELS[status], + "can_trade": can_trade, + "reason": reason, + "cooloff_until_ms": cooloff_until_ms, + "cooloff_until": _ms_to_local_str(cooloff_until_ms, fmt_local_ms) + 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, + } + except sqlite3.OperationalError: + # 渲染路径遇 database is locked 时降级为正常,避免整页 500 + return _disabled_or_fallback_risk_status(enabled=True) def account_risk_blocks_trading(