An event-driven backtesting framework for prediction-market strategies on venues such as Polymarket and Kalshi, written in strict TypeScript for Node.
Prediction-market shares are binary outcomes priced on the open interval
(0, 1) that settle to 0 or 1. The framework models that domain directly —
curved Polymarket taker fees, Kalshi expected-earnings fees, maker rebates, L2
order books, configurable execution latency, and long-biased position
accounting — and ships a small, dependency-light engine that replays market
data, matches orders, and produces a performance tearsheet.
- Event-driven engine — replays a time-ordered market-data stream, maintains a venue order book per instrument, and routes orders through a realistic matching engine (market & limit orders; IOC/FOK/GTC; reduce-only; book-walking taker fills; resting maker fills; bar/trade fallback fills).
- Venue fee models — Polymarket curved taker fee with category-aware maker rebates, and Kalshi proportional expected-earnings fee with fee-waiver support. All money math is decimal-precise.
- Built-in strategies — EMA crossover, mean reversion, breakout, RSI reversion, threshold momentum, and VWAP reversion, each with bar-driven and order-book-driven variants.
- Pluggable data sources — seeded synthetic generators, CSV loaders (bars, quotes, trades), and in-memory streams, with a helper to merge multiple feeds.
- Analysis & reporting — equity curve, returns, drawdown, annualized Sharpe/Sortino, Brier score & advantage, and trade statistics, rendered as a console tearsheet or written to JSON.
- CLI runner — discover and run backtests from the command line.
- Strict typing & tooling —
strictTypeScript, ESLint (flat config), Prettier, and Vitest, all green in CI.
- Node.js >= 20
npm install# List the bundled example backtests
npm run cli list # or: npx tsx src/cli.ts list
# Run one by name or index
npm run cli run synthetic_bar_mean_reversion
npm run cli run 2
# Run every example
npm run cli run -- --allExample output:
─────────────────────────────────────────
synthetic_bar_mean_reversion
─────────────────────────────────────────
Starting cash 100.00
Final equity 100.82
Total return 0.82 %
Realized PnL 0.99
Total commissions 0.17000
Max drawdown -1.20 %
Sharpe (ann.) 5.20
Fills 2
Events processed 1500
─────────────────────────────────────────
| Script | Description |
|---|---|
npm run build |
Compile src/ to dist/ (ESM + type declarations) |
npm run typecheck |
Type-check src/, test/, and backtests/ |
npm test |
Run the Vitest suite |
npm run test:watch |
Vitest in watch mode |
npm run lint / lint:fix |
Lint with ESLint |
npm run format / format:check |
Format with Prettier |
npm run cli |
Backtest runner CLI (pmbt) |
src/
core/ Domain model — enums, identifiers, decimal/money, the
BinaryOption instrument, market data (Bar/Trade/Quote/Book),
the L2 OrderBook, and order events
engine/ Backtest engine — Order/OrderFactory, Portfolio + Account,
MatchingEngine, FeeModel interface, LatencyModel, TestClock,
and the BacktestEngine conductor
strategy/ Strategy base class and LongOnlyPredictionMarketStrategy
(order plumbing, liquidity/balance-capped sizing, fee-adjusted
risk exits)
strategies/ Built-in strategies (bar + book variants)
adapters/ Venue specifics — polymarket/ and kalshi/ fee models, and
prediction-market order tags
data/ Data sources — synthetic generators, CSV loaders, stream merge,
and the platform/vendor/data-type taxonomy
analysis/ Tearsheet metrics and report formatting
backtesting/ High-level experiment layer — the Experiment manifest,
buildReplayExperiment / runExperiment, and execution &
market-data configuration
cli.ts Backtest runner CLI
index.ts Public API barrel
backtests/ Runnable example experiments (each exports `meta` and `run`)
test/ Vitest suites
- A
BacktestEngineis configured with an instrument, a fee model, a latency model, and one or more strategies. - A single time-ordered stream of market data is replayed. For each event the engine updates the venue order book and mark price, then dispatches it to the strategies subscribed to that data type.
- A strategy submits orders through its
OrderFactory; theMatchingEnginefills marketable orders against resting book liquidity (or the latest reference price when only bar/trade data is available), prices each fill through the fee model, and updates thePortfolio. - The engine records an equity curve and the full fill log, which the analysis layer condenses into a tearsheet.
Extend LongOnlyPredictionMarketStrategy, which provides the entry/exit
plumbing: liquidity- and balance-capped sizing, reduce-only exits with a
cooldown, and fee-adjusted take-profit / stop-loss handling.
import {
LongOnlyPredictionMarketStrategy,
BookType,
type StrategyConfig,
type OrderBook,
} from 'prediction-market-backtesting';
interface MyConfig extends StrategyConfig {
threshold: number;
}
class MyStrategy extends LongOnlyPredictionMarketStrategy<MyConfig> {
protected subscribe(): void {
this.subscribeOrderBookDeltas(this.config.instrumentId, BookType.L2_MBP);
}
override onOrderBook(book: OrderBook): void {
const ask = book.bestAskPrice();
if (ask !== null && ask < this.config.threshold && !this.inPosition()) {
this.submitEntry(ask, book.bestAskSize());
}
}
}import {
BinaryOption,
PUSD,
buildReplayExperiment,
runExperiment,
PolymarketFeeModel,
} from 'prediction-market-backtesting';
const experiment = buildReplayExperiment({
name: 'my_experiment',
description: 'demo',
currency: PUSD,
initialCash: 100,
instrument: new BinaryOption({ instrumentId: 'MKT-YES.POLYMARKET', takerFee: 0.007 }),
feeModel: new PolymarketFeeModel(),
strategies: [
(inst) => new MyStrategy({ instrumentId: inst.instrumentId, tradeSize: 5, threshold: 0.4 }),
],
data: {
platform: 'POLYMARKET',
dataType: 'BOOK',
vendor: 'CSV',
resolveStream: () => loadQuotesAsBook('data/market.csv', { instrumentId: ... }),
},
});
const { tearsheet } = runExperiment(experiment);Complete, runnable examples live in backtests/syntheticBookEmaCrossover.ts and backtests/syntheticBarMeanReversion.ts.
Three first-class stream sources are provided:
- Synthetic — seeded random-walk book and bar generators
(
src/data/synthetic.ts), so the framework runs end-to-end with no external data. - CSV — bars, top-of-book quotes (converted to book deltas), and trades
(
src/data/csv.ts). Timestamps may be ISO-8601 or numeric epoch values (ns / ms / s, auto-detected). - In-memory — pass any time-sorted
MarketData[]to an experiment'sresolveStream.
Combine multiple feeds into one ordered stream with mergeStreams(...).
- Polymarket — curved taker fee
fee = qty · rate · p · (1 − p), which peaks atp = 0.5and decays toward the price extremes, rounded to 5 decimal places. Eligible passive (limit) fills earn a maker rebate, modeled as a negative commission and inferred from the market category. - Kalshi — a proportional fee on expected earnings,
fee = ceil_to_cent(rate · qty · p · (1 − p)), with support for fee-waived markets.
Planned additions that build on the current engine:
- Additional strategies (microprice imbalance, binary pair arbitrage, late-favorite hold).
- Parameter optimization (grid / Bayesian search) over experiment configs.
- Chart export for the equity, drawdown, and allocation series.
- Live exchange data adapters for Polymarket and Kalshi.
This is research and educational software for backtesting trading strategies on historical or synthetic data. It is not financial advice and comes with no warranty. Backtested results do not predict live performance.
MIT — see LICENSE.