Sweep the R:R floor; fix a holdout metric artifact
min_rr = 2.0 was hand-set in Admin (2026-06-24) and never swept — the gate ablation only tested the floor on-vs-off, never its level. It was the last un-swept knob in the live gate. Swept against portfolio Sharpe under the real exit, with a parity self-check (reproduces_production_gate: the row at the live floor must rebuild production's exact 1,089-setup qualified set — it does). min_rr qualified in-sample Sh/CAGR OOS Sh/CAGR (entries >= 2024-07) 0.0 6636 1.98 / 58.5% 2.02 / 66.2% 1.2 3897 1.34 / 33.9% 1.12 / 28.8% 1.5 3127 1.20 / 29.6% 1.12 / 28.8% 1.75 1974 1.64 / 44.5% 1.15 / 27.4% 2.0 (live) 1089 2.04 / 50.4% 2.78 / 73.3% 2.25 577 1.64 / 31.8% 1.71 / 31.9% 2.5 286 1.67 / 29.0% 0.68 / 8.7% KEEP 2.0. It is the optimum in both windows, and a peak that reproduces in data it was never fitted to is real evidence. But treat it as fragile: unlike the ATR trail (a plateau), this is a spike with a trough beside it — +/-0.25 costs ~0.4 Sharpe in-sample and ~1.6 out-of-sample — and the curve is bimodal (floor-off is good, 1.2-1.75 is bad, 2.0 is good). The hand-set value landed on the peak by luck, not by tuning. Do not nudge it. Worth knowing: turning the floor OFF entirely is the second-best row in both windows, with substantially higher CAGR (58.5% / 66.2%) and more trades. If CAGR ever outranks Sharpe here, "no R:R floor" is a live option — and it would sever the gate's last dependency on the weak S/R detector. Also fixes a metric artifact in the holdout harness. The train book's equity curve ran to the end of the data while its entries stopped at the split, so it sat in flat cash for two years and deflated its own CAGR/Sharpe (reported 0.95 / 14.6%; actually 1.31 / 29.6%). _simulate_portfolio now truncates the calendar to hold_days after the last entry when end_date is set — it only triggers on the holdout train window, so no other number moves. The clear-air OOS verdict is unaffected: it rests on the test row, whose entries and curve both start at the split and were always clean. Both holdout reports regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -429,6 +429,7 @@ Research-only flags, all off by default (the default report is byte-identical to
|
|||||||
| Flag | What it does |
|
| Flag | What it does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `BACKTEST_HOLDOUT_SPLIT=YYYY-MM-DD` | Adds a `holdout` section: train (entries before) vs test (entries on/after), as disjoint books |
|
| `BACKTEST_HOLDOUT_SPLIT=YYYY-MM-DD` | Adds a `holdout` section: train (entries before) vs test (entries on/after), as disjoint books |
|
||||||
|
| `BACKTEST_MIN_RR_SWEEP=1` | Sweeps the activation R:R floor against portfolio Sharpe. Combine with `BACKTEST_HOLDOUT_SPLIT` to sweep out-of-sample |
|
||||||
| `BACKTEST_RESEARCH_EXITS=1` | Adds the rejected take-profit exit rows to the exit comparison |
|
| `BACKTEST_RESEARCH_EXITS=1` | Adds the rejected take-profit exit rows to the exit comparison |
|
||||||
| `BACKTEST_ATR_TARGET_FALLBACK=k` | Synthesizes a k×ATR target where S/R offers none |
|
| `BACKTEST_ATR_TARGET_FALLBACK=k` | Synthesizes a k×ATR target where S/R offers none |
|
||||||
| `BACKTEST_FALLBACK_CLEAR_AIR_ONLY=1` | Restricts that fallback to setups with genuinely no structure ahead |
|
| `BACKTEST_FALLBACK_CLEAR_AIR_ONLY=1` | Restricts that fallback to setups with genuinely no structure ahead |
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ held neutral here — this calibrates the price/S-R machinery only.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import bisect
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
@@ -58,6 +59,7 @@ from app.services.outcome_service import (
|
|||||||
from app.services.price_service import query_ohlcv
|
from app.services.price_service import query_ohlcv
|
||||||
from app.services.qualification import (
|
from app.services.qualification import (
|
||||||
HIGH_CONVICTION_ACTIONS,
|
HIGH_CONVICTION_ACTIONS,
|
||||||
|
MIN_TARGET_PROBABILITY,
|
||||||
_action_direction,
|
_action_direction,
|
||||||
best_target_probability,
|
best_target_probability,
|
||||||
setup_qualifies,
|
setup_qualifies,
|
||||||
@@ -1218,6 +1220,18 @@ def _simulate_portfolio(
|
|||||||
if not calendar:
|
if not calendar:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if end_ord is not None:
|
||||||
|
# Holdout train book: entries stop at the split, but the calendar would
|
||||||
|
# otherwise still run to the last bar in the data — leaving the book in
|
||||||
|
# flat cash for the whole test period and deflating CAGR/Sharpe into
|
||||||
|
# something that looks like a result and isn't. Every open position
|
||||||
|
# resolves within `hold_days` bars of the last entry, so cut there.
|
||||||
|
last_entry_ord = max(entries_by_ord)
|
||||||
|
cut = bisect.bisect_left(calendar, last_entry_ord) + hold_days + 1
|
||||||
|
calendar = calendar[:cut]
|
||||||
|
if not calendar:
|
||||||
|
return None
|
||||||
|
|
||||||
cash = SIM_STARTING_CAPITAL
|
cash = SIM_STARTING_CAPITAL
|
||||||
positions: dict[str, dict] = {}
|
positions: dict[str, dict] = {}
|
||||||
curve: list[tuple[int, float]] = []
|
curve: list[tuple[int, float]] = []
|
||||||
@@ -1944,6 +1958,145 @@ def _lookback_start(max_ord: int | None, days: int | None) -> date | None:
|
|||||||
return date.fromordinal(max_ord - days)
|
return date.fromordinal(max_ord - days)
|
||||||
|
|
||||||
|
|
||||||
|
# The R:R floor is the last un-swept knob in the live gate: `gate_ablation` shows
|
||||||
|
# removing it halves expectancy, but the *level* (prod: 2.0) was hand-set in Admin
|
||||||
|
# and never tuned. Sweep it against portfolio Sharpe, not per-setup expectancy.
|
||||||
|
MIN_RR_SWEEP_VALUES: tuple[float, ...] = (0.0, 1.2, 1.5, 1.75, 2.0, 2.25, 2.5, 3.0, 4.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _min_rr_sweep_enabled() -> bool:
|
||||||
|
return os.getenv("BACKTEST_MIN_RR_SWEEP", "").strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def _meets_core_at(cand: dict, activation: dict, min_rr: float) -> bool:
|
||||||
|
"""Recompute the live gate's ``meets_core`` for a candidate at a different
|
||||||
|
R:R floor, from stored fields, mirroring ``qualification.setup_qualifies``.
|
||||||
|
|
||||||
|
The live-R:R freshness check is skipped (it needs a current price, which
|
||||||
|
historical candidates don't carry) and the momentum percentile is applied
|
||||||
|
separately — exactly as ``core_config`` does when meets_core is first built.
|
||||||
|
"""
|
||||||
|
if cand["rr"] < min_rr:
|
||||||
|
return False
|
||||||
|
primary_prob = cand.get("primary_prob")
|
||||||
|
if primary_prob is None or float(primary_prob) < MIN_TARGET_PROBABILITY:
|
||||||
|
return False
|
||||||
|
if (cand["confidence"] or 0.0) < float(activation.get("min_confidence", 0.0)):
|
||||||
|
return False
|
||||||
|
if activation.get("exclude_neutral"):
|
||||||
|
action_direction = _action_direction(cand.get("action"))
|
||||||
|
if action_direction == "neutral" or action_direction != cand["direction"]:
|
||||||
|
return False
|
||||||
|
if activation.get("require_high_conviction") and (
|
||||||
|
(cand.get("action") or "") not in HIGH_CONVICTION_ACTIONS
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
if activation.get("exclude_conflicts") and (cand.get("risk_level") or "") != "Low":
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _min_rr_sweep(
|
||||||
|
candidates: list[dict],
|
||||||
|
prices: dict[str, tuple],
|
||||||
|
_spy_closes: dict[date, float] | None,
|
||||||
|
activation: dict,
|
||||||
|
threshold: float,
|
||||||
|
hold_days: int,
|
||||||
|
live_exit_policy: dict | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Portfolio economics of the production book at each R:R floor.
|
||||||
|
|
||||||
|
Graded on Sharpe/CAGR/DD under the real exit — not per-setup expectancy —
|
||||||
|
because that is the metric every other promotion decision used.
|
||||||
|
"""
|
||||||
|
strategy = next((s for s in PORTFOLIO_MONITOR_STRATEGIES if s.get("is_production")), None)
|
||||||
|
if strategy is None:
|
||||||
|
return {}
|
||||||
|
entry_cfg = _entry_variant_config(str(strategy["entry_variant"]))
|
||||||
|
if entry_cfg is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
exit_policy = str(strategy["exit_policy"])
|
||||||
|
row_hold_days = hold_days
|
||||||
|
trail_multiplier = ATR_TRAIL_MULTIPLIER
|
||||||
|
if strategy.get("use_live_config") and live_exit_policy is not None:
|
||||||
|
exit_policy = LIVE_EXIT_MODE_TO_SIM.get(
|
||||||
|
str(live_exit_policy.get("mode", "atr_trailing")), "atr_trail3"
|
||||||
|
)
|
||||||
|
row_hold_days = int(live_exit_policy.get("hold_days", hold_days))
|
||||||
|
trail_multiplier = float(live_exit_policy.get("atr_multiplier", ATR_TRAIL_MULTIPLIER))
|
||||||
|
|
||||||
|
live_min_rr = float(activation.get("min_rr", 0.0))
|
||||||
|
live_qualified = sum(1 for c in candidates if c.get("qualified"))
|
||||||
|
|
||||||
|
# When a holdout split is set, sweep on the TEST window only. A threshold
|
||||||
|
# picked off a full-history curve is fitted to that curve; the only way to
|
||||||
|
# know whether the peak is real is to look for it in data the choice never saw.
|
||||||
|
sweep_start = _holdout_split()
|
||||||
|
|
||||||
|
rows: list[dict] = []
|
||||||
|
for min_rr in MIN_RR_SWEEP_VALUES:
|
||||||
|
def qualified_fn(c: dict, min_rr: float = min_rr) -> bool:
|
||||||
|
if not _meets_core_at(c, activation, min_rr):
|
||||||
|
return False
|
||||||
|
if threshold <= 0:
|
||||||
|
return True
|
||||||
|
if c["direction"] == "short":
|
||||||
|
return False
|
||||||
|
mp = c.get(PRODUCTION_PERCENTILE_KEY)
|
||||||
|
return mp is not None and mp >= threshold
|
||||||
|
|
||||||
|
n_qualified = sum(1 for c in candidates if qualified_fn(c))
|
||||||
|
sim = _simulate_portfolio(
|
||||||
|
candidates,
|
||||||
|
prices,
|
||||||
|
_spy_closes,
|
||||||
|
exit_policy,
|
||||||
|
row_hold_days,
|
||||||
|
qualified_fn=qualified_fn,
|
||||||
|
ranking_key=str(entry_cfg.get("ranking_key") or entry_cfg["percentile_key"]),
|
||||||
|
max_positions=int(entry_cfg["max_positions"]),
|
||||||
|
risk_per_trade=float(entry_cfg["risk_per_trade"]),
|
||||||
|
atr_trail_multiplier=trail_multiplier,
|
||||||
|
start_date=sweep_start,
|
||||||
|
)
|
||||||
|
if sim is None:
|
||||||
|
continue
|
||||||
|
sim.pop("equity_curve", None)
|
||||||
|
sim.pop("benchmark_curve", None)
|
||||||
|
rows.append({
|
||||||
|
"min_rr": min_rr,
|
||||||
|
"is_live": abs(min_rr - live_min_rr) < 1e-9,
|
||||||
|
"qualified_setups": n_qualified,
|
||||||
|
**sim,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Parity self-check: at the live floor the reconstructed gate must reproduce
|
||||||
|
# the production qualified set exactly. If it doesn't, the sweep is measuring
|
||||||
|
# some other gate and every row below is worthless.
|
||||||
|
live_row = next((r for r in rows if r["is_live"]), None)
|
||||||
|
reproduces = live_row is not None and live_row["qualified_setups"] == live_qualified
|
||||||
|
|
||||||
|
return {
|
||||||
|
"live_min_rr": live_min_rr,
|
||||||
|
"live_qualified_setups": live_qualified,
|
||||||
|
"reproduces_production_gate": reproduces,
|
||||||
|
"exit_policy": exit_policy,
|
||||||
|
"entries_from": sweep_start.isoformat() if sweep_start else None,
|
||||||
|
"window": "out-of-sample (test)" if sweep_start else "full history (in-sample)",
|
||||||
|
"rows": rows,
|
||||||
|
"note": (
|
||||||
|
"Portfolio economics of the production book at each activation R:R floor "
|
||||||
|
"(all other gate floors held at their live values). `reproduces_production_gate` "
|
||||||
|
"must be true: the row at the live floor has to rebuild the exact qualified set, "
|
||||||
|
"otherwise the sweep is grading a gate we don't run. Set BACKTEST_HOLDOUT_SPLIT "
|
||||||
|
"to sweep on the held-out window instead — a threshold read off the full-history "
|
||||||
|
"curve is fitted to it."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _holdout_split() -> date | None:
|
def _holdout_split() -> date | None:
|
||||||
"""Train/test split date for out-of-sample validation, e.g.
|
"""Train/test split date for out-of-sample validation, e.g.
|
||||||
BACKTEST_HOLDOUT_SPLIT=2024-07-01. Off by default."""
|
BACKTEST_HOLDOUT_SPLIT=2024-07-01. Off by default."""
|
||||||
@@ -2625,6 +2778,7 @@ async def run_backtest(
|
|||||||
exit_policy_rows: list[dict] = []
|
exit_policy_rows: list[dict] = []
|
||||||
portfolio_monitor_report: dict | None = None
|
portfolio_monitor_report: dict | None = None
|
||||||
holdout_report: dict | None = None
|
holdout_report: dict | None = None
|
||||||
|
min_rr_sweep_report: dict | None = None
|
||||||
try:
|
try:
|
||||||
qual_symbols = sorted({
|
qual_symbols = sorted({
|
||||||
c["symbol"]
|
c["symbol"]
|
||||||
@@ -2679,6 +2833,11 @@ async def run_backtest(
|
|||||||
candidates, price_columns, spy_closes, hold_horizon, split,
|
candidates, price_columns, spy_closes, hold_horizon, split,
|
||||||
live_exit_policy=live_exit_policy,
|
live_exit_policy=live_exit_policy,
|
||||||
)
|
)
|
||||||
|
if _min_rr_sweep_enabled():
|
||||||
|
min_rr_sweep_report = _min_rr_sweep(
|
||||||
|
candidates, price_columns, spy_closes, activation, current_min_pct,
|
||||||
|
hold_horizon, live_exit_policy=live_exit_policy,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Portfolio simulation failed")
|
logger.exception("Portfolio simulation failed")
|
||||||
|
|
||||||
@@ -2754,6 +2913,7 @@ async def run_backtest(
|
|||||||
},
|
},
|
||||||
"portfolio_monitor": portfolio_monitor_report,
|
"portfolio_monitor": portfolio_monitor_report,
|
||||||
"holdout": holdout_report,
|
"holdout": holdout_report,
|
||||||
|
"min_rr_sweep": min_rr_sweep_report,
|
||||||
"signal_eval": _signal_evaluation(collected),
|
"signal_eval": _signal_evaluation(collected),
|
||||||
"signal_eval_note": (
|
"signal_eval_note": (
|
||||||
"Cross-sectional rank-IC of price-only signals vs the forward "
|
"Cross-sectional rank-IC of price-only signals vs the forward "
|
||||||
|
|||||||
@@ -58,6 +58,42 @@ invites overfitting.
|
|||||||
| Position sizing (equal-weight, inverse-vol, risk-% sweep) | **Keep 1% fixed-fractional** |
|
| Position sizing (equal-weight, inverse-vol, risk-% sweep) | **Keep 1% fixed-fractional** |
|
||||||
| Primary-target probability floor | **Keep 20%** — pruned lottery targets, 1,428 → 1,089 qualified, lifted Sharpe |
|
| Primary-target probability floor | **Keep 20%** — pruned lottery targets, 1,428 → 1,089 qualified, lifted Sharpe |
|
||||||
| Exit policy (hold / SMA50 / 20-day low / technical-40 / ATR trail) | **Keep 3× ATR trail** — best Sharpe (2.04) |
|
| Exit policy (hold / SMA50 / 20-day low / technical-40 / ATR trail) | **Keep 3× ATR trail** — best Sharpe (2.04) |
|
||||||
|
| **Activation R:R floor `min_rr`** (swept 2026-07-12) | **Keep 2.0** — best in-sample *and* out-of-sample. But it is a **spike, not a plateau** — see below |
|
||||||
|
|
||||||
|
### The `min_rr` sweep (2026-07-12)
|
||||||
|
|
||||||
|
`min_rr = 2.0` had been hand-set in Admin and **never swept** — the gate ablation only
|
||||||
|
tested the floor *on vs off*, never its level. Swept against portfolio Sharpe under the
|
||||||
|
real exit, with a parity self-check (`reproduces_production_gate: true` — the row at 2.0
|
||||||
|
rebuilds production's exact 1,089-setup qualified set).
|
||||||
|
|
||||||
|
Reports: `backtest-20260712-min-rr-sweep.json` (in-sample), `-oos.json` (test window only).
|
||||||
|
|
||||||
|
| min_rr | qualified | In-sample Sharpe / CAGR | **OOS** Sharpe / CAGR (entries ≥ 2024-07) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0.0 (floor off) | 6636 | 1.98 / 58.5% | 2.02 / 66.2% |
|
||||||
|
| 1.2 (code default) | 3897 | 1.34 / 33.9% | 1.12 / 28.8% |
|
||||||
|
| 1.5 | 3127 | 1.20 / 29.6% | 1.12 / 28.8% |
|
||||||
|
| 1.75 | 1974 | 1.64 / 44.5% | 1.15 / 27.4% |
|
||||||
|
| **2.0 (live)** | 1089 | **2.04 / 50.4%** | **2.78 / 73.3%** |
|
||||||
|
| 2.25 | 577 | 1.64 / 31.8% | 1.71 / 31.9% |
|
||||||
|
| 2.5 | 286 | 1.67 / 29.0% | 0.68 / 8.7% |
|
||||||
|
| 3.0 | 89 | 1.09 / 9.1% | 0.87 / 5.0% |
|
||||||
|
|
||||||
|
**Verdict: keep 2.0.** It is the optimum in **both** windows, and the peak reproducing in
|
||||||
|
data it was never fitted to is real evidence — the one thing the clear-air experiment
|
||||||
|
couldn't show.
|
||||||
|
|
||||||
|
**But treat it as fragile, and do not nudge it.** Unlike the ATR trail (a plateau above
|
||||||
|
2.5), this is a **spike with a trough beside it**: ±0.25 costs ~0.4 Sharpe in-sample and
|
||||||
|
~1.6 Sharpe out-of-sample. A knob that sharp is not a robustly identified parameter, and
|
||||||
|
the curve is *bimodal* (floor-off is good, 1.2–1.75 is bad, 2.0 is good) — which is not
|
||||||
|
how a well-behaved threshold behaves. We got lucky: the hand-set value landed on the peak.
|
||||||
|
|
||||||
|
**Also worth knowing:** turning the floor **off entirely** is the second-best row in both
|
||||||
|
windows — nearly the same Sharpe with **substantially higher CAGR** (58.5% / 66.2%) and
|
||||||
|
more trades. If CAGR ever matters more than Sharpe here, "no R:R floor" is a live option,
|
||||||
|
and it would also sever the last dependency the *gate* has on the weak S/R detector.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -283,17 +283,25 @@ Real split (`BACKTEST_HOLDOUT_SPLIT=2024-07-01`, production strategy, disjoint b
|
|||||||
Reports: `reports/backtest-20260712-holdout-control.json`,
|
Reports: `reports/backtest-20260712-holdout-control.json`,
|
||||||
`reports/backtest-20260712-holdout-clearair.json`
|
`reports/backtest-20260712-holdout-clearair.json`
|
||||||
|
|
||||||
| window | arm | Sharpe | CAGR | MaxDD | Calmar | trades |
|
| window | arm | Sharpe | CAGR | MaxDD | trades |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| train | control | 0.95 | 14.6% | 21.4% | 0.68 | 174 |
|
| train (2022-06 → 2024-08) | control | 1.31 | 29.6% | 21.4% | 174 |
|
||||||
| train | **clear-air** | **1.18** | **20.5%** | **20.1%** | **1.02** | 191 |
|
| train | **clear-air** | **1.63** | **42.6%** | **20.1%** | 191 |
|
||||||
| **test** | **control** | **2.78** | 73.3% | **11.7%** | **6.26** | 150 |
|
| **test** (2024-07 → 2026-07) | **control** | **2.78** | 73.3% | **11.7%** | 150 |
|
||||||
| **test** | clear-air | 2.45 | **83.0%** | 14.3% | 5.80 | 176 |
|
| **test** | clear-air | 2.45 | **83.0%** | 14.3% | 176 |
|
||||||
|
|
||||||
|
> **Harness bug, found and fixed 2026-07-12.** The train row first reported Sharpe 0.95 /
|
||||||
|
> CAGR 14.6% — wrong. Its equity curve ran to the *end of the data* while its entries
|
||||||
|
> stopped at the split, so the book sat in flat cash for two years and deflated its own
|
||||||
|
> metrics. `_simulate_portfolio` now truncates the calendar to `hold_days` after the last
|
||||||
|
> entry whenever `end_date` is set. **The verdict is unaffected** — it rests on the test
|
||||||
|
> row, whose entries and curve both start at the split and were always clean. But the
|
||||||
|
> broken numbers *looked* like a result, and nearly produced a false conclusion ("the
|
||||||
|
> first half of the sample was mediocre"). Corrected numbers above.
|
||||||
|
|
||||||
**In train the clear-air rule wins on every metric. Out of sample it does not.** On
|
**In train the clear-air rule wins on every metric. Out of sample it does not.** On
|
||||||
the held-out two years it delivers **more raw return (+9.7pp CAGR)** but at
|
the held-out two years it delivers **more raw return (+9.7pp CAGR)** but at
|
||||||
**lower Sharpe (2.78 → 2.45), higher drawdown (11.7% → 14.3%) and lower Calmar
|
**lower Sharpe (2.78 → 2.45)** and **higher drawdown (11.7% → 14.3%)**.
|
||||||
(6.26 → 5.80)**.
|
|
||||||
|
|
||||||
So the §4b headline — *"strictly better on all three metrics"* — was **an in-sample
|
So the §4b headline — *"strictly better on all three metrics"* — was **an in-sample
|
||||||
artifact.** Out of sample the rule is not a free win; it is a **risk/return trade**:
|
artifact.** Out of sample the rule is not a free win; it is a **risk/return trade**:
|
||||||
@@ -306,8 +314,8 @@ was accepted on Sharpe 1.51 → 2.00). By that standard the honest read of the o
|
|||||||
uncontaminated evidence is *no improvement*.
|
uncontaminated evidence is *no improvement*.
|
||||||
|
|
||||||
Notes for anyone revisiting:
|
Notes for anyone revisiting:
|
||||||
- Both arms show a large regime shift (train Sharpe ~1, test Sharpe ~2.5–2.8) — the
|
- Both arms show a large regime shift (train Sharpe ~1.3–1.6, test Sharpe ~2.5–2.8) —
|
||||||
test window was simply a much better market. That is why *relative* comparison
|
the test window was simply a much better market. That is why *relative* comparison
|
||||||
within a window is the only valid read.
|
within a window is the only valid read.
|
||||||
- n = 150/176 in test is decent but not large; the Sharpe gap (0.33) is not
|
- n = 150/176 in test is decent but not large; the Sharpe gap (0.33) is not
|
||||||
overwhelming. This is "not confirmed," not "definitively refuted."
|
overwhelming. This is "not confirmed," not "definitively refuted."
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user