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:
@@ -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", [])
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user