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,69 @@
|
||||
"""
|
||||
fetchers/base.py — Abstract base class for all macro data fetchers.
|
||||
|
||||
Provides retry logic, rate limiting, and a common interface.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import date as Date
|
||||
from typing import Optional
|
||||
import logging
|
||||
import time
|
||||
import requests
|
||||
|
||||
|
||||
class BaseFetcher(ABC):
|
||||
"""Abstract base for all macro data fetchers."""
|
||||
|
||||
def __init__(self, timeout: int = 30, max_retries: int = 3):
|
||||
self.timeout = timeout
|
||||
self.max_retries = max_retries
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
def _get(self, url: str, params: Optional[dict] = None,
|
||||
headers: Optional[dict] = None) -> dict:
|
||||
"""GET with retry and exponential backoff."""
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
resp = requests.get(
|
||||
url, params=params, headers=headers, timeout=self.timeout
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except requests.RequestException as e:
|
||||
wait = 2 ** attempt
|
||||
self.logger.warning(
|
||||
f"Request failed (attempt {attempt+1}/{self.max_retries}): {e}. "
|
||||
f"Retrying in {wait}s"
|
||||
)
|
||||
if attempt < self.max_retries - 1:
|
||||
time.sleep(wait)
|
||||
else:
|
||||
raise
|
||||
|
||||
def _get_raw(self, url: str, params: Optional[dict] = None,
|
||||
headers: Optional[dict] = None) -> bytes:
|
||||
"""GET raw bytes with retry (for non-JSON endpoints)."""
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
resp = requests.get(
|
||||
url, params=params, headers=headers, timeout=self.timeout
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.content
|
||||
except requests.RequestException as e:
|
||||
wait = 2 ** attempt
|
||||
if attempt < self.max_retries - 1:
|
||||
time.sleep(wait)
|
||||
else:
|
||||
raise
|
||||
|
||||
@abstractmethod
|
||||
def fetch(self, target_date: Optional[Date] = None) -> list[dict]:
|
||||
"""Fetch raw data. Returns list of record dicts."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def store(self, db_path: str, records: list[dict]) -> int:
|
||||
"""Store raw records into SQLite. Returns count of new rows."""
|
||||
...
|
||||
Reference in New Issue
Block a user