Go-live gate: refuse unsafe live trading

Setting MODE=live alone no longer starts trading. check_live_ready requires an
active risk limit, MAX_POSITION_PCT < 1.0, LIVE_CONFIRMATION exactly
"I UNDERSTAND THE RISK", a minimum balance, and exchange credentials. Enforced
in Runtime.start_many (both start paths) and surfaced at GET /api/live-check;
the dashboard shows a banner and blocks the start buttons. Adds tests.
This commit is contained in:
matitos committed 2026-09-22 12:55:12 +00:00
1 parent b60cddf13d
commit 0173e46cfb
10 files changed
+241 -1

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
from __future__ import annotations
import pytest
from app.config import Settings
from app.engine.live_guard import LIVE_CONFIRMATION_PHRASE, check_live_ready
def live_settings(**overrides) -> Settings:
base = {
"mode": "live",
"max_daily_loss": 100.0,
"max_position_pct": 0.5,
"live_confirmation": LIVE_CONFIRMATION_PHRASE,
"exchange_api_key": "k",
"exchange_api_secret": "s",
"starting_balance": 1000.0,
"min_live_balance": 100.0,
}
base.update(overrides)
return Settings(**base)
def test_paper_mode_always_ok() -> None:
check = check_live_ready(Settings(mode="paper", max_position_pct=1.0))
assert check.ok is True
assert check.blockers == []
def test_live_ok_when_fully_configured() -> None:
check = check_live_ready(live_settings())
assert check.ok is True
def test_live_blocked_without_any_risk_limit() -> None:
check = check_live_ready(
live_settings(max_daily_loss=0, max_drawdown_pct=0, max_trades_per_day=0)
)
assert check.ok is False
assert any("risk limit" in b for b in check.blockers)
def test_live_blocked_when_position_cap_is_full_account() -> None:
check = check_live_ready(live_settings(max_position_pct=1.0))
assert check.ok is False
assert any("MAX_POSITION_PCT" in b for b in check.blockers)
def test_live_blocked_without_confirmation_phrase() -> None:
check = check_live_ready(live_settings(live_confirmation=""))
assert check.ok is False
assert any("LIVE_CONFIRMATION" in b for b in check.blockers)
def test_live_blocked_below_min_balance() -> None:
check = check_live_ready(live_settings(starting_balance=50.0))
assert check.ok is False
assert any("MIN_LIVE_BALANCE" in b for b in check.blockers)
def test_live_blocked_without_credentials() -> None:
check = check_live_ready(live_settings(exchange_api_key="", exchange_api_secret=""))
assert check.ok is False
assert any("EXCHANGE_API_KEY" in b for b in check.blockers)
async def test_start_many_refuses_live_without_confirmation(settings) -> None:
from app.runtime import Runtime
from app.schemas import StartManyRequest, StrategySpec
settings.mode = "live"
settings.exchange_api_key = "k"
settings.exchange_api_secret = "s"
settings.max_position_pct = 1.0 # unsafe
runtime = Runtime(settings)
with pytest.raises(ValueError, match="live trading blocked"):
await runtime.start_many(
StartManyRequest(strategies=[StrategySpec(strategy_key="sma_cross")])
)
def test_live_check_endpoint(runtime) -> None:
from fastapi.testclient import TestClient
from app.main import create_app
app = create_app(runtime.settings, runtime)
with TestClient(app) as client:
body = client.get("/api/live-check").json()
assert body["ok"] is True # paper mode
assert body["mode"] == "paper"