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
+8 -146
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,115 +536,14 @@ 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,
closes,
[0] * len(closes),
tolerance,
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
return detect_sr_levels_legacy(
highs,
lows,
closes,
[0] * len(closes),
tolerance,
explicit_range_grid=True,
)
def _merge_levels(