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)