自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
197 lines
5.1 KiB
Python
197 lines
5.1 KiB
Python
"""威科夫分析入口: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"),
|
||
}
|