Cleanup retired S/R research scaffolding

- Remove unused _gate_eligible_levels filtering logic and its tests (research-only)
- Add prominent RESEARCH/DIAGNOSTIC markers and docs to clear-air/ATR fallback helpers
- Document production vs research BACKTEST_* environment variables in backtest_service
- Minor cleanups: update legacy report text, improve outdated function docstring
This commit is contained in:
2026-07-13 18:52:24 +02:00
parent 9f06304100
commit bddaeb9110
4 changed files with 33 additions and 79 deletions
+32 -22
View File
@@ -18,6 +18,18 @@ after D to record the realized outcome. The report contains:
Limitation: sentiment and fundamentals have no point-in-time history, so they're
held neutral here — this calibrates the price/S-R machinery only.
Environment variables (see also run_backtest_snapshot.py):
Production / general use:
BACKTEST_HOLDOUT_SPLIT=YYYY-MM-DD # disjoint train/test split
BACKTEST_SNAPSHOT_OFFLINE=1
BACKTEST_ALLOW_SPAWN=1 # for Windows multiprocessing
Research / diagnostic only (retired experiments — do not use for live decisions):
BACKTEST_ATR_TARGET_FALLBACK=3
BACKTEST_FALLBACK_CLEAR_AIR_ONLY=1
BACKTEST_RESEARCH_EXITS=1
BACKTEST_MIN_RR_SWEEP=1
"""
from __future__ import annotations
@@ -67,7 +79,6 @@ from app.services.qualification import (
from app.services.recommendation_service import (
_choose_recommended_action,
_classify_by_probability,
_gate_eligible_levels,
_prune_floor_pinned_targets,
_risk_level_from_conflicts,
_select_primary_target,
@@ -145,13 +156,20 @@ def validate_backtest_target_model(value: str) -> str:
return normalized
# ---------------------------------------------------------------------------
# RESEARCH / DIAGNOSTIC FALLBACKS (retired experiments)
#
# These implement behavior from experiments that were rejected for production
# (clear-air synthetic targets, blanket ATR fallbacks). They are OFF by default
# and exist only to reproduce historical research results or run future ablations.
# See docs/research/sr-levels-and-exits.md.
# Do NOT enable for production decision making.
# ---------------------------------------------------------------------------
def _atr_target_fallback_k() -> float | None:
"""Research ablation: k for a synthetic k*ATR target when a direction has no
S/R level to aim at. Off (None) by default, which is production behavior
no resistance above means no long setup at all. That veto lands hardest on
names at 52-week highs (clear air above), i.e. exactly what the momentum gate
selects, so this flag exists to measure what the veto costs. Set
BACKTEST_ATR_TARGET_FALLBACK=3 to enable. See docs/research/sr-levels-and-exits.md."""
"""RESEARCH DIAGNOSTIC: k for a synthetic k*ATR target when no S/R level.
Off (None) by default (production behavior). Set BACKTEST_ATR_TARGET_FALLBACK=3
to enable. See docs/research/sr-levels-and-exits.md."""
raw = os.getenv("BACKTEST_ATR_TARGET_FALLBACK", "").strip()
if not raw:
return None
@@ -163,13 +181,8 @@ def _atr_target_fallback_k() -> float | None:
def _fallback_clear_air_only() -> bool:
"""Restrict the fallback to setups with genuinely NO structure ahead.
Without this, the fallback also fires when levels DO exist ahead but
``TargetGenerator``'s distance filters rejected them (nearer than 1 ATR, or
past ``max_atr_multiple``). Measured on the snapshot, that's 65% of what the
fallback admits — a different population from the clear-air breakouts, which
confounds the famine test. Set BACKTEST_FALLBACK_CLEAR_AIR_ONLY=1."""
"""RESEARCH DIAGNOSTIC: restrict fallback to genuine clear-air cases only.
Set BACKTEST_FALLBACK_CLEAR_AIR_ONLY=1."""
return os.getenv("BACKTEST_FALLBACK_CLEAR_AIR_ONLY", "").strip().lower() in {
"1", "true", "yes", "on",
}
@@ -190,11 +203,7 @@ def _has_structure_ahead(direction: str, entry: float, sr_levels: list[Any]) ->
def _atr_fallback_target(
direction: str, entry: float, stop: float, atr: float, k: float
) -> dict:
"""A synthetic target k*ATR from entry, shaped like a TargetGenerator row.
``sr_strength`` is 50 (neutral) so the probability model's strength magnet
contributes nothing — the target stands on distance alone.
"""
"""RESEARCH DIAGNOSTIC: synthetic target k*ATR (neutral strength)."""
price = entry + k * atr if direction == "long" else entry - k * atr
distance = abs(price - entry)
risk = abs(entry - stop)
@@ -254,7 +263,7 @@ def _window_setups(
if not sr_levels:
return []
gate_levels = _gate_eligible_levels(sr_levels)
gate_levels = list(sr_levels)
technical = (compute_technical_from_arrays(highs, lows, closes, volumes)[0]) or 50.0
momentum = (compute_momentum_from_closes(closes)[0]) or 50.0
@@ -280,8 +289,9 @@ def _window_setups(
fallback_k = _atr_target_fallback_k()
if fallback_k is None:
continue
# RESEARCH DIAGNOSTIC only (see _atr_target_fallback_k etc.)
if _fallback_clear_air_only() and _has_structure_ahead(direction, entry, sr_levels):
continue # structure exists ahead; the distance filters rejected it, not the famine
continue
targets = [_atr_fallback_target(direction, entry, stop, atr, fallback_k)]
for t in targets:
t["probability"] = probability_estimator.estimate_probability(
@@ -2452,7 +2462,7 @@ def _sharpe_key(row: dict) -> float:
def _build_research_recommendation(report: dict) -> dict:
"""Advisory rules for the remaining research variants after residual promotion."""
"""Build advisory notes from any strategy variants present in the report."""
variants = {
v.get("variant"): v
for v in (report.get("strategy_variants") or {}).get("variants", [])
-32
View File
@@ -56,38 +56,6 @@ def _clamp(value: float, low: float, high: float) -> float:
return max(low, min(high, value))
def _gate_eligible_levels(
sr_levels: list[Any],
*,
confirmed_rounds_only: bool = False,
exclude_standalone_rounds: bool = False,
min_round_rejections: int = 2,
) -> list[Any]:
"""Return structures allowed to influence entry qualification.
Round numbers remain useful visual landmarks, but an untouched standalone
round number is not observed market structure. Research variants can require
either confluence with a pivot/volume source or distinct rejection clusters
before such a level is allowed to manufacture a gate target.
"""
if not confirmed_rounds_only and not exclude_standalone_rounds:
return list(sr_levels)
eligible: list[Any] = []
for level in sr_levels:
sources = set(getattr(level, "sources", None) or [
getattr(level, "detection_method", "unknown")
])
is_round_only = sources == {"round_number"}
rejections = int(getattr(level, "rejection_count", 0) or 0)
if exclude_standalone_rounds and is_round_only:
continue
if confirmed_rounds_only and is_round_only and rejections < min_round_rejections:
continue
eligible.append(level)
return eligible
def _zone_representative_levels(
sr_levels: list[SRLevel],
entry_price: float,