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
+105 -1
View File
@@ -33,7 +33,9 @@ import statistics
from collections import defaultdict
from collections.abc import Callable
from concurrent.futures import ProcessPoolExecutor
from dataclasses import asdict, dataclass
from datetime import date, datetime, timedelta, timezone
from functools import lru_cache
from types import SimpleNamespace
from typing import Any
@@ -71,6 +73,7 @@ from app.services.recommendation_service import (
_prune_floor_pinned_targets,
_risk_level_from_conflicts,
_select_primary_target,
_SR_ZONE_TOLERANCE,
_zone_representative_levels,
direction_analyzer,
get_recommendation_config,
@@ -83,6 +86,7 @@ from app.services.scoring_service import (
compute_technical_from_arrays,
)
from app.services.sr_service import (
GateTargetLadderConfig,
MAX_LEVELS,
detect_gate_target_ladder,
detect_sr_levels,
@@ -106,6 +110,7 @@ STRUCTURAL_OVERLAY_SOURCE_VARIANT = (
STRUCTURAL_OVERLAY_WEIGHT = 0.05
STRUCTURAL_OVERLAY_SCORE_KEY = "structural_overlay_95_5_score"
EXPLICIT_TARGET_LADDER_VARIANT = "explicit_target_ladder"
GTL_TUNING_VARIANT = "gtl_tuning"
RANGE_RESIDUAL_VARIANTS = {
"rewrite_range504_structural_legacy_primary",
"rewrite_range504_structural_primary2",
@@ -116,6 +121,52 @@ RANGE_FACTOR_VARIANTS = {
*RANGE_RESIDUAL_VARIANTS,
}
@dataclass(frozen=True)
class GTLResearchConfig:
"""Research-only controls for one GTL matrix arm.
The default values reproduce the explicit Gate Target Ladder. Production
never reads ``BACKTEST_GTL_CONFIG``.
"""
name: str = "control"
lookback_bars: int | None = None
grid_bins: int = 20
include_pivots: bool = True
pivot_window: int = 2
touch_tolerance: float = 0.005
merge_tolerance: float = 0.005
strength_scale: float = 500.0
zone_tolerance: float = 0.02
candidate_limit: int | None = 5
max_target_atr: float | None = None
def __post_init__(self) -> None:
if not self.name.strip():
raise ValueError("GTL research config name must not be empty")
if not isinstance(self.include_pivots, bool):
raise ValueError("GTL include_pivots must be a boolean")
if not 0.0 <= self.zone_tolerance < 0.10:
raise ValueError("GTL zone_tolerance must be in [0, 0.10)")
if self.candidate_limit is not None and self.candidate_limit < 1:
raise ValueError("GTL candidate_limit must be positive or null")
if self.max_target_atr is not None and self.max_target_atr <= 0:
raise ValueError("GTL max_target_atr must be positive or null")
# Reuse the detector's validation for its subset of fields.
self.ladder_config()
def ladder_config(self) -> GateTargetLadderConfig:
return GateTargetLadderConfig(
lookback_bars=self.lookback_bars,
grid_bins=self.grid_bins,
include_pivots=self.include_pivots,
pivot_window=self.pivot_window,
touch_tolerance=self.touch_tolerance,
merge_tolerance=self.merge_tolerance,
strength_scale=self.strength_scale,
)
# Cross-sectional signal evaluation (factor IC). Each candidate signal is a
# point-in-time number computed from closes alone (sentiment/fundamentals have no
# history here), sampled one as-of per ISO week, and graded by how its rank
@@ -170,6 +221,7 @@ SR_RESEARCH_VARIANTS = {
"legacy_range_grid_touch",
"legacy_range_grid_neutral",
EXPLICIT_TARGET_LADDER_VARIANT,
GTL_TUNING_VARIANT,
STRUCTURAL_OVERLAY_VARIANT,
*RANGE_FACTOR_VARIANTS,
}
@@ -184,6 +236,31 @@ def _sr_research_variant() -> str:
return value
@lru_cache(maxsize=64)
def _parse_gtl_research_config(raw: str) -> GTLResearchConfig:
"""Parse one auditable JSON configuration passed by the offline runner."""
if not raw.strip():
return GTLResearchConfig()
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError("BACKTEST_GTL_CONFIG must be valid JSON") from exc
if not isinstance(payload, dict):
raise ValueError("BACKTEST_GTL_CONFIG must be a JSON object")
allowed = set(GTLResearchConfig.__dataclass_fields__)
unknown = sorted(set(payload) - allowed)
if unknown:
raise ValueError(f"Unknown GTL research config fields: {', '.join(unknown)}")
try:
return GTLResearchConfig(**payload)
except TypeError as exc:
raise ValueError(f"Invalid GTL research config: {exc}") from exc
def _gtl_research_config() -> GTLResearchConfig:
return _parse_gtl_research_config(os.getenv("BACKTEST_GTL_CONFIG", ""))
def _sr_detector_variant(sr_variant: str) -> str:
"""Map factor-gated research arms to the detector they hold fixed."""
if sr_variant in {"production_range504", STRUCTURAL_OVERLAY_VARIANT}:
@@ -224,6 +301,7 @@ def _primary_min_rr_for_variant(sr_variant: str, activation: dict) -> float:
"production_control",
"production_range504",
EXPLICIT_TARGET_LADDER_VARIANT,
GTL_TUNING_VARIANT,
STRUCTURAL_OVERLAY_VARIANT,
}
or sr_variant.endswith("_legacy_primary")
@@ -366,6 +444,7 @@ def _window_setups(
sr_variant = sr_variant or _sr_research_variant()
detector_variant = _sr_detector_variant(sr_variant)
gtl_config = _gtl_research_config() if sr_variant == GTL_TUNING_VARIANT else None
range_504_log = _range_504_log(highs, lows)
if sr_variant == "legacy_geometry_neutral":
detected_levels = detect_sr_levels_legacy(
@@ -385,6 +464,15 @@ def _window_setups(
lows,
closes,
)
elif sr_variant == GTL_TUNING_VARIANT:
if gtl_config is None: # pragma: no cover - guarded by the variant above
raise RuntimeError("GTL tuning variant requires a research config")
detected_levels = detect_gate_target_ladder(
highs,
lows,
closes,
config=gtl_config.ladder_config(),
)
elif sr_variant in {"legacy_range_grid_touch", "legacy_range_grid_neutral"}:
detected_levels = detect_sr_levels_legacy(
highs,
@@ -435,9 +523,20 @@ def _window_setups(
gate_levels,
entry,
strength_mode=zone_strength_mode,
tolerance=(
gtl_config.zone_tolerance if gtl_config else _SR_ZONE_TOLERANCE
),
)
zone_levels = _apply_zone_strength_variant(zone_levels, sr_variant)
targets = target_generator.generate_targets(direction, entry, stop, zone_levels, atr)
targets = target_generator.generate_targets(
direction,
entry,
stop,
zone_levels,
atr,
max_targets=(gtl_config.candidate_limit if gtl_config else 5),
max_atr_multiple_override=(gtl_config.max_target_atr if gtl_config else None),
)
if not targets:
fallback_k = _atr_target_fallback_k()
if fallback_k is None:
@@ -3326,6 +3425,11 @@ async def run_backtest(
"min_lookback": MIN_LOOKBACK,
"cost_per_side_pct": round(COST_PER_SIDE * 100, 3),
"sr_variant": _sr_research_variant(),
"gtl_config": (
asdict(_gtl_research_config())
if _sr_research_variant() == GTL_TUNING_VARIANT
else None
),
"range_factor_lookback": RANGE_FACTOR_LOOKBACK,
"range_factor_min_log": RANGE_FACTOR_MIN_LOG,
"range_factor_min_ratio": round(math.exp(RANGE_FACTOR_MIN_LOG), 4),