371 lines
12 KiB
Python
371 lines
12 KiB
Python
"""
|
||
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
|