math-backend / config_schema.py
engineportf's picture
Upload folder using huggingface_hub
558db1e verified
Raw
History Blame Contribute Delete
6.23 kB
from pydantic import BaseModel, Field, model_validator
from typing import Dict, List, Any
from constants import DEFAULT_ADV
class ConfigError(Exception):
pass
class BenchmarksConfig(BaseModel):
equity: str = "SPY"
volatility: str = "^VIX"
risk_free: str = "^TNX"
def get(self, key: str, default: Any = None) -> Any:
return getattr(self, key, default)
class AppConfig(BaseModel):
risk_free_rate: float = Field(0.04, ge=0.0, le=0.20)
transaction_cost: float = Field(0.001, ge=0.0, le=0.05)
trading_days_per_year: int = Field(252, ge=100, le=365)
rolling_cov_days: int = 756
currency_symbol: str = "$"
default_adv_proxy: float = Field(DEFAULT_ADV, ge=0.0)
benchmarks: BenchmarksConfig = Field(default_factory=BenchmarksConfig)
single_asset_min: float = Field(-1.0, ge=-1.0, le=1.0)
single_asset_max: float = Field(0.40, ge=0.01, le=1.0)
sector_limit: float = Field(0.40, ge=0.01, le=1.0)
gross_leverage_cap: float = Field(2.0, ge=1.0, le=5.0)
short_borrow_cost: float = Field(0.015, ge=0.0, le=0.50)
max_turnover: float = Field(3.0, ge=0.0)
tax_rate_lt: float = Field(0.20, ge=0.0, le=1.0)
tax_rate_st: float = Field(0.35, ge=0.0, le=1.0)
lt_days: int = Field(366, ge=1)
hrp_tax_lambda: float = Field(2.5, ge=0.0)
cvar_alpha: float = Field(0.95, ge=0.50, le=0.999)
cvar_lambda: float = Field(0.5, ge=0.0, le=20.0)
baseline_risk_factor: float = Field(3.0, ge=0.1, le=25.0)
monte_carlo_sims: int = Field(1500, ge=100, le=100_000)
monte_carlo_years: float = Field(1.0, ge=0.1, le=50.0)
garch_enabled: bool = True
cvar_enabled: bool = True
tax_enabled: bool = False
dynamic_risk: bool = True
hmm_regime: bool = True
arima_enabled: bool = False
anova_enabled: bool = False
with_futures: bool = False
overlay_mode: str = "beta_hedge"
futures_universe: List[str] = Field(default_factory=lambda: ["MES", "ES"])
futures_safety_multiplier: float = Field(3.0, ge=0.0, le=10.0)
futures_target_beta: float = Field(0.0, ge=-5.0, le=5.0)
futures_margin_headroom: float = Field(0.05, ge=0.0, le=1.0)
return_frequency: str = "daily"
# End-to-End Differentiable Optimization (Model 6)
e2e_loss_type: str = "spo" # "spo", "sharpe", or "calmar"
e2e_epochs: int = Field(default=50)
e2e_batch_size: int = Field(default=32)
e2e_lr: float = 1e-3
e2e_cache_dir: str = ".e2e_cache"
universe_categories: Dict[str, List[str]] = Field(default_factory=lambda: {
"Core Equities": ["SPY", "QQQ", "DIA", "IWM"],
"Bonds & Rates": ["TLT", "IEF", "SHY", "AGG"],
"Tech & Growth": ["AAPL", "MSFT", "NVDA", "TSLA"],
"Defensive/Value": ["JNJ", "PG", "KO", "XLP"],
"Commodities": ["GLD", "SLV", "USO", "PDBC"],
"International": ["VEA", "VWO", "EFA", "EEM"],
"Crypto Proxies": ["IBIT", "FBTC", "ETHE", "MSTR"]
})
# ─────────────────────────────────────────────
# EXTENDED HISTORY & BOOTSTRAPPING
# ─────────────────────────────────────────────
extended_history: bool = False
bootstrap_samples: int = 100
stitch_overlap_days: int = 252
proxy_mappings: Dict[str, Dict] = Field(default_factory=lambda: {
'SPY': {'proxy': '^GSPC', 'proxy_start': '1950-01-03', 'overlap_days': 252},
'TLT': {'proxy': '^TYX', 'proxy_start': '1977-01-03', 'is_yield': True},
'GLD': {'proxy': 'GC=F', 'proxy_start': '1974-12-31'},
'QQQ': {'proxy': '^IXIC', 'proxy_start': '1971-02-05'}
})
bond_metadata: Dict[str, Any] = Field(default_factory=dict)
model_config = {"extra": "allow"}
@model_validator(mode='after')
def check_logic(self):
if self.single_asset_min > self.single_asset_max:
raise ConfigError("single_asset_min cannot be greater than single_asset_max.")
if self.sector_limit < self.single_asset_max:
raise ConfigError(f"sector_limit ({self.sector_limit}) cannot be smaller than single_asset_max ({self.single_asset_max}).")
if self.tax_rate_lt > self.tax_rate_st:
raise ConfigError("Long-term tax rate is mathematically expected to be <= short-term tax rate.")
return self
def get(self, key: str, default: Any = None) -> Any:
"""Backward compatibility for dict-like access."""
if hasattr(self, key):
return getattr(self, key)
if self.model_extra is not None and key in self.model_extra:
return self.model_extra[key]
return default
def setdefault(self, key: str, default: Any = None) -> Any:
val = self.get(key, None)
if val is None:
self[key] = default
return default
return val
def __getitem__(self, key: str) -> Any:
if hasattr(self, key):
return getattr(self, key)
if self.model_extra is not None and key in self.model_extra:
return self.model_extra[key]
raise KeyError(key)
def __setitem__(self, key: str, value: Any) -> None:
if hasattr(self, key) or key in self.model_fields:
setattr(self, key, value)
elif key.startswith('_'):
# Internal transient keys (e.g., _risk_input, _is_historical_backtest) are allowed silently
if self.model_extra is None:
self.__dict__['__pydantic_extra__'] = {}
self.model_extra[key] = value
else:
import warnings
warnings.warn(
f"AppConfig: setting unknown key '{key}'. Did you mean one of "
f"{sorted(list(self.model_fields.keys()))[:8]}...?",
stacklevel=2
)
if self.model_extra is None:
self.__dict__['__pydantic_extra__'] = {}
self.model_extra[key] = value
def update(self, other: dict) -> None:
for k, v in other.items():
setattr(self, k, v)
DEFAULT_CONFIG = AppConfig()