Retire completed GTL tuning harnesses
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from datetime import date, timedelta
|
||||
from types import SimpleNamespace
|
||||
@@ -804,8 +803,6 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch):
|
||||
"rewrite_range504_structural_primary2",
|
||||
"production_structural_overlay",
|
||||
"explicit_target_ladder",
|
||||
"gtl_tuning",
|
||||
"gtl_confirmation",
|
||||
):
|
||||
monkeypatch.setenv("BACKTEST_SR_VARIANT", variant)
|
||||
assert bt._sr_research_variant() == variant
|
||||
@@ -864,48 +861,6 @@ 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
|
||||
assert bt._primary_min_rr_for_variant("gtl_confirmation", 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_gtl_confirmation_config_parses_and_validates_composition():
|
||||
config = bt._parse_gtl_confirmation_config(json.dumps({
|
||||
"name": "touch_strength_intersection",
|
||||
"mode": "intersection",
|
||||
"confirmations": [
|
||||
{"name": "touch", "touch_tolerance": 0.0025},
|
||||
{"name": "strength", "strength_scale": 1000.0},
|
||||
],
|
||||
}))
|
||||
assert config.name == "touch_strength_intersection"
|
||||
assert config.mode == "intersection"
|
||||
assert len(config.confirmations) == 2
|
||||
assert config.confirmations[0].touch_tolerance == 0.0025
|
||||
assert config.confirmations[1].strength_scale == 1000.0
|
||||
|
||||
with pytest.raises(ValueError, match="exactly one tuned variant"):
|
||||
bt._parse_gtl_confirmation_config(json.dumps({
|
||||
"name": "invalid_union",
|
||||
"mode": "union",
|
||||
"confirmations": [],
|
||||
}))
|
||||
|
||||
|
||||
def test_structural_overlay_tags_production_geometry_without_replacing_it(monkeypatch):
|
||||
@@ -1017,116 +972,6 @@ 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
|
||||
|
||||
|
||||
def test_gtl_confirmation_intersection_keeps_control_geometry(monkeypatch):
|
||||
production = [{
|
||||
"direction": "long",
|
||||
"target": 111.0,
|
||||
"rr": 2.2,
|
||||
"meets_core": True,
|
||||
"sr_variant": bt.EXPLICIT_TARGET_LADDER_VARIANT,
|
||||
}]
|
||||
|
||||
def fake_window_setups(*args, sr_variant=None, gtl_research_config=None, **kwargs):
|
||||
if sr_variant == bt.EXPLICIT_TARGET_LADDER_VARIANT:
|
||||
return production
|
||||
assert sr_variant == bt.GTL_TUNING_VARIANT
|
||||
return [{
|
||||
"direction": "long",
|
||||
"target": 115.0,
|
||||
"rr": 3.0,
|
||||
"meets_core": gtl_research_config.name == "pass",
|
||||
}]
|
||||
|
||||
monkeypatch.setattr(bt, "_window_setups", fake_window_setups)
|
||||
research = bt.GTLConfirmationConfig(
|
||||
name="two_filters",
|
||||
mode="intersection",
|
||||
confirmations=(
|
||||
bt.GTLResearchConfig(name="pass"),
|
||||
bt.GTLResearchConfig(name="fail"),
|
||||
),
|
||||
)
|
||||
rows = bt._gtl_confirmation_window_setups(
|
||||
[], {}, {}, confirmation_config=research
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["target"] == 111.0
|
||||
assert rows[0]["rr"] == 2.2
|
||||
assert rows[0]["meets_core"] is False
|
||||
assert rows[0]["gtl_confirmation_passes"] == [True, False]
|
||||
assert production[0]["meets_core"] is True
|
||||
|
||||
|
||||
def test_gtl_confirmation_union_uses_tuned_geometry_only_for_addition(monkeypatch):
|
||||
production = [
|
||||
{"direction": "long", "target": 111.0, "meets_core": False},
|
||||
{"direction": "short", "target": 90.0, "meets_core": True},
|
||||
]
|
||||
tuned = [
|
||||
{"direction": "long", "target": 115.0, "meets_core": True},
|
||||
{"direction": "short", "target": 85.0, "meets_core": False},
|
||||
]
|
||||
|
||||
def fake_window_setups(*args, sr_variant=None, **kwargs):
|
||||
return production if sr_variant == bt.EXPLICIT_TARGET_LADDER_VARIANT else tuned
|
||||
|
||||
monkeypatch.setattr(bt, "_window_setups", fake_window_setups)
|
||||
research = bt.GTLConfirmationConfig(
|
||||
name="strength_union",
|
||||
mode="union",
|
||||
confirmations=(bt.GTLResearchConfig(name="strength"),),
|
||||
)
|
||||
rows = bt._gtl_confirmation_window_setups(
|
||||
[], {}, {}, confirmation_config=research
|
||||
)
|
||||
by_direction = {row["direction"]: row for row in rows}
|
||||
|
||||
assert by_direction["long"]["target"] == 115.0
|
||||
assert by_direction["long"]["gtl_confirmation_source"] == "tuned_addition"
|
||||
assert by_direction["short"]["target"] == 90.0
|
||||
assert by_direction["short"]["gtl_confirmation_source"] == "control"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"variant",
|
||||
["legacy_geometry_neutral", "legacy_range_grid_neutral"],
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.sr_service import (
|
||||
GateTargetLadderConfig,
|
||||
MAX_LEVELS,
|
||||
_bar_respect_weight,
|
||||
_cap_levels,
|
||||
@@ -317,70 +314,6 @@ 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)]
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"""Tests for the evidence-selected GTL composition matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from scripts import run_gtl_confirmation_matrix as matrix
|
||||
|
||||
|
||||
def test_confirmation_matrix_is_pre_registered_and_well_formed():
|
||||
assert len(matrix.GTL_CONFIRMATION_ARMS) == 13
|
||||
assert matrix.GTL_CONFIRMATION_ARMS[0]["name"] == "control"
|
||||
names = [arm["name"] for arm in matrix.GTL_CONFIRMATION_ARMS]
|
||||
assert len(names) == len(set(names))
|
||||
|
||||
for arm in matrix.GTL_CONFIRMATION_ARMS:
|
||||
config = matrix._config(arm)
|
||||
assert config["mode"] in {"intersection", "union"}
|
||||
if config["mode"] == "union":
|
||||
assert len(config["confirmations"]) == 1
|
||||
for confirmation in config["confirmations"]:
|
||||
assert confirmation["name"] in {
|
||||
"touch_0_25pct",
|
||||
"strength_1000",
|
||||
"merge_0_25pct",
|
||||
"pivots_none",
|
||||
}
|
||||
|
||||
|
||||
def test_signature_uses_only_control_parity_fields():
|
||||
arm = {
|
||||
"candidates": 100,
|
||||
"qualified": 10,
|
||||
"qualified_net_avg_r": 0.2,
|
||||
"qualified_net_avg_r_ex_top5": 0.05,
|
||||
"full_book": {"sharpe": 2.0},
|
||||
"holdout": {"train": {"sharpe": 1.0}},
|
||||
"unrelated": "ignored",
|
||||
}
|
||||
assert "unrelated" not in matrix._signature(arm)
|
||||
assert matrix._signature(arm)["full_book"] == {"sharpe": 2.0}
|
||||
@@ -1,31 +0,0 @@
|
||||
"""Tests for the pre-registered strength-confirmation sensitivity band."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from scripts import run_gtl_strength_sensitivity as matrix
|
||||
|
||||
|
||||
def test_strength_sensitivity_is_ordered_and_contains_replication_arm():
|
||||
assert len(matrix.GTL_STRENGTH_ARMS) == 9
|
||||
assert matrix.GTL_STRENGTH_ARMS[0]["name"] == "control"
|
||||
assert list(matrix.STRENGTH_SCALES) == sorted(matrix.STRENGTH_SCALES)
|
||||
assert "strength_1000_intersection" in {
|
||||
arm["name"] for arm in matrix.GTL_STRENGTH_ARMS
|
||||
}
|
||||
for arm in matrix.GTL_STRENGTH_ARMS[1:]:
|
||||
config = matrix.composition._config(arm)
|
||||
assert config["mode"] == "intersection"
|
||||
assert len(config["confirmations"]) == 1
|
||||
|
||||
|
||||
def test_stable_plateau_requires_adjacent_passing_scales():
|
||||
arms = [
|
||||
{"name": "control", "screen": {"advances": False}},
|
||||
{"name": "strength_625", "screen": {"advances": True}},
|
||||
{"name": "strength_750", "screen": {"advances": True}},
|
||||
{"name": "strength_875", "screen": {"advances": False}},
|
||||
{"name": "strength_1000", "screen": {"advances": True}},
|
||||
]
|
||||
assert matrix._stable_plateau_pairs(arms) == [
|
||||
["strength_625", "strength_750"]
|
||||
]
|
||||
@@ -1,50 +0,0 @@
|
||||
"""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
|
||||
@@ -252,52 +252,6 @@ 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 = {
|
||||
@@ -377,23 +331,6 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user