Add single-command GTL tuning matrix

This commit is contained in:
2026-07-13 13:13:18 +02:00
parent 0873176f64
commit 3999c5efc1
12 changed files with 1032 additions and 17 deletions
+59
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import json
import math
from datetime import date, timedelta
from types import SimpleNamespace
@@ -803,6 +804,7 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch):
"rewrite_range504_structural_primary2",
"production_structural_overlay",
"explicit_target_ladder",
"gtl_tuning",
):
monkeypatch.setenv("BACKTEST_SR_VARIANT", variant)
assert bt._sr_research_variant() == variant
@@ -861,6 +863,24 @@ def test_residual_arms_change_only_the_primary_rr_floor():
"explicit_target_ladder",
activation,
) == 1.5
assert bt._primary_min_rr_for_variant("gtl_tuning", activation) == 1.5
def test_gtl_research_config_parses_and_rejects_unknown_fields():
config = bt._parse_gtl_research_config(json.dumps({
"name": "lookback_504",
"lookback_bars": 504,
"candidate_limit": None,
}))
assert config.name == "lookback_504"
assert config.lookback_bars == 504
assert config.candidate_limit is None
assert config.ladder_config().grid_bins == 20
with pytest.raises(ValueError, match="Unknown GTL research config fields"):
bt._parse_gtl_research_config('{"mystery_knob": 1}')
with pytest.raises(ValueError, match="valid JSON"):
bt._parse_gtl_research_config("{")
def test_structural_overlay_tags_production_geometry_without_replacing_it(monkeypatch):
@@ -972,6 +992,45 @@ def test_window_setups_routes_full_explicit_target_ladder(monkeypatch):
}
def test_window_setups_routes_gtl_tuning_config(monkeypatch):
captured = {}
def fake_detector(highs, lows, closes, *, config):
captured.update({
"highs": highs,
"lows": lows,
"closes": closes,
"config": config,
})
return []
monkeypatch.setenv("BACKTEST_SR_VARIANT", bt.GTL_TUNING_VARIANT)
monkeypatch.setenv("BACKTEST_GTL_CONFIG", json.dumps({
"name": "lookback_252",
"lookback_bars": 252,
"grid_bins": 12,
}))
monkeypatch.setattr(bt, "detect_gate_target_ladder", 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["highs"] == [101.0] * bt.MIN_LOOKBACK
assert captured["lows"] == [99.0] * bt.MIN_LOOKBACK
assert captured["closes"] == [100.0] * bt.MIN_LOOKBACK
assert captured["config"].lookback_bars == 252
assert captured["config"].grid_bins == 12
@pytest.mark.parametrize(
"variant",
["legacy_geometry_neutral", "legacy_range_grid_neutral"],
+67
View File
@@ -2,7 +2,10 @@
from __future__ import annotations
import pytest
from app.services.sr_service import (
GateTargetLadderConfig,
MAX_LEVELS,
_bar_respect_weight,
_cap_levels,
@@ -314,6 +317,70 @@ class TestDetectSrLevels:
assert detect_gate_target_ladder(highs, lows, closes) == expected
def test_configured_gate_target_ladder_defaults_preserve_parity(self):
highs, lows, closes, volumes = _make_series(n=500)
expected = detect_sr_levels_legacy(
highs,
lows,
closes,
volumes,
explicit_range_grid=True,
)
configured = detect_gate_target_ladder(
highs,
lows,
closes,
config=GateTargetLadderConfig(),
)
assert configured == expected
def test_configured_gate_target_ladder_bounds_history(self):
old_highs = [1_000.0] * 40
old_lows = [10.0] * 40
old_closes = [100.0] * 40
recent_highs = [110.0] * 40
recent_lows = [90.0] * 40
recent_closes = [100.0] * 40
config = GateTargetLadderConfig(
lookback_bars=40,
include_pivots=False,
)
levels = detect_gate_target_ladder(
old_highs + recent_highs,
old_lows + recent_lows,
old_closes + recent_closes,
config=config,
)
assert levels
assert all(90.0 <= level["price_level"] <= 110.0 for level in levels)
def test_configured_gate_target_ladder_separates_merge_tolerance(self):
highs, lows, closes, _ = _make_series(n=300)
tight = detect_gate_target_ladder(
highs,
lows,
closes,
config=GateTargetLadderConfig(merge_tolerance=0.0025),
)
wide = detect_gate_target_ladder(
highs,
lows,
closes,
config=GateTargetLadderConfig(merge_tolerance=0.01),
)
assert len(wide) <= len(tight)
def test_gate_target_ladder_config_rejects_invalid_values(self):
with pytest.raises(ValueError, match="lookback_bars"):
GateTargetLadderConfig(lookback_bars=19)
with pytest.raises(ValueError, match="touch_tolerance"):
GateTargetLadderConfig(touch_tolerance=-0.1)
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)]
+50
View File
@@ -0,0 +1,50 @@
"""Tests for the single-command GTL research matrix."""
from __future__ import annotations
from scripts import run_gtl_tuning_matrix as matrix
def test_matrix_is_single_variable_and_has_unique_names():
assert len(matrix.GTL_TUNING_ARMS) == 20
assert matrix.GTL_TUNING_ARMS[0]["name"] == "control"
names = [arm["name"] for arm in matrix.GTL_TUNING_ARMS]
assert len(names) == len(set(names))
for arm in matrix.GTL_TUNING_ARMS[1:]:
config = matrix._arm_config(arm)
changed = [
key
for key, value in matrix.BASE_CONFIG.items()
if config[key] != value
]
assert len(changed) == 1, arm["name"]
def _compact_result(*, sharpe: float, drawdown: float, trades: int, robust_r: float) -> dict:
return {
"full_book": {
"sharpe": sharpe,
"max_drawdown_pct": drawdown,
"trades": trades,
},
"holdout": {
"train": {"sharpe": sharpe},
"test": {"sharpe": sharpe},
},
"qualified_net_avg_r_ex_top5": robust_r,
}
def test_screen_requires_every_pre_registered_guardrail():
control = _compact_result(sharpe=2.0, drawdown=20.0, trades=100, robust_r=0.1)
passing = _compact_result(sharpe=2.1, drawdown=19.0, trades=90, robust_r=0.05)
failing = _compact_result(sharpe=2.1, drawdown=19.0, trades=79, robust_r=0.05)
passed = matrix._screen_arm(passing, control)
failed = matrix._screen_arm(failing, control)
assert passed["advances"] is True
assert passed["passed"] == passed["total"] == 6
assert failed["advances"] is False
assert failed["checks"]["retains_80pct_trades"] is False
+63
View File
@@ -252,6 +252,52 @@ def test_generate_targets_spreads_across_distance_bands():
assert any(m > 4.6 for m in multiples), "expected an aggressive (far) target"
def test_generate_targets_research_candidate_limit_can_expand_or_disable_cap():
levels = [
_SRLevelStub(
id=index,
price_level=100.0 + index * 2.0,
type="resistance",
strength=50 + index,
)
for index in range(1, 9)
]
default = target_generator.generate_targets(
"long", 100.0, 97.0, levels, 2.0 # type: ignore[arg-type]
)
expanded = target_generator.generate_targets(
"long", 100.0, 97.0, levels, 2.0, # type: ignore[arg-type]
max_targets=8,
)
uncapped = target_generator.generate_targets(
"long", 100.0, 97.0, levels, 2.0, # type: ignore[arg-type]
max_targets=None,
)
assert len(default) == 5
assert len(expanded) == 8
assert len(uncapped) == 8
assert [row["price"] for row in uncapped] == sorted(
row["price"] for row in uncapped
)
def test_generate_targets_research_max_atr_override_is_universal():
levels = [
_SRLevelStub(id=1, price_level=110.0, type="resistance", strength=60),
_SRLevelStub(id=2, price_level=112.0, type="resistance", strength=60),
]
targets = target_generator.generate_targets(
"long", 100.0, 97.0, levels, 2.0, # type: ignore[arg-type]
max_targets=None,
max_atr_multiple_override=5.5,
)
assert [row["price"] for row in targets] == [110.0]
def test_probability_decreases_with_distance():
"""A far target must be far less likely than a near one — no 90% at +39%."""
config = {
@@ -331,6 +377,23 @@ def test_zone_representative_levels_singletons_unchanged():
assert {round(r.price_level) for r in reps} == {120, 150}
def test_zone_representative_levels_accepts_research_tolerance():
from types import SimpleNamespace
from app.services.recommendation_service import _zone_representative_levels
levels = [
SimpleNamespace(id=1, price_level=110.0, type="resistance", strength=50),
SimpleNamespace(id=2, price_level=111.5, type="resistance", strength=50),
]
tight = _zone_representative_levels(levels, 100.0, tolerance=0.01)
wide = _zone_representative_levels(levels, 100.0, tolerance=0.02)
assert len(tight) == 2
assert len(wide) == 1
assert wide[0].price_level == 110.0
def test_zone_representative_levels_soft_strength_avoids_resaturation():
from types import SimpleNamespace
from app.services.recommendation_service import _zone_representative_levels