Retire completed GTL tuning harnesses
This commit is contained in:
@@ -33,9 +33,7 @@ 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
|
||||
|
||||
@@ -73,7 +71,6 @@ 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,
|
||||
@@ -86,7 +83,6 @@ 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,
|
||||
@@ -110,8 +106,6 @@ 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"
|
||||
GTL_CONFIRMATION_VARIANT = "gtl_confirmation"
|
||||
RANGE_RESIDUAL_VARIANTS = {
|
||||
"rewrite_range504_structural_legacy_primary",
|
||||
"rewrite_range504_structural_primary2",
|
||||
@@ -122,69 +116,6 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GTLConfirmationConfig:
|
||||
"""Research-only composition of the frozen GTL and tuned variants."""
|
||||
|
||||
name: str = "control"
|
||||
mode: str = "intersection"
|
||||
confirmations: tuple[GTLResearchConfig, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.name.strip():
|
||||
raise ValueError("GTL confirmation config name must not be empty")
|
||||
if self.mode not in {"intersection", "union"}:
|
||||
raise ValueError("GTL confirmation mode must be intersection or union")
|
||||
if self.mode == "union" and len(self.confirmations) != 1:
|
||||
raise ValueError("GTL union mode requires exactly one tuned variant")
|
||||
|
||||
# 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
|
||||
@@ -239,8 +170,6 @@ SR_RESEARCH_VARIANTS = {
|
||||
"legacy_range_grid_touch",
|
||||
"legacy_range_grid_neutral",
|
||||
EXPLICIT_TARGET_LADDER_VARIANT,
|
||||
GTL_TUNING_VARIANT,
|
||||
GTL_CONFIRMATION_VARIANT,
|
||||
STRUCTURAL_OVERLAY_VARIANT,
|
||||
*RANGE_FACTOR_VARIANTS,
|
||||
}
|
||||
@@ -255,72 +184,6 @@ 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", ""))
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _parse_gtl_confirmation_config(raw: str) -> GTLConfirmationConfig:
|
||||
"""Parse one composition arm for the offline confirmation matrix."""
|
||||
if not raw.strip():
|
||||
return GTLConfirmationConfig()
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError("BACKTEST_GTL_CONFIRM_CONFIG must be valid JSON") from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("BACKTEST_GTL_CONFIRM_CONFIG must be a JSON object")
|
||||
allowed = {"name", "mode", "confirmations"}
|
||||
unknown = sorted(set(payload) - allowed)
|
||||
if unknown:
|
||||
raise ValueError(
|
||||
f"Unknown GTL confirmation config fields: {', '.join(unknown)}"
|
||||
)
|
||||
raw_confirmations = payload.get("confirmations", [])
|
||||
if not isinstance(raw_confirmations, list):
|
||||
raise ValueError("GTL confirmations must be a JSON array")
|
||||
confirmations: list[GTLResearchConfig] = []
|
||||
for item in raw_confirmations:
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError("Each GTL confirmation must be a JSON object")
|
||||
confirmations.append(_parse_gtl_research_config(json.dumps(item)))
|
||||
try:
|
||||
return GTLConfirmationConfig(
|
||||
name=payload.get("name", "control"),
|
||||
mode=payload.get("mode", "intersection"),
|
||||
confirmations=tuple(confirmations),
|
||||
)
|
||||
except (AttributeError, TypeError, ValueError) as exc:
|
||||
raise ValueError(f"Invalid GTL confirmation config: {exc}") from exc
|
||||
|
||||
|
||||
def _gtl_confirmation_config() -> GTLConfirmationConfig:
|
||||
return _parse_gtl_confirmation_config(
|
||||
os.getenv("BACKTEST_GTL_CONFIRM_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}:
|
||||
@@ -361,8 +224,6 @@ def _primary_min_rr_for_variant(sr_variant: str, activation: dict) -> float:
|
||||
"production_control",
|
||||
"production_range504",
|
||||
EXPLICIT_TARGET_LADDER_VARIANT,
|
||||
GTL_TUNING_VARIANT,
|
||||
GTL_CONFIRMATION_VARIANT,
|
||||
STRUCTURAL_OVERLAY_VARIANT,
|
||||
}
|
||||
or sr_variant.endswith("_legacy_primary")
|
||||
@@ -477,7 +338,6 @@ def _window_setups(
|
||||
activation: dict,
|
||||
*,
|
||||
sr_variant: str | None = None,
|
||||
gtl_research_config: GTLResearchConfig | None = None,
|
||||
) -> list[dict]:
|
||||
"""Rebuild the setup(s) at the last bar of ``window_records`` (the as-of date),
|
||||
using only those bars. Returns one dict per tradeable direction."""
|
||||
@@ -506,11 +366,6 @@ def _window_setups(
|
||||
|
||||
sr_variant = sr_variant or _sr_research_variant()
|
||||
detector_variant = _sr_detector_variant(sr_variant)
|
||||
gtl_config = (
|
||||
gtl_research_config or _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(
|
||||
@@ -530,15 +385,6 @@ 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,
|
||||
@@ -589,20 +435,9 @@ 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,
|
||||
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),
|
||||
)
|
||||
targets = target_generator.generate_targets(direction, entry, stop, zone_levels, atr)
|
||||
if not targets:
|
||||
fallback_k = _atr_target_fallback_k()
|
||||
if fallback_k is None:
|
||||
@@ -749,87 +584,6 @@ def _structural_overlay_window_setups(
|
||||
return tagged
|
||||
|
||||
|
||||
def _gtl_confirmation_window_setups(
|
||||
window_records: list,
|
||||
config: dict,
|
||||
activation: dict,
|
||||
*,
|
||||
confirmation_config: GTLConfirmationConfig | None = None,
|
||||
) -> list[dict]:
|
||||
"""Compose tuned GTLs around the frozen ladder without changing it silently.
|
||||
|
||||
``intersection`` retains the frozen setup geometry and requires every tuned
|
||||
variant to clear the core gate in the same direction. ``union`` preserves a
|
||||
frozen setup whenever it already clears the core gate, and otherwise admits
|
||||
the one tuned variant's setup. This makes retained, removed, and added
|
||||
cohorts explicit instead of conflating them in a replacement arm.
|
||||
"""
|
||||
research = confirmation_config or _gtl_confirmation_config()
|
||||
production = _window_setups(
|
||||
window_records,
|
||||
config,
|
||||
activation,
|
||||
sr_variant=EXPLICIT_TARGET_LADDER_VARIANT,
|
||||
)
|
||||
tuned_sets = [
|
||||
_window_setups(
|
||||
window_records,
|
||||
config,
|
||||
activation,
|
||||
sr_variant=GTL_TUNING_VARIANT,
|
||||
gtl_research_config=tuned_config,
|
||||
)
|
||||
for tuned_config in research.confirmations
|
||||
]
|
||||
tuned_by_direction = [
|
||||
{row["direction"]: row for row in rows}
|
||||
for rows in tuned_sets
|
||||
]
|
||||
|
||||
def annotate(row: dict, passes: list[bool], source: str) -> dict:
|
||||
tagged = dict(row)
|
||||
tagged["sr_variant"] = GTL_CONFIRMATION_VARIANT
|
||||
tagged["gtl_confirmation_name"] = research.name
|
||||
tagged["gtl_confirmation_mode"] = research.mode
|
||||
tagged["gtl_confirmation_source"] = source
|
||||
tagged["gtl_confirmation_passes"] = passes
|
||||
tagged["gtl_confirmation_all_pass"] = all(passes)
|
||||
return tagged
|
||||
|
||||
if research.mode == "intersection":
|
||||
tagged: list[dict] = []
|
||||
for production_row in production:
|
||||
direction = production_row["direction"]
|
||||
passes = [
|
||||
bool(rows.get(direction) and rows[direction].get("meets_core"))
|
||||
for rows in tuned_by_direction
|
||||
]
|
||||
row = annotate(production_row, passes, "control")
|
||||
row["meets_core"] = bool(production_row.get("meets_core")) and all(
|
||||
passes
|
||||
)
|
||||
tagged.append(row)
|
||||
return tagged
|
||||
|
||||
# Union mode is validated to contain exactly one tuned variant. Keep one
|
||||
# setup per direction: frozen geometry wins whenever it already qualifies;
|
||||
# tuned geometry is used only for a genuinely added core-qualified setup.
|
||||
production_by_direction = {row["direction"]: row for row in production}
|
||||
tuned_by_dir = tuned_by_direction[0]
|
||||
tagged = []
|
||||
for direction in sorted(set(production_by_direction) | set(tuned_by_dir)):
|
||||
production_row = production_by_direction.get(direction)
|
||||
tuned_row = tuned_by_dir.get(direction)
|
||||
tuned_pass = bool(tuned_row and tuned_row.get("meets_core"))
|
||||
if production_row is not None and production_row.get("meets_core"):
|
||||
tagged.append(annotate(production_row, [tuned_pass], "control"))
|
||||
elif tuned_pass and tuned_row is not None:
|
||||
tagged.append(annotate(tuned_row, [True], "tuned_addition"))
|
||||
elif production_row is not None:
|
||||
tagged.append(annotate(production_row, [tuned_pass], "control"))
|
||||
return tagged
|
||||
|
||||
|
||||
def _stop_fill_r(direction: str, entry: float, stop: float, bar) -> float:
|
||||
"""Realized R when the stop is hit on ``bar``: filled at the stop, or at the
|
||||
bar's open when price gapped through it — so a gap can lose more than −1R,
|
||||
@@ -934,17 +688,16 @@ def _replay_ticker(
|
||||
)
|
||||
vol_6m = _realized_vol_6m(closes, len(window) - 1)
|
||||
|
||||
if sr_variant == STRUCTURAL_OVERLAY_VARIANT:
|
||||
setups = _structural_overlay_window_setups(window, config, activation)
|
||||
elif sr_variant == GTL_CONFIRMATION_VARIANT:
|
||||
setups = _gtl_confirmation_window_setups(window, config, activation)
|
||||
else:
|
||||
setups = _window_setups(
|
||||
setups = (
|
||||
_structural_overlay_window_setups(window, config, activation)
|
||||
if sr_variant == STRUCTURAL_OVERLAY_VARIANT
|
||||
else _window_setups(
|
||||
window,
|
||||
config,
|
||||
activation,
|
||||
sr_variant=sr_variant,
|
||||
)
|
||||
)
|
||||
for s in setups:
|
||||
outcome, outcome_date = evaluate_setup_against_bars(
|
||||
s["direction"], s["stop"], s["target"], forward_bars, HORIZON
|
||||
@@ -1010,13 +763,6 @@ def _replay_ticker(
|
||||
"structural_overlay_gate_level_count": s.get(
|
||||
"structural_overlay_gate_level_count"
|
||||
),
|
||||
"gtl_confirmation_name": s.get("gtl_confirmation_name"),
|
||||
"gtl_confirmation_mode": s.get("gtl_confirmation_mode"),
|
||||
"gtl_confirmation_source": s.get("gtl_confirmation_source"),
|
||||
"gtl_confirmation_passes": s.get("gtl_confirmation_passes"),
|
||||
"gtl_confirmation_all_pass": s.get(
|
||||
"gtl_confirmation_all_pass"
|
||||
),
|
||||
"outcome": outcome,
|
||||
"target_hit": target_hit,
|
||||
"realized_r": realized_r,
|
||||
@@ -1089,9 +835,6 @@ def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
|
||||
range_logs: list[float] = []
|
||||
overlay_rows = 0
|
||||
overlay_pass = 0
|
||||
confirmation_rows = 0
|
||||
confirmation_pass = 0
|
||||
confirmation_tuned_additions = 0
|
||||
for cand in candidates:
|
||||
sources = list(cand.get("primary_sources") or [])
|
||||
for source in sources:
|
||||
@@ -1107,12 +850,6 @@ def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
|
||||
if cand.get("structural_overlay_pass") is not None:
|
||||
overlay_rows += 1
|
||||
overlay_pass += int(bool(cand["structural_overlay_pass"]))
|
||||
if cand.get("gtl_confirmation_all_pass") is not None:
|
||||
confirmation_rows += 1
|
||||
confirmation_pass += int(bool(cand["gtl_confirmation_all_pass"]))
|
||||
confirmation_tuned_additions += int(
|
||||
cand.get("gtl_confirmation_source") == "tuned_addition"
|
||||
)
|
||||
|
||||
def avg(values: list[float] | list[int]) -> float | None:
|
||||
return round(sum(values) / len(values), 3) if values else None
|
||||
@@ -1137,9 +874,6 @@ def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
|
||||
"structural_overlay_weight": (
|
||||
STRUCTURAL_OVERLAY_WEIGHT if overlay_rows else None
|
||||
),
|
||||
"gtl_confirmation_rows": confirmation_rows,
|
||||
"gtl_confirmation_pass": confirmation_pass,
|
||||
"gtl_confirmation_tuned_additions": confirmation_tuned_additions,
|
||||
}
|
||||
|
||||
|
||||
@@ -1186,15 +920,6 @@ def _sr_candidate_audit(candidates: list[dict], min_percentile: float) -> list[d
|
||||
"structural_overlay_gate_level_count": int(
|
||||
cand.get("structural_overlay_gate_level_count", 0) or 0
|
||||
),
|
||||
"gtl_confirmation_name": cand.get("gtl_confirmation_name"),
|
||||
"gtl_confirmation_mode": cand.get("gtl_confirmation_mode"),
|
||||
"gtl_confirmation_source": cand.get("gtl_confirmation_source"),
|
||||
"gtl_confirmation_passes": list(
|
||||
cand.get("gtl_confirmation_passes") or []
|
||||
),
|
||||
"gtl_confirmation_all_pass": cand.get(
|
||||
"gtl_confirmation_all_pass"
|
||||
),
|
||||
"rr": round(float(cand.get("rr", 0.0)), 6),
|
||||
"primary_prob": round(float(cand.get("primary_prob", 0.0)), 6),
|
||||
"primary_sources": list(cand.get("primary_sources") or []),
|
||||
@@ -3601,16 +3326,6 @@ 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
|
||||
),
|
||||
"gtl_confirmation_config": (
|
||||
asdict(_gtl_confirmation_config())
|
||||
if _sr_research_variant() == GTL_CONFIRMATION_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),
|
||||
|
||||
Reference in New Issue
Block a user