修复页面握库拉余额导致 database is locked 整页 500。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-07 13:11:37 +08:00
parent c021c38e2d
commit ddacdbc1e8
2 changed files with 160 additions and 113 deletions
+44 -14
View File
@@ -1619,22 +1619,38 @@ def account_risk_status(conn):
ensure_account_risk_schema, ensure_account_risk_schema,
) )
ensure_account_risk_schema(conn)
now = app_now() now = app_now()
st = compute_account_risk_status( try:
conn, ensure_account_risk_schema(conn)
trading_day=get_trading_day(), st = compute_account_risk_status(
now=now, conn,
fmt_local_ms=ms_to_app_local_str, trading_day=get_trading_day(),
) now=now,
st = enrich_risk_status_countdown(st, now=now, daily_reset_hour=TRADING_DAY_RESET_HOUR) fmt_local_ms=ms_to_app_local_str,
from lib.trade.trade_labels_lib import count_position_limit_active_monitors )
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( return apply_position_limit_risk(
st, st,
count_position_limit_active_monitors(conn), count_position_limit_active_monitors(conn),
max_active_positions=MAX_ACTIVE_POSITIONS, 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( def hub_user_initiated_close(
@@ -3863,6 +3879,11 @@ def render_main_page(page="options", embed_mode=None):
conn = get_db() conn = get_db()
session_row = ensure_session(conn, trading_day) session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
try:
conn.commit()
except Exception:
pass
conn.close()
from lib.instance.instance_embed_context_lib import ( from lib.instance.instance_embed_context_lib import (
embed_render_plan, embed_render_plan,
minimal_stats_bundle, minimal_stats_bundle,
@@ -3875,6 +3896,7 @@ def render_main_page(page="options", embed_mode=None):
) )
plan = embed_render_plan(page, embed_mode) plan = embed_render_plan(page, embed_mode)
# 拉交易所余额前不持库连接,避免 database is locked → 页面 500
if plan.exchange_capitals: if plan.exchange_capitals:
funding_capital, trading_capital = get_exchange_capitals() funding_capital, trading_capital = get_exchange_capitals()
else: else:
@@ -3938,6 +3960,7 @@ def render_main_page(page="options", embed_mode=None):
options_margin_mode = "coin" options_margin_mode = "coin"
options_underly = "ETH" options_underly = "ETH"
recommended_capital = get_recommended_capital(capital_for_sizing) recommended_capital = get_recommended_capital(capital_for_sizing)
conn = get_db()
key_list = ( key_list = (
conn.execute("SELECT * FROM key_monitors").fetchall() if plan.key_list else [] conn.execute("SELECT * FROM key_monitors").fetchall() if plan.key_list else []
) )
@@ -4328,7 +4351,13 @@ def api_account_snapshot():
conn = get_db() conn = get_db()
session_row = ensure_session(conn, trading_day) session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"]) 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") force_refresh = (request.args.get("force") or "").strip().lower() in ("1", "true", "yes")
# 拉交易所余额前不持库连接
funding_capital, trading_capital = get_exchange_capitals(force=force_refresh) 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 funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
# 顶栏只展示交易所余额;勿用会话资金回退(避免模拟盘 30U 残留到实盘) # 顶栏只展示交易所余额;勿用会话资金回退(避免模拟盘 30U 残留到实盘)
@@ -4372,6 +4401,7 @@ def api_account_snapshot():
options_margin_mode = "coin" options_margin_mode = "coin"
options_underly = "ETH" options_underly = "ETH"
recommended_capital = get_recommended_capital(capital_for_sizing) recommended_capital = get_recommended_capital(capital_for_sizing)
conn = get_db()
from lib.trade.trade_labels_lib import count_position_limit_active_monitors from lib.trade.trade_labels_lib import count_position_limit_active_monitors
position_limit_count = count_position_limit_active_monitors(conn) position_limit_count = count_position_limit_active_monitors(conn)
+116 -99
View File
@@ -275,33 +275,39 @@ def _load_state(conn):
def _sync_trading_day(conn, trading_day: str, now: Optional[datetime] = None) -> Any: def _sync_trading_day(conn, trading_day: str, now: Optional[datetime] = None) -> Any:
import sqlite3
row = _load_state(conn) row = _load_state(conn)
td = (trading_day or "").strip() td = (trading_day or "").strip()
stored = str(_row_get(row, "trading_day") or "").strip() stored = str(_row_get(row, "trading_day") or "").strip()
if stored != td: if stored != td:
now_ms = _now_ms(now) now_ms = _now_ms(now)
cooloff_active = _resolved_cooloff_until_ms(row, now_ms) cooloff_active = _resolved_cooloff_until_ms(row, now_ms)
conn.execute( try:
"""UPDATE account_risk_state SET conn.execute(
trading_day=?, """UPDATE account_risk_state SET
manual_close_count=0, trading_day=?,
daily_loss_count=0, manual_close_count=0,
daily_frozen=0, daily_loss_count=0,
cooloff_until_ms=?, daily_frozen=0,
cooloff_hours=?, cooloff_until_ms=?,
last_close_at_ms=?, cooloff_hours=?,
pending_journal_trade_id=NULL, last_close_at_ms=?,
updated_at=? pending_journal_trade_id=NULL,
WHERE id=1""", updated_at=?
( WHERE id=1""",
td, (
cooloff_active, td,
_row_get(row, "cooloff_hours") if cooloff_active else None, cooloff_active,
_row_get(row, "last_close_at_ms") if cooloff_active else None, _row_get(row, "cooloff_hours") if cooloff_active else None,
(now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"), _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) )
row = _load_state(conn)
except sqlite3.OperationalError:
# 页面渲染勿因跨日写库锁死而 500;下次再同步
pass
return row return row
@@ -800,6 +806,24 @@ def apply_position_limit_risk(
return out 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( def compute_account_risk_status(
conn, conn,
*, *,
@@ -807,88 +831,81 @@ def compute_account_risk_status(
now: Optional[datetime] = None, now: Optional[datetime] = None,
fmt_local_ms: Optional[Callable[[int], str]] = None, fmt_local_ms: Optional[Callable[[int], str]] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
import sqlite3
if not risk_control_enabled(): if not risk_control_enabled():
return { return _disabled_or_fallback_risk_status(enabled=False)
"enabled": False, try:
"status": STATUS_NORMAL, row = _sync_trading_day(conn, trading_day, now=now)
"status_label": STATUS_LABELS[STATUS_NORMAL], now_ms = _now_ms(now)
"can_trade": True, daily_frozen = int(_row_get(row, "daily_frozen") or 0) == 1
"reason": "", pending = _row_get(row, "pending_journal_trade_id")
"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)
cooloff_until_ms = _resolved_cooloff_until_ms(row, now_ms) cooloff_until_ms = _resolved_cooloff_until_ms(row, now_ms)
manual_close_count = int(_row_get(row, "manual_close_count") or 0) if (
daily_loss_count = int(_row_get(row, "daily_loss_count") or 0) not daily_frozen
loss_limit = daily_loss_limit() 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 status = STATUS_NORMAL
reason = "" reason = ""
if daily_frozen: if daily_frozen:
status = STATUS_DAILY status = STATUS_DAILY
parts = [] parts = []
if loss_limit > 0 and daily_loss_count >= loss_limit: if loss_limit > 0 and daily_loss_count >= loss_limit:
parts.append(f"日亏损 {daily_loss_count}/{loss_limit} 次") parts.append(f"日亏损 {daily_loss_count}/{loss_limit} 次")
if manual_close_count >= manual_close_daily_limit(): if manual_close_count >= manual_close_daily_limit():
parts.append(f"手动平仓 {manual_close_count} 次") parts.append(f"手动平仓 {manual_close_count} 次")
if not parts: if not parts:
parts.append("手动平仓/日亏损达限或复盘情绪标签") parts.append("手动平仓/日亏损达限或复盘情绪标签")
reason = "账户今日已冻结(" + "、".join(parts) + ")" reason = "账户今日已冻结(" + "、".join(parts) + ")"
elif cooloff_until_ms is not None: elif cooloff_until_ms is not None:
remaining_ms = cooloff_until_ms - now_ms remaining_ms = cooloff_until_ms - now_ms
hours = _cooloff_hours_value(row) hours = _cooloff_hours_value(row)
status = _freeze_tier_from_remaining_ms(remaining_ms, hours) 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
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)}中"
label = status_label if until_str:
reason = f"账户{label}中" reason += f",至 {until_str}"
if until_str:
reason += f",至 {until_str}"
can_trade = status == STATUS_NORMAL can_trade = status == STATUS_NORMAL
freeze_remaining_sec = ( freeze_remaining_sec = (
max(0, (cooloff_until_ms - now_ms) // 1000) if cooloff_until_ms is not None else 0 max(0, (cooloff_until_ms - now_ms) // 1000) if cooloff_until_ms is not None else 0
) )
return { return {
"enabled": True, "enabled": True,
"status": status, "status": status,
"status_label": _freeze_status_label(_cooloff_hours_value(row), status) "status_label": _freeze_status_label(_cooloff_hours_value(row), status)
if status in (STATUS_FREEZE_1H, STATUS_FREEZE_4H) if status in (STATUS_FREEZE_1H, STATUS_FREEZE_4H)
else STATUS_LABELS[status], else STATUS_LABELS[status],
"can_trade": can_trade, "can_trade": can_trade,
"reason": reason, "reason": reason,
"cooloff_until_ms": cooloff_until_ms, "cooloff_until_ms": cooloff_until_ms,
"cooloff_until": _ms_to_local_str(cooloff_until_ms, fmt_local_ms) "cooloff_until": _ms_to_local_str(cooloff_until_ms, fmt_local_ms)
if fmt_local_ms and cooloff_until_ms if fmt_local_ms and cooloff_until_ms
else None, else None,
"manual_close_count": manual_close_count, "manual_close_count": manual_close_count,
"daily_loss_count": daily_loss_count, "daily_loss_count": daily_loss_count,
"daily_loss_limit": loss_limit, "daily_loss_limit": loss_limit,
"daily_frozen": daily_frozen, "daily_frozen": daily_frozen,
"pending_journal_trade_id": pending, "pending_journal_trade_id": pending,
"freeze_remaining_sec": freeze_remaining_sec if not can_trade else 0, "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( def account_risk_blocks_trading(