"""Pydantic Settings: legge .env e variabili d'ambiente.""" from __future__ import annotations from pydantic import Field, SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict class _Sub(BaseSettings): """Base per sub-settings, condivide model_config con env_file.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) class DeribitSettings(_Sub): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", env_prefix="DERIBIT_", extra="ignore", ) client_id: str | None = None client_secret: SecretStr | None = None client_id_testnet: str | None = None client_secret_testnet: SecretStr | None = None client_id_live: str | None = None client_secret_live: SecretStr | None = None url_live: str url_testnet: str max_leverage: int = 3 def credentials(self, env: str) -> tuple[str, str]: """Return (client_id, client_secret) for the given env. Prefers env-specific (_TESTNET / _LIVE) pair; falls back to base (DERIBIT_CLIENT_ID / DERIBIT_CLIENT_SECRET) for legacy single-pair setups. """ if env == "testnet": cid = self.client_id_testnet or self.client_id csec = self.client_secret_testnet or self.client_secret elif env == "mainnet": cid = self.client_id_live or self.client_id csec = self.client_secret_live or self.client_secret else: raise ValueError(f"unknown deribit env: {env}") if not cid or csec is None: raise ValueError(f"Deribit credentials not configured for env={env}") return cid, csec.get_secret_value() class BybitSettings(_Sub): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", env_prefix="BYBIT_", extra="ignore", ) api_key: str api_secret: SecretStr url_live: str url_testnet: str max_leverage: int = 3 class HyperliquidSettings(_Sub): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", env_prefix="HYPERLIQUID_", extra="ignore", ) wallet_address: str api_wallet_address: str private_key: SecretStr url_live: str url_testnet: str max_leverage: int = 3 class AlpacaSettings(_Sub): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", env_prefix="ALPACA_", extra="ignore", ) api_key_id: str secret_key: SecretStr url_live: str url_testnet: str max_leverage: int = 1 class IBKRSettings(_Sub): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", env_prefix="IBKR_", extra="ignore", ) consumer_key: str | None = None access_token: str | None = None access_token_secret: SecretStr | None = None signature_key_path: str | None = None encryption_key_path: str | None = None dh_prime: SecretStr | None = None consumer_key_testnet: str | None = None access_token_testnet: str | None = None access_token_secret_testnet: SecretStr | None = None signature_key_path_testnet: str | None = None encryption_key_path_testnet: str | None = None account_id_testnet: str | None = None consumer_key_live: str | None = None access_token_live: str | None = None access_token_secret_live: SecretStr | None = None signature_key_path_live: str | None = None encryption_key_path_live: str | None = None account_id_live: str | None = None url_live: str = "https://api.ibkr.com/v1/api" url_testnet: str = "https://api.ibkr.com/v1/api" ws_url_live: str = "wss://api.ibkr.com/v1/api/ws" ws_url_testnet: str = "wss://api.ibkr.com/v1/api/ws" max_leverage: int = 4 ws_max_subscriptions: int = 80 ws_idle_timeout_s: int = 300 def credentials(self, env: str) -> dict: if env == "testnet": ck = self.consumer_key_testnet or self.consumer_key at = self.access_token_testnet or self.access_token ats = self.access_token_secret_testnet or self.access_token_secret sigp = self.signature_key_path_testnet or self.signature_key_path encp = self.encryption_key_path_testnet or self.encryption_key_path acct = self.account_id_testnet elif env == "mainnet": ck = self.consumer_key_live or self.consumer_key at = self.access_token_live or self.access_token ats = self.access_token_secret_live or self.access_token_secret sigp = self.signature_key_path_live or self.signature_key_path encp = self.encryption_key_path_live or self.encryption_key_path acct = self.account_id_live else: raise ValueError(f"unknown ibkr env: {env}") missing = [ n for n, v in [ ("consumer_key", ck), ("access_token", at), ("access_token_secret", ats), ("signature_key_path", sigp), ("encryption_key_path", encp), ("account_id", acct), ("dh_prime", self.dh_prime), ] if not v ] if missing: raise ValueError( f"IBKR credentials not configured for env={env}: missing {missing}" ) return { "consumer_key": ck, "access_token": at, "access_token_secret": ats.get_secret_value(), # type: ignore[union-attr] "signature_key_path": sigp, "encryption_key_path": encp, "account_id": acct, "dh_prime": self.dh_prime.get_secret_value(), # type: ignore[union-attr] } class MacroSettings(_Sub): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) fred_api_key: SecretStr finnhub_api_key: SecretStr class SentimentSettings(_Sub): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) cryptopanic_key: SecretStr lunarcrush_key: SecretStr class Settings(_Sub): host: str = "0.0.0.0" port: int = 9000 log_level: str = "info" testnet_token: SecretStr mainnet_token: SecretStr deribit: DeribitSettings = Field(default_factory=lambda: DeribitSettings()) # type: ignore[call-arg] bybit: BybitSettings = Field(default_factory=lambda: BybitSettings()) # type: ignore[call-arg] hyperliquid: HyperliquidSettings = Field(default_factory=lambda: HyperliquidSettings()) # type: ignore[call-arg] alpaca: AlpacaSettings = Field(default_factory=lambda: AlpacaSettings()) # type: ignore[call-arg] ibkr: IBKRSettings = Field(default_factory=lambda: IBKRSettings()) # type: ignore[call-arg] macro: MacroSettings = Field(default_factory=lambda: MacroSettings()) # type: ignore[call-arg] sentiment: SentimentSettings = Field(default_factory=lambda: SentimentSettings()) # type: ignore[call-arg]