Finalize GTL and retire S/R research harness
This commit is contained in:
@@ -33,7 +33,7 @@ import statistics
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from datetime import date, datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
@@ -82,12 +82,7 @@ from app.services.scoring_service import (
|
||||
compute_momentum_from_closes,
|
||||
compute_technical_from_arrays,
|
||||
)
|
||||
from app.services.sr_service import (
|
||||
MAX_LEVELS,
|
||||
detect_gate_target_ladder,
|
||||
detect_sr_levels,
|
||||
detect_sr_levels_legacy,
|
||||
)
|
||||
from app.services.sr_service import detect_gate_target_ladder, detect_sr_levels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -97,23 +92,11 @@ STEP_DAYS = 5 # weekly cadence (≈ 5 trading days)
|
||||
MIN_LOOKBACK = 60 # bars needed before D for indicators (EMA cross needs 51)
|
||||
HORIZON = 30 # trading days to resolve an outcome (matches the evaluator)
|
||||
ATR_MULTIPLIER = 1.5
|
||||
RANGE_FACTOR_LOOKBACK = 504
|
||||
RANGE_FACTOR_MIN_LOG = 1.0 # approximately a 2.7x high/low span
|
||||
STRUCTURAL_OVERLAY_VARIANT = "production_structural_overlay"
|
||||
STRUCTURAL_OVERLAY_SOURCE_VARIANT = (
|
||||
"rewrite_range504_structural_legacy_primary"
|
||||
)
|
||||
STRUCTURAL_OVERLAY_WEIGHT = 0.05
|
||||
STRUCTURAL_OVERLAY_SCORE_KEY = "structural_overlay_95_5_score"
|
||||
EXPLICIT_TARGET_LADDER_VARIANT = "explicit_target_ladder"
|
||||
RANGE_RESIDUAL_VARIANTS = {
|
||||
"rewrite_range504_structural_legacy_primary",
|
||||
"rewrite_range504_structural_primary2",
|
||||
}
|
||||
RANGE_FACTOR_VARIANTS = {
|
||||
"production_range504",
|
||||
"rewrite_range504_legacy_primary",
|
||||
*RANGE_RESIDUAL_VARIANTS,
|
||||
PRODUCTION_GTL_TARGET_MODEL = "production_gtl"
|
||||
STRUCTURAL_SR_TARGET_MODEL = "structural_sr"
|
||||
BACKTEST_TARGET_MODELS = {
|
||||
PRODUCTION_GTL_TARGET_MODEL: "Live GTL (production)",
|
||||
STRUCTURAL_SR_TARGET_MODEL: "Structural S/R (comparison)",
|
||||
}
|
||||
|
||||
# Cross-sectional signal evaluation (factor IC). Each candidate signal is a
|
||||
@@ -153,128 +136,13 @@ def _wrap_levels(level_dicts: list[dict]) -> list[Any]:
|
||||
]
|
||||
|
||||
|
||||
SR_RESEARCH_VARIANTS = {
|
||||
"production_control",
|
||||
"rr_aligned_control",
|
||||
"rewrite",
|
||||
"soft_zones",
|
||||
"confirmed_rounds",
|
||||
"gate_v2",
|
||||
"rewrite_legacy_primary",
|
||||
"soft_zones_legacy_primary",
|
||||
"confirmed_rounds_legacy_primary",
|
||||
"gate_v2_legacy_primary",
|
||||
"legacy_geometry_neutral",
|
||||
"legacy_pivots_only",
|
||||
"legacy_traffic_grid_only",
|
||||
"legacy_range_grid_touch",
|
||||
"legacy_range_grid_neutral",
|
||||
EXPLICIT_TARGET_LADDER_VARIANT,
|
||||
STRUCTURAL_OVERLAY_VARIANT,
|
||||
*RANGE_FACTOR_VARIANTS,
|
||||
}
|
||||
|
||||
|
||||
def _sr_research_variant() -> str:
|
||||
"""Backtest target policy; research arms require an explicit override.
|
||||
|
||||
The unconfigured online/scheduled backtest must replay the same transient
|
||||
Gate Target Ladder as the live scanner. Clean Structural S/R remains an
|
||||
opt-in research arm here because it is intentionally chart/alert structure,
|
||||
not the production gate's target source.
|
||||
"""
|
||||
value = os.getenv(
|
||||
"BACKTEST_SR_VARIANT",
|
||||
EXPLICIT_TARGET_LADDER_VARIANT,
|
||||
).strip().lower()
|
||||
if value not in SR_RESEARCH_VARIANTS:
|
||||
allowed = ", ".join(sorted(SR_RESEARCH_VARIANTS))
|
||||
raise ValueError(f"Unknown BACKTEST_SR_VARIANT={value!r}; expected one of {allowed}")
|
||||
return value
|
||||
|
||||
|
||||
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}:
|
||||
return "production_control"
|
||||
if (
|
||||
sr_variant == "rewrite_range504_legacy_primary"
|
||||
or sr_variant in RANGE_RESIDUAL_VARIANTS
|
||||
):
|
||||
return "rewrite"
|
||||
return sr_variant.removesuffix("_legacy_primary")
|
||||
|
||||
|
||||
def _range_504_log(highs: list[float], lows: list[float]) -> float:
|
||||
"""Multiplicative high/low range over the last two trading years."""
|
||||
window_highs = highs[-RANGE_FACTOR_LOOKBACK:]
|
||||
window_lows = lows[-RANGE_FACTOR_LOOKBACK:]
|
||||
if not window_highs or not window_lows:
|
||||
return 0.0
|
||||
high = max(window_highs)
|
||||
low = min(window_lows)
|
||||
if high <= 0 or low <= 0 or high < low:
|
||||
return 0.0
|
||||
return math.log(high / low)
|
||||
|
||||
|
||||
def _range_factor_allows(sr_variant: str, range_504_log: float) -> bool:
|
||||
"""Apply the explicit range factor only in its diagnostic arms."""
|
||||
return (
|
||||
sr_variant not in RANGE_FACTOR_VARIANTS
|
||||
or range_504_log >= RANGE_FACTOR_MIN_LOG
|
||||
)
|
||||
|
||||
|
||||
def _primary_min_rr_for_variant(sr_variant: str, activation: dict) -> float:
|
||||
"""Preserve the deployed selector except in explicit primary-2 research."""
|
||||
if (
|
||||
sr_variant in {
|
||||
"production_control",
|
||||
"production_range504",
|
||||
EXPLICIT_TARGET_LADDER_VARIANT,
|
||||
STRUCTURAL_OVERLAY_VARIANT,
|
||||
}
|
||||
or sr_variant.endswith("_legacy_primary")
|
||||
or sr_variant.startswith("legacy_")
|
||||
):
|
||||
return 1.5
|
||||
return float(activation.get("min_rr", 0.0))
|
||||
|
||||
|
||||
def _apply_zone_strength_variant(zone_levels: list[Any], sr_variant: str) -> list[Any]:
|
||||
"""Apply post-cluster research controls without changing zone geometry."""
|
||||
if sr_variant in {"legacy_geometry_neutral", "legacy_range_grid_neutral"}:
|
||||
# Neutrality must be enforced after the shared zone cluster: its legacy
|
||||
# sum mode can otherwise turn two 50-strength constituents back into a
|
||||
# 100-strength target and silently invalidate the ablation.
|
||||
for level in zone_levels:
|
||||
level.strength = 50
|
||||
return zone_levels
|
||||
|
||||
|
||||
def _backtest_entry_bounds() -> tuple[date | None, date | None]:
|
||||
"""Optional research-only entry bounds used to protect validation data."""
|
||||
parsed: list[date | None] = []
|
||||
for key in ("BACKTEST_ENTRY_START", "BACKTEST_ENTRY_END"):
|
||||
raw = os.getenv(key, "").strip()
|
||||
if not raw:
|
||||
parsed.append(None)
|
||||
continue
|
||||
try:
|
||||
parsed.append(date.fromisoformat(raw))
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"{key} must be YYYY-MM-DD, got {raw!r}") from exc
|
||||
start, end = parsed
|
||||
if start is not None and end is not None and start > end:
|
||||
raise ValueError("BACKTEST_ENTRY_START must be on or before BACKTEST_ENTRY_END")
|
||||
return start, end
|
||||
|
||||
|
||||
def _sr_audit_enabled() -> bool:
|
||||
return os.getenv("BACKTEST_SR_AUDIT", "").strip().lower() in {
|
||||
"1", "true", "yes", "on",
|
||||
}
|
||||
def validate_backtest_target_model(value: str) -> str:
|
||||
"""Validate the small, user-facing set of supported backtest target models."""
|
||||
normalized = value.strip().lower()
|
||||
if normalized not in BACKTEST_TARGET_MODELS:
|
||||
allowed = ", ".join(BACKTEST_TARGET_MODELS)
|
||||
raise ValueError(f"Unknown backtest target model {value!r}; expected one of {allowed}")
|
||||
return normalized
|
||||
|
||||
|
||||
def _atr_target_fallback_k() -> float | None:
|
||||
@@ -346,7 +214,7 @@ def _window_setups(
|
||||
config: dict,
|
||||
activation: dict,
|
||||
*,
|
||||
sr_variant: str | None = None,
|
||||
target_model: str = PRODUCTION_GTL_TARGET_MODEL,
|
||||
) -> 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."""
|
||||
@@ -373,58 +241,20 @@ def _window_setups(
|
||||
if atr <= 0:
|
||||
return []
|
||||
|
||||
sr_variant = sr_variant or _sr_research_variant()
|
||||
detector_variant = _sr_detector_variant(sr_variant)
|
||||
range_504_log = _range_504_log(highs, lows)
|
||||
if sr_variant == "legacy_geometry_neutral":
|
||||
detected_levels = detect_sr_levels_legacy(
|
||||
highs, lows, closes, volumes, neutral_strength=True
|
||||
)
|
||||
elif sr_variant == "legacy_pivots_only":
|
||||
detected_levels = detect_sr_levels_legacy(
|
||||
highs, lows, closes, volumes, include_volume_profile=False
|
||||
)
|
||||
elif sr_variant == "legacy_traffic_grid_only":
|
||||
detected_levels = detect_sr_levels_legacy(
|
||||
highs, lows, closes, volumes, include_pivots=False
|
||||
)
|
||||
elif sr_variant == EXPLICIT_TARGET_LADDER_VARIANT:
|
||||
target_model = validate_backtest_target_model(target_model)
|
||||
if target_model == PRODUCTION_GTL_TARGET_MODEL:
|
||||
detected_levels = detect_gate_target_ladder(
|
||||
highs,
|
||||
lows,
|
||||
closes,
|
||||
)
|
||||
elif sr_variant in {"legacy_range_grid_touch", "legacy_range_grid_neutral"}:
|
||||
detected_levels = detect_sr_levels_legacy(
|
||||
highs,
|
||||
lows,
|
||||
closes,
|
||||
volumes,
|
||||
include_pivots=False,
|
||||
neutral_strength=sr_variant == "legacy_range_grid_neutral",
|
||||
explicit_range_grid=True,
|
||||
)
|
||||
elif detector_variant in {"production_control", "rr_aligned_control"}:
|
||||
detected_levels = detect_sr_levels_legacy(highs, lows, closes, volumes)
|
||||
else:
|
||||
detector_cap = 0 if detector_variant == "gate_v2" else MAX_LEVELS
|
||||
detected_levels = detect_sr_levels(
|
||||
highs, lows, closes, volumes, max_levels=detector_cap
|
||||
)
|
||||
detected_levels = detect_sr_levels(highs, lows, closes, volumes)
|
||||
sr_levels = _wrap_levels(detected_levels)
|
||||
if not sr_levels:
|
||||
return []
|
||||
|
||||
gate_levels = _gate_eligible_levels(
|
||||
sr_levels,
|
||||
confirmed_rounds_only=detector_variant in {"confirmed_rounds", "gate_v2"},
|
||||
exclude_standalone_rounds=sr_variant in RANGE_RESIDUAL_VARIANTS,
|
||||
)
|
||||
zone_strength_mode = (
|
||||
"soft"
|
||||
if detector_variant in {"soft_zones", "confirmed_rounds", "gate_v2"}
|
||||
else "sum"
|
||||
)
|
||||
gate_levels = _gate_eligible_levels(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
|
||||
@@ -443,9 +273,8 @@ def _window_setups(
|
||||
zone_levels = _zone_representative_levels(
|
||||
gate_levels,
|
||||
entry,
|
||||
strength_mode=zone_strength_mode,
|
||||
strength_mode="sum",
|
||||
)
|
||||
zone_levels = _apply_zone_strength_variant(zone_levels, sr_variant)
|
||||
targets = target_generator.generate_targets(direction, entry, stop, zone_levels, atr)
|
||||
if not targets:
|
||||
fallback_k = _atr_target_fallback_k()
|
||||
@@ -462,10 +291,9 @@ def _window_setups(
|
||||
# Collapse duplicate floor-pinned lottery targets (parity with
|
||||
# enhance_trade_setup).
|
||||
targets = _prune_floor_pinned_targets(targets)
|
||||
primary_min_rr = _primary_min_rr_for_variant(sr_variant, activation)
|
||||
primary = _select_primary_target(
|
||||
targets,
|
||||
min_rr=primary_min_rr,
|
||||
min_rr=1.5,
|
||||
)
|
||||
if primary is None:
|
||||
continue
|
||||
@@ -507,10 +335,6 @@ def _window_setups(
|
||||
# week are known. run_backtest ranks momentum and finalizes `qualified`.
|
||||
core_config = {**activation, "min_momentum_percentile": 0.0}
|
||||
meets_core = setup_qualifies(setup_ns, core_config)
|
||||
meets_core = meets_core and _range_factor_allows(
|
||||
sr_variant,
|
||||
range_504_log,
|
||||
)
|
||||
best_prob = best_target_probability(setup_ns)
|
||||
out.append({
|
||||
"direction": direction,
|
||||
@@ -525,7 +349,7 @@ def _window_setups(
|
||||
"meets_core": meets_core,
|
||||
"action": action,
|
||||
"risk_level": risk_level,
|
||||
"sr_variant": sr_variant,
|
||||
"target_model": target_model,
|
||||
"primary_sources": list(primary.get("sr_sources") or []),
|
||||
"primary_strength": float(primary.get("sr_strength", 0.0)),
|
||||
"primary_rejection_count": int(
|
||||
@@ -537,62 +361,10 @@ def _window_setups(
|
||||
),
|
||||
"raw_level_count": len(sr_levels),
|
||||
"gate_level_count": len(gate_levels),
|
||||
"range_504_log": range_504_log,
|
||||
"range_504_ratio": math.exp(range_504_log),
|
||||
"range_factor_pass": range_504_log >= RANGE_FACTOR_MIN_LOG,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _structural_overlay_window_setups(
|
||||
window_records: list,
|
||||
config: dict,
|
||||
activation: dict,
|
||||
) -> list[dict]:
|
||||
"""Production setups tagged by the clean structural/range candidate.
|
||||
|
||||
The returned population and setup geometry remain production-identical.
|
||||
The clean detector is only a point-in-time feature, so this arm can test a
|
||||
ranking overlay without silently changing admission breadth or targets.
|
||||
"""
|
||||
production = _window_setups(
|
||||
window_records,
|
||||
config,
|
||||
activation,
|
||||
sr_variant="production_control",
|
||||
)
|
||||
if not production:
|
||||
return []
|
||||
structural = _window_setups(
|
||||
window_records,
|
||||
config,
|
||||
activation,
|
||||
sr_variant=STRUCTURAL_OVERLAY_SOURCE_VARIANT,
|
||||
)
|
||||
structural_by_direction = {row["direction"]: row for row in structural}
|
||||
tagged: list[dict] = []
|
||||
for production_row in production:
|
||||
row = dict(production_row)
|
||||
structural_row = structural_by_direction.get(row["direction"])
|
||||
row["sr_variant"] = STRUCTURAL_OVERLAY_VARIANT
|
||||
row["structural_overlay_pass"] = bool(
|
||||
structural_row and structural_row.get("meets_core")
|
||||
)
|
||||
row["structural_overlay_rr"] = (
|
||||
float(structural_row["rr"]) if structural_row is not None else None
|
||||
)
|
||||
row["structural_overlay_sources"] = (
|
||||
list(structural_row.get("primary_sources") or [])
|
||||
if structural_row is not None else []
|
||||
)
|
||||
row["structural_overlay_gate_level_count"] = (
|
||||
int(structural_row.get("gate_level_count", 0) or 0)
|
||||
if structural_row is not None else 0
|
||||
)
|
||||
tagged.append(row)
|
||||
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,
|
||||
@@ -672,6 +444,7 @@ def _replay_ticker(
|
||||
config: dict,
|
||||
activation: dict,
|
||||
benchmark_closes: dict[date, float] | None = None,
|
||||
target_model: str = PRODUCTION_GTL_TARGET_MODEL,
|
||||
) -> list[dict]:
|
||||
"""Walk one ticker's history weekly, building setups and their realized outcomes."""
|
||||
candidates: list[dict] = []
|
||||
@@ -679,14 +452,7 @@ def _replay_ticker(
|
||||
if n < MIN_LOOKBACK + HORIZON:
|
||||
return candidates
|
||||
|
||||
entry_start, entry_end = _backtest_entry_bounds()
|
||||
sr_variant = _sr_research_variant()
|
||||
for i in range(MIN_LOOKBACK - 1, n - HORIZON, STEP_DAYS):
|
||||
as_of = records[i].date
|
||||
if entry_start is not None and as_of < entry_start:
|
||||
continue
|
||||
if entry_end is not None and as_of > entry_end:
|
||||
continue
|
||||
window = records[: i + 1]
|
||||
forward = records[i + 1 :]
|
||||
forward_bars = [Bar(date=r.date, high=r.high, low=r.low) for r in forward]
|
||||
@@ -697,15 +463,11 @@ def _replay_ticker(
|
||||
)
|
||||
vol_6m = _realized_vol_6m(closes, len(window) - 1)
|
||||
|
||||
setups = (
|
||||
_structural_overlay_window_setups(window, config, activation)
|
||||
if sr_variant == STRUCTURAL_OVERLAY_VARIANT
|
||||
else _window_setups(
|
||||
window,
|
||||
config,
|
||||
activation,
|
||||
sr_variant=sr_variant,
|
||||
)
|
||||
setups = _window_setups(
|
||||
window,
|
||||
config,
|
||||
activation,
|
||||
target_model=target_model,
|
||||
)
|
||||
for s in setups:
|
||||
outcome, outcome_date = evaluate_setup_against_bars(
|
||||
@@ -755,7 +517,7 @@ def _replay_ticker(
|
||||
# every candidate looks NEUTRAL and the ablation rows collapse.
|
||||
"action": s["action"],
|
||||
"risk_level": s["risk_level"],
|
||||
"sr_variant": s["sr_variant"],
|
||||
"target_model": s["target_model"],
|
||||
"primary_sources": s["primary_sources"],
|
||||
"primary_strength": s["primary_strength"],
|
||||
"primary_rejection_count": s["primary_rejection_count"],
|
||||
@@ -763,15 +525,6 @@ def _replay_ticker(
|
||||
"primary_distance_atr": s["primary_distance_atr"],
|
||||
"raw_level_count": s["raw_level_count"],
|
||||
"gate_level_count": s["gate_level_count"],
|
||||
"range_504_log": s["range_504_log"],
|
||||
"range_504_ratio": s["range_504_ratio"],
|
||||
"range_factor_pass": s["range_factor_pass"],
|
||||
"structural_overlay_pass": s.get("structural_overlay_pass"),
|
||||
"structural_overlay_rr": s.get("structural_overlay_rr"),
|
||||
"structural_overlay_sources": s.get("structural_overlay_sources"),
|
||||
"structural_overlay_gate_level_count": s.get(
|
||||
"structural_overlay_gate_level_count"
|
||||
),
|
||||
"outcome": outcome,
|
||||
"target_hit": target_hit,
|
||||
"realized_r": realized_r,
|
||||
@@ -832,8 +585,8 @@ def _robustness_stats(net_rs: list[float]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
|
||||
"""Compact evidence audit for the active local S/R research arm."""
|
||||
def _target_model_diagnostics(candidates: list[dict], target_model: str) -> dict:
|
||||
"""Compact target-source diagnostics for the selected supported model."""
|
||||
source_counts: dict[str, int] = defaultdict(int)
|
||||
round_only = 0
|
||||
strengths: list[float] = []
|
||||
@@ -841,9 +594,6 @@ def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
|
||||
rejections: list[int] = []
|
||||
raw_counts: list[int] = []
|
||||
gate_counts: list[int] = []
|
||||
range_logs: list[float] = []
|
||||
overlay_rows = 0
|
||||
overlay_pass = 0
|
||||
for cand in candidates:
|
||||
sources = list(cand.get("primary_sources") or [])
|
||||
for source in sources:
|
||||
@@ -855,16 +605,13 @@ def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
|
||||
rejections.append(int(cand.get("primary_rejection_count", 0) or 0))
|
||||
raw_counts.append(int(cand.get("raw_level_count", 0) or 0))
|
||||
gate_counts.append(int(cand.get("gate_level_count", 0) or 0))
|
||||
range_logs.append(float(cand.get("range_504_log", 0.0) or 0.0))
|
||||
if cand.get("structural_overlay_pass") is not None:
|
||||
overlay_rows += 1
|
||||
overlay_pass += int(bool(cand["structural_overlay_pass"]))
|
||||
|
||||
def avg(values: list[float] | list[int]) -> float | None:
|
||||
return round(sum(values) / len(values), 3) if values else None
|
||||
|
||||
return {
|
||||
"variant": _sr_research_variant(),
|
||||
"target_model": target_model,
|
||||
"target_model_label": BACKTEST_TARGET_MODELS[target_model],
|
||||
"candidate_count": len(candidates),
|
||||
"primary_source_counts": dict(sorted(source_counts.items())),
|
||||
"primary_round_only": round_only,
|
||||
@@ -874,80 +621,9 @@ def _sr_variant_diagnostics(candidates: list[dict]) -> dict:
|
||||
"avg_primary_rejection_count": avg(rejections),
|
||||
"avg_raw_level_count": avg(raw_counts),
|
||||
"avg_gate_level_count": avg(gate_counts),
|
||||
"avg_range_504_log": avg(range_logs),
|
||||
"range_factor_pass": sum(
|
||||
1 for value in range_logs if value >= RANGE_FACTOR_MIN_LOG
|
||||
),
|
||||
"structural_overlay_rows": overlay_rows,
|
||||
"structural_overlay_pass": overlay_pass,
|
||||
"structural_overlay_weight": (
|
||||
STRUCTURAL_OVERLAY_WEIGHT if overlay_rows else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _sr_candidate_audit(candidates: list[dict], min_percentile: float) -> list[dict] | None:
|
||||
"""Candidate-level audit for paired S/R variant comparisons.
|
||||
|
||||
Limit the sidecar population to the long momentum slice that could reach
|
||||
production qualification. This keeps reports reviewable while retaining
|
||||
gate failures, additions, removals, and portfolio-relevant near misses.
|
||||
"""
|
||||
if not _sr_audit_enabled():
|
||||
return None
|
||||
rows: list[dict] = []
|
||||
for cand in candidates:
|
||||
percentile = cand.get(PRODUCTION_PERCENTILE_KEY)
|
||||
if cand.get("direction") != "long" or percentile is None:
|
||||
continue
|
||||
if float(percentile) < min_percentile:
|
||||
continue
|
||||
rows.append({
|
||||
"symbol": cand["symbol"],
|
||||
"date": cand["date"],
|
||||
"direction": cand["direction"],
|
||||
"qualified": bool(cand.get("qualified")),
|
||||
"meets_core": bool(cand.get("meets_core")),
|
||||
"momentum_percentile": round(float(percentile), 6),
|
||||
"strategy_rank": round(float(cand.get(RESIDUAL_HIGH_VOL_BLEND_KEY, 0.0) or 0.0), 6),
|
||||
"production_rank": round(
|
||||
float(cand.get(RESIDUAL_HIGH_VOL_BLEND_80_20_KEY, 0.0) or 0.0),
|
||||
6,
|
||||
),
|
||||
"structural_overlay_pass": cand.get("structural_overlay_pass"),
|
||||
"structural_overlay_score": (
|
||||
round(float(cand[STRUCTURAL_OVERLAY_SCORE_KEY]), 6)
|
||||
if cand.get(STRUCTURAL_OVERLAY_SCORE_KEY) is not None else None
|
||||
),
|
||||
"structural_overlay_rr": (
|
||||
round(float(cand["structural_overlay_rr"]), 6)
|
||||
if cand.get("structural_overlay_rr") is not None else None
|
||||
),
|
||||
"structural_overlay_sources": list(
|
||||
cand.get("structural_overlay_sources") or []
|
||||
),
|
||||
"structural_overlay_gate_level_count": int(
|
||||
cand.get("structural_overlay_gate_level_count", 0) or 0
|
||||
),
|
||||
"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 []),
|
||||
"primary_strength": round(float(cand.get("primary_strength", 0.0)), 3),
|
||||
"primary_rejection_count": int(cand.get("primary_rejection_count", 0) or 0),
|
||||
"primary_distance_atr": round(float(cand.get("primary_distance_atr", 0.0)), 6),
|
||||
"raw_level_count": int(cand.get("raw_level_count", 0) or 0),
|
||||
"gate_level_count": int(cand.get("gate_level_count", 0) or 0),
|
||||
"range_504_log": round(float(cand.get("range_504_log", 0.0)), 6),
|
||||
"range_504_ratio": round(float(cand.get("range_504_ratio", 1.0)), 6),
|
||||
"range_factor_pass": bool(cand.get("range_factor_pass")),
|
||||
"outcome": cand.get("outcome"),
|
||||
"net_r": round(float(cand.get("realized_r", 0.0)) - _cost_r(cand), 6),
|
||||
"hold30_r": round(float((cand.get("time_r") or {}).get(30, 0.0)), 6),
|
||||
})
|
||||
rows.sort(key=lambda row: (row["date"], row["symbol"], row["direction"]))
|
||||
return rows
|
||||
|
||||
|
||||
# The fixed take-profit and trailing-stop sweeps were retired 2026-07: swept
|
||||
# TPs never found an interior optimum (momentum's edge lives in the right tail)
|
||||
# and wide trails converged to the hold-to-horizon exit, so the time-exit sweep
|
||||
@@ -1280,6 +956,7 @@ def _replay_and_signals(
|
||||
config: dict,
|
||||
activation: dict,
|
||||
benchmark_closes: dict[date, float] | None = None,
|
||||
target_model: str = PRODUCTION_GTL_TARGET_MODEL,
|
||||
) -> tuple[list[dict], dict]:
|
||||
"""The CPU-bound per-ticker work, as a top-level (picklable) function so it can
|
||||
run in a worker process. Takes primitive column arrays (cheap to pickle),
|
||||
@@ -1292,7 +969,14 @@ def _replay_and_signals(
|
||||
for o, op, hi, lo, cl, vo in zip(date_ords, opens, highs, lows, closes, volumes)
|
||||
]
|
||||
return (
|
||||
_replay_ticker(symbol, bars, config, activation, benchmark_closes),
|
||||
_replay_ticker(
|
||||
symbol,
|
||||
bars,
|
||||
config,
|
||||
activation,
|
||||
benchmark_closes,
|
||||
target_model,
|
||||
),
|
||||
_signal_series(bars, benchmark_closes),
|
||||
)
|
||||
|
||||
@@ -1464,23 +1148,6 @@ def _assign_residual_high_vol_blend(candidates: list[dict]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _assign_structural_overlay_score(candidates: list[dict]) -> None:
|
||||
"""Conservative rank nudge for production setups confirmed by clean S/R."""
|
||||
for cand in candidates:
|
||||
if cand.get("structural_overlay_pass") is None:
|
||||
cand[STRUCTURAL_OVERLAY_SCORE_KEY] = None
|
||||
continue
|
||||
production_rank = cand.get(RESIDUAL_HIGH_VOL_BLEND_80_20_KEY)
|
||||
if production_rank is None:
|
||||
cand[STRUCTURAL_OVERLAY_SCORE_KEY] = None
|
||||
continue
|
||||
structural_score = 100.0 if cand["structural_overlay_pass"] else 0.0
|
||||
cand[STRUCTURAL_OVERLAY_SCORE_KEY] = (
|
||||
float(production_rank) * (1.0 - STRUCTURAL_OVERLAY_WEIGHT)
|
||||
+ structural_score * STRUCTURAL_OVERLAY_WEIGHT
|
||||
)
|
||||
|
||||
|
||||
def _momentum_qualifies(cand: dict, threshold: float) -> bool:
|
||||
"""Whether a candidate clears the floors (meets_core) and the momentum gate.
|
||||
Threshold 0 disables the momentum gate (floors only). The gate is long-only:
|
||||
@@ -1641,17 +1308,9 @@ def _simulate_portfolio(
|
||||
qualified_fn = _default_qualified
|
||||
|
||||
entries_by_ord: dict[int, list[dict]] = defaultdict(list)
|
||||
configured_start, configured_end = _backtest_entry_bounds()
|
||||
effective_start = start_date if start_date is not None else configured_start
|
||||
start_ord = effective_start.toordinal() if effective_start is not None else None
|
||||
if end_date is not None:
|
||||
# Explicit simulator/holdout end dates are exclusive split boundaries.
|
||||
end_ord = end_date.toordinal()
|
||||
elif configured_end is not None:
|
||||
# BACKTEST_ENTRY_END is documented and applied as an inclusive bound.
|
||||
end_ord = (configured_end + timedelta(days=1)).toordinal()
|
||||
else:
|
||||
end_ord = None
|
||||
start_ord = start_date.toordinal() if start_date is not None else None
|
||||
# Explicit simulator/holdout end dates are exclusive split boundaries.
|
||||
end_ord = end_date.toordinal() if end_date is not None else None
|
||||
for c in candidates:
|
||||
if not qualified_fn(c) or c.get("direction") != "long":
|
||||
continue
|
||||
@@ -2333,7 +1992,6 @@ PORTFOLIO_MONITOR_LOOKBACKS: tuple[dict, ...] = (
|
||||
)
|
||||
|
||||
PRODUCTION_PORTFOLIO_STRATEGY = "residual80_highvol80_20_atr3"
|
||||
STRUCTURAL_OVERLAY_PORTFOLIO_STRATEGY = "production_structural_overlay5_atr3"
|
||||
PORTFOLIO_MONITOR_STRATEGIES: tuple[dict, ...] = (
|
||||
{
|
||||
"strategy": "legacy_residual80_hold",
|
||||
@@ -2368,22 +2026,7 @@ PORTFOLIO_MONITOR_STRATEGIES: tuple[dict, ...] = (
|
||||
|
||||
|
||||
def _portfolio_monitor_strategies() -> tuple[dict, ...]:
|
||||
"""Add the frozen overlay only inside its explicit research arm."""
|
||||
if _sr_research_variant() != STRUCTURAL_OVERLAY_VARIANT:
|
||||
return PORTFOLIO_MONITOR_STRATEGIES
|
||||
return PORTFOLIO_MONITOR_STRATEGIES + ({
|
||||
"strategy": STRUCTURAL_OVERLAY_PORTFOLIO_STRATEGY,
|
||||
"label": "Research: production gate + 5% clean-structure rank overlay",
|
||||
"description": (
|
||||
"Production-qualified universe and live exit, ranked by 95% current "
|
||||
"80/20 strategy rank plus 5% clean structural/range confirmation."
|
||||
),
|
||||
"entry_variant": "residual80_highvol_blend80_20_fixed10",
|
||||
"exit_policy": "atr_trail3",
|
||||
"ranking_key": STRUCTURAL_OVERLAY_SCORE_KEY,
|
||||
"use_live_config": True,
|
||||
"is_production": False,
|
||||
},)
|
||||
return PORTFOLIO_MONITOR_STRATEGIES
|
||||
|
||||
|
||||
def _entry_variant_config(variant: str) -> dict | None:
|
||||
@@ -3132,8 +2775,11 @@ def _build_recommendation(report: dict) -> dict:
|
||||
async def run_backtest(
|
||||
db: AsyncSession,
|
||||
progress_cb: Callable[[int, int, str], None] | None = None,
|
||||
*,
|
||||
target_model: str = PRODUCTION_GTL_TARGET_MODEL,
|
||||
) -> dict:
|
||||
"""Replay every ticker and aggregate the Phase-1 reports for the current config."""
|
||||
target_model = validate_backtest_target_model(target_model)
|
||||
config = await get_recommendation_config(db)
|
||||
activation = await get_activation_config(db)
|
||||
|
||||
@@ -3198,6 +2844,7 @@ async def run_backtest(
|
||||
futures.append(loop.run_in_executor(
|
||||
pool, _replay_and_signals, ticker.symbol, columns, config, activation,
|
||||
benchmark_closes,
|
||||
target_model,
|
||||
))
|
||||
for result in await asyncio.gather(*futures, return_exceptions=True):
|
||||
if isinstance(result, Exception):
|
||||
@@ -3219,6 +2866,7 @@ async def run_backtest(
|
||||
_merge(await asyncio.to_thread(
|
||||
_replay_and_signals, ticker.symbol, columns, config, activation,
|
||||
benchmark_closes,
|
||||
target_model,
|
||||
))
|
||||
except Exception:
|
||||
logger.exception("Backtest replay failed for %s", ticker.symbol)
|
||||
@@ -3235,7 +2883,6 @@ async def run_backtest(
|
||||
_assign_activation_momentum_percentiles(candidates)
|
||||
_assign_residual_low_vol_blend(candidates)
|
||||
_assign_residual_high_vol_blend(candidates)
|
||||
_assign_structural_overlay_score(candidates)
|
||||
current_min_pct = float(activation.get("min_momentum_percentile", 80.0))
|
||||
for c in candidates:
|
||||
c["qualified"] = _momentum_qualifies(c, current_min_pct)
|
||||
@@ -3334,26 +2981,9 @@ async def run_backtest(
|
||||
"horizon_days": HORIZON,
|
||||
"min_lookback": MIN_LOOKBACK,
|
||||
"cost_per_side_pct": round(COST_PER_SIDE * 100, 3),
|
||||
"sr_variant": _sr_research_variant(),
|
||||
"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),
|
||||
"structural_overlay_weight": (
|
||||
STRUCTURAL_OVERLAY_WEIGHT
|
||||
if _sr_research_variant() == STRUCTURAL_OVERLAY_VARIANT else None
|
||||
),
|
||||
"structural_overlay_source_variant": (
|
||||
STRUCTURAL_OVERLAY_SOURCE_VARIANT
|
||||
if _sr_research_variant() == STRUCTURAL_OVERLAY_VARIANT else None
|
||||
),
|
||||
"entry_start": (
|
||||
_backtest_entry_bounds()[0].isoformat()
|
||||
if _backtest_entry_bounds()[0] is not None else None
|
||||
),
|
||||
"entry_end": (
|
||||
_backtest_entry_bounds()[1].isoformat()
|
||||
if _backtest_entry_bounds()[1] is not None else None
|
||||
),
|
||||
"target_model": target_model,
|
||||
"target_model_label": BACKTEST_TARGET_MODELS[target_model],
|
||||
"is_production_target_model": target_model == PRODUCTION_GTL_TARGET_MODEL,
|
||||
},
|
||||
"activation": activation,
|
||||
"overall_qualified": _bucket_stats(qualified),
|
||||
@@ -3417,8 +3047,10 @@ async def run_backtest(
|
||||
"portfolio_monitor": portfolio_monitor_report,
|
||||
"holdout": holdout_report,
|
||||
"min_rr_sweep": min_rr_sweep_report,
|
||||
"sr_variant_diagnostics": _sr_variant_diagnostics(candidates),
|
||||
"sr_candidate_audit": _sr_candidate_audit(candidates, current_min_pct),
|
||||
"target_model_diagnostics": _target_model_diagnostics(
|
||||
candidates,
|
||||
target_model,
|
||||
),
|
||||
"signal_eval": _signal_evaluation(collected),
|
||||
"signal_eval_note": (
|
||||
"Cross-sectional rank-IC of price-only signals vs the forward "
|
||||
@@ -3446,9 +3078,11 @@ async def run_backtest(
|
||||
async def run_and_store(
|
||||
db: AsyncSession,
|
||||
progress_cb: Callable[[int, int, str], None] | None = None,
|
||||
*,
|
||||
target_model: str = PRODUCTION_GTL_TARGET_MODEL,
|
||||
) -> dict:
|
||||
"""Run the backtest and cache the report in a SystemSetting. Job entrypoint."""
|
||||
report = await run_backtest(db, progress_cb)
|
||||
report = await run_backtest(db, progress_cb, target_model=target_model)
|
||||
await update_setting(db, KEY_REPORT, json.dumps(report))
|
||||
return report
|
||||
|
||||
|
||||
Reference in New Issue
Block a user