"""威科夫阶段与事件(启发式)。""" from __future__ import annotations from typing import Any, Dict, List, Tuple import numpy as np import pandas as pd def _bar_time(df: pd.DataFrame, i: int): row = df.iloc[i] if "date" in df.columns and pd.notna(row["date"]): return row["date"] if "timestamp" in df.columns: return row["timestamp"] return i 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 detect_bias_and_events( df: pd.DataFrame, tr: Dict[str, Any], ) -> Tuple[str, List[Dict[str, Any]], Dict[str, Any]]: """ 返回 bias、events、volume_confirm。 """ hi = float(tr["high"]) lo = float(tr["low"]) mid = float(tr["mid"]) tol = float(tr.get("tol") or (hi - lo) * 0.05) s = int(tr["abs_start_idx"]) e = int(tr["abs_end_idx"]) events: List[Dict[str, Any]] = [] # 扫描区间内及之后(含 tail_reserve) scan_end = int(tr.get("abs_scan_end_idx", min(len(df) - 1, e + 15))) scan_end = min(len(df) - 1, max(scan_end, e)) spring = None utad = None sos = None sod = None # sign of weakness / distribution breakdown lps = None lpsy = None for i in range(s + 2, scan_end + 1): row = df.iloc[i] low = float(row["low"]) high = float(row["high"]) close = float(row["close"]) vol = float(row["volume"]) if "volume" in df.columns else 0.0 avg_v = _avg_vol(df, i) ratio = vol / avg_v if avg_v else 0.0 # Spring: pierce below low then close back above low if spring is None and low < lo - tol * 0.5 and close >= lo - tol * 0.2: vol_ok = ratio <= 1.35 or (i + 1 <= scan_end and float(df.iloc[min(i + 1, scan_end)]["volume"]) / avg_v < 1.2) spring = { "type": "Spring", "time": _bar_time(df, i), "price": low, "note": "假破下沿后收回", "volume_ratio": round(ratio, 3), "volume_ok": bool(vol_ok), "idx": i, } # UTAD: pierce above high then close back below if utad is None and high > hi + tol * 0.5 and close <= hi + tol * 0.2: vol_ok = ratio >= 0.8 utad = { "type": "UTAD", "time": _bar_time(df, i), "price": high, "note": "假破上沿后跌回", "volume_ratio": round(ratio, 3), "volume_ok": bool(vol_ok), "idx": i, } # SOS: close above high with volume if sos is None and close > hi + tol * 0.15: vol_ok = ratio >= 1.15 sos = { "type": "SOS", "time": _bar_time(df, i), "price": close, "note": "放量上破交易区间", "volume_ratio": round(ratio, 3), "volume_ok": bool(vol_ok), "idx": i, } # SOW / breakdown if sod is None and close < lo - tol * 0.15: vol_ok = ratio >= 1.15 sod = { "type": "SOW", "time": _bar_time(df, i), "price": close, "note": "放量下破交易区间", "volume_ratio": round(ratio, 3), "volume_ok": bool(vol_ok), "idx": i, } # LPS after SOS: pullback that holds above mid/high-band with lighter volume if sos is not None: si = int(sos["idx"]) for i in range(si + 1, min(len(df), si + 25)): row = df.iloc[i] low = float(row["low"]) close = float(row["close"]) vol = float(row["volume"]) if "volume" in df.columns else 0.0 avg_v = _avg_vol(df, i) ratio = vol / avg_v if avg_v else 0.0 if low >= mid - tol and close >= hi - tol * 2: vol_ok = ratio <= 1.05 lps = { "type": "LPS", "time": _bar_time(df, i), "price": low, "note": "突破后缩量回踩不破", "volume_ratio": round(ratio, 3), "volume_ok": bool(vol_ok), "idx": i, } break if sod is not None: si = int(sod["idx"]) for i in range(si + 1, min(len(df), si + 25)): row = df.iloc[i] high = float(row["high"]) close = float(row["close"]) vol = float(row["volume"]) if "volume" in df.columns else 0.0 avg_v = _avg_vol(df, i) ratio = vol / avg_v if avg_v else 0.0 if high <= mid + tol and close <= lo + tol * 2: vol_ok = ratio <= 1.05 lpsy = { "type": "LPSY", "time": _bar_time(df, i), "price": high, "note": "下跌突破后缩量反抽不过", "volume_ratio": round(ratio, 3), "volume_ok": bool(vol_ok), "idx": i, } break for ev in (spring, sos, lps, utad, sod, lpsy): if ev: events.append({k: v for k, v in ev.items() if k != "idx"}) # bias last_c = float(df["close"].iloc[-1]) bias = "unknown" if sos and (not sod or int(sos.get("idx", 0)) >= int(sod.get("idx", 0))): bias = "accumulation" elif sod and (not sos or int(sod.get("idx", 0)) > int(sos.get("idx", 0))): bias = "distribution" elif spring and not utad: bias = "accumulation" elif utad and not spring: bias = "distribution" elif last_c >= mid: bias = "accumulation" else: bias = "distribution" avg_volume = float(df["volume"].astype(float).iloc[max(0, e - 20) : e + 1].mean()) if "volume" in df.columns else 0.0 volume_confirm = { "avg_volume": avg_volume, "event_checks": {ev["type"]: {"volume_ok": ev.get("volume_ok"), "volume_ratio": ev.get("volume_ratio")} for ev in events}, } return bias, events, volume_confirm def build_phases( df: pd.DataFrame, tr: Dict[str, Any], bias: str, events: List[Dict[str, Any]], min_bars: int = 3, ) -> List[Dict[str, Any]]: """按时间切分 A–E 粗阶段;保证非重叠且每段至少 min_bars 根(空间不足则截断尾部阶段)。""" s = int(tr["abs_start_idx"]) e = int(tr["abs_end_idx"]) n_last = len(df) - 1 min_span = max(2, min_bars - 1) event_idx = {} for ev in events: t = ev.get("time") for i in range(s, min(len(df), e + 20)): if _bar_time(df, i) == t: event_idx[ev["type"]] = i break a_end = s + max(min_bars, (e - s) // 5) c_anchor = event_idx.get("Spring") or event_idx.get("UTAD") or (s + (e - s) // 2) d_anchor = event_idx.get("SOS") or event_idx.get("SOW") or e def _lab(phase: str) -> str: if bias == "distribution": m = {"A": "A停止上涨", "B": "B筑顶", "C": "C测试", "D": "D派发", "E": "E下跌"} else: m = {"A": "A停止下跌", "B": "B筑底", "C": "C测试", "D": "D拉升", "E": "E离开"} return m.get(phase, phase) # 理想切点(随后再强制非重叠 + 最小跨度) raw = [ ("A", s, a_end), ("B", a_end, c_anchor), ("C", c_anchor, d_anchor), ("D", d_anchor, min(n_last, d_anchor + max(min_bars, (e - s) // 6))), ("E", min(n_last, d_anchor + max(min_bars, (e - s) // 6)), min(n_last, max(e, d_anchor + max(min_bars * 2, 8)))), ] phases: List[Dict[str, Any]] = [] cursor = s for phase, _a, _b in raw: if cursor >= n_last: break a = max(int(_a), cursor) b = int(max(_b, a + min_span)) b = int(np.clip(b, a, n_last)) if b - a < min_span: # 尾部空间不足:并入上一段终点并停止新增 if phases: phases[-1]["end_time"] = _bar_time(df, n_last) break phases.append( { "phase": phase, "label": _lab(phase), "start_time": _bar_time(df, a), "end_time": _bar_time(df, b), } ) cursor = b return phases