Retire completed GTL tuning harnesses

This commit is contained in:
2026-07-13 16:40:29 +02:00
parent 1d84a40c04
commit 8e09f239c8
16 changed files with 59 additions and 2057 deletions
+7 -59
View File
@@ -459,65 +459,14 @@ metrics. Keep the SSH tunnel open only while creating the snapshot; the backtest
run itself is local/offline. `backtest_snapshots/` and generated backtest reports
are git-ignored.
### One-command GTL tuning run
### Archived GTL tuning decision
The Gate Target Ladder has a research-only, single-variable matrix for testing
whether its useful screening behavior can be made more explicit. It runs 20
complete backtests **sequentially** so each arm gets the full worker pool:
```bash
# macOS/Linux
.venv/bin/python scripts/run_gtl_tuning_matrix.py \
backtest_snapshots/prod.sqlite --workers 14
```
The arms cover GTL history length, grid density, pivots, price-traffic scoring,
proposal merging, target-zone width, candidate count, and maximum target
distance. Every arm includes the full-period production book, the fixed
`2024-07-01` train/test split, a candidate-level paired audit, and the same
pre-registered robustness screen. This is one long command, not a Cartesian
parameter search; expect total runtime to be roughly 20 times one full local
backtest.
Progress is checkpointed after every arm to
`reports/backtest-YYYYMMDD-gtl-tuning-matrix.json` and the matching `.md` table.
The large per-arm reports are removed only after successful consolidation. If
the run fails, they remain available for diagnosis; pass `--keep-arm-reports`
to retain them after success too. No arm changes live scanner defaults or
deploys anything.
The replacement matrix found no winning single constant. Its evidence-selected
follow-up keeps control geometry and isolates retained versus added cohorts in
one 13-arm confirmation/union run:
```bash
# macOS/Linux
.venv/bin/python scripts/run_gtl_confirmation_matrix.py \
backtest_snapshots/prod.sqlite --workers 12
```
It first verifies exact control parity with the completed tuning matrix, then
checkpoints consolidated JSON and Markdown reports under
`reports/backtest-YYYYMMDD-gtl-confirmation-matrix.*`.
The confirmation matrix's only near-hit was strength-1000 intersection: it
improved full/train/post-2024 Sharpe but missed the unchanged drawdown guardrail
by 0.3 percentage points. The final narrow sensitivity check is:
```bash
.venv/bin/python scripts/run_gtl_strength_sensitivity.py \
backtest_snapshots/prod.sqlite --workers 12
```
It checks eight coarse scales around 1000, verifies exact control and
strength-1000 replication, and requires two adjacent scales to pass every
original guardrail before calling the result stable.
Final result: control and strength-1000 replication both passed, but there was
no adjacent passing plateau. Scale 1500 passed in isolation while lowering CAGR
and setup expectancy; its neighbors failed. The research decision is therefore
to keep the frozen GTL unchanged and evaluate any future challenger only on new
forward data. See the [full research record](docs/research/sr-levels-and-exits.md#gtl-tuning-matrix).
The completed replacement, cohort-composition, and strength-sensitivity
matrices found no stable improvement over the frozen Gate Target Ladder. The
temporary matrix runners and tuning hooks have been retired; their three compact
consolidated report pairs remain in `reports/` as the decision audit. Keep the
GTL unchanged and evaluate any future challenger only on new forward data. See
the [full research record](docs/research/sr-levels-and-exits.md#gtl-tuning-matrix).
### Reading a local backtest report
@@ -557,7 +506,6 @@ Research-only flags, all off by default (the default report is byte-identical to
| `BACKTEST_HOLDOUT_SPLIT=YYYY-MM-DD` | Adds a `holdout` section: train (entries before) vs test (entries on/after), as disjoint books |
| `BACKTEST_MIN_RR_SWEEP=1` | Sweeps the activation R:R floor against portfolio Sharpe. Combine with `BACKTEST_HOLDOUT_SPLIT` to sweep out-of-sample |
| `BACKTEST_SR_VARIANT=<arm>` | Research-only S/R detector/gate arm; see `docs/research/sr-levels-and-exits.md` for the detector and hidden-feature matrices |
| `BACKTEST_GTL_CONFIG=<json>` | Parameterizes only the `gtl_tuning` research arm; normally set by `run_gtl_tuning_matrix.py` |
| `BACKTEST_ENTRY_START=YYYY-MM-DD` | Restrict candidate entry dates to a validation window |
| `BACKTEST_ENTRY_END=YYYY-MM-DD` | Restrict candidate entry dates to a training window |
| `BACKTEST_SR_AUDIT=1` | Add momentum-slice candidate rows for paired S/R cohort comparison |
+6 -291
View File
@@ -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),
+3 -20
View File
@@ -93,7 +93,6 @@ def _zone_representative_levels(
entry_price: float,
*,
strength_mode: str = "sum",
tolerance: float = _SR_ZONE_TOLERANCE,
) -> list[Any]:
"""Collapse near-duplicate S/R levels into one representative per zone.
@@ -125,7 +124,7 @@ def _zone_representative_levels(
zones = cluster_sr_zones(
level_dicts,
entry_price,
tolerance=tolerance,
tolerance=_SR_ZONE_TOLERANCE,
strength_mode=strength_mode,
)
@@ -316,16 +315,9 @@ class TargetGenerator:
stop_loss: float,
sr_levels: list[SRLevel],
atr_value: float,
*,
max_targets: int | None = 5,
max_atr_multiple_override: float | None = None,
) -> list[dict[str, Any]]:
if atr_value <= 0:
return []
if max_targets is not None and max_targets < 1:
raise ValueError("max_targets must be positive or None")
if max_atr_multiple_override is not None and max_atr_multiple_override <= 0:
raise ValueError("max_atr_multiple_override must be positive or None")
risk = abs(entry_price - stop_loss)
if risk <= 0:
@@ -334,8 +326,7 @@ class TargetGenerator:
candidates: list[dict[str, Any]] = []
atr_pct = atr_value / entry_price if entry_price > 0 else 0.0
max_atr_multiple: float | None = max_atr_multiple_override
if max_atr_multiple is None:
max_atr_multiple: float | None = None
if atr_pct > 0.05:
max_atr_multiple = 10.0
elif atr_pct < 0.02:
@@ -392,12 +383,6 @@ class TargetGenerator:
if not candidates:
return []
if max_targets is None:
candidates.sort(key=lambda row: row["distance_from_entry"])
for target in candidates:
target.pop("quality", None)
return candidates
# Select up to 5 targets that SPAN the distance range, instead of the
# top-5 by quality (which biases toward far, high-R:R levels and buries
# every nearby target). Guarantees the nearest level plus a
@@ -411,8 +396,6 @@ class TargetGenerator:
selected_ids: set[int] = set()
def _add(candidate: dict[str, Any] | None) -> None:
if len(selected) >= max_targets:
return
if candidate is not None and candidate["sr_level_id"] not in selected_ids:
selected.append(candidate)
selected_ids.add(candidate["sr_level_id"])
@@ -425,7 +408,7 @@ class TargetGenerator:
_add(max(bucket, key=lambda c: c["quality"]))
# Fill remaining slots with the next-best by quality
for candidate in sorted(candidates, key=lambda c: c["quality"], reverse=True):
if len(selected) >= max_targets:
if len(selected) >= 5:
break
_add(candidate)
-138
View File
@@ -10,7 +10,6 @@ and persists to DB.
from __future__ import annotations
import math
from dataclasses import dataclass
from datetime import datetime
from sqlalchemy import delete, select
@@ -33,40 +32,6 @@ from app.services.price_service import query_ohlcv
DEFAULT_TOLERANCE = 0.005 # fallback when ATR unavailable; also API legacy default
@dataclass(frozen=True)
class GateTargetLadderConfig:
"""Research controls for the transient Gate Target Ladder.
Live callers omit this object and retain the frozen legacy-parity path.
The backtest may pass an explicit configuration to isolate one ladder
mechanism at a time without changing Structural S/R.
"""
lookback_bars: int | None = None
grid_bins: int = 20
include_pivots: bool = True
pivot_window: int = 2
touch_tolerance: float = DEFAULT_TOLERANCE
merge_tolerance: float = DEFAULT_TOLERANCE
strength_scale: float = 500.0
def __post_init__(self) -> None:
if self.lookback_bars is not None and self.lookback_bars < 20:
raise ValueError("GTL lookback_bars must be at least 20 or None")
if self.grid_bins < 2:
raise ValueError("GTL grid_bins must be at least 2")
if self.pivot_window < 1:
raise ValueError("GTL pivot_window must be at least 1")
for name, value in (
("touch_tolerance", self.touch_tolerance),
("merge_tolerance", self.merge_tolerance),
):
if not 0.0 <= value < 0.10:
raise ValueError(f"GTL {name} must be in [0, 0.10)")
if self.strength_scale <= 0:
raise ValueError("GTL strength_scale must be positive")
VP_LOOKBACK = 252
TOUCH_LOOKBACK = 252
PIVOT_LOOKBACK = 504
@@ -562,8 +527,6 @@ def detect_gate_target_ladder(
lows: list[float],
closes: list[float],
tolerance: float = DEFAULT_TOLERANCE,
*,
config: GateTargetLadderConfig | None = None,
) -> list[dict]:
"""Build the scanner's internal, volume-free target proposal ladder.
@@ -573,9 +536,6 @@ def detect_gate_target_ladder(
profile calculation. The returned levels are transient and must not be
persisted as chart S/R.
"""
if config is None:
# Frozen live/default path. Keeping the established helper here makes
# the absence of research configuration an exact compatibility promise.
return detect_sr_levels_legacy(
highs,
lows,
@@ -585,104 +545,6 @@ def detect_gate_target_ladder(
explicit_range_grid=True,
)
if tolerance != DEFAULT_TOLERANCE:
raise ValueError("Pass either GTL config tolerances or tolerance, not both")
if not closes:
return []
if not (len(highs) == len(lows) == len(closes)):
raise ValueError("GTL highs, lows, and closes must have equal lengths")
if config.lookback_bars is not None:
highs = highs[-config.lookback_bars:]
lows = lows[-config.lookback_bars:]
closes = closes[-config.lookback_bars:]
candidates: list[tuple[float, str]] = []
try:
candidates.extend(
(float(price), "range_grid")
for price in _legacy_range_grid_nodes(
highs,
lows,
closes,
num_bins=config.grid_bins,
)
)
except ValidationError:
pass
if config.include_pivots:
try:
pivots = compute_pivot_points(
highs,
lows,
closes,
window=config.pivot_window,
)
candidates.extend(
(float(price), "pivot_point")
for price in pivots.get("swing_highs", []) + pivots.get("swing_lows", [])
)
except ValidationError:
pass
if not candidates:
return []
total_bars = len(closes)
raw: list[dict] = []
for price, method in candidates:
touch_band = abs(price) * config.touch_tolerance if price else config.touch_tolerance
touches = sum(
1
for low, high in zip(lows, highs, strict=False)
if low - touch_band <= price <= high + touch_band
)
strength = max(
0,
min(100, int(round((touches / total_bars) * config.strength_scale))),
)
raw.append({
"price_level": price,
"strength": strength,
"detection_method": method,
"type": "",
"sources": [method],
"rejection_count": touches,
"last_rejection_age": None,
"weighted_respects": float(touches),
})
merged: list[dict] = []
for level in sorted(raw, key=lambda row: row["price_level"]):
if not merged:
merged.append(dict(level))
continue
last = merged[-1]
ref = last["price_level"]
merge_band = abs(ref) * config.merge_tolerance if ref else config.merge_tolerance
if abs(level["price_level"] - ref) > merge_band:
merged.append(dict(level))
continue
last["price_level"] = round(
(last["price_level"] + level["price_level"]) / 2.0,
4,
)
last["strength"] = min(100, last["strength"] + level["strength"])
sources = set(last.get("sources") or [last["detection_method"]])
sources |= set(level.get("sources") or [level["detection_method"]])
last["sources"] = sorted(sources)
last["detection_method"] = (
next(iter(sources)) if len(sources) == 1 else "merged"
)
last["rejection_count"] = max(
int(last.get("rejection_count", 0)),
int(level.get("rejection_count", 0)),
)
_tag_levels(merged, closes[-1])
merged.sort(key=lambda row: row["strength"], reverse=True)
return merged
def _merge_levels(
levels: list[dict],
+16 -28
View File
@@ -806,10 +806,10 @@ or perform a production deployment.
#### GTL tuning matrix
Exact parity establishes a safe, explicit control, but it does not prove that
the inherited GTL constants are optimal. The `gtl_tuning` backtest arm exposes
only those constants to an offline configuration; the live scanner continues
to call the frozen default helper and cannot read this research configuration.
Exact parity established a safe, explicit control but did not prove that the
inherited GTL constants were optimal. A temporary offline harness exposed those
constants without changing the live scanner. That harness has now been retired;
the compact consolidated reports remain as the reproducible decision record.
The single-command matrix contains 20 full-period arms. Each non-control arm
changes exactly one input:
@@ -826,11 +826,6 @@ changes exactly one input:
| Pivots | Five-bar swings | None / eleven-bar swings | Do pivots add anything beyond the range ladder? |
| Traffic strength scale | 500 | 250 / 1000 | Does strength saturation affect probability/selection? |
```bash
.venv/bin/python scripts/run_gtl_tuning_matrix.py \
backtest_snapshots/prod.sqlite --workers 14
```
Arms execute sequentially so multiprocessing pools never compete. Each arm
produces full-period production metrics, train/test books split at 2024-07-01,
robust expectancy after removing the top 5% of setups, and retained/added/
@@ -862,22 +857,16 @@ The paired cohorts do expose a narrower mechanism worth testing:
| Grid without pivots | 428 at +0.232R (+0.100) | 392 at +0.176R (+0.046) | 658 at +0.208R (+0.041) |
Replacement mixes the retained and added cohorts and also discards the removed
cohort, so its portfolio result cannot say which part helped. The follow-up
`gtl_confirmation` matrix therefore preserves frozen control geometry and
decomposes each selected variant into:
cohort, so its portfolio result could not say which part helped. The archived
confirmation matrix therefore preserved frozen control geometry and decomposed
each selected variant into:
- **intersection** — only control setups also core-qualified by the variant;
- **union** — all core-qualified control setups plus genuinely added variant
setups, using tuned geometry only for those additions.
It also tests pre-registered intersections among the three high-breadth
confirmers. Its control path must exactly reproduce the completed tuning
matrix before any research arm is accepted.
```bash
.venv/bin/python scripts/run_gtl_confirmation_matrix.py \
backtest_snapshots/prod.sqlite --workers 12
```
confirmers. Its control path exactly reproduced the completed tuning matrix.
Result: **13/13 arms completed with exact control parity; no arm passed all six
guardrails.** The decomposition does identify one near-hit:
@@ -894,17 +883,12 @@ ex-top-5%. This is consistent with a weak tail-dependence filter. It is not yet
a winner: the original drawdown guardrail remains fixed, and 21.7% is worse
than 21.4% even though the difference is small.
The final parameter test is therefore deliberately one-dimensional. It sweeps
The final parameter test was deliberately one-dimensional. It swept
coarse strength scales around 1000 (625, 750, 875, 1000, 1125, 1250, 1500,
2000), using intersection only. It must reproduce both the frozen control and
the completed strength-1000 result exactly. Promotion requires at least two
2000), using intersection only. It reproduced both the frozen control and the
completed strength-1000 result exactly. Promotion required at least two
adjacent non-control scales to pass all six original checks; an isolated winner
is rejected as sensitivity.
```bash
.venv/bin/python scripts/run_gtl_strength_sensitivity.py \
backtest_snapshots/prod.sqlite --workers 12
```
was rejected as sensitivity.
Final result: **9/9 arms completed, control parity passed, and the
strength-1000 replication passed.** Scale 1500 was the only arm to clear all
@@ -928,6 +912,10 @@ model. This snapshot is now exhausted for GTL fitting; any future challenger
must be pre-registered and evaluated on genuinely new forward data rather than
another iteration over the same history.
The temporary GTL matrix scripts, configurable detector branches, and
confirmation hooks were removed after this decision. The normal snapshot
backtester and frozen `explicit_target_ladder` parity arm remain.
The post-2024 window has been opened and is now analysis data, not a valid final
promotion holdout. These arms can isolate mechanism, but neither may ship without
new future data or a separately pre-registered walk-forward protocol.
+15 -14
View File
@@ -61,32 +61,30 @@ after scanner integration passed: the regenerated report changed only its
`generated_at` timestamp, confirming that the shared helper preserves exact
parity. Nothing in this research branch deploys the change to production.
The next decision point is generated by the one-command GTL parameter run:
The archived single-parameter decision point is:
- `backtest-YYYYMMDD-gtl-tuning-matrix.json`
- `backtest-YYYYMMDD-gtl-tuning-matrix.md`
- `backtest-20260713-gtl-tuning-matrix.json`
- `backtest-20260713-gtl-tuning-matrix.md`
These two consolidated files are the reports worth retaining. The runner uses
a hidden temporary directory for its 20 full per-arm reports and removes it
after successful consolidation unless `--keep-arm-reports` is supplied.
These two compact consolidated files are retained; the 20 full per-arm reports
were removed after consolidation.
The completed 2026-07-13 matrix found no single-parameter replacement that
passed all robustness checks. Retain its JSON and Markdown as that decision
point. The evidence-selected retained-versus-added decomposition writes:
point. The retained-versus-added decomposition is retained as:
- `backtest-YYYYMMDD-gtl-confirmation-matrix.json`
- `backtest-YYYYMMDD-gtl-confirmation-matrix.md`
- `backtest-20260713-gtl-confirmation-matrix.json`
- `backtest-20260713-gtl-confirmation-matrix.md`
Those become the next decision point; detailed per-arm reports remain temporary
unless explicitly retained.
Detailed per-arm reports were not retained.
The completed confirmation matrix found one near-hit but no formal winner:
strength-1000 intersection improved full/train/post-2024 Sharpe and CAGR, while
max drawdown worsened from 21.4% to 21.7%. Retain that matrix as the cohort-
decomposition decision point. The final stability check writes:
decomposition decision point. The final stability check is retained as:
- `backtest-YYYYMMDD-gtl-strength-sensitivity.json`
- `backtest-YYYYMMDD-gtl-strength-sensitivity.md`
- `backtest-20260713-gtl-strength-sensitivity.json`
- `backtest-20260713-gtl-strength-sensitivity.md`
Its pre-registered decision requires at least two adjacent scales to pass all
six unchanged checks.
@@ -96,3 +94,6 @@ Scale 1500 was the sole 6/6 arm, but no adjacent scale passed, so the stable-
plateau rule rejected it. Retain the consolidated JSON/Markdown as the closing
GTL decision point. No confirmation or tuned strength value should be promoted
from this snapshot.
The temporary matrix runners and their configurable backtest hooks were removed
after consolidation. The normal local snapshot backtester remains.
-25
View File
@@ -61,25 +61,10 @@ def _parse_args() -> argparse.Namespace:
"rewrite_range504_structural_primary2",
"production_structural_overlay",
"explicit_target_ladder",
"gtl_tuning",
"gtl_confirmation",
),
default=None,
help="Research-only S/R detector/gate arm.",
)
parser.add_argument(
"--gtl-config",
default=None,
help="Research-only GTL configuration as a JSON object (requires --sr-variant gtl_tuning).",
)
parser.add_argument(
"--gtl-confirm-config",
default=None,
help=(
"Research-only GTL intersection/union configuration as JSON "
"(requires --sr-variant gtl_confirmation)."
),
)
parser.add_argument(
"--entry-start",
default=None,
@@ -215,16 +200,6 @@ async def _main() -> None:
os.environ["BACKTEST_ALLOW_SPAWN"] = "1"
if args.sr_variant:
os.environ["BACKTEST_SR_VARIANT"] = args.sr_variant
if args.gtl_config:
if args.sr_variant != "gtl_tuning":
raise SystemExit("--gtl-config requires --sr-variant gtl_tuning")
os.environ["BACKTEST_GTL_CONFIG"] = args.gtl_config
if args.gtl_confirm_config:
if args.sr_variant != "gtl_confirmation":
raise SystemExit(
"--gtl-confirm-config requires --sr-variant gtl_confirmation"
)
os.environ["BACKTEST_GTL_CONFIRM_CONFIG"] = args.gtl_confirm_config
if args.entry_start:
os.environ["BACKTEST_ENTRY_START"] = args.entry_start
if args.entry_end:
-360
View File
@@ -1,360 +0,0 @@
"""Run the evidence-selected GTL confirmation/union matrix with one command.
The first GTL tuning matrix tested replacements. This follow-up decomposes the
four informative variants into retained-only intersections and control-plus-
addition unions while preserving frozen control geometry wherever possible.
"""
from __future__ import annotations
import argparse
import json
import math
import subprocess
import sys
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts import run_gtl_tuning_matrix as common # noqa: E402
RUNNER = ROOT / "scripts" / "run_backtest_snapshot.py"
REFERENCE_MATRIX = ROOT / "reports" / "backtest-20260713-gtl-tuning-matrix.json"
TOUCH = {"name": "touch_0_25pct", "touch_tolerance": 0.0025}
STRENGTH = {"name": "strength_1000", "strength_scale": 1000.0}
MERGE = {"name": "merge_0_25pct", "merge_tolerance": 0.0025}
GRID_ONLY = {"name": "pivots_none", "include_pivots": False}
def _arm(
name: str,
description: str,
mode: str,
*confirmations: dict[str, Any],
) -> dict[str, Any]:
return {
"name": name,
"description": description,
"mode": mode,
"confirmations": list(confirmations),
}
# Pre-registered from the replacement matrix's paired cohorts. Replacement
# arms that plainly removed strong control setups or added weak cohorts are not
# repeated here.
GTL_CONFIRMATION_ARMS: tuple[dict[str, Any], ...] = (
_arm("control", "Frozen explicit GTL; composition-path parity control.", "intersection"),
_arm(
"touch_intersection",
"Retain control setups also qualified with 0.25% touch padding.",
"intersection",
TOUCH,
),
_arm(
"touch_union",
"Keep control and admit additions from 0.25% touch padding.",
"union",
TOUCH,
),
_arm(
"strength_intersection",
"Retain control setups also qualified at strength scale 1000.",
"intersection",
STRENGTH,
),
_arm(
"strength_union",
"Keep control and admit additions from strength scale 1000.",
"union",
STRENGTH,
),
_arm(
"merge_intersection",
"Retain control setups also qualified with 0.25% proposal merging.",
"intersection",
MERGE,
),
_arm(
"merge_union",
"Keep control and admit additions from 0.25% proposal merging.",
"union",
MERGE,
),
_arm(
"grid_intersection",
"Retain control setups also qualified by the range grid without pivots.",
"intersection",
GRID_ONLY,
),
_arm(
"grid_union",
"Keep control and admit additions from the range grid without pivots.",
"union",
GRID_ONLY,
),
_arm(
"touch_strength_intersection",
"Require both tighter-touch and faster-strength confirmation.",
"intersection",
TOUCH,
STRENGTH,
),
_arm(
"touch_merge_intersection",
"Require both tighter-touch and tighter-merge confirmation.",
"intersection",
TOUCH,
MERGE,
),
_arm(
"strength_merge_intersection",
"Require both faster-strength and tighter-merge confirmation.",
"intersection",
STRENGTH,
MERGE,
),
_arm(
"touch_strength_merge_intersection",
"Require all three high-breadth confirmation variants.",
"intersection",
TOUCH,
STRENGTH,
MERGE,
),
)
def _args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"snapshot",
nargs="?",
default="backtest_snapshots/prod.sqlite",
help="Local SQLite snapshot path.",
)
parser.add_argument("--workers", type=int, default=7)
parser.add_argument("--holdout-split", default="2024-07-01")
parser.add_argument(
"--out",
default=None,
help=(
"Consolidated JSON path. Defaults to "
"reports/backtest-YYYYMMDD-gtl-confirmation-matrix.json."
),
)
parser.add_argument("--keep-arm-reports", action="store_true")
return parser.parse_args()
def _config(arm: dict[str, Any]) -> dict[str, Any]:
return {
"name": arm["name"],
"mode": arm["mode"],
"confirmations": arm["confirmations"],
}
def _signature(arm: dict) -> dict:
return {
"candidates": arm.get("candidates"),
"qualified": arm.get("qualified"),
"qualified_net_avg_r": arm.get("qualified_net_avg_r"),
"qualified_net_avg_r_ex_top5": arm.get("qualified_net_avg_r_ex_top5"),
"full_book": arm.get("full_book"),
"holdout": arm.get("holdout"),
}
def _reference_control() -> dict | None:
if not REFERENCE_MATRIX.exists():
return None
with REFERENCE_MATRIX.open(encoding="utf-8") as handle:
payload = json.load(handle)
return next(
(arm for arm in payload.get("arms") or [] if arm.get("name") == "control"),
None,
)
def _write_markdown(path: Path, payload: dict) -> None:
rows = [
"# GTL confirmation/union matrix",
"",
f"Status: **{payload['status']}** ",
f"Holdout split: `{payload['holdout_split']}` ",
f"Completed arms: {len(payload['arms'])}/{payload['arm_count']}",
"",
"| Arm | Mode | Qualified | Full Sharpe | CAGR | Max DD | Trades | Train Sharpe | Test Sharpe | Ex-top-5% R | Screen |",
"|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
]
for arm in payload["arms"]:
full = arm.get("full_book") or {}
holdout = arm.get("holdout") or {}
screen = arm.get("screen") or {}
rows.append(
"| "
+ " | ".join((
arm["name"],
arm["config"]["mode"],
str(arm.get("qualified") or "-"),
common._fmt(full.get("sharpe")),
common._fmt(full.get("cagr_pct"), 1),
common._fmt(full.get("max_drawdown_pct"), 1),
str(full.get("trades") or "-"),
common._fmt((holdout.get("train") or {}).get("sharpe")),
common._fmt((holdout.get("test") or {}).get("sharpe")),
common._fmt(arm.get("qualified_net_avg_r_ex_top5"), 3),
f"{screen.get('passed', '-')}/{screen.get('total', '-')}",
))
+ " |"
)
rows.extend((
"",
"## Interpretation guardrail",
"",
"Intersections test the retained control cohort; unions test control plus genuinely added setups. The post-2024 interval is a robustness check, not a pristine holdout. Passing does not authorize deployment.",
"",
))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(rows), encoding="utf-8")
def _run_arm(
arm: dict[str, Any],
snapshot: Path,
workers: int,
holdout_split: str,
output: Path,
) -> None:
command = [
sys.executable,
str(RUNNER),
str(snapshot),
"--workers", str(workers),
"--allow-spawn",
"--sr-variant", "gtl_confirmation",
"--gtl-confirm-config", json.dumps(_config(arm), separators=(",", ":")),
"--holdout-split", holdout_split,
"--sr-audit",
"--out", str(output),
]
subprocess.run(command, cwd=ROOT, check=True)
def main() -> None:
args = _args()
snapshot = Path(args.snapshot).resolve()
if not snapshot.exists():
raise SystemExit(f"Snapshot not found: {snapshot}")
if args.workers < 1:
raise SystemExit("--workers must be at least 1")
try:
date.fromisoformat(args.holdout_split)
except ValueError as exc:
raise SystemExit("--holdout-split must use YYYY-MM-DD") from exc
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
default_out = ROOT / "reports" / f"backtest-{stamp[:8]}-gtl-confirmation-matrix.json"
out_path = Path(args.out) if args.out else default_out
if not out_path.is_absolute():
out_path = ROOT / out_path
markdown_path = out_path.with_suffix(".md")
work_dir = ROOT / "reports" / f".gtl-confirmation-work-{stamp}"
work_dir.mkdir(parents=True, exist_ok=False)
reference = _reference_control()
payload: dict[str, Any] = {
"status": "running",
"generated_at": datetime.now(timezone.utc).isoformat(),
"snapshot": str(snapshot),
"workers": args.workers,
"holdout_split": args.holdout_split,
"arm_count": len(GTL_CONFIRMATION_ARMS),
"reference_matrix": str(REFERENCE_MATRIX) if reference else None,
"arms": [],
}
common._write_json(out_path, payload)
_write_markdown(markdown_path, payload)
control_report: dict | None = None
arm_outputs: list[Path] = []
try:
for index, arm in enumerate(GTL_CONFIRMATION_ARMS, start=1):
name = arm["name"]
output = work_dir / f"{index:02d}-{name}.json"
arm_outputs.append(output)
print(f"\n[{index}/{len(GTL_CONFIRMATION_ARMS)}] {name}", flush=True)
print(f" {arm['description']}", flush=True)
_run_arm(arm, snapshot, args.workers, args.holdout_split, output)
report = common._load_report(output)
compact = common._compact_arm(report, _config(arm), control_report)
compact["description"] = arm["description"]
if control_report is None:
control_report = report
compact["screen"] = {
"checks": {}, "passed": 0, "total": 0, "advances": False,
}
if reference is not None and _signature(compact) != _signature(reference):
raise RuntimeError(
"Confirmation-path control does not reproduce the frozen GTL matrix control"
)
payload["control_parity"] = "pass" if reference is not None else "not_checked"
else:
compact["screen"] = common._screen_arm(compact, payload["arms"][0])
payload["arms"].append(compact)
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
common._write_json(out_path, payload)
_write_markdown(markdown_path, payload)
except BaseException as exc:
payload["status"] = "failed"
payload["failed_at"] = datetime.now(timezone.utc).isoformat()
payload["error"] = f"{type(exc).__name__}: {exc}"
payload["work_dir"] = str(work_dir)
common._write_json(out_path, payload)
_write_markdown(markdown_path, payload)
raise
payload["status"] = "complete"
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
payload["advancing_arms"] = [
arm["name"]
for arm in payload["arms"]
if (arm.get("screen") or {}).get("advances")
]
payload["ranking_by_full_sharpe"] = [
arm["name"]
for arm in sorted(
payload["arms"],
key=lambda row: float(
(row.get("full_book") or {}).get("sharpe") or -math.inf
),
reverse=True,
)
]
if args.keep_arm_reports:
payload["arm_report_directory"] = str(work_dir)
common._write_json(out_path, payload)
_write_markdown(markdown_path, payload)
if not args.keep_arm_reports:
for path in arm_outputs:
path.unlink(missing_ok=True)
work_dir.rmdir()
print("\nGTL confirmation matrix complete.")
print(f" JSON: {out_path}")
print(f" Markdown: {markdown_path}")
if payload["advancing_arms"]:
print(" Passing arms: " + ", ".join(payload["advancing_arms"]))
else:
print(" No arm passed every pre-registered screen.")
if __name__ == "__main__":
main()
-287
View File
@@ -1,287 +0,0 @@
"""Run the pre-registered GTL strength-confirmation sensitivity band.
The strength-1000 intersection was the only composition arm to improve
full/train/test Sharpe, but it missed the unchanged drawdown guardrail. This
matrix checks whether that result is a stable one-dimensional plateau rather
than tuning the guardrail or launching another broad parameter search.
"""
from __future__ import annotations
import argparse
import json
import math
import sys
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts import run_gtl_confirmation_matrix as composition # noqa: E402
from scripts import run_gtl_tuning_matrix as common # noqa: E402
REFERENCE_TUNING = ROOT / "reports" / "backtest-20260713-gtl-tuning-matrix.json"
REFERENCE_COMPOSITION = (
ROOT / "reports" / "backtest-20260713-gtl-confirmation-matrix.json"
)
STRENGTH_SCALES = (625.0, 750.0, 875.0, 1000.0, 1125.0, 1250.0, 1500.0, 2000.0)
GTL_STRENGTH_ARMS: tuple[dict[str, Any], ...] = (
composition._arm(
"control",
"Frozen GTL composition-path parity control.",
"intersection",
),
*(
composition._arm(
f"strength_{int(scale)}_intersection",
f"Require control confirmation at traffic-strength scale {scale:g}.",
"intersection",
{"name": f"strength_{int(scale)}", "strength_scale": scale},
)
for scale in STRENGTH_SCALES
),
)
def _args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"snapshot",
nargs="?",
default="backtest_snapshots/prod.sqlite",
help="Local SQLite snapshot path.",
)
parser.add_argument("--workers", type=int, default=7)
parser.add_argument("--holdout-split", default="2024-07-01")
parser.add_argument(
"--out",
default=None,
help=(
"Consolidated JSON path. Defaults to "
"reports/backtest-YYYYMMDD-gtl-strength-sensitivity.json."
),
)
parser.add_argument("--keep-arm-reports", action="store_true")
return parser.parse_args()
def _reference_arm(path: Path, name: str) -> dict | None:
if not path.exists():
return None
with path.open(encoding="utf-8") as handle:
payload = json.load(handle)
return next(
(arm for arm in payload.get("arms") or [] if arm.get("name") == name),
None,
)
def _write_markdown(path: Path, payload: dict) -> None:
rows = [
"# GTL strength-confirmation sensitivity",
"",
f"Status: **{payload['status']}** ",
f"Holdout split: `{payload['holdout_split']}` ",
f"Completed arms: {len(payload['arms'])}/{payload['arm_count']}",
"",
"| Arm | Qualified | Full Sharpe | CAGR | Max DD | Trades | Train Sharpe | Test Sharpe | Ex-top-5% R | Screen |",
"|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
]
for arm in payload["arms"]:
full = arm.get("full_book") or {}
holdout = arm.get("holdout") or {}
screen = arm.get("screen") or {}
rows.append(
"| "
+ " | ".join((
arm["name"],
str(arm.get("qualified") or "-"),
common._fmt(full.get("sharpe")),
common._fmt(full.get("cagr_pct"), 1),
common._fmt(full.get("max_drawdown_pct"), 1),
str(full.get("trades") or "-"),
common._fmt((holdout.get("train") or {}).get("sharpe")),
common._fmt((holdout.get("test") or {}).get("sharpe")),
common._fmt(arm.get("qualified_net_avg_r_ex_top5"), 3),
f"{screen.get('passed', '-')}/{screen.get('total', '-')}",
))
+ " |"
)
rows.extend((
"",
"## Pre-registered interpretation",
"",
"The six original guardrails remain unchanged. A stable candidate requires at least two adjacent non-control scales to pass all six; an isolated passing scale is rejected as sensitivity, not promoted.",
"",
))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(rows), encoding="utf-8")
def _stable_plateau_pairs(arms: list[dict]) -> list[list[str]]:
sensitivity = [arm for arm in arms if arm.get("name") != "control"]
return [
[left["name"], right["name"]]
for left, right in zip(sensitivity, sensitivity[1:], strict=False)
if (left.get("screen") or {}).get("advances")
and (right.get("screen") or {}).get("advances")
]
def main() -> None:
args = _args()
snapshot = Path(args.snapshot).resolve()
if not snapshot.exists():
raise SystemExit(f"Snapshot not found: {snapshot}")
if args.workers < 1:
raise SystemExit("--workers must be at least 1")
try:
date.fromisoformat(args.holdout_split)
except ValueError as exc:
raise SystemExit("--holdout-split must use YYYY-MM-DD") from exc
reference_control = _reference_arm(REFERENCE_TUNING, "control")
reference_1000 = _reference_arm(
REFERENCE_COMPOSITION,
"strength_intersection",
)
if reference_control is None or reference_1000 is None:
raise SystemExit(
"The completed GTL tuning and confirmation matrices are required "
"for control/replication checks"
)
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
default_out = (
ROOT / "reports" / f"backtest-{stamp[:8]}-gtl-strength-sensitivity.json"
)
out_path = Path(args.out) if args.out else default_out
if not out_path.is_absolute():
out_path = ROOT / out_path
markdown_path = out_path.with_suffix(".md")
work_dir = ROOT / "reports" / f".gtl-strength-work-{stamp}"
work_dir.mkdir(parents=True, exist_ok=False)
payload: dict[str, Any] = {
"status": "running",
"generated_at": datetime.now(timezone.utc).isoformat(),
"snapshot": str(snapshot),
"workers": args.workers,
"holdout_split": args.holdout_split,
"arm_count": len(GTL_STRENGTH_ARMS),
"strength_scales": list(STRENGTH_SCALES),
"arms": [],
"promotion_rule": (
"At least two adjacent non-control scales must pass all six original "
"guardrails."
),
}
common._write_json(out_path, payload)
_write_markdown(markdown_path, payload)
control_report: dict | None = None
arm_outputs: list[Path] = []
try:
for index, arm in enumerate(GTL_STRENGTH_ARMS, start=1):
name = arm["name"]
output = work_dir / f"{index:02d}-{name}.json"
arm_outputs.append(output)
print(f"\n[{index}/{len(GTL_STRENGTH_ARMS)}] {name}", flush=True)
print(f" {arm['description']}", flush=True)
composition._run_arm(
arm,
snapshot,
args.workers,
args.holdout_split,
output,
)
report = common._load_report(output)
compact = common._compact_arm(
report,
composition._config(arm),
control_report,
)
compact["description"] = arm["description"]
if control_report is None:
control_report = report
compact["screen"] = {
"checks": {}, "passed": 0, "total": 0, "advances": False,
}
if composition._signature(compact) != composition._signature(
reference_control
):
raise RuntimeError(
"Strength sensitivity control does not reproduce frozen GTL control"
)
payload["control_parity"] = "pass"
else:
compact["screen"] = common._screen_arm(
compact,
payload["arms"][0],
)
if name == "strength_1000_intersection":
if composition._signature(compact) != composition._signature(
reference_1000
):
raise RuntimeError(
"Strength-1000 arm does not reproduce the completed composition result"
)
payload["strength_1000_replication"] = "pass"
payload["arms"].append(compact)
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
common._write_json(out_path, payload)
_write_markdown(markdown_path, payload)
except BaseException as exc:
payload["status"] = "failed"
payload["failed_at"] = datetime.now(timezone.utc).isoformat()
payload["error"] = f"{type(exc).__name__}: {exc}"
payload["work_dir"] = str(work_dir)
common._write_json(out_path, payload)
_write_markdown(markdown_path, payload)
raise
payload["status"] = "complete"
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
payload["advancing_arms"] = [
arm["name"]
for arm in payload["arms"]
if (arm.get("screen") or {}).get("advances")
]
payload["stable_plateau_pairs"] = _stable_plateau_pairs(payload["arms"])
payload["stable_candidate"] = bool(payload["stable_plateau_pairs"])
payload["ranking_by_full_sharpe"] = [
arm["name"]
for arm in sorted(
payload["arms"],
key=lambda row: float(
(row.get("full_book") or {}).get("sharpe") or -math.inf
),
reverse=True,
)
]
if args.keep_arm_reports:
payload["arm_report_directory"] = str(work_dir)
common._write_json(out_path, payload)
_write_markdown(markdown_path, payload)
if not args.keep_arm_reports:
for path in arm_outputs:
path.unlink(missing_ok=True)
work_dir.rmdir()
print("\nGTL strength sensitivity complete.")
print(f" JSON: {out_path}")
print(f" Markdown: {markdown_path}")
if payload["stable_candidate"]:
pairs = [" + ".join(pair) for pair in payload["stable_plateau_pairs"]]
print(" Stable passing plateau: " + "; ".join(pairs))
else:
print(" No stable adjacent passing plateau.")
if __name__ == "__main__":
main()
-418
View File
@@ -1,418 +0,0 @@
"""Run the complete Gate Target Ladder tuning matrix with one command.
Every arm is a full production-parity backtest. Arms run sequentially so each
one can use the requested worker pool without competing with another arm. Large
per-arm reports live in a temporary run directory and are removed after a
successful consolidation unless ``--keep-arm-reports`` is supplied.
"""
from __future__ import annotations
import argparse
import json
import math
import subprocess
import sys
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
RUNNER = ROOT / "scripts" / "run_backtest_snapshot.py"
BASE_CONFIG: dict[str, Any] = {
"lookback_bars": None,
"grid_bins": 20,
"include_pivots": True,
"pivot_window": 2,
"touch_tolerance": 0.005,
"merge_tolerance": 0.005,
"strength_scale": 500.0,
"zone_tolerance": 0.02,
"candidate_limit": 5,
"max_target_atr": None,
}
# Single-variable arms only. The control value is represented by BASE_CONFIG;
# there is deliberately no Cartesian product.
GTL_TUNING_ARMS: tuple[dict[str, Any], ...] = (
{"name": "control", "description": "Frozen explicit GTL defaults."},
{"name": "lookback_252", "description": "One-year GTL history.", "lookback_bars": 252},
{"name": "lookback_504", "description": "Two-year GTL history.", "lookback_bars": 504},
{"name": "lookback_756", "description": "Three-year GTL history.", "lookback_bars": 756},
{"name": "candidates_8", "description": "Retain up to eight candidates before probability.", "candidate_limit": 8},
{"name": "candidates_all", "description": "Score every eligible target before primary selection.", "candidate_limit": None},
{"name": "max_atr_5_5", "description": "Universal 5.5 ATR maximum target distance.", "max_target_atr": 5.5},
{"name": "max_atr_8", "description": "Universal 8 ATR maximum target distance.", "max_target_atr": 8.0},
{"name": "touch_0", "description": "Strict candle-range crossings with no touch padding.", "touch_tolerance": 0.0},
{"name": "touch_0_25pct", "description": "Use 0.25% padding when counting price traffic.", "touch_tolerance": 0.0025},
{"name": "merge_0_25pct", "description": "Merge GTL proposals within 0.25%.", "merge_tolerance": 0.0025},
{"name": "merge_1pct", "description": "Merge GTL proposals within 1%.", "merge_tolerance": 0.01},
{"name": "zones_1pct", "description": "Cluster target zones within 1%.", "zone_tolerance": 0.01},
{"name": "zones_3pct", "description": "Cluster target zones within 3%.", "zone_tolerance": 0.03},
{"name": "grid_12", "description": "Use 12 evenly spaced range centers.", "grid_bins": 12},
{"name": "grid_32", "description": "Use 32 evenly spaced range centers.", "grid_bins": 32},
{"name": "pivots_none", "description": "Range grid only; omit swing pivots.", "include_pivots": False},
{"name": "pivots_11bar", "description": "Use an 11-bar swing-pivot window.", "pivot_window": 5},
{"name": "strength_250", "description": "Slower traffic-strength saturation.", "strength_scale": 250.0},
{"name": "strength_1000", "description": "Faster traffic-strength saturation.", "strength_scale": 1000.0},
)
BOOK_FIELDS = (
"sharpe",
"cagr_pct",
"max_drawdown_pct",
"trades",
"win_rate",
"avg_hold_days",
"skipped_book_full",
)
def _args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"snapshot",
nargs="?",
default="backtest_snapshots/prod.sqlite",
help="Local SQLite snapshot path.",
)
parser.add_argument("--workers", type=int, default=7)
parser.add_argument(
"--holdout-split",
default="2024-07-01",
help="Disjoint train/test split included in every arm.",
)
parser.add_argument(
"--out",
default=None,
help="Consolidated JSON path. Defaults to reports/backtest-YYYYMMDD-gtl-tuning-matrix.json.",
)
parser.add_argument(
"--keep-arm-reports",
action="store_true",
help="Keep the large temporary per-arm JSON reports after consolidation.",
)
return parser.parse_args()
def _arm_config(arm: dict[str, Any]) -> dict[str, Any]:
config = {**BASE_CONFIG, **{key: value for key, value in arm.items() if key != "description"}}
return config
def _load_report(path: Path) -> dict:
with path.open(encoding="utf-8") as handle:
report = json.load(handle)
if report.get("sr_candidate_audit") is None:
raise ValueError(f"Report lacks sr_candidate_audit: {path}")
return report
def _compact_book(row: dict | None) -> dict | None:
if row is None:
return None
return {key: row.get(key) for key in BOOK_FIELDS}
def _full_book(report: dict) -> dict | None:
runs = ((report.get("portfolio_monitor") or {}).get("runs") or [])
return _compact_book(next(
(
row
for row in runs
if row.get("is_production") and row.get("lookback") == "all"
),
None,
))
def _holdout_books(report: dict) -> dict[str, dict | None]:
rows = ((report.get("holdout") or {}).get("rows") or [])
return {
window: _compact_book(next((row for row in rows if row.get("window") == window), None))
for window in ("train", "test")
}
def _audit_key(row: dict) -> tuple[str, str, str]:
return row["symbol"], row["date"], row["direction"]
def _cohort_stats(rows: list[dict]) -> dict:
net = [float(row.get("net_r", 0.0)) for row in rows]
trimmed = sorted(net, reverse=True)[math.ceil(len(net) * 0.05):]
return {
"count": len(rows),
"net_avg_r": round(sum(net) / len(net), 4) if net else None,
"net_avg_r_ex_top5": round(sum(trimmed) / len(trimmed), 4) if trimmed else None,
}
def _cohort_comparison(control: dict, variant: dict) -> dict:
control_rows = {
_audit_key(row): row for row in control.get("sr_candidate_audit") or []
}
variant_rows = {
_audit_key(row): row for row in variant.get("sr_candidate_audit") or []
}
control_q = {key for key, row in control_rows.items() if row.get("qualified")}
variant_q = {key for key, row in variant_rows.items() if row.get("qualified")}
retained = control_q & variant_q
added = variant_q - control_q
removed = control_q - variant_q
return {
"retained": _cohort_stats([variant_rows[key] for key in retained]),
"added": _cohort_stats([variant_rows[key] for key in added]),
"removed": _cohort_stats([control_rows[key] for key in removed]),
}
def _compact_arm(report: dict, config: dict, control: dict | None) -> dict:
qualified = report.get("overall_qualified") or {}
result = {
"name": config["name"],
"config": config,
"candidates": report.get("candidates"),
"qualified": report.get("qualified"),
"qualified_net_avg_r": qualified.get("net_avg_r"),
"qualified_net_avg_r_ex_top5": qualified.get("net_avg_r_ex_top5"),
"full_book": _full_book(report),
"holdout": _holdout_books(report),
"gtl_diagnostics": report.get("sr_variant_diagnostics"),
"cohort_vs_control": (
_cohort_comparison(control, report) if control is not None else None
),
}
return result
def _screen_arm(arm: dict, control: dict) -> dict:
full = arm.get("full_book") or {}
base_full = control.get("full_book") or {}
train = (arm.get("holdout") or {}).get("train") or {}
base_train = (control.get("holdout") or {}).get("train") or {}
test = (arm.get("holdout") or {}).get("test") or {}
base_test = (control.get("holdout") or {}).get("test") or {}
def at_least(value: Any, baseline: Any) -> bool:
return value is not None and baseline is not None and float(value) >= float(baseline)
control_trades = float(base_full.get("trades") or 0.0)
arm_trades = float(full.get("trades") or 0.0)
checks = {
"full_sharpe_not_worse": at_least(full.get("sharpe"), base_full.get("sharpe")),
"train_sharpe_not_worse": at_least(train.get("sharpe"), base_train.get("sharpe")),
"test_sharpe_not_worse": at_least(test.get("sharpe"), base_test.get("sharpe")),
"drawdown_not_worse": (
full.get("max_drawdown_pct") is not None
and base_full.get("max_drawdown_pct") is not None
and abs(float(full["max_drawdown_pct"]))
<= abs(float(base_full["max_drawdown_pct"]))
),
"retains_80pct_trades": control_trades > 0 and arm_trades >= control_trades * 0.8,
"robust_expectancy_positive": (
arm.get("qualified_net_avg_r_ex_top5") is not None
and float(arm["qualified_net_avg_r_ex_top5"]) > 0
),
}
return {
"checks": checks,
"passed": sum(checks.values()),
"total": len(checks),
"advances": all(checks.values()),
}
def _write_json(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2)
handle.write("\n")
def _fmt(value: Any, digits: int = 2) -> str:
return "-" if value is None else f"{float(value):.{digits}f}"
def _write_markdown(path: Path, payload: dict) -> None:
rows = [
"# GTL tuning matrix",
"",
f"Status: **{payload['status']}** ",
f"Holdout split: `{payload['holdout_split']}` ",
f"Completed arms: {len(payload['arms'])}/{payload['arm_count']}",
"",
"| Arm | Full Sharpe | CAGR | Max DD | Trades | Train Sharpe | Test Sharpe | Ex-top-5% R | Screen |",
"|---|---:|---:|---:|---:|---:|---:|---:|---:|",
]
for arm in payload["arms"]:
full = arm.get("full_book") or {}
holdout = arm.get("holdout") or {}
train = holdout.get("train") or {}
test = holdout.get("test") or {}
screen = arm.get("screen") or {}
rows.append(
"| "
+ " | ".join((
arm["name"],
_fmt(full.get("sharpe")),
_fmt(full.get("cagr_pct"), 1),
_fmt(full.get("max_drawdown_pct"), 1),
str(full.get("trades") or "-"),
_fmt(train.get("sharpe")),
_fmt(test.get("sharpe")),
_fmt(arm.get("qualified_net_avg_r_ex_top5"), 3),
f"{screen.get('passed', '-')}/{screen.get('total', '-')}",
))
+ " |"
)
rows.extend((
"",
"## Interpretation guardrail",
"",
"The post-2024 interval has already informed prior research. The train/test columns are robustness checks, not a pristine holdout. A passing arm is a candidate for forward paper validation, not automatic production promotion.",
"",
))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(rows), encoding="utf-8")
def _run_arm(
*,
arm: dict[str, Any],
snapshot: str,
workers: int,
holdout_split: str,
output: Path,
) -> None:
config = _arm_config(arm)
command = [
sys.executable,
str(RUNNER),
snapshot,
"--workers", str(workers),
"--allow-spawn",
"--sr-variant", "gtl_tuning",
"--gtl-config", json.dumps(config, separators=(",", ":")),
"--holdout-split", holdout_split,
"--sr-audit",
"--out", str(output),
]
subprocess.run(command, cwd=ROOT, check=True)
def main() -> None:
args = _args()
snapshot = Path(args.snapshot).resolve()
if not snapshot.exists():
raise SystemExit(f"Snapshot not found: {snapshot}")
if args.workers < 1:
raise SystemExit("--workers must be at least 1")
try:
date.fromisoformat(args.holdout_split)
except ValueError as exc:
raise SystemExit("--holdout-split must use YYYY-MM-DD") from exc
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
default_out = ROOT / "reports" / f"backtest-{stamp[:8]}-gtl-tuning-matrix.json"
out_path = Path(args.out) if args.out else default_out
if not out_path.is_absolute():
out_path = ROOT / out_path
markdown_path = out_path.with_suffix(".md")
work_dir = ROOT / "reports" / f".gtl-tuning-work-{stamp}"
work_dir.mkdir(parents=True, exist_ok=False)
payload: dict[str, Any] = {
"status": "running",
"generated_at": datetime.now(timezone.utc).isoformat(),
"snapshot": str(snapshot.resolve()),
"workers": args.workers,
"holdout_split": args.holdout_split,
"arm_count": len(GTL_TUNING_ARMS),
"arms": [],
"caveat": (
"The split is a robustness check, not a pristine holdout; post-2024 "
"data has already informed earlier research."
),
}
_write_json(out_path, payload)
_write_markdown(markdown_path, payload)
control_report: dict | None = None
arm_outputs: list[Path] = []
try:
for index, arm in enumerate(GTL_TUNING_ARMS, start=1):
name = str(arm["name"])
output = work_dir / f"{index:02d}-{name}.json"
arm_outputs.append(output)
print(f"\n[{index}/{len(GTL_TUNING_ARMS)}] GTL arm: {name}", flush=True)
print(f" {arm['description']}", flush=True)
_run_arm(
arm=arm,
snapshot=str(snapshot),
workers=args.workers,
holdout_split=args.holdout_split,
output=output,
)
report = _load_report(output)
config = _arm_config(arm)
compact = _compact_arm(report, config, control_report)
compact["description"] = arm["description"]
if control_report is None:
control_report = report
compact["screen"] = {
"checks": {},
"passed": 0,
"total": 0,
"advances": False,
}
else:
compact["screen"] = _screen_arm(compact, payload["arms"][0])
payload["arms"].append(compact)
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
_write_json(out_path, payload)
_write_markdown(markdown_path, payload)
except Exception as exc:
payload["status"] = "failed"
payload["failed_at"] = datetime.now(timezone.utc).isoformat()
payload["error"] = f"{type(exc).__name__}: {exc}"
payload["work_dir"] = str(work_dir)
_write_json(out_path, payload)
_write_markdown(markdown_path, payload)
raise
payload["status"] = "complete"
payload["completed_at"] = datetime.now(timezone.utc).isoformat()
payload["advancing_arms"] = [
arm["name"] for arm in payload["arms"] if (arm.get("screen") or {}).get("advances")
]
payload["ranking_by_full_sharpe"] = [
arm["name"]
for arm in sorted(
payload["arms"],
key=lambda row: float((row.get("full_book") or {}).get("sharpe") or -math.inf),
reverse=True,
)
]
if args.keep_arm_reports:
payload["arm_report_directory"] = str(work_dir)
_write_json(out_path, payload)
_write_markdown(markdown_path, payload)
if not args.keep_arm_reports:
for path in arm_outputs:
path.unlink(missing_ok=True)
work_dir.rmdir()
print("\nGTL tuning matrix complete.")
print(f" JSON: {out_path}")
print(f" Markdown: {markdown_path}")
if payload["advancing_arms"]:
print(f" Arms passing every pre-registered screen: {', '.join(payload['advancing_arms'])}")
else:
print(" No arm passed every pre-registered screen.")
if __name__ == "__main__":
main()
-155
View File
@@ -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"],
-67
View File
@@ -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"]
]
-50
View File
@@ -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
-63
View File
@@ -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