Isolate legacy range-grid features
This commit is contained in:
@@ -144,6 +144,8 @@ SR_RESEARCH_VARIANTS = {
|
||||
"legacy_geometry_neutral",
|
||||
"legacy_pivots_only",
|
||||
"legacy_traffic_grid_only",
|
||||
"legacy_range_grid_touch",
|
||||
"legacy_range_grid_neutral",
|
||||
}
|
||||
|
||||
|
||||
@@ -158,18 +160,7 @@ def _sr_research_variant() -> str:
|
||||
|
||||
def _apply_zone_strength_variant(zone_levels: list[Any], sr_variant: str) -> list[Any]:
|
||||
"""Apply post-cluster research controls without changing zone geometry."""
|
||||
if sr_variant == "legacy_geometry_neutral":
|
||||
# Neutrality must be enforced after the shared zone cluster: its legacy
|
||||
# sum mode can otherwise turn two 50-strength constituents back into a
|
||||
# 100-strength target and silently invalidate the ablation.
|
||||
for level in zone_levels:
|
||||
level.strength = 50
|
||||
return zone_levels
|
||||
|
||||
|
||||
def _apply_zone_strength_variant(zone_levels: list[Any], sr_variant: str) -> list[Any]:
|
||||
"""Apply post-cluster research controls without changing zone geometry."""
|
||||
if sr_variant == "legacy_geometry_neutral":
|
||||
if sr_variant in {"legacy_geometry_neutral", "legacy_range_grid_neutral"}:
|
||||
# Neutrality must be enforced after the shared zone cluster: its legacy
|
||||
# sum mode can otherwise turn two 50-strength constituents back into a
|
||||
# 100-strength target and silently invalidate the ablation.
|
||||
@@ -310,6 +301,16 @@ def _window_setups(
|
||||
detected_levels = detect_sr_levels_legacy(
|
||||
highs, lows, closes, volumes, include_pivots=False
|
||||
)
|
||||
elif sr_variant in {"legacy_range_grid_touch", "legacy_range_grid_neutral"}:
|
||||
detected_levels = detect_sr_levels_legacy(
|
||||
highs,
|
||||
lows,
|
||||
closes,
|
||||
volumes,
|
||||
include_pivots=False,
|
||||
neutral_strength=sr_variant == "legacy_range_grid_neutral",
|
||||
explicit_range_grid=True,
|
||||
)
|
||||
elif detector_variant in {"production_control", "rr_aligned_control"}:
|
||||
detected_levels = detect_sr_levels_legacy(highs, lows, closes, volumes)
|
||||
else:
|
||||
@@ -351,7 +352,6 @@ def _window_setups(
|
||||
strength_mode=zone_strength_mode,
|
||||
)
|
||||
zone_levels = _apply_zone_strength_variant(zone_levels, sr_variant)
|
||||
zone_levels = _apply_zone_strength_variant(zone_levels, sr_variant)
|
||||
targets = target_generator.generate_targets(direction, entry, stop, zone_levels, atr)
|
||||
if not targets:
|
||||
fallback_k = _atr_target_fallback_k()
|
||||
|
||||
@@ -394,6 +394,34 @@ def _legacy_volume_profile_nodes(
|
||||
return hvn + lvn
|
||||
|
||||
|
||||
def _legacy_range_grid_nodes(
|
||||
highs: list[float],
|
||||
lows: list[float],
|
||||
closes: list[float],
|
||||
num_bins: int = 20,
|
||||
) -> list[float]:
|
||||
"""Return every deployed grid center without the irrelevant volume pass.
|
||||
|
||||
The legacy profile returns both its above-average (HVN) and below-average
|
||||
(LVN) bins. Their union is therefore the complete range grid except for the
|
||||
rare bin whose traffic equals the average exactly. This explicit helper
|
||||
isolates that geometry from the misleading volume-profile label.
|
||||
"""
|
||||
if len(closes) < 20:
|
||||
raise ValidationError(
|
||||
f"Range grid requires at least 20 bars, got {len(closes)}"
|
||||
)
|
||||
price_min = min(lows)
|
||||
price_max = max(highs)
|
||||
if price_max == price_min:
|
||||
price_max = price_min + 1.0
|
||||
bin_width = (price_max - price_min) / num_bins
|
||||
return [
|
||||
round(price_min + (i + 0.5) * bin_width, 4)
|
||||
for i in range(num_bins)
|
||||
]
|
||||
|
||||
|
||||
def detect_sr_levels_legacy(
|
||||
highs: list[float],
|
||||
lows: list[float],
|
||||
@@ -404,6 +432,7 @@ def detect_sr_levels_legacy(
|
||||
include_volume_profile: bool = True,
|
||||
include_pivots: bool = True,
|
||||
neutral_strength: bool = False,
|
||||
explicit_range_grid: bool = False,
|
||||
) -> list[dict]:
|
||||
"""Deployed detector plus source-isolation controls for local research.
|
||||
|
||||
@@ -417,7 +446,12 @@ def detect_sr_levels_legacy(
|
||||
candidates: list[tuple[float, str]] = []
|
||||
if include_volume_profile:
|
||||
try:
|
||||
for price in _legacy_volume_profile_nodes(highs, lows, closes, volumes):
|
||||
nodes = (
|
||||
_legacy_range_grid_nodes(highs, lows, closes)
|
||||
if explicit_range_grid
|
||||
else _legacy_volume_profile_nodes(highs, lows, closes, volumes)
|
||||
)
|
||||
for price in nodes:
|
||||
candidates.append((float(price), "volume_profile"))
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
@@ -481,13 +481,13 @@ into production as-is.
|
||||
|
||||
### Hidden-feature isolation
|
||||
|
||||
The remaining hypothesis is that the deployed detector accidentally measures
|
||||
long-memory historical price traffic rather than genuine S/R. A dedicated matrix
|
||||
holds the complete gate fixed and changes one legacy component at a time:
|
||||
The deployed detector accidentally measures long-memory historical price traffic
|
||||
rather than genuine S/R. A dedicated matrix holds the complete gate fixed and
|
||||
changes one legacy component at a time:
|
||||
|
||||
- `legacy_geometry_neutral`: old locations, every merged strength fixed at 50;
|
||||
- `legacy_pivots_only`: unfiltered full-history pivots, no VP grid;
|
||||
- `legacy_traffic_grid_only`: old range-volume price grid, no pivots.
|
||||
- `legacy_traffic_grid_only`: deployed HVN+LVN grid, no pivots.
|
||||
|
||||
Run on macOS:
|
||||
|
||||
@@ -498,29 +498,54 @@ Run on macOS:
|
||||
Do not validate any traffic arm yet. First establish whether geometry, pivots, or
|
||||
the range-occupancy grid reproduces production on pre-2024 training data.
|
||||
|
||||
Initial result:
|
||||
Corrected training result:
|
||||
|
||||
| arm | Sharpe | CAGR | MaxDD | net avg R | ex-top-5% |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| production control | 1.28 | 28.8% | 21.4% | 0.230 | 0.066 |
|
||||
| pivots only | 1.38 | 32.9% | 25.2% | 0.236 | 0.076 |
|
||||
| traffic grid only | **1.72** | **41.8%** | **16.8%** | **0.271** | **0.136** |
|
||||
| arm | qualified | Sharpe | CAGR | MaxDD | net avg R | ex-top-5% |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| production control | 676 | 1.28 | 28.8% | 21.4% | 0.230 | 0.066 |
|
||||
| corrected neutral geometry | 505 | 1.48 | 33.5% | 18.2% | 0.230 | 0.087 |
|
||||
| pivots only | 717 | 1.38 | 32.9% | 25.2% | 0.236 | 0.076 |
|
||||
| traffic grid only | 504 | **1.72** | **41.8%** | **16.8%** | **0.271** | **0.136** |
|
||||
|
||||
The traffic grid is the first research arm to beat control simultaneously on
|
||||
Sharpe, CAGR, drawdown, and robust expectancy. It retains 264 production setups,
|
||||
adds 240 positive-expectancy setups, and removes 412 weaker setups. This supports
|
||||
the hypothesis that the useful legacy feature is the volume-weighted range-
|
||||
occupancy grid, not pivots or visually meaningful S/R.
|
||||
Sharpe, CAGR, drawdown, and robust expectancy. It retains 264 production setups
|
||||
at +0.214R ex-top-5%, adds 240 at +0.051R, and removes 412 at only +0.010R.
|
||||
Against corrected neutral geometry, only 239 qualified setups overlap; the 265
|
||||
traffic-only setups return +0.140R ex-top-5% versus +0.073R for the 266
|
||||
neutral-only setups. This is different selection, not merely a lower trade count.
|
||||
|
||||
The first geometry-neutral result is not interpretable: neutral strength was set
|
||||
before the shared zone cluster, which summed multiple 50-strength constituents
|
||||
back to 100. Neutrality is now enforced after clustering. Rerun only that arm:
|
||||
The old `volume_profile` name is misleading. Its helper returns both HVN and LVN
|
||||
bins, so their union retains almost every one of the 20 evenly spaced centers over
|
||||
the expanding historical high-low range (19.987 levels on average in the audit).
|
||||
The later strength calculation does not use volume; it counts bars whose ranges
|
||||
cross each center. The candidate feature is therefore:
|
||||
|
||||
1. a normalized, expanding 20-bin price-range grid;
|
||||
2. range-touch occupancy strength;
|
||||
3. no full-history pivot ladder or pivot/grid strength saturation.
|
||||
|
||||
Two final training arms isolate the first two items explicitly:
|
||||
|
||||
- `legacy_range_grid_touch`: all 20 range centers, no volume calculation, legacy
|
||||
touch strength;
|
||||
- `legacy_range_grid_neutral`: identical centers, strength fixed at 50 after
|
||||
clustering.
|
||||
|
||||
Run only these new arms on macOS:
|
||||
|
||||
```bash
|
||||
.venv/bin/python scripts/run_sr_v2_matrix.py traffic \
|
||||
--only-arm legacy_geometry_neutral --workers 14
|
||||
--only-arm legacy_range_grid_touch --workers 14
|
||||
|
||||
.venv/bin/python scripts/run_sr_v2_matrix.py traffic \
|
||||
--only-arm legacy_range_grid_neutral --workers 14
|
||||
```
|
||||
|
||||
The touch arm should closely reproduce `legacy_traffic_grid_only`; that is the
|
||||
volume-removal parity check. The touch-versus-neutral comparison then attributes
|
||||
any remaining difference to occupancy strength. Freeze the winner before running
|
||||
the post-2024-06-30 validation command; do not tune the bin count on training data.
|
||||
|
||||
**Next runs, if picked back up:**
|
||||
|
||||
- A **per-name target model** for clear-air setups instead of a constant k×ATR. This
|
||||
|
||||
@@ -54,7 +54,8 @@ def _parse_args() -> argparse.Namespace:
|
||||
"rewrite_legacy_primary", "soft_zones_legacy_primary",
|
||||
"confirmed_rounds_legacy_primary", "gate_v2_legacy_primary",
|
||||
"legacy_geometry_neutral", "legacy_pivots_only",
|
||||
"legacy_traffic_grid_only",
|
||||
"legacy_traffic_grid_only", "legacy_range_grid_touch",
|
||||
"legacy_range_grid_neutral",
|
||||
),
|
||||
default=None,
|
||||
help="Research-only S/R detector/gate arm.",
|
||||
|
||||
@@ -22,12 +22,17 @@ TRAINING_ARMS = (
|
||||
"confirmed_rounds_legacy_primary",
|
||||
"gate_v2_legacy_primary",
|
||||
)
|
||||
LOCKABLE_ARMS = TRAINING_ARMS[1:]
|
||||
RANGE_GRID_ARMS = (
|
||||
"legacy_traffic_grid_only",
|
||||
"legacy_range_grid_touch",
|
||||
"legacy_range_grid_neutral",
|
||||
)
|
||||
LOCKABLE_ARMS = TRAINING_ARMS[1:] + RANGE_GRID_ARMS
|
||||
TRAFFIC_ARMS = (
|
||||
"production_control",
|
||||
"legacy_geometry_neutral",
|
||||
"legacy_pivots_only",
|
||||
"legacy_traffic_grid_only",
|
||||
*RANGE_GRID_ARMS,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,7 +52,7 @@ def _args() -> argparse.Namespace:
|
||||
_add_common(train)
|
||||
traffic = commands.add_parser(
|
||||
"traffic",
|
||||
help="Isolate legacy geometry, pivots, and price-traffic grid on training data.",
|
||||
help="Isolate pivots, range-grid geometry, and touch strength on training data.",
|
||||
)
|
||||
_add_common(traffic)
|
||||
traffic.add_argument(
|
||||
|
||||
@@ -759,22 +759,76 @@ def test_window_setups_too_short_returns_empty():
|
||||
|
||||
|
||||
def test_sr_research_variant_is_explicit_and_validated(monkeypatch):
|
||||
monkeypatch.setenv("BACKTEST_SR_VARIANT", "production_control")
|
||||
assert bt._sr_research_variant() == "production_control"
|
||||
for variant in (
|
||||
"production_control",
|
||||
"legacy_range_grid_touch",
|
||||
"legacy_range_grid_neutral",
|
||||
):
|
||||
monkeypatch.setenv("BACKTEST_SR_VARIANT", variant)
|
||||
assert bt._sr_research_variant() == variant
|
||||
monkeypatch.setenv("BACKTEST_SR_VARIANT", "not-a-variant")
|
||||
with pytest.raises(ValueError, match="Unknown BACKTEST_SR_VARIANT"):
|
||||
bt._sr_research_variant()
|
||||
|
||||
|
||||
def test_geometry_neutral_strength_is_enforced_after_zone_clustering():
|
||||
@pytest.mark.parametrize(
|
||||
("variant", "neutral_strength"),
|
||||
[
|
||||
("legacy_range_grid_touch", False),
|
||||
("legacy_range_grid_neutral", True),
|
||||
],
|
||||
)
|
||||
def test_window_setups_routes_explicit_range_grid(
|
||||
monkeypatch,
|
||||
variant,
|
||||
neutral_strength,
|
||||
):
|
||||
captured = {}
|
||||
|
||||
def fake_detector(*args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return []
|
||||
|
||||
monkeypatch.setenv("BACKTEST_SR_VARIANT", variant)
|
||||
monkeypatch.setattr(bt, "detect_sr_levels_legacy", fake_detector)
|
||||
records = [
|
||||
SimpleNamespace(
|
||||
date=date(2024, 1, 1) + timedelta(days=i),
|
||||
open=100.0,
|
||||
high=101.0,
|
||||
low=99.0,
|
||||
close=100.0,
|
||||
volume=1_000_000,
|
||||
)
|
||||
for i in range(bt.MIN_LOOKBACK)
|
||||
]
|
||||
assert bt._window_setups(records, {}, {}) == []
|
||||
assert captured == {
|
||||
"include_pivots": False,
|
||||
"neutral_strength": neutral_strength,
|
||||
"explicit_range_grid": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"variant",
|
||||
["legacy_geometry_neutral", "legacy_range_grid_neutral"],
|
||||
)
|
||||
def test_neutral_strength_is_enforced_after_zone_clustering(variant):
|
||||
levels = [
|
||||
SimpleNamespace(strength=100),
|
||||
SimpleNamespace(strength=75),
|
||||
]
|
||||
result = bt._apply_zone_strength_variant(levels, "legacy_geometry_neutral")
|
||||
result = bt._apply_zone_strength_variant(levels, variant)
|
||||
assert [level.strength for level in result] == [50, 50]
|
||||
|
||||
|
||||
def test_touch_strength_survives_post_cluster_research_hook():
|
||||
levels = [SimpleNamespace(strength=82), SimpleNamespace(strength=37)]
|
||||
result = bt._apply_zone_strength_variant(levels, "legacy_range_grid_touch")
|
||||
assert [level.strength for level in result] == [82, 37]
|
||||
|
||||
|
||||
def test_backtest_entry_bounds_validate_dates(monkeypatch):
|
||||
monkeypatch.setenv("BACKTEST_ENTRY_START", "2024-07-01")
|
||||
monkeypatch.setenv("BACKTEST_ENTRY_END", "2024-12-31")
|
||||
|
||||
@@ -6,6 +6,8 @@ from app.services.sr_service import (
|
||||
MAX_LEVELS,
|
||||
_bar_respect_weight,
|
||||
_cap_levels,
|
||||
_legacy_range_grid_nodes,
|
||||
_legacy_volume_profile_nodes,
|
||||
_merge_levels,
|
||||
_round_number_candidates,
|
||||
_strength_from_respects,
|
||||
@@ -267,3 +269,65 @@ class TestDetectSrLevels:
|
||||
assert pivots and traffic
|
||||
assert all(level["sources"] == ["pivot_point"] for level in pivots)
|
||||
assert all(level["sources"] == ["volume_profile"] for level in traffic)
|
||||
|
||||
def test_legacy_profile_union_matches_explicit_range_grid(self):
|
||||
highs, lows, closes, volumes = _make_series(n=500)
|
||||
deployed = _legacy_volume_profile_nodes(highs, lows, closes, volumes)
|
||||
explicit = _legacy_range_grid_nodes(highs, lows, closes)
|
||||
assert sorted(deployed) == explicit
|
||||
assert len(explicit) == 20
|
||||
|
||||
def test_explicit_range_grid_is_volume_independent(self):
|
||||
highs, lows, closes, volumes = _make_series(n=500)
|
||||
shifted_volumes = [volume * (i + 1) for i, volume in enumerate(volumes)]
|
||||
baseline = detect_sr_levels_legacy(
|
||||
highs,
|
||||
lows,
|
||||
closes,
|
||||
volumes,
|
||||
include_pivots=False,
|
||||
explicit_range_grid=True,
|
||||
)
|
||||
shifted = detect_sr_levels_legacy(
|
||||
highs,
|
||||
lows,
|
||||
closes,
|
||||
shifted_volumes,
|
||||
include_pivots=False,
|
||||
explicit_range_grid=True,
|
||||
)
|
||||
assert shifted == baseline
|
||||
|
||||
def test_explicit_range_grid_neutral_changes_only_strength(self):
|
||||
highs, lows, closes, volumes = _make_series(n=500)
|
||||
touch = detect_sr_levels_legacy(
|
||||
highs,
|
||||
lows,
|
||||
closes,
|
||||
volumes,
|
||||
include_pivots=False,
|
||||
explicit_range_grid=True,
|
||||
)
|
||||
neutral = detect_sr_levels_legacy(
|
||||
highs,
|
||||
lows,
|
||||
closes,
|
||||
volumes,
|
||||
include_pivots=False,
|
||||
neutral_strength=True,
|
||||
explicit_range_grid=True,
|
||||
)
|
||||
touch_by_price = {level["price_level"]: level for level in touch}
|
||||
neutral_by_price = {level["price_level"]: level for level in neutral}
|
||||
assert neutral_by_price.keys() == touch_by_price.keys()
|
||||
for price, neutral_level in neutral_by_price.items():
|
||||
assert neutral_level["strength"] == 50
|
||||
assert {
|
||||
key: value
|
||||
for key, value in neutral_level.items()
|
||||
if key != "strength"
|
||||
} == {
|
||||
key: value
|
||||
for key, value in touch_by_price[price].items()
|
||||
if key != "strength"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user