Harden strategy SoT: fail-closed funds gate, shared open pipeline, LIVE switch guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-30 01:06:32 +08:00
parent 4c537e7520
commit 200702d066
14 changed files with 471 additions and 139 deletions
+9 -2
View File
@@ -83,10 +83,17 @@ async def download_backup(
raise HTTPException(status_code=400, detail=str(e)) from e
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
from starlette.background import BackgroundTask
from ..backup import materialize_download_zip
# 下载包剥离 .env,机内完整备份仍保留供恢复
safe = materialize_download_zip(path)
return FileResponse(
path,
safe,
media_type="application/zip",
filename=path.name,
filename=path.name.replace(".zip", "_noenv.zip"),
background=BackgroundTask(lambda: safe.unlink(missing_ok=True)),
)
+6
View File
@@ -230,6 +230,12 @@ async def funds_transfer(
)
finally:
client.close()
try:
from ..strategy.open_capacity import invalidate_live_balance_cache
invalidate_live_balance_cache()
except Exception:
pass
if not r.get("ok"):
raise HTTPException(status_code=400, detail=r.get("detail") or "划转失败")
return r
+50
View File
@@ -315,6 +315,32 @@ async def put_strategy_settings(
detail="有未平仓,无法切换永续保证金模式;请先平仓后再改",
)
# 持仓中禁止改动会影响本组成交/出场的参数(基线:运行中无人工开平仓,策略锁定本组成交)
if Matcher(db).has_open_position():
locked_keys = (
"net_profit_target",
"exit_mode",
"premium_exit_multiple",
"perp_qty_eth",
"option_qty_eth",
"leverage",
"sizing_mode",
"risk_perp_unit",
"risk_option_unit",
"risk_exit_unit",
"risk_loss_mode",
"risk_loss_pct",
"risk_loss_usdt",
"risk_capital_source",
"risk_manual_capital_usdt",
)
hit = [k for k in locked_keys if k in data]
if hit:
raise HTTPException(
status_code=409,
detail=f"有未平仓,禁止修改本组成交相关参数:{', '.join(hit)};请先平仓",
)
# 以损定仓 ↔ 手动仓位互斥;开启以损定仓时强制 fixed_usdt,并忽略手填名义/出场
sizing_mode = str(
data.get(
@@ -458,6 +484,12 @@ async def put_runtime_settings(
status_code=400,
detail="切换到 LIVE 须在 confirm_live_phrase 传入 LIVE",
)
secret = (s.auth_secret or "").strip()
if not secret or secret == "change-me-eth-hedge-sim-secret":
raise HTTPException(
status_code=400,
detail="切到 LIVE 前请在 .env 设置非默认 AUTH_SECRET",
)
updates: dict[str, str] = {}
if body.okx_api_key is not None and body.okx_api_key.strip():
@@ -495,6 +527,24 @@ async def put_runtime_settings(
detail="切到 LIVE 前请先配置完整币安 API Key/Secret",
)
# 热切 LIVE:强制暂停策略(对齐冷启动护栏;运行中禁止人工开平仓基线)
if new_mode == "LIVE" and cur_mode != "LIVE":
db.execute(
"UPDATE strategy_state SET running=0, phase=?, last_error=? WHERE id=1",
("paused", "已切换 LIVE,策略已强制暂停;确认就绪后再启动"),
)
try:
from ..strategy import get_engine
# 同步停循环标志(pause 为 async,此处只写状态)
get_engine()._set_state(
running=0,
phase="paused",
last_error="已切换 LIVE,策略已强制暂停;确认就绪后再启动",
)
except Exception:
pass
try:
from ..strategy import get_engine
+4 -30
View File
@@ -105,41 +105,15 @@ async def sim_open_group(
wkey = window_key()
db = get_db()
from ..strategy.risk_sizing import apply_risk_sizing_to_ledger
from ..strategy.open_pipeline import size_and_gate
rs = apply_risk_sizing_to_ledger(
prep = size_and_gate(
index_px=float(pick.underlying_px),
option_ask=sizing_ask,
db=db,
)
if not rs.ok:
raise HTTPException(status_code=409, detail=rs.detail)
try:
from ..strategy.auto_usdc import ensure_okx_trading_usdc
ensure_okx_trading_usdc(db)
except Exception:
pass
try:
from ..strategy.open_capacity import assess_open_capacity
cap = assess_open_capacity(db)
if cap.get("perp_can_open") is False or cap.get("option_can_open") is False:
detail = (
f"{cap.get('perp_label')} · {cap.get('option_label')}"
f"永续需≈{cap.get('perp_need_usdt')}U/有{cap.get('perp_have_usdt')}U"
f"期权需≈{cap.get('option_need_usdc')}U/有{cap.get('option_have_usdc')}U"
)
raise HTTPException(
status_code=409,
detail=f"资金不足,暂不可开新仓:{detail}",
)
except HTTPException:
raise
except Exception:
pass
if not prep.ok:
raise HTTPException(status_code=409, detail=prep.detail)
count = len(
db.fetchall("SELECT group_id FROM groups WHERE group_id LIKE ?", (f"G-{wkey}-%",))