Skip to content
Denzell Twerdohlib.dev

Event-Driven Backtest Engine

A shipped, correctness-first event-driven backtesting engine in TypeScript: deterministic replay, explicit execution assumptions, testable boundaries.

ShippedTypeScriptBacktestingArchitecture

Event-Driven Backtest Engine

Shipped

TL;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)/2 or 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.