Event-Driven Backtest Engine
A shipped, correctness-first event-driven backtesting engine in TypeScript: deterministic replay, explicit execution assumptions, testable boundaries.
Event-Driven Backtest Engine
ShippedTL;DR
A minimal, event-driven backtesting engine in TypeScript for single-instrument OHLC bars, designed to make execution assumptions explicit (spread crossing, fees, slippage) and keep runs deterministic for debugging and testing.
Why this project
A lot of backtests are written as vectorized notebooks. They’re great for iteration speed, but they often blur important mechanics:
- the order lifecycle (new → live → partial fill → filled/canceled)
- the difference between a signal and an executable order
- execution assumptions (spread crossing, participation, fees)
- reproducibility: “why did this run change?”
This engine models the pipeline the way real trading systems think about it: events in, decisions out, fills in, risk/PnL out.
Design goals
- Deterministic replay: same inputs + config ⇒ same fills/PnL.
- Clear interfaces between modules (easy to test in isolation).
- Explicit execution assumptions (no hidden “magic fills”).
- Correctness first, then optimize hot paths.
Scope (v1)
- Market data: OHLC bars (
open/high/low/close, optional volume) - Universe: single instrument (no multi-symbol coordination yet)
- Execution models:
- Spread-crossing as the default realism baseline
- Mid-fill as a debugging baseline (sanity checks)
Non-goals (for now)
- ultra low-latency / HFT-grade performance
- corporate actions/dividends
- multi-asset portfolio optimization
- distributed simulation
Core architecture
DataFeed → Strategy → Broker/ExecutionModel → Portfolio → Metrics
- DataFeed emits bar events (OHLC).
- Strategy consumes events and emits intents (signals).
- Broker turns intents into orders and uses an ExecutionModel to simulate fills.
- Portfolio updates position, cash, and realized/unrealized PnL.
- Metrics computes summary stats (returns, drawdown, turnover, etc.).
API sketch (boundaries)
These are the kinds of interfaces I’m aiming for (subject to change):
export type Bar = {
ts: number; // unix ms
open: number;
high: number;
low: number;
close: number;
volume?: number;
};
export type Signal = {
ts: number;
side: "buy" | "sell";
qty: number;
reason?: string;
};
export interface Strategy {
onBar(bar: Bar): Signal[];
}
export interface ExecutionModel {
fill(order: Order, bar: Bar): Fill[];
}
Correctness & testing philosophy
The most important output of this project (for a quant dev role) is not “a high Sharpe backtest”—it’s trustworthy infrastructure.
Planned testing strategy:
- Golden tests for deterministic runs (same input → same fills/PnL)
- Invariant tests (accounting should always balance)
- position updates match fills
- cash changes match fills + fees
- fills never exceed order quantity
- Fixture-based bar streams (small synthetic sequences that isolate edge cases)
Execution assumptions (made explicit)
The point is to separate “strategy edge” from “fill model fantasy”.
Mid-fill (debug baseline)
- buy/sell fill at the bar midpoint (e.g.
(high + low)/2or a configurable proxy) - useful for validating plumbing and isolating execution sensitivity
Spread-crossing (default)
With only OHLC data you don’t have true bid/ask, so the model makes a transparent approximation:
- Buy fills at
close + halfSpread - Sell fills at
close - halfSpread
Where halfSpread is configurable (fixed, bps, or regime-based later). The key is that the assumption is explicit and can be stress-tested.
Roadmap
This repo is usable end-to-end (example + tests). Next improvements:
- [ ] Add order lifecycle (partial fills, cancels)
- [ ] Add additional execution models (fees presets, slippage)
- [ ] Add metrics/reporting summary
- [ ] Add 1–2 reference strategies
- [ ] Expand documentation with examples
If you want to view the implementation, see the GitHub repo linked on this page.