fix: ECR-004 威科夫区间评分硬化与 VP 绘图减负(已审)

评分选 TR、阶段最小跨度、elements_only 门闩、Top-8 VP;无币种独立参数。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-06 18:46:08 +08:00
co-authored by Cursor
parent ac6be80278
commit d3188ca83c
19 changed files with 275 additions and 89 deletions
+22 -11
View File
@@ -1,4 +1,4 @@
"""威科夫引擎单测:合成震荡箱 + Spring/SOS + VP POC。"""
"""威科夫引擎单测:合成震荡箱 + Spring/SOS + VP POCECR-004 收紧)"""
from __future__ import annotations
import sys
@@ -11,10 +11,11 @@ ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from chanlun.analysis.wyckoff import analyze_wyckoff # noqa: E402
from chanlun.analysis.wyckoff.range import detect_trading_range # noqa: E402
def _box_df(n_box: int = 60, spring: bool = True, sos: bool = True) -> pd.DataFrame:
"""构造明显箱体:40~60,可选假破与上破。"""
"""构造明显箱体:40~60前 20 根下跌趋势,可选假破与上破。"""
rng = np.random.default_rng(7)
rows = []
t0 = pd.Timestamp("2024-06-01", tz="UTC")
@@ -32,7 +33,6 @@ def _box_df(n_box: int = 60, spring: bool = True, sos: bool = True) -> pd.DataFr
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:
@@ -49,7 +49,6 @@ def _box_df(n_box: int = 60, spring: bool = True, sos: bool = True) -> pd.DataFr
)
base = 20 + n_box
if spring:
# 假破下沿
rows.append(
(
t0 + pd.Timedelta(minutes=5 * base),
@@ -73,7 +72,6 @@ def _box_df(n_box: int = 60, spring: bool = True, sos: bool = True) -> pd.DataFr
)
)
base += 1
# LPS 缩量回踩
rows.append(
(
t0 + pd.Timedelta(minutes=5 * base),
@@ -85,8 +83,7 @@ def _box_df(n_box: int = 60, spring: bool = True, sos: bool = True) -> pd.DataFr
)
)
df = pd.DataFrame(rows, columns=["date", "open", "high", "low", "close", "volume"])
return df
return pd.DataFrame(rows, columns=["date", "open", "high", "low", "close", "volume"])
def test_wyckoff_detects_range_and_events():
@@ -94,15 +91,29 @@ def test_wyckoff_detects_range_and_events():
out = analyze_wyckoff(df, lookback=200)
assert out["trading_range"] is not None
tr = out["trading_range"]
assert tr["high"] > tr["low"]
assert 38.0 <= tr["low"] <= 42.0
assert 58.0 <= tr["high"] <= 62.0
# 起点不应落入前 20 根下跌段(允许少量 overlap)
box_start = df["date"].iloc[20]
assert tr["start_time"] is not None
start_ts = pd.Timestamp(tr["start_time"])
assert start_ts >= box_start - pd.Timedelta(minutes=5 * 8)
types = {e["type"] for e in out["events"]}
assert "Spring" in types or "SOS" in types
assert "Spring" in types
assert "SOS" in types
assert out["bias"] in ("accumulation", "distribution", "unknown")
assert len(out["phases"]) >= 3
keys = [(p["start_time"], p["end_time"]) for p in out["phases"]]
assert len(keys) == len(set(keys)), "phases must not share identical start/end"
def test_range_scoring_skips_pretrend():
df = _box_df(spring=False, sos=False)
tr = detect_trading_range(df, lookback=200)
assert tr is not None
assert tr["abs_start_idx"] >= 12 # 不应从 bar 0 吞掉整段下跌
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):
@@ -114,4 +125,4 @@ def test_volume_profile_poc_on_heavy_bin():
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
assert abs(vp["poc"] - 50.0) < 1.0