"""Scan pipeline: load local frames → engines → store.""" from __future__ import annotations import json import logging from datetime import date, datetime, timezone from crypto_wyckoff.cycle import CycleEngine from crypto_wyckoff.decision import DecisionEngine from crypto_wyckoff.domain_models import WyckoffScanRow from crypto_wyckoff.event import EventEngine from crypto_wyckoff.features import FeatureEngine from crypto_wyckoff.io import LOOKBACK, TF_LIST, load_frame from crypto_wyckoff.phase import PhaseEngine from crypto_wyckoff.plan import PlanEngine from crypto_wyckoff.signal import SignalEngine from crypto_wyckoff.store import upsert_row from crypto_wyckoff.version import WYCKOFF_ENGINE_VERSION logger = logging.getLogger(__name__) def analyze_symbol( daily_frame, weekly_frame, monthly_frame, *, feature_eng: FeatureEngine, cycle_eng: CycleEngine, phase_eng: PhaseEngine, event_eng: EventEngine, signal_eng: SignalEngine, decision_eng: DecisionEngine, plan_eng: PlanEngine, ) -> dict: f_d = feature_eng.run(daily_frame, "1d") f_w = feature_eng.run(weekly_frame, "1w") f_m = feature_eng.run(monthly_frame, "1M") c_m = cycle_eng.run(f_m, "1M") c_w = cycle_eng.run(f_w, "1w") p_w = phase_eng.run(c_w, f_w, "1w") p_d = phase_eng.run(c_w, f_d, "1d") e_w = event_eng.run(c_w, p_w, f_w, "1w") e_d = event_eng.run(c_w, p_d, f_d, "1d") s_d = signal_eng.run(e_d, p_d) decision = decision_eng.run(c_m, c_w, p_w, e_w, e_d, s_d) plan = plan_eng.run(f_d, decision) return { "f_d": f_d, "f_w": f_w, "f_m": f_m, "c_m": c_m, "c_w": c_w, "p_w": p_w, "e_w": e_w, "e_d": e_d, "s_d": s_d, "decision": decision, "plan": plan, } def _to_row(trade_date: date, symbol: str, result: dict) -> WyckoffScanRow: d = result["decision"] p = result["plan"] c_m, c_w, p_w = result["c_m"], result["c_w"], result["p_w"] e_w, e_d, s_d = result["e_w"], result["e_d"], result["s_d"] f_d, f_w, f_m = result["f_d"], result["f_w"], result["f_m"] snapshot = { "daily": {k: f_d.payload.get(k) for k in ( "ma20", "ma60", "ma120", "atr", "adx", "volume_ratio", "range_high", "range_low", "swing_high", "swing_low", "close", )}, "weekly": {k: f_w.payload.get(k) for k in ("ma20", "ma60", "adx", "close")}, "monthly": {k: f_m.payload.get(k) for k in ("ma20", "ma60", "adx", "close")}, } markers = [] for key, typ in (("entry", "entry"), ("stop", "stop"), ("target1", "target1"), ("target2", "target2")): if p.payload.get(key) is not None: markers.append({"type": typ, "price": p.payload[key]}) return WyckoffScanRow( trade_date=trade_date, ts_code=symbol, name=symbol, industry="crypto", engine_version=WYCKOFF_ENGINE_VERSION, m_cycle=c_m.payload.get("cycle", "Unknown"), cycle_confidence=c_m.confidence, trend_score=float(d.payload.get("trend_score", c_m.score)), w_cycle=c_w.payload.get("cycle", "Unknown"), w_phase=p_w.payload.get("phase", "None"), w_current_event=e_w.payload.get("current_event", "None"), w_recent_events_json=json.dumps( e_w.payload.get("active_events") or e_w.payload.get("recent_events") or [], ensure_ascii=False, ), phase_confidence=p_w.confidence, structure_score=float(d.payload.get("structure_score", p_w.score)), d_current_event=e_d.payload.get("current_event", "None"), d_recent_events_json=json.dumps( e_d.payload.get("active_events") or e_d.payload.get("recent_events") or [], ensure_ascii=False, ), event_confidence=e_d.confidence, entry_score=float(d.payload.get("entry_score", e_d.score)), entry=p.payload.get("entry"), stop=p.payload.get("stop"), target1=p.payload.get("target1"), target2=p.payload.get("target2"), rr=p.payload.get("rr"), alignment=float(d.payload.get("alignment", 0)), stars=int(d.payload.get("stars", 1)), decision_signal=d.payload.get("decision_signal", "Watch"), signal_confidence=s_d.confidence, overall_confidence=float(d.payload.get("overall_confidence", d.confidence)), overall_score=float(d.payload.get("overall_score", d.score)), risk=d.payload.get("risk", "Medium"), reasons_json=json.dumps(d.reasons + d.warnings, ensure_ascii=False), feature_snapshot_json=json.dumps(snapshot, ensure_ascii=False), markers_json=json.dumps(markers, ensure_ascii=False), scanned_at=datetime.now(timezone.utc), ) _ENGINES = None def _engines(): global _ENGINES if _ENGINES is None: _ENGINES = { "feature_eng": FeatureEngine(), "cycle_eng": CycleEngine(), "phase_eng": PhaseEngine(), "event_eng": EventEngine(), "signal_eng": SignalEngine(), "decision_eng": DecisionEngine(), "plan_eng": PlanEngine(), } return _ENGINES def analyze_and_store(symbol: str, trade_date: date | None = None) -> WyckoffScanRow | None: eng = _engines() daily = load_frame(symbol, "1d", LOOKBACK["1d"]) weekly = load_frame(symbol, "1w", LOOKBACK["1w"]) monthly = load_frame(symbol, "1M", LOOKBACK["1M"]) if daily is None or len(daily) < 40: return None result = analyze_symbol(daily, weekly, monthly, **eng) td = trade_date or ( daily.trade_dates[-1] if daily.trade_dates else datetime.now(timezone.utc).date() ) row = _to_row(td, symbol, result) upsert_row(row) return row