Fix bounded S/R training portfolio calendars
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user