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.
92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
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"
|