refactor(runtime): shared event-driven backtest core + opt-in intraday tail model (I5a) - #33
Merged
Merged
Conversation
Extract the backtest ledger from BacktestDriver into a reusable BacktestEngine driven by an EventModel (schedule + pricing + feasibility). DailyCloseEventModel reproduces the accepted monthly close-to-close behaviour byte-for-byte; IntradayTailEventModel prices fills at 1min execution bars (next_minute_close) with exec-to-exec holding returns and explicit blocks (never a daily-close fallback). BacktestDriver is now a thin import-compatible wrapper around the engine + DailyCloseEventModel, so qt.pipeline / qt.oos_stability / qt.phase2_baseline are untouched. events.py: HoldingPeriod + the shared monthly rebalance schedule. engine.py: the single loop (universe -> scores -> constructor -> feasible fill via SimExecution/simulate_fills -> settle -> NAV/feasibility/holdings/ event logs). No fill/cash/settlement logic is duplicated.
Add an optional IntradayCfg (enabled/decision_time/data_lag/session_open/ execution_model/execution_window/require_cache_coverage/missing_execution) and constrain backtest.event_order to a Literal. A RootConfig validator requires intraday.enabled=true when event_order='intraday_tail_rebalance'; the daily default needs no intraday section, so every existing config validates unchanged. Unsupported execution models and malformed/ordered execution windows fail with readable errors.
New CLI run mode + qt/intraday_tail_framework.py: build the daily panel/ universe via the existing P4 cache, read required 1min bars from the EXISTING intraday cache only (a cache miss is a hard blocker, never a silent warm -> zero stk_mins live calls), score with the PIT-safe I3 intraday_ret_0930_1450 feature, and run the engine with the intraday tail event model. Writes an auditable report (event timestamps, exec-to-exec statement, blocked fills, achieved holdings, turnover/cost/cash, limitations). config/phase_i5a_intraday_tail_framework.yaml is a SH/SZ SSE50 smoke. Tests cover the daily golden (engine == driver), event-model units, intraday PIT (exec!=close-to-close; perturb post-cutoff bars -> feature unchanged; perturb exec bar -> returns move; perturb daily close -> intraday unchanged; missing bar blocks; holdings/turnover are achieved), and config validation.
Review fixes: - require_cache_coverage=true now fails loudly when ANY requested symbol is uncovered (previously it only failed when NONE were covered, silently dropping uncovered names and biasing the realized universe). Lenient mode (false) still drops + discloses, and an all-uncovered set is a clear blocker. Adds partial-coverage tests (required-fails / lenient-drops). - Make the holding period's exit execution anchor explicit (HoldingPeriod.exit_execution_ts) so audit timing no longer has to be inferred from the next period (and the final period shows a real exit anchor, not NaT). Engine event_log adds exit_execution_ts + next_execution_ts; the I5a report shows planned vs ACTUAL execution-bar time per period (a 14:52 fill when 14:51 is missing is now visible). Daily behaviour unchanged (phase0 ic 0.9600/annual 0.8408).
StackOverFlow11
added a commit
that referenced
this pull request
Jun 15, 2026
docs: record P-I5a event-driven backtest refactor (PR #33)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why — ARCHITECTURE / FRAMEWORK PR (not a research result)
This is an architecture/framework I5a PR. It refactors the backtest layer into
a shared event-driven core so that daily close-to-close and intraday tail
rebalance are two event models over one achieved-book ledger — no new
research alpha, no performance claim. The smoke score is a single PIT-safe I3
feature used solely to exercise the engine.
Motivation: the old daily
BacktestDriveris explicitly close-to-close and notminute-PIT-safe. Rather than bolt on a second, incompatible intraday backtest
stack, the fill/cash/settlement ledger is extracted once and shared.
Daily compatibility strategy & evidence
Strategy:
BacktestDriverbecomes a thin import-compatible wrapper around thenew
BacktestEngine+DailyCloseEventModel. Same constructor and public surface;no duplicated book/settlement logic.
qt/pipeline.py,qt/oos_stability.py,qt/phase2_baseline.pyare untouched.Evidence:
test_engine_daily_equals_driver_wrapper: engine(DailyCloseEventModel) NAV /feasibility_log / holdings_log are
assert_frame_equalto the legacy driver.test_backtest_driver,test_driver_feasibility,test_phase2_baseline,test_oos_stability).ic_mean=0.9600,annual_return=0.8408.pytest tests→ 518 passed (502 baseline + 16 new);ruff check .clean;all 14 configs validate.
Intraday tail event model (opt-in)
event_order: intraday_tail_rebalance+intraday.enabled: true(validated;daily configs validate unchanged).
available_time <= decision_time); execution atthe first valid 1min close in
[14:51, 14:56:59]; holding return isexec-to-exec, never close-to-close.
fallback. Turnover/cost/holdings are the achieved book after feasible fills.
run-phase-i5a-intradayCLI reads minute bars from the existing intradaycache only (a miss is a hard blocker → zero
stk_minslive calls).Real smoke (SSE50, 2026-03-03 → 2026-06-12)
periods=3, covered=58/58, stk_mins_live_calls=0, blocked_fills=0— report atartifacts/reports/phase_i5a_intraday_tail_framework.md(gitignored). Event tableshows 14:50 decision / 14:51 execution anchors; exec-to-exec NAV (not a perf claim).
Guarantees
No daily result drift · no duplicated fill/ledger semantics · missing minute bars
never fall back to daily close · no token leak · no raw non-1min fetch · no BJ
backfill / cache writer.
Test plan
pytest tests— 518 passedruff check .— cleanconfig/*.yamlic_mean=0.9600 / annual_return=0.8408Known limitations (disclosed in the report)
Execution-time feasibility is the minimum rule (missing/NaN blocks both); raw
stk_limitvs execution-minute close is deferred. ST/suspension not consulted at14:50 (suspended names have no minute bars → blocked anyway). Selection-level daily
limit flags disabled in the smoke config (EOD, not known at 14:50).