"""Runtime configuration (environment-injected; never log secrets).""" from __future__ import annotations from functools import lru_cache from typing import Annotated, Literal from pydantic import Field, field_validator from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict TradingMode = Literal["offline", "paper", "live"] class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix="KTRADE_", env_file=".env", extra="ignore") env: str = "dev" app_base_url: str = "http://127.0.0.1:8471" api_host: str = "127.0.0.1" api_port: int = 8470 log_level: str = "INFO" log_json: bool = True database_url: str = "postgresql+asyncpg://ktrade:ktrade@127.0.0.1:5432/ktrade" redis_url: str = "redis://127.0.0.1:6379/3" # Session / auth secret_key: str = Field(default="dev-insecure-change-me", description="Signs session cookies") owner_password_hash: str = Field(default="", description="bcrypt hash of the owner password") session_ttl_seconds: int = 12 * 3600 cookie_secure: bool = False # Kraken — three least-privilege key pairs. Data/account keys must NOT have trade permission; # the trade key is only read when live mode is armed. Withdrawal permission is never used. kraken_rest_url: str = "https://api.kraken.com" kraken_ws_url: str = "wss://ws.kraken.com/v2" kraken_ws_auth_url: str = "wss://ws-auth.kraken.com/v2" kraken_api_key: str = "" kraken_api_secret: str = "" kraken_trade_api_key: str = "" kraken_trade_api_secret: str = "" kraken_tier: Literal["starter", "intermediate", "pro"] = "starter" # Universe & ingestion universe: Annotated[list[str], NoDecode] = Field(default_factory=lambda: ["BTC/USD", "ETH/USD", "SOL/USD", "XRP/USD", "ADA/USD", "LINK/USD", "DOT/USD", "LTC/USD"]) candle_intervals: Annotated[list[int], NoDecode] = Field(default_factory=lambda: [1, 5, 60, 1440]) backfill_poll_seconds: int = 60 book_depth: int = 25 stale_data_seconds: int = 120 # Trading mode gates trading_mode: TradingMode = "offline" live_trading_enabled: bool = False # feature flag; UI activation also required (Phase 5) # Observability metrics_enabled: bool = True @field_validator("universe", "candle_intervals", mode="before") @classmethod def _split_csv(cls, v): # noqa: ANN001 if isinstance(v, str): parts = [p.strip() for p in v.split(",") if p.strip()] return [int(p) if p.isdigit() else p for p in parts] return v @property def sync_database_url(self) -> str: return self.database_url.replace("+asyncpg", "") def has_data_key(self) -> bool: return bool(self.kraken_api_key and self.kraken_api_secret) def has_trade_key(self) -> bool: return bool(self.kraken_trade_api_key and self.kraken_trade_api_secret) @lru_cache def get_settings() -> Settings: return Settings()