"""威科夫分析入口:Cycle → Phase → Event → VP + Live(MULTI-CYCLE / LIVE-STRUCTURE)。 range.py 只产 TradingRange;Confirmed 走 events.py;Live 走 live.py。 cycles[0]=ACTIVE;禁止 cycles[-1] 取 active。 Execution 只消费 Confirmed(见 live.execution_signal_from_wyckoff)。 """ from __future__ import annotations from typing import Any, Dict, List, Optional import pandas as pd from .events import build_phases, detect_bias_and_events from .live import analyze_live_structure from .range import detect_trading_ranges from .volume_profile import compute_volume_profile def _fmt_time(v) -> Optional[str]: if v is None: return None if hasattr(v, "isoformat"): try: return v.isoformat() except Exception: pass return str(v) def _empty(vp_bins: int) -> Dict[str, Any]: return { "cycles": [], "trading_range": None, "bias": "unknown", "phases": [], "events": [], "volume_profile": {"bins": [], "poc": None, "vah": None, "val": None, "bin_count": vp_bins}, "volume_confirm": {"avg_volume": 0.0, "event_checks": {}}, "live": None, } def _confidence_for_confirmed( tr: Dict[str, Any], phases: List[Dict[str, Any]], events: List[Dict[str, Any]], ) -> Dict[str, float]: range_c = float(tr.get("range_confidence") or 0.5) labels = {p.get("phase") for p in phases} phase_c = 0.35 if "A" in labels and "B" in labels: phase_c += 0.15 if "C" in labels: phase_c += 0.2 if "D" in labels or "E" in labels: phase_c += 0.15 phase_c = min(0.95, phase_c) types = {e.get("type") for e in events} event_c = 0.25 for t in ("Spring", "UTAD", "SOS", "SOW", "LPS", "LPSY"): if t in types: event_c += 0.12 event_c = min(0.95, event_c) overall = 0.4 * range_c + 0.3 * phase_c + 0.3 * event_c return { "range": round(range_c, 3), "phase": round(phase_c, 3), "event": round(event_c, 3), "overall": round(overall, 3), } def _build_cycle( work: pd.DataFrame, tr: Dict[str, Any], cycle_id: int, vp_bins: int, ) -> Dict[str, Any]: bias, events, volume_confirm = detect_bias_and_events(work, tr) phases = build_phases(work, tr, bias, events) vp = compute_volume_profile( work, int(tr["abs_start_idx"]), int(tr["abs_end_idx"]), bin_count=vp_bins, ) for ev in events: ev["time"] = _fmt_time(ev.get("time")) for ph in phases: ph["start_time"] = _fmt_time(ph.get("start_time")) ph["end_time"] = _fmt_time(ph.get("end_time")) is_active = cycle_id == 0 trading_range = { "start_time": _fmt_time(tr.get("start_time")), "end_time": _fmt_time(tr.get("end_time")), "high": float(tr["high"]), "low": float(tr["low"]), "mid": float(tr["mid"]), "active": bool(is_active), "bars": int(tr.get("bars", 0)), } conf = _confidence_for_confirmed(tr, phases, events) # Live 层:仅 ACTIVE 周期做推演;历史周期归档为 COMPLETED if is_active: live = analyze_live_structure( work, tr, confirmed_events=events, confirmed_phases=phases, bias=bias, ) lifecycle = live.get("lifecycle") or "FORMING" else: live = None lifecycle = "COMPLETED" return { "id": int(cycle_id), "role": "latest" if is_active else "historical", # MULTI-CYCLE:时间线角色 "status": "ACTIVE" if is_active else "HISTORICAL", # LIVE-STRUCTURE:生命周期 "lifecycle": lifecycle, "direction": "latest" if is_active else "historical", "period": { "start_time": _fmt_time(tr.get("start_time")), "end_time": _fmt_time(tr.get("end_time")), "bars": int(tr.get("bars", 0)), }, "confidence": conf, "trading_range": trading_range, "bias": bias, # 兼容旧读法:顶层 phases/events = confirmed "phases": phases, "events": events, "confirmed": { "phases": phases, "events": events, "volume_confirm": volume_confirm, }, "live": live, "volume_profile": vp, "volume_confirm": volume_confirm, } def analyze_wyckoff( df: pd.DataFrame, lookback: int = 120, vp_bins: int = 50, min_bars: int = 24, atr_mult: float = 1.2, range_start_time=None, prefer_start_time=None, max_cycles: int = 8, ) -> Dict[str, Any]: """ 多周期威科夫分析。 cycles[0] = ACTIVE;顶层 phases/events 只镜像 Confirmed。 顶层 live 镜像 cycles[0].live。 """ empty = _empty(vp_bins) if df is None or len(df) < 30: return empty if not all(c in df.columns for c in ("open", "high", "low", "close")): return empty work = df.copy() if "volume" not in work.columns: work["volume"] = 1.0 trs = detect_trading_ranges( work, lookback=lookback, min_bars=max(8, int(min_bars)), atr_mult=atr_mult, max_cycles=max(1, min(8, int(max_cycles))), prefer_start_time=prefer_start_time, range_start_time=range_start_time, ) if not trs: return empty cycles: List[Dict[str, Any]] = [] for i, tr in enumerate(trs): cycles.append(_build_cycle(work, tr, cycle_id=i, vp_bins=vp_bins)) active = cycles[0] return { "cycles": cycles, "trading_range": active["trading_range"], "bias": active["bias"], "phases": active["confirmed"]["phases"], "events": active["confirmed"]["events"], "volume_profile": active["volume_profile"], "volume_confirm": active["volume_confirm"], "live": active.get("live"), "lifecycle": active.get("lifecycle"), }