feat: ECR-003 主站威科夫分析与图表叠层(已审)

独立 wyckoff 引擎 + 按需 include_wyckoff;主站 Lightweight 绘制区间/阶段/事件/VP。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-06 18:33:57 +08:00
co-authored by Cursor
parent df27b4dde8
commit 081a57a90e
30 changed files with 1330 additions and 52 deletions
+117
View File
@@ -0,0 +1,117 @@
"""威科夫引擎单测:合成震荡箱 + Spring/SOS + VP POC。"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from chanlun.analysis.wyckoff import analyze_wyckoff # noqa: E402
def _box_df(n_box: int = 60, spring: bool = True, sos: bool = True) -> pd.DataFrame:
"""构造明显箱体:40~60,可选假破与上破。"""
rng = np.random.default_rng(7)
rows = []
t0 = pd.Timestamp("2024-06-01", tz="UTC")
price = 50.0
# 进入箱体前下跌
for i in range(20):
price -= 0.3 + rng.random() * 0.1
o, c = price + 0.2, price
h, l = max(o, c) + 0.15, min(o, c) - 0.15
rows.append((t0 + pd.Timedelta(minutes=5 * i), o, h, l, c, 100 + rng.random() * 20))
# 箱体 40-60
lo, hi = 40.0, 60.0
for i in range(n_box):
c = lo + (hi - lo) * (0.3 + 0.4 * rng.random())
o = c + rng.normal(0, 0.5)
h = min(hi + 0.5, max(o, c) + abs(rng.normal(0.5, 0.2)))
l = max(lo - 0.5, min(o, c) - abs(rng.normal(0.5, 0.2)))
# 触及边界
if i % 7 == 0:
h = hi - 0.1
if i % 7 == 3:
l = lo + 0.1
rows.append(
(
t0 + pd.Timedelta(minutes=5 * (20 + i)),
o,
h,
l,
c,
80 + rng.random() * 40,
)
)
base = 20 + n_box
if spring:
# 假破下沿
rows.append(
(
t0 + pd.Timedelta(minutes=5 * base),
42.0,
43.0,
37.0,
41.5,
90.0,
)
)
base += 1
if sos:
rows.append(
(
t0 + pd.Timedelta(minutes=5 * base),
58.0,
66.0,
57.0,
64.0,
220.0,
)
)
base += 1
# LPS 缩量回踩
rows.append(
(
t0 + pd.Timedelta(minutes=5 * base),
62.0,
63.0,
59.5,
61.0,
70.0,
)
)
df = pd.DataFrame(rows, columns=["date", "open", "high", "low", "close", "volume"])
return df
def test_wyckoff_detects_range_and_events():
df = _box_df()
out = analyze_wyckoff(df, lookback=200)
assert out["trading_range"] is not None
tr = out["trading_range"]
assert tr["high"] > tr["low"]
types = {e["type"] for e in out["events"]}
assert "Spring" in types or "SOS" in types
assert out["bias"] in ("accumulation", "distribution", "unknown")
assert len(out["phases"]) >= 3
def test_volume_profile_poc_on_heavy_bin():
# 平坦箱 + 中间价放量
dates = pd.date_range("2024-01-01", periods=40, freq="5min", tz="UTC")
rows = []
for i, d in enumerate(dates):
c = 50.0 + (i % 5) * 0.1
vol = 1000.0 if 49.8 <= c <= 50.2 else 10.0
rows.append((d, c, c + 0.2, c - 0.2, c, vol))
df = pd.DataFrame(rows, columns=["date", "open", "high", "low", "close", "volume"])
out = analyze_wyckoff(df, lookback=80, vp_bins=20)
vp = out["volume_profile"]
assert vp["poc"] is not None
assert vp["vah"] is not None and vp["val"] is not None
assert abs(vp["poc"] - 50.0) < 2.0