Add S/R v2 research and validation harness

This commit is contained in:
2026-07-12 21:15:18 +02:00
parent 57ac1d2cdd
commit 19b81c169d
19 changed files with 1575 additions and 117 deletions
+48
View File
@@ -164,6 +164,34 @@ class TestComputeVolumeProfile:
with pytest.raises(ValidationError, match="Volume Profile requires"):
compute_volume_profile(highs, lows, closes, volumes)
def test_close_bin_volume_no_double_count(self):
"""Each bar's volume is counted once (close bin), not per span."""
# Wide bars that would span many bins under the old algorithm
n = 25
closes = [100.0 + (i % 5) for i in range(n)]
highs = [c + 20 for c in closes] # wide range
lows = [c - 20 for c in closes]
volumes = [1000] * n
result = compute_volume_profile(highs, lows, closes, volumes, num_bins=20)
# Binned total equals true volume (close-bin assignment)
# We only expose poc/hvn; reconstruct by checking score fields exist
assert result["poc"] > 0
# With volume concentrated on a few close prices, HVNs should be few local peaks
assert len(result["hvn"]) < 20
def test_hvn_are_local_peaks_not_all_above_mean(self):
"""HVN should be local histogram peaks, not every above-mean bin."""
# Two clusters of closes → two volume peaks
closes = [80.0] * 10 + [120.0] * 10 + [100.0] * 5
highs = [c + 1 for c in closes]
lows = [c - 1 for c in closes]
volumes = [1000] * len(closes)
result = compute_volume_profile(highs, lows, closes, volumes, num_bins=20)
# At most a handful of local peaks (not ~half of 20 bins)
assert len(result["hvn"]) <= 6
# POC should land near one of the high-volume clusters
assert result["poc"] < 95 or result["poc"] > 105
# ---------------------------------------------------------------------------
# Pivot Points
@@ -184,6 +212,26 @@ class TestComputePivotPoints:
with pytest.raises(ValidationError, match="Pivot Points requires"):
compute_pivot_points([1, 2], [0, 1], [0.5, 1.5])
def test_prominence_filters_tiny_swings(self):
# Mix of a large swing (depth ~10) and tiny fractal noise (depth ~1)
closes = [
10, 10.2, 10.5, 10.2, 10, # tiny high around idx 2
10, 15, 20, 15, 10, # large high around idx 7
10, 10.3, 10.6, 10.3, 10, # tiny high around idx 12
]
highs = list(closes)
lows = [c - 0.5 for c in closes]
highs[2] = 10.8
highs[7] = 20.5
highs[12] = 10.9
lows[7] = 10.0 # large window range at major swing
unfiltered = compute_pivot_points(highs, lows, closes, min_prominence=None)
filtered = compute_pivot_points(highs, lows, closes, min_prominence=5.0)
assert unfiltered["pivot_count"] > 0
assert filtered["pivot_count"] < unfiltered["pivot_count"]
# Major swing high should survive
assert any(h >= 20.0 for h in filtered["swing_highs"])
# ---------------------------------------------------------------------------
# EMA Cross