Skip to content

How we verify it

A backtest is only worth what you can check about it. The engine exists twice — JavaScript (the CLI, the hosted queue, Node strategies) and Python (Python strategies, locally and in the sandbox) — and the archive decoder and the report exist twice — JavaScript (the CLI and the queue) and Python (outcometick.backtest). Two implementations of the same rules drift, silently, unless something compares them. These are the things that do.

Engine against engine

runner/conformance/ (in polymarket-backtest-js) runs both harnesses as real subprocesses over identical inputs and compares every trade and fill field: delayed orders, hold expiry, partial reductions, unusable orders, one-sided snapshots, best-bid/ask pruning, budget and log limits, session mode across overlapping markets, and the venue fee in each of its states. Expected values for the fee cases are worked by hand, not taken from either engine — agreement alone would pass two engines that are wrong the same way.

Decoder and report against decoder and report

verify/equivalence.test.mjs (in polymarket-backtest) decodes the same archive with both decoders and compares every market and every event. It then runs Python strategies through the command line (the Python harness fed by Node) and through outcometick.backtest (the same harness loop fed in-process) and requires every report field — except wall-clock timing — and every trade and fill row to match. Finally it runs the same strategy written in JavaScript and in Python and requires the same trades, fills and logs, byte for byte.

It runs on two synthetic archives built to hold what real days rarely do: a market crossing midnight, a market with no settlement ticks, an unresolved one, a Predict.fun tie, fees off and fees unreadable, a market on the 30-second TWAP, five kinds of event on one millisecond, deltas landing exactly on a cadence boundary, an empty file. With OT_SAMPLE_DIR pointing at the unpacked public samples it runs on those as well. It caught, on its first CI run, that Python's os.scandir returns files in filesystem order where Node's readdir sorts them.

verify/js-semantics.test.mjs checks the JavaScript number rules the Python decoder reproduces — Number(" 12 "), Number("0x10"), (0.125).toFixed(2), Math.round(0.49999999999999994), number-to-string past 1e21 — against JavaScript itself.

Your own data

verify/generic-equivalence.test.mjs does the same for data you bring (any Polymarket binary market): a synthetic dataset holding every frame form, each resolution source, unresolved markets valued three ways, one market refused per strictness rule, a window opening mid-stream, same-millisecond sequences across markets, a gzipped file, CRLF line ends and junk lines. Both decoders must produce the same markets, refusals, row counts and events; then the same strategies — orders by outcome label, resting quotes, session mode — run through the command line in JavaScript and in Python and through the Python library, and every trade, fill, report field and log line must match. The conformance suite adds generic markets to the engine-against-engine comparison: mirrored deltas, a bound in the same millisecond as the level it removes, trading ending before settlement, valued and unvalued positions. And on the real public sample, an archive day written back out as generic data (frames in the archive's own order) replays to the same fills, and to the same trades except the settlement millisecond.

Answers worked by hand

tests/test_backtest.py (Python repository) builds a one-market archive and checks the result against arithmetic done on paper: the fill price across two levels, the fee per level, the payout at settlement, a known-zero fee, a flat override, latency, the outcome hidden until settlement, and that a cached decode replays identically.

Run them

Python repository:

pip install .
python -m unittest discover -s tests
npm install --no-save outcometick@"$(python -c 'import json; print(json.load(open("verify/version.json"))["version"])')"
node --test 'verify/*.test.mjs'                        # needs Node 24 and python3
OT_SAMPLE_DIR=/path/to/samples node --test 'verify/*.test.mjs'   # also on the real sample days

JavaScript repository:

npm install && npm test
node --test runner/conformance/*.test.mjs              # needs python3

What is not proven here

  • That the archive is complete: it is not. Gaps in what was captured are published per day by the data product, and a backtest lists every market-day it could not replay.
  • That a fee estimated per book level equals what the venue charged per match: see methodology § 5.
  • That a strategy which worked on these days will work on others.