"""威科夫 Live / Developing 层(WYCKOFF-LIVE-STRUCTURE-001)。 独立于 Confirmed Engine:不修改 events 确认条件,不写入 confirmed.events。 Execution 不得消费本模块输出。 """ from __future__ import annotations from typing import Any, Dict, List, Optional, Set import numpy as np import pandas as pd def _avg_vol(df: pd.DataFrame, i: int, win: int = 20) -> float: a = max(0, i - win + 1) v = df["volume"].astype(float).iloc[a : i + 1] m = float(v.mean()) if len(v) else 0.0 return m if m > 0 else 1.0 def _empty_live() -> Dict[str, Any]: return { "lifecycle": "UNKNOWN", "range_formation": None, "phase_candidate": None, "event_candidates": [], "next_expected": None, "confidence": { "cycle": 0.0, "phase": 0.0, "event": 0.0, "structure": 0.0, "volume": 0.0, "overall": 0.0, }, "note": "", } def analyze_live_structure( df: pd.DataFrame, tr: Optional[Dict[str, Any]], confirmed_events: Optional[List[Dict[str, Any]]] = None, confirmed_phases: Optional[List[Dict[str, Any]]] = None, bias: str = "unknown", ) -> Dict[str, Any]: """ 基于当前 TradingRange 与已确认事件,推演 Live candidates。 confirmed_* 只读,用于避免重复提示已确认事件,不修改之。 """ out = _empty_live() if df is None or len(df) < 20 or tr is None: out["note"] = "insufficient structure" return out confirmed_events = confirmed_events or [] confirmed_phases = confirmed_phases or [] confirmed_types: Set[str] = {str(e.get("type")) for e in confirmed_events if e.get("type")} s = int(tr["abs_start_idx"]) e = int(tr["abs_end_idx"]) scan_end = int(tr.get("abs_scan_end_idx", len(df) - 1)) scan_end = min(len(df) - 1, max(scan_end, e)) hi = float(tr["high"]) lo = float(tr["low"]) mid = float(tr["mid"]) tol = float(tr.get("tol") or (hi - lo) * 0.05) atr = float(tr.get("atr") or max((hi - lo) * 0.2, 1e-9)) seg = df.iloc[s : e + 1] if len(seg) < 8: out["note"] = "range too short" return out # —— Range Formation(横盘 / 波动收敛)—— closes = seg["close"].astype(float) highs = seg["high"].astype(float) lows = seg["low"].astype(float) vols = seg["volume"].astype(float) if "volume" in seg.columns else pd.Series([1.0] * len(seg)) half = max(4, len(seg) // 2) vol_early = float(np.std(closes.iloc[:half])) if half > 1 else 0.0 vol_late = float(np.std(closes.iloc[-half:])) if half > 1 else 0.0 width = hi - lo width_atr = width / atr if atr > 0 else 99.0 converging = vol_early > 1e-12 and vol_late < vol_early * 0.85 range_ok = 1.2 <= width_atr <= 10.0 and len(seg) >= 16 structure_score = 0.35 if range_ok: structure_score += 0.25 if converging: structure_score += 0.2 if width_atr <= 6.0: structure_score += 0.1 structure_score = float(min(0.95, structure_score)) out["range_formation"] = { "potential_trading_range": bool(range_ok), "converging": bool(converging), "width_atr": round(width_atr, 3), "bars": int(len(seg)), } # —— 最近 K 形态(Phase C / Event candidates)—— i = scan_end row = df.iloc[i] o = float(row["open"]) h = float(row["high"]) l = float(row["low"]) c = float(row["close"]) rng = max(h - l, 1e-9) lower_wick = min(o, c) - l upper_wick = h - max(o, c) avg_v = _avg_vol(df, i) vol = float(row["volume"]) if "volume" in df.columns else avg_v vol_ratio = vol / avg_v if avg_v else 1.0 volume_score = float(np.clip(1.1 - abs(vol_ratio - 1.0) * 0.35, 0.2, 0.95)) phase_candidate = None phase_conf = 0.0 # Phase C:测低 + 下影 + 缩量(吸筹语境) near_lo = l <= lo + tol * 1.2 test_low = l < mid and lower_wick >= rng * 0.35 vol_contract = vol_ratio <= 1.05 if bias != "distribution" and near_lo and test_low and vol_contract: phase_candidate = "C" phase_conf = 0.55 + (0.1 if lower_wick >= rng * 0.5 else 0) + (0.08 if vol_ratio < 0.9 else 0) # Phase D 候选:价格在箱上半、有上破意图但未确认 SOS elif c >= mid and (h >= hi - tol or c > hi - tol * 0.5): phase_candidate = "D" phase_conf = 0.5 + (0.1 if c > mid else 0) elif c < mid and (l <= lo + tol): phase_candidate = "B" phase_conf = 0.45 # 已有 confirmed phase 时,candidate 取「下一阶段」提示,不覆盖事实 confirmed_phase_set = {str(p.get("phase")) for p in confirmed_phases} if "E" in confirmed_phase_set: phase_candidate = phase_candidate or "E" phase_conf = max(phase_conf, 0.7) elif "D" in confirmed_phase_set and phase_candidate is None: phase_candidate = "D" phase_conf = max(phase_conf, 0.65) out["phase_candidate"] = phase_candidate phase_conf = float(min(0.92, phase_conf)) # —— Event candidates(仅 Spring / SOS / LPS / UTAD)—— candidates: List[Dict[str, Any]] = [] def _add(typ: str, conf: float, note: str) -> None: if typ in confirmed_types: return # 已确认则不再作为 candidate candidates.append( { "type": typ, "confidence": round(float(min(0.9, conf)), 3), "confirmed": False, "note": note, } ) # Spring candidate:刺破或贴近下沿,收盘收回,但未达 Confirmed 规则(或不在 confirmed) pierce_lo = l < lo - tol * 0.15 close_back = c >= lo - tol * 0.5 if pierce_lo and close_back: _add("Spring", 0.5 + (0.12 if vol_ratio <= 1.2 else 0) + (0.08 if close_back else 0), "假破下沿收回(未确认)") elif l <= lo + tol * 0.35 and close_back and lower_wick >= rng * 0.4: _add("Spring", 0.45 + (0.1 if vol_contract else 0), "测下沿长下影(未确认)") # UTAD candidate pierce_hi = h > hi + tol * 0.15 close_back_dn = c <= hi + tol * 0.5 if pierce_hi and close_back_dn: _add("UTAD", 0.5 + (0.1 if vol_ratio >= 0.9 else 0), "假破上沿跌回(未确认)") # SOS candidate:接近/轻破上沿,量能一般,未确认 if c > hi - tol * 0.4 or h >= hi: sos_conf = 0.48 + (0.12 if c > hi else 0) + (0.1 if vol_ratio >= 1.05 else 0) _add("SOS", sos_conf, "上破/逼近箱顶(未确认)") # LPS candidate:站上 mid/上沿带后回踩 if c >= mid and l >= mid - tol * 1.5 and l > lo + (hi - lo) * 0.25: _add("LPS", 0.46 + (0.1 if vol_ratio <= 1.0 else 0), "箱内上沿带回踩(未确认)") candidates.sort(key=lambda x: x["confidence"], reverse=True) out["event_candidates"] = candidates[:4] event_score = float(candidates[0]["confidence"]) if candidates else 0.25 # next_expected(简规则) next_exp = None if "Spring" in confirmed_types and "SOS" not in confirmed_types: next_exp = "SOS" elif "SOS" in confirmed_types and "LPS" not in confirmed_types: next_exp = "LPS" elif "UTAD" in confirmed_types and "SOW" not in confirmed_types: next_exp = "SOW" elif any(c["type"] == "Spring" for c in candidates): next_exp = "Test" elif any(c["type"] == "SOS" for c in candidates): next_exp = "LPS" out["next_expected"] = next_exp # —— lifecycle —— key_confirmed = confirmed_types & {"Spring", "SOS", "UTAD", "SOW", "LPS", "LPSY"} if key_confirmed: lifecycle = "CONFIRMED" elif range_ok or phase_candidate or candidates: lifecycle = "FORMING" else: lifecycle = "UNKNOWN" out["lifecycle"] = lifecycle cycle_c = structure_score overall = 0.35 * cycle_c + 0.25 * phase_conf + 0.25 * event_score + 0.15 * volume_score out["confidence"] = { "cycle": round(cycle_c, 3), "phase": round(phase_conf, 3), "event": round(event_score, 3), "structure": round(structure_score, 3), "volume": round(volume_score, 3), "overall": round(float(overall), 3), } parts = [] if out["range_formation"]["potential_trading_range"]: parts.append("Potential TR") if phase_candidate: parts.append(f"Phase {phase_candidate} candidate") if candidates: parts.append(f"{candidates[0]['type']} candidate") out["note"] = "; ".join(parts) if parts else "observing" return out def execution_signal_from_wyckoff(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]: """ Execution 边界:只允许 Confirmed。 返回 source='confirmed' 的信号描述;Live-only 时返回 None。 """ if not payload: return None cycles = payload.get("cycles") or [] active = cycles[0] if cycles else None events = [] if active and isinstance(active.get("confirmed"), dict): events = list(active["confirmed"].get("events") or []) if not events: # 兼容旧顶层 events(均为 confirmed 镜像) events = list(payload.get("events") or []) if not events: return None last = events[-1] return { "source": "confirmed", "type": last.get("type"), "time": last.get("time"), "lifecycle": (active or {}).get("lifecycle") or "CONFIRMED", }