Critical gotchas, caveats, and institutional knowledge discovered during development. This is NOT documentation — it's a living log of things that bite you if you don't know them.
- Always use
uv run, notpythonorpytestdirectly — e.g.,uv run pytest tests/backtest/ -v - This project uses
uv, NOTpoetry(CLAUDE.md mentions poetry but that applies to a different project — OMSpy) - Python 3.13.5 is active on this machine
- 244 tests, all passing:
uv run pytest tests/backtest/ -v - Any PR must preserve this — zero regressions on
period="day"(default)
BarContext._trade_date= the period start date (first date of the period). Set by engine once per period. Exposed viactx.trade_dateproperty. Never changes within a period.ctx.date(new) = the actual date of the current tick. For single-day it equals_trade_date. For multi-day it changes at each day boundary.- Strategy code using
ctx.trade_datewill always get the period start — this is backward compatible.
len(period_dates) == 1→ simple ticks:"09:15:00"(unchanged, backward compatible)len(period_dates) > 1→ composite ticks:"2025-01-02 09:15:00"period=1(int) produces len-1 groups → same format asperiod="day"— this is intentional
- This is the single place that decides simple vs composite keys
- If
multi_day = len(period_dates) > 1, keys are prefixed with"YYYY-MM-DD " - Used in both
DayStartContext.prefetchandBarContext._lazy_fetch— must be kept in sync
- Currently both call
get_instrument_data(self.trade_date / self._trade_date, ...)— single date only - After multi-period change, both must loop over all
period_datesusing_fetch_and_merge - Critical: forgetting to update one of them means prefetch and lazy-fetch produce different key formats → cache miss on every tick after prefetch
- In
_run_period, the underlying is merged similarly:key = f"{d} {time_key}" if multi_day else time_key - This is done in the engine, not in
_fetch_and_merge(underlying is not an instrument)
- No existing tests reference
_run_dayby name — confirmed via grep. Safe to remove. _run_periodtakesperiod_dates: List[str], derivesmulti_day = len(period_dates) > 1
- For
period=2, a trade entered on day 1 can survive to day 2 and is force-closed at the last tick of day 2 _eod_force_closeandon_day_endare called once per_run_periodcall_reset_for_new_dayis also called once per_run_period— state machine resets once per period
- This exposes the period start date to all strategy hooks
- Not changed by the clock loop — intentional
tests/backtest/test_context.py'spopulated_cachehas keys like"09:15:00"- New
TestBarContextDateTimetests that test COMPOSITE tick parsing still use this fixture — that's fine becauseadvance()doesn't look up the cache - But if a test both uses composite ticks AND calls
get_price(), the cache won't have matching keys → always returns(None, -1)
DATES = ["2025-01-02", "2025-01-03"]— any date in DATES gets same underlying/option data- This is intentional for multi-day tests: simple to assert, no date-conditional logic needed
FourDayDataSourceintest_multiperiod.pyuses per-date different prices for realistic scenarios
- Uses a dict to accumulate groups (insertion-ordered in Python 3.7+)
- Dates are iterated in the order
get_available_dates()returns them — must be sorted
- The current
test_engine.pydoesn't have a standalonedsfixture at module level (it's class-scoped or inline) test_group_by_expirytakesdsas a parameter — must add@pytest.fixture def ds()totest_engine.py
- The
advance()method must setself._prev_tick = self.tickBEFORE overwritingself.tick - If reversed,
_prev_tickalways equalstickafter first call →changed()never returns True on first tick
- Tasks 1 & 2 (BarContext additions) must be done before Task 4 (engine uses period_dates in contexts)
- Task 3 (grouping functions) is independent and can be done before or after Tasks 1 & 2
- Task 5 (multiperiod tests) requires Tasks 1–4 all complete
- Use a git worktree for isolation:
uv run pytestfrom the worktree root - Main branch:
master
- The plan notes: "add
Unionto the existingfrom typing importline" before usingUnion[str, int]for theperiodparameter
period_dates: Optional[List[str]] = None— all existing call sites pass 3 positional args and still work
- Same:
period_dates: Optional[List[str]] = None— existing tests constructBarContext(cache, ds, date, clock)unmodified