"""Scan pipeline: load local frames → engines → store (per TF combo).""" from __future__ import annotations import json import logging from datetime import date, datetime, timezone from crypto_wyckoff.combos import ROLE_HIGH, ROLE_LOW, ROLE_MID, get_combo, lookback_for 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 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.symbols_cn import display_name_cn from crypto_wyckoff.version import WYCKOFF_ENGINE_VERSION logger = logging.getLogger(__name__) def analyze_symbol( low_frame, mid_frame, high_frame, *, feature_eng: FeatureEngine, cycle_eng: CycleEngine, phase_eng: PhaseEngine, event_eng: EventEngine, signal_eng: SignalEngine, decision_eng: DecisionEngine, plan_eng: PlanEngine, ) -> dict: """Run engines with D/W/M *role* aliases so existing rules match. Frames may be any TF combo (e.g. 1h/4h/8h); rules still see 1d/1w/1M roles. """ f_d = feature_eng.run(low_frame, ROLE_LOW) f_w = feature_eng.run(mid_frame, ROLE_MID) f_m = feature_eng.run(high_frame, ROLE_HIGH) c_m = cycle_eng.run(f_m, ROLE_HIGH) c_w = cycle_eng.run(f_w, ROLE_MID) p_w = phase_eng.run(c_w, f_w, ROLE_MID) p_d = phase_eng.run(c_w, f_d, ROLE_LOW) e_w = event_eng.run(c_w, p_w, f_w, ROLE_MID) e_d = event_eng.run(c_w, p_d, f_d, ROLE_LOW) 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, *, combo_id: str, combo_label: str, ) -> 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 = { "combo_id": combo_id, "combo_label": combo_label, "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=display_name_cn(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), combo_id=combo_id, ) _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, *, combo_id: str | None = None, ) -> WyckoffScanRow | None: eng = _engines() combo = get_combo(combo_id) low_tf, mid_tf, high_tf = combo["low"], combo["mid"], combo["high"] low = load_frame(symbol, low_tf, lookback_for(low_tf)) mid = load_frame(symbol, mid_tf, lookback_for(mid_tf)) high = load_frame(symbol, high_tf, lookback_for(high_tf)) if low is None or len(low) < 40: return None result = analyze_symbol(low, mid, high, **eng) td = trade_date or ( low.trade_dates[-1] if low.trade_dates else datetime.now(timezone.utc).date() ) row = _to_row(td, symbol, result, combo_id=combo["id"], combo_label=combo["label"]) upsert_row(row) return row