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,143 @@
|
||||
"""
|
||||
scoring/volatility_regime.py — Volatility Regime Classification.
|
||||
|
||||
4 regimes from OHLCV data:
|
||||
LOW_VOL: ATR/Close < 2% → compression, breakout imminent
|
||||
NORMAL_VOL: ATR/Close 2-5% → normal trading
|
||||
HIGH_VOL: ATR/Close 5-10% → trend acceleration, wider stops
|
||||
EXPLOSIVE_VOL: ATR/Close > 10% → extreme, reduce or wait
|
||||
|
||||
Uses: ATR(14)/Close, HV(20)/HV(60) ratio, BB width ratio.
|
||||
OHLCV-only — never goes offline.
|
||||
"""
|
||||
|
||||
from datetime import date as Date
|
||||
import sqlite3
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from .base import BaseScorer
|
||||
from .constants import (
|
||||
VOL_LOW, VOL_HIGH, VOL_REGIME_SCORES, HV_RATIO_LOW, HV_RATIO_HIGH,
|
||||
)
|
||||
from models import FactorScore, VolatilityRegimeScore, VolRegime, MacroDirection
|
||||
from config import config
|
||||
|
||||
|
||||
class VolatilityRegimeScorer(BaseScorer):
|
||||
"""Classifies volatility regime from OHLCV data."""
|
||||
|
||||
def compute(self, target_date: Date) -> VolatilityRegimeScore:
|
||||
conn = self.get_connection()
|
||||
try:
|
||||
df = pd.read_sql_query(
|
||||
"SELECT * FROM ohlcv_daily WHERE date <= ? ORDER BY date DESC LIMIT 120",
|
||||
conn, params=(str(target_date),)
|
||||
)
|
||||
if df.empty:
|
||||
return VolatilityRegimeScore(
|
||||
name="Volatility Regime",
|
||||
score=50.0,
|
||||
label="No Data",
|
||||
)
|
||||
|
||||
df = df.sort_values("date").reset_index(drop=True)
|
||||
|
||||
# 1. ATR/Close %
|
||||
latest = df.iloc[-1]
|
||||
atr = latest.get("atr_14")
|
||||
close = float(latest["close"])
|
||||
atr_pct = (atr / close * 100) if atr and not pd.isna(atr) and close > 0 else 3.0
|
||||
|
||||
# 2. HV(20) / HV(60) ratio
|
||||
hv_ratio = self._compute_hv_ratio(df)
|
||||
|
||||
# 3. BB width ratio
|
||||
bb_ratio = self._compute_bb_ratio(df)
|
||||
|
||||
# Classify regime
|
||||
regime = self._classify(atr_pct, hv_ratio, bb_ratio)
|
||||
|
||||
# Score
|
||||
score = VOL_REGIME_SCORES.get(regime.value, 50)
|
||||
|
||||
# Narrative
|
||||
narrative = self._build_narrative(regime, atr_pct, hv_ratio, bb_ratio)
|
||||
|
||||
return VolatilityRegimeScore(
|
||||
name="Volatility Regime",
|
||||
score=float(score),
|
||||
label=regime.value,
|
||||
direction=MacroDirection.NEUTRAL,
|
||||
vol_regime=regime,
|
||||
atr_pct=round(atr_pct, 2),
|
||||
hv_ratio=round(hv_ratio, 2),
|
||||
bb_width_ratio=round(bb_ratio, 2),
|
||||
sub_scores={
|
||||
"atr_pct": round(atr_pct, 2),
|
||||
"hv_ratio": round(hv_ratio, 2),
|
||||
"bb_width_ratio": round(bb_ratio, 2),
|
||||
},
|
||||
narrative=narrative,
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _compute_hv_ratio(self, df: pd.DataFrame) -> float:
|
||||
"""Compute HV(20) / HV(60) ratio."""
|
||||
closes = df["close"].astype(float)
|
||||
returns = closes.pct_change().dropna()
|
||||
|
||||
if len(returns) < 60:
|
||||
return 1.0
|
||||
|
||||
hv20 = returns.tail(20).std() * np.sqrt(365) * 100
|
||||
hv60 = returns.tail(60).std() * np.sqrt(365) * 100
|
||||
|
||||
if hv60 == 0:
|
||||
return 1.0
|
||||
|
||||
return hv20 / hv60
|
||||
|
||||
def _compute_bb_ratio(self, df: pd.DataFrame) -> float:
|
||||
"""Compute current BB width / 20d average BB width."""
|
||||
bb_widths = df["bb_width"].dropna().tail(40)
|
||||
if len(bb_widths) < 20:
|
||||
return 1.0
|
||||
|
||||
current = bb_widths.iloc[-1]
|
||||
avg = bb_widths.tail(20).mean()
|
||||
if avg == 0:
|
||||
return 1.0
|
||||
|
||||
return current / avg
|
||||
|
||||
@staticmethod
|
||||
def _classify(atr_pct: float, hv_ratio: float, bb_ratio: float) -> VolRegime:
|
||||
"""Classify volatility regime from multiple indicators."""
|
||||
# Primary: ATR/Close %
|
||||
if atr_pct > 10.0:
|
||||
return VolRegime.EXPLOSIVE_VOL
|
||||
elif atr_pct > VOL_HIGH:
|
||||
return VolRegime.HIGH_VOL
|
||||
elif atr_pct < VOL_LOW:
|
||||
return VolRegime.LOW_VOL
|
||||
|
||||
# Secondary: HV ratio and BB ratio for edge cases
|
||||
if hv_ratio > HV_RATIO_HIGH and bb_ratio > 1.3:
|
||||
return VolRegime.HIGH_VOL
|
||||
elif hv_ratio < HV_RATIO_LOW and bb_ratio < 0.8:
|
||||
return VolRegime.LOW_VOL
|
||||
|
||||
return VolRegime.NORMAL_VOL
|
||||
|
||||
@staticmethod
|
||||
def _build_narrative(regime: VolRegime, atr_pct: float,
|
||||
hv_ratio: float, bb_ratio: float) -> str:
|
||||
mapping = {
|
||||
VolRegime.LOW_VOL: f"低波动(ATR={atr_pct:.1f}%), 布林带收窄, 突破前兆",
|
||||
VolRegime.NORMAL_VOL: f"正常波动(ATR={atr_pct:.1f}%), 正常交易环境",
|
||||
VolRegime.HIGH_VOL: f"高波动(ATR={atr_pct:.1f}%), 趋势加速, 放宽止损",
|
||||
VolRegime.EXPLOSIVE_VOL: f"极端波动(ATR={atr_pct:.1f}%), 减仓或等待",
|
||||
}
|
||||
return mapping.get(regime, "Unknown")
|
||||
Reference in New Issue
Block a user