Isolate residual S/R target selection
This commit is contained in:
@@ -94,9 +94,14 @@ HORIZON = 30 # trading days to resolve an outcome (matches the evaluat
|
|||||||
ATR_MULTIPLIER = 1.5
|
ATR_MULTIPLIER = 1.5
|
||||||
RANGE_FACTOR_LOOKBACK = 504
|
RANGE_FACTOR_LOOKBACK = 504
|
||||||
RANGE_FACTOR_MIN_LOG = 1.0 # approximately a 2.7x high/low span
|
RANGE_FACTOR_MIN_LOG = 1.0 # approximately a 2.7x high/low span
|
||||||
|
RANGE_RESIDUAL_VARIANTS = {
|
||||||
|
"rewrite_range504_structural_legacy_primary",
|
||||||
|
"rewrite_range504_structural_primary2",
|
||||||
|
}
|
||||||
RANGE_FACTOR_VARIANTS = {
|
RANGE_FACTOR_VARIANTS = {
|
||||||
"production_range504",
|
"production_range504",
|
||||||
"rewrite_range504_legacy_primary",
|
"rewrite_range504_legacy_primary",
|
||||||
|
*RANGE_RESIDUAL_VARIANTS,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cross-sectional signal evaluation (factor IC). Each candidate signal is a
|
# Cross-sectional signal evaluation (factor IC). Each candidate signal is a
|
||||||
@@ -169,7 +174,10 @@ def _sr_detector_variant(sr_variant: str) -> str:
|
|||||||
"""Map factor-gated research arms to the detector they hold fixed."""
|
"""Map factor-gated research arms to the detector they hold fixed."""
|
||||||
if sr_variant == "production_range504":
|
if sr_variant == "production_range504":
|
||||||
return "production_control"
|
return "production_control"
|
||||||
if sr_variant == "rewrite_range504_legacy_primary":
|
if (
|
||||||
|
sr_variant == "rewrite_range504_legacy_primary"
|
||||||
|
or sr_variant in RANGE_RESIDUAL_VARIANTS
|
||||||
|
):
|
||||||
return "rewrite"
|
return "rewrite"
|
||||||
return sr_variant.removesuffix("_legacy_primary")
|
return sr_variant.removesuffix("_legacy_primary")
|
||||||
|
|
||||||
@@ -195,6 +203,17 @@ def _range_factor_allows(sr_variant: str, range_504_log: float) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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"}
|
||||||
|
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]:
|
def _apply_zone_strength_variant(zone_levels: list[Any], sr_variant: str) -> list[Any]:
|
||||||
"""Apply post-cluster research controls without changing zone geometry."""
|
"""Apply post-cluster research controls without changing zone geometry."""
|
||||||
if sr_variant in {"legacy_geometry_neutral", "legacy_range_grid_neutral"}:
|
if sr_variant in {"legacy_geometry_neutral", "legacy_range_grid_neutral"}:
|
||||||
@@ -363,6 +382,7 @@ def _window_setups(
|
|||||||
gate_levels = _gate_eligible_levels(
|
gate_levels = _gate_eligible_levels(
|
||||||
sr_levels,
|
sr_levels,
|
||||||
confirmed_rounds_only=detector_variant in {"confirmed_rounds", "gate_v2"},
|
confirmed_rounds_only=detector_variant in {"confirmed_rounds", "gate_v2"},
|
||||||
|
exclude_standalone_rounds=sr_variant in RANGE_RESIDUAL_VARIANTS,
|
||||||
)
|
)
|
||||||
zone_strength_mode = (
|
zone_strength_mode = (
|
||||||
"soft"
|
"soft"
|
||||||
@@ -406,13 +426,7 @@ def _window_setups(
|
|||||||
# Collapse duplicate floor-pinned lottery targets (parity with
|
# Collapse duplicate floor-pinned lottery targets (parity with
|
||||||
# enhance_trade_setup).
|
# enhance_trade_setup).
|
||||||
targets = _prune_floor_pinned_targets(targets)
|
targets = _prune_floor_pinned_targets(targets)
|
||||||
primary_min_rr = (
|
primary_min_rr = _primary_min_rr_for_variant(sr_variant, activation)
|
||||||
1.5
|
|
||||||
if sr_variant in {"production_control", "production_range504"}
|
|
||||||
or sr_variant.endswith("_legacy_primary")
|
|
||||||
or sr_variant.startswith("legacy_")
|
|
||||||
else float(activation.get("min_rr", 0.0))
|
|
||||||
)
|
|
||||||
primary = _select_primary_target(
|
primary = _select_primary_target(
|
||||||
targets,
|
targets,
|
||||||
min_rr=primary_min_rr,
|
min_rr=primary_min_rr,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ def _gate_eligible_levels(
|
|||||||
sr_levels: list[Any],
|
sr_levels: list[Any],
|
||||||
*,
|
*,
|
||||||
confirmed_rounds_only: bool = False,
|
confirmed_rounds_only: bool = False,
|
||||||
|
exclude_standalone_rounds: bool = False,
|
||||||
min_round_rejections: int = 2,
|
min_round_rejections: int = 2,
|
||||||
) -> list[Any]:
|
) -> list[Any]:
|
||||||
"""Return structures allowed to influence entry qualification.
|
"""Return structures allowed to influence entry qualification.
|
||||||
@@ -69,7 +70,7 @@ def _gate_eligible_levels(
|
|||||||
either confluence with a pivot/volume source or distinct rejection clusters
|
either confluence with a pivot/volume source or distinct rejection clusters
|
||||||
before such a level is allowed to manufacture a gate target.
|
before such a level is allowed to manufacture a gate target.
|
||||||
"""
|
"""
|
||||||
if not confirmed_rounds_only:
|
if not confirmed_rounds_only and not exclude_standalone_rounds:
|
||||||
return list(sr_levels)
|
return list(sr_levels)
|
||||||
|
|
||||||
eligible: list[Any] = []
|
eligible: list[Any] = []
|
||||||
@@ -79,8 +80,11 @@ def _gate_eligible_levels(
|
|||||||
])
|
])
|
||||||
is_round_only = sources == {"round_number"}
|
is_round_only = sources == {"round_number"}
|
||||||
rejections = int(getattr(level, "rejection_count", 0) or 0)
|
rejections = int(getattr(level, "rejection_count", 0) or 0)
|
||||||
if not is_round_only or rejections >= min_round_rejections:
|
if exclude_standalone_rounds and is_round_only:
|
||||||
eligible.append(level)
|
continue
|
||||||
|
if confirmed_rounds_only and is_round_only and rejections < min_round_rejections:
|
||||||
|
continue
|
||||||
|
eligible.append(level)
|
||||||
return eligible
|
return eligible
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -585,6 +585,41 @@ Run on pre-2024 training data only:
|
|||||||
.venv/bin/python scripts/run_sr_v2_matrix.py factor --workers 14
|
.venv/bin/python scripts/run_sr_v2_matrix.py factor --workers 14
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
| training arm | qualified | Sharpe | CAGR | MaxDD | net avg R | ex-top-5% |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| production control | 676 | 1.28 | 28.8% | 21.4% | 0.230 | 0.066 |
|
||||||
|
| production + range504 | 180 | **1.65** | 31.4% | **15.9%** | **0.476** | **0.250** |
|
||||||
|
| clean rewrite | 1,200 | 0.96 | 21.0% | 22.2% | 0.037 | -0.102 |
|
||||||
|
| clean rewrite + range504 | 291 | 1.46 | **31.5%** | 18.4% | 0.292 | 0.158 |
|
||||||
|
|
||||||
|
The scalar recovers most of the detector rewrite's regression. The clean arm now
|
||||||
|
beats the original production baseline on Sharpe, CAGR, drawdown, and robust
|
||||||
|
expectancy. Old target geometry retains a smaller edge over the equally filtered
|
||||||
|
clean arm.
|
||||||
|
|
||||||
|
The residual cohort is target selection, not another range feature. The clean arm
|
||||||
|
retains 63 exceptionally strong common setups (+0.665R ex-top-5%), adds 228 weak
|
||||||
|
setups (+0.018R), and misses 117 strong old-geometry setups (+0.175R). Of the
|
||||||
|
clean-only additions, 156 have a standalone round-number primary; all 162
|
||||||
|
round-only qualified setups have fewer than two observed rejections and return
|
||||||
|
-0.024R ex-top-5%. Structural-only and round-confluent primaries return +0.394R
|
||||||
|
and +0.369R.
|
||||||
|
|
||||||
|
For 113 of the 117 missed setups, the clean detector does produce a target, but
|
||||||
|
its selected primary averages 1.56R; 77 land in the 1.5-2R veto band. A final
|
||||||
|
two-arm residual matrix therefore holds the clean detector and range factor fixed:
|
||||||
|
|
||||||
|
- `rewrite_range504_structural_legacy_primary`: exclude standalone round targets,
|
||||||
|
retain the deployed 1.5 primary floor;
|
||||||
|
- `rewrite_range504_structural_primary2`: identical, but select the primary from
|
||||||
|
targets clearing 2.0R.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python scripts/run_sr_v2_matrix.py residual --workers 14
|
||||||
|
```
|
||||||
|
|
||||||
The post-2024 window has been opened and is now analysis data, not a valid final
|
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
|
promotion holdout. These arms can isolate mechanism, but neither may ship without
|
||||||
new future data or a separately pre-registered walk-forward protocol.
|
new future data or a separately pre-registered walk-forward protocol.
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ def _parse_args() -> argparse.Namespace:
|
|||||||
"legacy_traffic_grid_only", "legacy_range_grid_touch",
|
"legacy_traffic_grid_only", "legacy_range_grid_touch",
|
||||||
"legacy_range_grid_neutral",
|
"legacy_range_grid_neutral",
|
||||||
"production_range504", "rewrite_range504_legacy_primary",
|
"production_range504", "rewrite_range504_legacy_primary",
|
||||||
|
"rewrite_range504_structural_legacy_primary",
|
||||||
|
"rewrite_range504_structural_primary2",
|
||||||
),
|
),
|
||||||
default=None,
|
default=None,
|
||||||
help="Research-only S/R detector/gate arm.",
|
help="Research-only S/R detector/gate arm.",
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ RANGE_FACTOR_ARMS = (
|
|||||||
"production_range504",
|
"production_range504",
|
||||||
"rewrite_range504_legacy_primary",
|
"rewrite_range504_legacy_primary",
|
||||||
)
|
)
|
||||||
|
RANGE_RESIDUAL_ARMS = (
|
||||||
|
"rewrite_range504_structural_legacy_primary",
|
||||||
|
"rewrite_range504_structural_primary2",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _add_common(parser: argparse.ArgumentParser) -> None:
|
def _add_common(parser: argparse.ArgumentParser) -> None:
|
||||||
@@ -76,6 +80,17 @@ def _args() -> argparse.Namespace:
|
|||||||
default=None,
|
default=None,
|
||||||
help="Run one range-factor arm without repeating the pair.",
|
help="Run one range-factor arm without repeating the pair.",
|
||||||
)
|
)
|
||||||
|
residual = commands.add_parser(
|
||||||
|
"residual",
|
||||||
|
help="Isolate standalone rounds and the 1.5-2R primary-target veto.",
|
||||||
|
)
|
||||||
|
_add_common(residual)
|
||||||
|
residual.add_argument(
|
||||||
|
"--only-arm",
|
||||||
|
choices=RANGE_RESIDUAL_ARMS,
|
||||||
|
default=None,
|
||||||
|
help="Run one residual target-geometry arm without repeating the pair.",
|
||||||
|
)
|
||||||
validate = commands.add_parser(
|
validate = commands.add_parser(
|
||||||
"validate",
|
"validate",
|
||||||
help="Run production control and one locked arm from 2024-07-01.",
|
help="Run production control and one locked arm from 2024-07-01.",
|
||||||
@@ -166,6 +181,9 @@ def main() -> None:
|
|||||||
elif args.command == "factor":
|
elif args.command == "factor":
|
||||||
arms = (args.only_arm,) if args.only_arm else RANGE_FACTOR_ARMS
|
arms = (args.only_arm,) if args.only_arm else RANGE_FACTOR_ARMS
|
||||||
_train(args, arms, "backtest-sr-range-factor-train")
|
_train(args, arms, "backtest-sr-range-factor-train")
|
||||||
|
elif args.command == "residual":
|
||||||
|
arms = (args.only_arm,) if args.only_arm else RANGE_RESIDUAL_ARMS
|
||||||
|
_train(args, arms, "backtest-sr-range-residual-train")
|
||||||
else:
|
else:
|
||||||
_validate(args)
|
_validate(args)
|
||||||
|
|
||||||
|
|||||||
@@ -765,6 +765,8 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch):
|
|||||||
"legacy_range_grid_neutral",
|
"legacy_range_grid_neutral",
|
||||||
"production_range504",
|
"production_range504",
|
||||||
"rewrite_range504_legacy_primary",
|
"rewrite_range504_legacy_primary",
|
||||||
|
"rewrite_range504_structural_legacy_primary",
|
||||||
|
"rewrite_range504_structural_primary2",
|
||||||
):
|
):
|
||||||
monkeypatch.setenv("BACKTEST_SR_VARIANT", variant)
|
monkeypatch.setenv("BACKTEST_SR_VARIANT", variant)
|
||||||
assert bt._sr_research_variant() == variant
|
assert bt._sr_research_variant() == variant
|
||||||
@@ -776,6 +778,10 @@ def test_sr_research_variant_is_explicit_and_validated(monkeypatch):
|
|||||||
def test_range_factor_detector_mapping_is_explicit():
|
def test_range_factor_detector_mapping_is_explicit():
|
||||||
assert bt._sr_detector_variant("production_range504") == "production_control"
|
assert bt._sr_detector_variant("production_range504") == "production_control"
|
||||||
assert bt._sr_detector_variant("rewrite_range504_legacy_primary") == "rewrite"
|
assert bt._sr_detector_variant("rewrite_range504_legacy_primary") == "rewrite"
|
||||||
|
assert bt._sr_detector_variant(
|
||||||
|
"rewrite_range504_structural_legacy_primary"
|
||||||
|
) == "rewrite"
|
||||||
|
assert bt._sr_detector_variant("rewrite_range504_structural_primary2") == "rewrite"
|
||||||
assert bt._sr_detector_variant("soft_zones_legacy_primary") == "soft_zones"
|
assert bt._sr_detector_variant("soft_zones_legacy_primary") == "soft_zones"
|
||||||
|
|
||||||
|
|
||||||
@@ -789,6 +795,10 @@ def test_range_factor_gate_is_research_arm_only():
|
|||||||
below = bt.RANGE_FACTOR_MIN_LOG - 0.01
|
below = bt.RANGE_FACTOR_MIN_LOG - 0.01
|
||||||
assert not bt._range_factor_allows("production_range504", below)
|
assert not bt._range_factor_allows("production_range504", below)
|
||||||
assert not bt._range_factor_allows("rewrite_range504_legacy_primary", below)
|
assert not bt._range_factor_allows("rewrite_range504_legacy_primary", below)
|
||||||
|
assert not bt._range_factor_allows(
|
||||||
|
"rewrite_range504_structural_primary2",
|
||||||
|
below,
|
||||||
|
)
|
||||||
assert bt._range_factor_allows(
|
assert bt._range_factor_allows(
|
||||||
"production_range504",
|
"production_range504",
|
||||||
bt.RANGE_FACTOR_MIN_LOG,
|
bt.RANGE_FACTOR_MIN_LOG,
|
||||||
@@ -796,6 +806,18 @@ def test_range_factor_gate_is_research_arm_only():
|
|||||||
assert bt._range_factor_allows("production_control", below)
|
assert bt._range_factor_allows("production_control", below)
|
||||||
|
|
||||||
|
|
||||||
|
def test_residual_arms_change_only_the_primary_rr_floor():
|
||||||
|
activation = {"min_rr": 2.0}
|
||||||
|
assert bt._primary_min_rr_for_variant(
|
||||||
|
"rewrite_range504_structural_legacy_primary",
|
||||||
|
activation,
|
||||||
|
) == 1.5
|
||||||
|
assert bt._primary_min_rr_for_variant(
|
||||||
|
"rewrite_range504_structural_primary2",
|
||||||
|
activation,
|
||||||
|
) == 2.0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("variant", "neutral_strength"),
|
("variant", "neutral_strength"),
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -374,3 +374,6 @@ def test_gate_requires_confirmation_for_standalone_round_number():
|
|||||||
assert _gate_eligible_levels(
|
assert _gate_eligible_levels(
|
||||||
[untouched, confirmed, confluent], confirmed_rounds_only=True
|
[untouched, confirmed, confluent], confirmed_rounds_only=True
|
||||||
) == [confirmed, confluent]
|
) == [confirmed, confluent]
|
||||||
|
assert _gate_eligible_levels(
|
||||||
|
[untouched, confirmed, confluent], exclude_standalone_rounds=True
|
||||||
|
) == [confluent]
|
||||||
|
|||||||
Reference in New Issue
Block a user