chanmacro: Signal Expectancy Engine V1 — Market Memory System
Phase A-C complete: 4 core factors, regime detection, signal tracking, Bayesian expectancy. chanmacro/ (32 files, ~4000 lines): - models: 12 enums + 15 Pydantic v2 models (DateAwareModel, MarketStateVector, etc.) - fetchers: OHLCV + Breadth (from data_provider) + Derivatives (new endpoint) - scoring: Price Structure / Breadth (quantile buckets) / OI Matrix (5 discrete states) / Volatility Regime - regime_detector: 3-state (TREND/RANGE/PANIC), factor-locked (Price+Breadth+Vol), versioned, 2-day confirmation - expectancy: SignalTracker (record+outcomes), TimeDecay (half-life=180d), BayesianExpectancyEngine (Empirical Bayes, Leveled, SufficiencyGuard) - validation: FactorValidator (IC/ICIR/Hit Ratio), RegimeValidator (MI/KL/ANOVA), TransitionValidator (stability) - CLI: fetch|score|regime|track|backfill|expectancy|validate|serve - tests: 52 passing (models, scoring, regime, expectancy) data_provider: - /api/derivatives endpoint: funding rate, OI, OI change, basis - _derivatives storage: same persist pattern as K-line (merge→lock→snapshot→atomic write) - background refresh every 60s Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user