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,81 @@
|
||||
"""
|
||||
trend_detector.py — Trend strength and maturity helpers.
|
||||
|
||||
Utility functions for computing trend alignment, acceleration, persistence.
|
||||
Used by regime_detector and price_structure scorer.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def ema_alignment_score(close: float, ema20: float, ema60: float, ema120: float) -> float:
|
||||
"""Score EMA alignment: 0=bearish, 50=neutral, 100=bullish."""
|
||||
if any(pd.isna(x) for x in [ema20, ema60, ema120]):
|
||||
return 50.0
|
||||
|
||||
alignments = 0
|
||||
if ema20 > ema60:
|
||||
alignments += 1
|
||||
if ema60 > ema120:
|
||||
alignments += 1
|
||||
if ema20 > ema120:
|
||||
alignments += 1
|
||||
|
||||
if alignments == 3:
|
||||
return 85.0
|
||||
elif alignments == 2:
|
||||
return 65.0
|
||||
elif alignments == 1:
|
||||
return 35.0
|
||||
else:
|
||||
return 15.0
|
||||
|
||||
|
||||
def adx_trend_score(adx: float) -> float:
|
||||
"""Convert ADX value to trend score: 0-100."""
|
||||
if pd.isna(adx):
|
||||
return 50.0
|
||||
if adx > 40:
|
||||
return 90.0
|
||||
elif adx > 25:
|
||||
return 60.0 + (adx - 25) / 15 * 30
|
||||
elif adx > 15:
|
||||
return 40.0 + (adx - 15) / 10 * 20
|
||||
else:
|
||||
return max(10.0, adx / 15 * 40)
|
||||
|
||||
|
||||
def breadth_persistence(breadth_scores: list[float], window: int = 5) -> float:
|
||||
"""How consistently has breadth stayed at its current level? 0-100."""
|
||||
if len(breadth_scores) < window:
|
||||
return 50.0
|
||||
recent = breadth_scores[-window:]
|
||||
mean_val = np.mean(recent)
|
||||
std_val = np.std(recent) if len(recent) > 1 else 0
|
||||
# Low std = high persistence
|
||||
persistence = 100 - min(std_val * 5, 100)
|
||||
# Bias: higher breadth = higher persistence score
|
||||
return persistence * 0.5 + mean_val * 0.5
|
||||
|
||||
|
||||
def trend_strength_composite(ema_score: float, adx_score: float,
|
||||
breadth_score: float) -> float:
|
||||
"""Composite trend strength 0-100."""
|
||||
return ema_score * 0.25 + adx_score * 0.25 + breadth_score * 0.50
|
||||
|
||||
|
||||
def compute_maturity(trend_strength: float, breadth_persistence: float,
|
||||
vol_expansion: float) -> float:
|
||||
"""
|
||||
Compute regime maturity score 0-100.
|
||||
|
||||
EMERGING (0-30): trend accelerating, breadth expanding
|
||||
CONFIRMED (30-70): trend stable, breadth stable
|
||||
EXHAUSTING (70-100): trend decelerating, breadth contracting, vol abnormal
|
||||
"""
|
||||
return (
|
||||
trend_strength * 0.50 +
|
||||
breadth_persistence * 0.30 +
|
||||
(100 - vol_expansion) * 0.20 # inverted: low vol = early stage
|
||||
)
|
||||
Reference in New Issue
Block a user