Files
Cerbero-mcp/src/cerbero_mcp/settings.py
T
root 3a85ff05e6 feat(V2): IBKR settings + env-specific credentials
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:00:15 +00:00

207 lines
7.0 KiB
Python

"""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]