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.
63 lines
2.1 KiB
Python
63 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from app.config import Settings
|
|
|
|
# The exact phrase required in LIVE_CONFIRMATION before live trading starts.
|
|
LIVE_CONFIRMATION_PHRASE = "I UNDERSTAND THE RISK"
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class LiveCheck:
|
|
"""Result of the go-live preflight."""
|
|
|
|
ok: bool
|
|
mode: str
|
|
# Each failure is a human-readable reason live trading is blocked.
|
|
blockers: list[str] = field(default_factory=list)
|
|
|
|
def as_dict(self) -> dict:
|
|
return {"ok": self.ok, "mode": self.mode, "blockers": self.blockers}
|
|
|
|
|
|
def check_live_ready(settings: Settings, starting_balance: float | None = None) -> LiveCheck:
|
|
"""Validate that the configuration is safe enough to trade real money.
|
|
|
|
Paper mode always passes. Live mode must satisfy every guard; the point is
|
|
that ``MODE=live`` alone is not sufficient to start live trading.
|
|
"""
|
|
if settings.mode != "live":
|
|
return LiveCheck(ok=True, mode=settings.mode)
|
|
|
|
blockers: list[str] = []
|
|
|
|
has_limit = (
|
|
settings.max_daily_loss > 0
|
|
or settings.max_drawdown_pct > 0
|
|
or settings.max_trades_per_day > 0
|
|
)
|
|
if not has_limit:
|
|
blockers.append(
|
|
"no active risk limit: set MAX_DAILY_LOSS, MAX_DRAWDOWN_PCT or MAX_TRADES_PER_DAY"
|
|
)
|
|
|
|
if settings.max_position_pct >= 1.0:
|
|
blockers.append(
|
|
"MAX_POSITION_PCT must be below 1.0 so a single position cannot be the whole account"
|
|
)
|
|
|
|
if settings.live_confirmation != LIVE_CONFIRMATION_PHRASE:
|
|
blockers.append(f'LIVE_CONFIRMATION must be exactly "{LIVE_CONFIRMATION_PHRASE}"')
|
|
|
|
balance = starting_balance if starting_balance is not None else settings.starting_balance
|
|
if balance < settings.min_live_balance:
|
|
blockers.append(
|
|
f"starting balance {balance:g} is below MIN_LIVE_BALANCE {settings.min_live_balance:g}"
|
|
)
|
|
|
|
if not settings.exchange_api_key or not settings.exchange_api_secret:
|
|
blockers.append("EXCHANGE_API_KEY and EXCHANGE_API_SECRET are required for live")
|
|
|
|
return LiveCheck(ok=not blockers, mode=settings.mode, blockers=blockers)
|