""" models.py — Pydantic v2 models and enums for ChanMacro. All market state types, factor scores, and database record models. """ from datetime import date as Date from enum import Enum from typing import Optional from pydantic import BaseModel, Field, field_validator # ═══════════════════════════════════════════════════════════════ # Shared validators # ═══════════════════════════════════════════════════════════════ def _parse_date(v): """Reusable date-string parser for field_validator.""" if isinstance(v, str): return Date.fromisoformat(v) return v # ═══════════════════════════════════════════════════════════════ # Enums # ═══════════════════════════════════════════════════════════════ class MarketRegime(str, Enum): """V1: 3-state regime (factor-locked: Price + Breadth + Vol).""" TREND = "TREND" RANGE = "RANGE" PANIC = "PANIC" class OIState(str, Enum): """Discrete OI × Price state machine. NOT compressed into a score.""" NEW_LONGS = "New Longs" SHORT_COVERING = "Short Covering" NEW_SHORTS = "New Shorts" LONG_EXIT = "Long Exit" NEUTRAL = "Neutral" class BreadthBucket(str, Enum): """Quantile-based breadth buckets — always have samples regardless of cycle.""" EXTREME = "EXTREME" STRONG = "STRONG" NORMAL = "NORMAL" WEAK = "WEAK" PANIC = "PANIC" class VolRegime(str, Enum): """Volatility regime classification.""" LOW_VOL = "LOW_VOL" NORMAL_VOL = "NORMAL_VOL" HIGH_VOL = "HIGH_VOL" EXPLOSIVE_VOL = "EXPLOSIVE_VOL" class MacroDirection(str, Enum): BULLISH = "bullish" NEUTRAL = "neutral" BEARISH = "bearish" class MarketEmotion(str, Enum): EXTREME_FEAR = "Extreme Fear" FEAR = "Fear" NEUTRAL = "Neutral" GREED = "Greed" EXTREME_GREED = "Extreme Greed" class FlowState(str, Enum): STRONG_INFLOW = "Strong Inflow" INFLOW = "Inflow" NEUTRAL = "Neutral" OUTFLOW = "Outflow" STRONG_OUTFLOW = "Strong Outflow" class CapitalState(str, Enum): ENTERING = "Entering" STABLE = "Stable" EXITING = "Exiting" class SufficiencyLevel(str, Enum): HIGH = "HIGH" MEDIUM = "MEDIUM" LOW = "LOW" INSUFFICIENT = "INSUFFICIENT" class SignalGrade(str, Enum): A = "A" B = "B" C = "C" # ═══════════════════════════════════════════════════════════════ # L0: Raw Data Models # ═══════════════════════════════════════════════════════════════ class OHLCVDaily(BaseModel): _parse_date = field_validator("date", mode="before")(_parse_date) date: Date symbol: str open: float high: float low: float close: float volume: float ema20: Optional[float] = None ema60: Optional[float] = None ema120: Optional[float] = None atr_14: Optional[float] = None bb_width: Optional[float] = None adx_14: Optional[float] = None class BreadthRecord(BaseModel): _parse_date = field_validator("date", mode="before")(_parse_date) date: Date total_tracked: int = 50 advance_top50: int = 0 decline_top50: int = 0 above_ema20_top50: int = 0 new_highs_20d_top50: int = 0 btc_dominance: Optional[float] = None advance_top20: int = 0 advance_top30: int = 0 above_ema20_top20: int = 0 above_ema20_top30: int = 0 new_highs_20d_top20: int = 0 new_highs_20d_top30: int = 0 class DerivativesRecord(BaseModel): _parse_date = field_validator("date", mode="before")(_parse_date) date: Date symbol: str = "BTC/USDT:USDT" funding_rate: Optional[float] = None open_interest: Optional[float] = None oi_24h_change_pct: Optional[float] = None long_liquidations: Optional[float] = None short_liquidations: Optional[float] = None basis_annualised_pct: Optional[float] = None source: str = "binance" class ETFFlowRecord(BaseModel): _parse_date = field_validator("date", mode="before")(_parse_date) date: Date product: str net_flow_million: float price: Optional[float] = None source: str = "farside" class StablecoinSupplyRecord(BaseModel): _parse_date = field_validator("date", mode="before")(_parse_date) date: Date token: str chain: str = "all" supply: float source: str = "defillama" # ═══════════════════════════════════════════════════════════════ # L1: Factor Score Models # ═══════════════════════════════════════════════════════════════ class FactorScore(BaseModel): """Single factor scoring output.""" name: str = "" score: float = Field(default=50.0, ge=0.0, le=100.0) label: str = "" direction: MacroDirection = MacroDirection.NEUTRAL sub_scores: dict = Field(default_factory=dict) narrative: str = "" class PriceStructureScore(FactorScore): """Price Structure — 3 sub-dimensions.""" trend_strength: float = 0.0 volatility_compression: float = 0.0 momentum: float = 0.0 class BreadthScore(FactorScore): """Breadth — multi-tier market diffusion.""" breadth_top20: float = 0.0 breadth_top30: float = 0.0 breadth_top50: float = 0.0 breadth_bucket: BreadthBucket = BreadthBucket.NORMAL breadth_divergence: float = 0.0 advance_pct_top50: float = 0.0 above_ema20_pct_top50: float = 0.0 new_highs_top50: int = 0 btc_dominance_7d_chg: Optional[float] = None class OIMatrixScore(FactorScore): """OI Matrix — discrete state + continuous score.""" oi_state: OIState = OIState.NEUTRAL price_change_pct: float = 0.0 oi_change_pct: float = 0.0 class VolatilityRegimeScore(FactorScore): """Volatility regime classification.""" vol_regime: VolRegime = VolRegime.NORMAL_VOL atr_pct: float = 0.0 hv_ratio: float = 1.0 bb_width_ratio: float = 1.0 # ═══════════════════════════════════════════════════════════════ # L4: Market State Vector (the final product) # ═══════════════════════════════════════════════════════════════ class MarketStateVector(BaseModel): """L4: Complete market state description. NOT compressed into one number.""" _parse_date = field_validator("date", mode="before")(_parse_date) date: Date symbol: str = "BTC/USDT:USDT" regime: MarketRegime regime_confidence: float = Field(ge=0.0, le=1.0) regime_version: str regime_maturity_score: float = Field(ge=0.0, le=100.0, default=50.0) breadth_top20: float = Field(default=50.0, ge=0.0, le=100.0) breadth_top30: float = Field(default=50.0, ge=0.0, le=100.0) breadth_top50: float = Field(default=50.0, ge=0.0, le=100.0) breadth_bucket: BreadthBucket = BreadthBucket.NORMAL breadth_divergence: float = 0.0 oi_state: OIState = OIState.NEUTRAL volatility_regime: VolRegime = VolRegime.NORMAL_VOL price_structure_score: FactorScore = Field(default_factory=FactorScore) breadth_score: BreadthScore = Field(default_factory=BreadthScore) oi_matrix_score: OIMatrixScore = Field(default_factory=OIMatrixScore) volatility_regime_score: VolatilityRegimeScore = Field(default_factory=VolatilityRegimeScore) market_state_hash: str = "" def compute_hash(self) -> str: import hashlib key = f"{self.regime.value}|{self.breadth_bucket.value}|{self.oi_state.value}|{self.volatility_regime.value}" return hashlib.md5(key.encode()).hexdigest()[:12] def state_embedding(self) -> list[float]: return [ self.breadth_top20, self.breadth_top30, self.breadth_top50, self.regime_maturity_score, self.price_structure_score.score, ] # ═══════════════════════════════════════════════════════════════ # Factor Contribution # ═══════════════════════════════════════════════════════════════ class FactorContribution(BaseModel): """How much a factor contributed to the overall score.""" factor: str raw_score: float weight: float impact: float direction: str # 'bullish' / 'bearish' / 'neutral' # ═══════════════════════════════════════════════════════════════ # Regime Result # ═══════════════════════════════════════════════════════════════ class RegimeResult(BaseModel): _parse_date = field_validator("date", mode="before")(_parse_date) date: Date regime: MarketRegime confidence: float regime_version: str maturity_score: float all_scores: dict = Field(default_factory=dict) prior_regime: Optional[MarketRegime] = None confirmation_days: int = 0 class SignalFeatureRecord(BaseModel): """A single signal → market state → outcome record.""" _parse_date = field_validator("date", mode="before")(_parse_date) date: Date signal_type: str signal_version: str = "b3_v1" symbol: str = "BTC/USDT:USDT" regime_version: str signal_grade: Optional[SignalGrade] = None signal_strength: Optional[float] = None regime: MarketRegime regime_confidence: float regime_maturity_score: float market_state_hash: str state_embedding: str = "[]" breadth_top20: float breadth_top30: float breadth_top50: float breadth_bucket: BreadthBucket breadth_divergence: float oi_state: OIState volatility_regime: VolRegime price_structure_score: float chan_trend_direction: Optional[str] = None chan_pivot_count: Optional[int] = None chan_divergence_type: Optional[str] = None entry_price: Optional[float] = None result_1d: Optional[float] = None result_3d: Optional[float] = None result_5d: Optional[float] = None result_7d: Optional[float] = None result_14d: Optional[float] = None max_favorable_excursion: Optional[float] = None max_adverse_excursion: Optional[float] = None is_win_7d: Optional[int] = None class ExpectancyLayer(BaseModel): name: str posterior_winrate: float raw_winrate: Optional[float] = None samples: int = 0 effective_samples: float = 0.0 avg_return: Optional[float] = None class ExpectancyReport(BaseModel): _parse_date = field_validator("date", mode="before")(_parse_date) signal_type: str date: Date layers: list[ExpectancyLayer] = Field(default_factory=list) final_estimate: float sufficiency: SufficiencyLevel = SufficiencyLevel.INSUFFICIENT prior_strength: int = 40 half_life_days: int = 180 source: str = "bayesian" avg_return_7d: Optional[float] = None profit_factor: Optional[float] = None max_adverse_excursion: Optional[float] = None class DailyOutput(BaseModel): """Final daily output: Market State + Expectancy.""" _parse_date = field_validator("date", mode="before")(_parse_date) date: Date market_state: MarketStateVector expectancy: dict[str, ExpectancyReport] = Field(default_factory=dict) ai_report_en: Optional[str] = None ai_report_zh: Optional[str] = None