Simulation API
The/v1/simulation surface is a live, key-gated historical fill and
backtest contract on production (https://api.polysimulator.com, since
2026-09-05). Without a key it returns 401, not 404 — the endpoints exist
and are serving.
Each API host has an operator switch for this surface
(FEATURE_SIMULATION_API_ENABLED, off unless the host sets it; production sets
it on). Where it is off, every /v1/simulation path answers 404 with
{"error": "NOT_FOUND"}, with or without a key. The staging host
(https://staging-api.polysimulator.com) runs pre-release code; it has the
switch on and the same archive as production since 2026-10-22. A 404
NOT_FOUND on every simulation call usually means POLYSIM_BASE_URL points at
another host.
First successful call
simulateFill is the hook: one condition, one timestamp, one memorable
VWAP. Do not start with a backtest.
Decimal, never float.
Measured on staging (2026-10-22): vwap_fill_price "0.4500", 10 shares,
fee "0.12375". A condition_id or hour that is not in the archive does
not return an error from /fill: it returns 200 with
vwap_fill_price: null, filled_shares: "0.0000" and
price_source: "missing". Check price_source before using the price.
Markets you can backtest
The archive is a curated sample, not the whole venue: hourly order-book snapshots of about 960 markets, between 2026-09-05 and 2026-09-08. There is no listing endpoint yet. Most markets in it hold a single archived hour (crypto up/down markets live for one hour), and a single-hour window cannot fill at the defaultlatency_ms (see below). These markets hold several
consecutive archived hours and were checked with GET /v1/simulation/coverage
(100%, universe_eligible: true) on 2026-10-22:
strategy.outcome must be one of the market’s own labels: "Yes" only fits
the Yes / No rows; for the others name a team or player. A backtest’s charge is
the window’s hour marks times the number of markets (14:00 to 21:00 is 8).
Latency (latency_ms)
POST /v1/simulation/backtests takes latency_ms, default 104: our
measured production API p50, in milliseconds. An order decided at a tick is
priced on the book as it stood latency_ms later, which is how a real order
would have reached the venue. Where the archive cannot reconstruct that
instant, the order fills on the first archived book at a later tick in the
window; with neither, it does not fill and the summary counts it under
execution.orders_refused as NO_BOOK_AFTER_LATENCY.
The archive is hourly, so in practice every order fills on the next hour’s
book, and a window with a single archived hour never fills at the default.
"latency_ms": 0 prices each order on the book its own decision saw; the
summary then reports execution.signals_used_pre_decision_data_only: false,
because no real order could have traded at that price. Results at 0 look
better than a live bot would do.
POST /v1/simulation/fill also takes latency_ms and prices the book at
at + latency_ms, but there it defaults to 0 (echoed in
leakage.latency_ms). Pass 104 to price a fill the way a backtest does.
The latency_ms model ships with the 2.0 engine that staging runs. Until 2.0
reaches https://api.polysimulator.com, production prices backtest orders on
their decision tick’s own book.
Paths
GET /v1/simulation/book resolves a bounded hour record from Redis or
an adjacent v4 sidecar. It never walks an unbound hours list.
Token identity
Fill and book responses echo the resolvedtoken_id and
comp_token_id. Backtest create accepts an optional token_ids map
(condition_id → token_id). The job stores the resolved map so clients
can verify the exact books that were walked. Exact asset_id matching
is preserved: primary / yes / no never match a real token.
Errors
Every simulation error is a structured/v1 envelope. Branch on
X-Polysim-Code, not the body text. Declared codes include:
INVALID_REQUEST,INVALID_WINDOW,INVALID_CURSORUPGRADE_REQUIRED,INSUFFICIENT_PERMISSION,NOT_FOUNDIDEMPOTENCY_KEY_REUSE,IDEMPOTENCY_CONFLICT_PENDINGUNIVERSE_EXCLUDEDBACKTEST_MONTHLY_CAP,BACKTEST_MARKET_CAP,BACKTEST_WINDOW_CAPSIMULATION_HOURS_EXCEEDEDMETERING_UNAVAILABLE,JOB_STORE_UNAVAILABLE(503)CONDITION_HOUR_NOT_COVERED,ARCHIVE_UNAVAILABLE,ARCHIVE_NOT_FOUND
503 plus those codes. A missing sidecar next to a
v4 capture object is fail-closed (quarantined), not a fabricated
valid hour.
Volume caps (matrix, not activation)
FEATURE_MATRIX gates volume, not the fill/coverage features:
analytics.backtesting remains false for free/pro. depth_walk_depletion
and signed exports still require dataset.historical_full. Do not treat
these numbers as a live commercial SKU.
A tier’s monthly budget always exceeds one maximum-size backtest
(max_days x 24 x max_markets) by at least 3x — otherwise the plan would
advertise a run it then refuses to meter. backend/tests/test_backtest_ budget_invariant.py asserts that for every tier. unlimited is a real
absence of a limit, not a large number.
SDK
wait() polls get_backtest until completed / failed / cancelled.
It does not enable the flag and does not talk to Telonex.