Add S/R v2 research and validation harness
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user