Fix bounded S/R training portfolio calendars

This commit is contained in:
2026-07-12 22:45:05 +02:00
parent 4ca48ac590
commit cb64f7bf65
4 changed files with 52 additions and 4 deletions
+12 -3
View File
@@ -33,7 +33,7 @@ import statistics
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable from collections.abc import Callable
from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ProcessPoolExecutor
from datetime import date, datetime, timezone from datetime import date, datetime, timedelta, timezone
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any from typing import Any
@@ -1376,8 +1376,17 @@ def _simulate_portfolio(
qualified_fn = _default_qualified qualified_fn = _default_qualified
entries_by_ord: dict[int, list[dict]] = defaultdict(list) entries_by_ord: dict[int, list[dict]] = defaultdict(list)
start_ord = start_date.toordinal() if start_date is not None else None configured_start, configured_end = _backtest_entry_bounds()
end_ord = end_date.toordinal() if end_date is not None else None 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
for c in candidates: for c in candidates:
if not qualified_fn(c) or c.get("direction") != "long": if not qualified_fn(c) or c.get("direction") != "long":
continue continue
+7
View File
@@ -429,6 +429,13 @@ Choose one arm and record that lock before running exactly control and that arm:
Replace `confirmed_rounds` with the recorded winner. The validation command also Replace `confirmed_rounds` with the recorded winner. The validation command also
calls `scripts/compare_sr_variants.py` to produce the paired cohort CSV and JSON. calls `scripts/compare_sr_variants.py` to produce the paired cohort CSV and JSON.
> Validation result: `confirmed_rounds` is rejected and is no longer a lockable
> arm. It remains in the corrected training matrix only to preserve the causal
> experiment record. The first training reports used an entry end bound without
> forwarding it to the portfolio calendar, leaving each book in flat cash through
> the test period. The simulator now treats `BACKTEST_ENTRY_END` as an inclusive
> entry bound and truncates the calendar after the final position can resolve.
The post-2024 interval has informed earlier research, so this is validation rather The post-2024 interval has informed earlier research, so this is validation rather
than a pristine holdout; do not sweep variants on it. No deployment follows than a pristine holdout; do not sweep variants on it. No deployment follows
automatically. A lower validation Sharpe or higher drawdown remains a no-ship automatically. A lower validation Sharpe or higher drawdown remains a no-ship
+5 -1
View File
@@ -23,7 +23,11 @@ TRAINING_ARMS = (
"confirmed_rounds", "confirmed_rounds",
"gate_v2", "gate_v2",
) )
LOCKABLE_ARMS = TRAINING_ARMS[1:] # confirmed_rounds failed the post-2024 validation decisively and must not be
# selected again merely because a corrected training curve looks attractive.
LOCKABLE_ARMS = tuple(
arm for arm in TRAINING_ARMS[1:] if arm != "confirmed_rounds"
)
def _add_common(parser: argparse.ArgumentParser) -> None: def _add_common(parser: argparse.ArgumentParser) -> None:
+28
View File
@@ -604,6 +604,34 @@ class TestSimulatePortfolio:
def test_nothing_qualified_returns_none(self): def test_nothing_qualified_returns_none(self):
assert bt._simulate_portfolio([], {}, None, "hold", 30) is None assert bt._simulate_portfolio([], {}, None, "hold", 30) is None
def test_configured_entry_end_truncates_flat_calendar_tail(self, monkeypatch):
closes = [100.0 + i for i in range(100)]
prices = {"AAA": _sim_prices(self.ORD, closes)}
cand = _sim_cand("AAA", self.ORD, entry=100.0, stop=95.0, target=130.0)
monkeypatch.setenv(
"BACKTEST_ENTRY_END", date.fromordinal(self.ORD).isoformat()
)
sim = bt._simulate_portfolio([cand], prices, None, "hold", 3)
assert sim is not None
assert sim["end_date"] == date.fromordinal(self.ORD + 3).isoformat()
def test_configured_entry_start_aligns_book_calendar(self, monkeypatch):
closes = [100.0 + i for i in range(10)]
prices = {"AAA": _sim_prices(self.ORD, closes)}
cand = _sim_cand(
"AAA", self.ORD + 1, entry=101.0, stop=96.0, target=130.0
)
monkeypatch.setenv(
"BACKTEST_ENTRY_START", date.fromordinal(self.ORD).isoformat()
)
sim = bt._simulate_portfolio([cand], prices, None, "hold", 3)
assert sim is not None
assert sim["start_date"] == date.fromordinal(self.ORD).isoformat()
def test_bucket_stats_counts_and_expectancy(): def test_bucket_stats_counts_and_expectancy():
cands = [ cands = [