Add live.py lifecycle and event candidates; assemble confirmed vs live in engine; Summary partition; execution_signal source=confirmed only. Keep strategies untouched; do not lower Confirmed thresholds for Live. Co-authored-by: Cursor <cursoragent@cursor.com>
259 lines
8.4 KiB
Python
259 lines
8.4 KiB
Python
"""威科夫 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",
|
||
}
|