Backtesting & replay

Replay a market’s real recorded history through a WebSocket with a test key, trade into it with simulated orders, and get a P&L summary — with zero real-money surface.

Replay streams a market’s actual recorded history — the trade tape, the quote at each trade, and the price ticks — over a dedicated WebSocket, paced at a speed you choose. You can place simulated orders into the stream and they fill against subsequent recorded prints, so a strategy can be validated against real markets before it ever touches money.

Replay and the sandbox scenario injector are sandbox products. They accept a drzl_test_ key only — a live key is refused by design. Start at Get an API key and mint one with environment: "test".

What replay contains — and what it does not

This is the honest boundary, and every session opens with a coverage frame that restates it for your requested window:

FrameWhat it isBoundary
tradeRecorded prints — sequence number, price, size, valueThe lifetime record; never pruned. Anonymized exactly like the public tape: no user ids, no order ids.
bboBest bid / ask, reconstructed as midpoint ± quoted_spread/2Flagged derived. Exists only at trade instants — it is not a continuous quote. bid/ask are null where only one side of the book was quoted.
tickProbability and price per outcomeEvery 60 seconds, retained 30 days. A window starting before that says so in coverage.ticks.partial_before.

Two things are deliberately not offered rather than approximated:

  • No order-book (L2) ladder replay. No book-ladder history is recorded anywhere on the platform, so replaying one would mean fabricating book states.
  • No deep-history market-by-order (MBO) replay. The trading event log is archived and then deleted, so an MBO stream over old history would have gaps it could not honestly declare.

Every frame carries replay: true, and the replay envelope deliberately omits the live stream’s sequence and channel keys — replayed history can never be mistaken for the live feed.

Connect

wss://staging.drazill.com/api/v1/ws/replay

The first frame authenticates. A live key is refused before a socket is worth opening, so the client checks the prefix itself:

Python
def auth_frame(api_key: str) -> dict[str, Any]:
"""The first frame. A live key is refused by design — fail before connecting."""
if not api_key.startswith(TEST_KEY_PREFIX):
raise ValueError(
"Replay is a sandbox product: use a drzl_test_ key. A live key is "
"closed with REPLAY_LIVE_KEY_REFUSED."
)
return {"type": "auth", "api_key": api_key}

Three gates apply, each with its own close reason so you know which one failed:

Reason codeMeaning
REPLAY_AUTH_FAILEDThe key is invalid, expired, or missing the websocket scope.
REPLAY_LIVE_KEY_REFUSEDIt is a live key. Replay is sandbox-only.
REPLAY_SANDBOX_DISABLEDDEVELOPER_SANDBOX_ENABLED is off on this environment.
REPLAY_SESSION_LIMITToo many concurrent replay sessions for the account.

Ask for a window

start is inclusive and end is exclusive, so two adjacent windows tile the tape exactly once with no duplicated print. Speeds are 1, 10, 60, or "max"; max streams as fast as backpressure allows, bounded by a per-session frame budget rather than a clock.

Python
def start_frame(
market_id: str,
start: str,
end: str,
*,
speed: Any = "max",
frames: Iterable[str] = ("trade", "bbo", "tick"),
) -> dict[str, Any]:
"""Ask for one market and window. `start` is inclusive, `end` is exclusive."""
return {
"type": "start",
"market_id": market_id,
"start": start,
"end": end,
"speed": speed,
"frames": list(frames),
}

Then {"type": "pause"}, {"type": "resume"}, {"type": "seek", "to": "<ISO-8601 instant inside the window>"} and {"type": "stop"} control playback. replay_progress heartbeats report how far along a long window is.

Trade into the replay

An order control frame places a simulated order into the session.

Python
def order_frame(
side: str, price: str, size: str, *, outcome_id: str | None = None, tif: str = "GTC"
) -> dict[str, Any]:
"""A SIMULATED order. It fills against recorded prints and persists nothing."""
frame: dict[str, Any] = {"type": "order", "side": side, "price": price, "size": size}
if outcome_id:
frame["outcome_id"] = outcome_id
frame["tif"] = tif
return frame

The fill model, stated plainly

  • A resting BUY fills when a recorded print occurs at or below its limit; a SELL mirrors that.
  • Fill size is capped by the recorded print’s own size. That is deliberately conservative: the tape is history, and a simulated order cannot change what actually printed.
  • Partial fills accumulate across subsequent prints until the order is complete or the session ends.
  • There is no queue-position modelling and no market impact. The same assumptions are returned inside the session’s own P&L summary, not just here.
  • tif is IOC (one look at the next print, then expire) or GTC (rests for the remainder of the session).

Fills arrive as replay_fill frames carrying synthetic: true and livemode: false. Nothing is persisted — a replay session is ephemeral by design, so no paper or real balance moves and no webhook fires. On stop (or when the window is exhausted) a replay_complete frame carries the session P&L.

Run the whole thing

The tested quickstart lives at sdks/python/examples/replay_quickstart.py, with a matching notebook (replay_quickstart.ipynb) whose cells import from it.

Python
async with websockets.connect(WS_URL) as socket:
await socket.send(json.dumps(auth_frame(api_key)))
await socket.send(json.dumps(start_frame(market_id, start, end, speed=REPLAY_SPEED)))
await socket.send(json.dumps(order_frame("BUY", LIMIT_PRICE, ORDER_SIZE)))

Requires pip install drazill[websocket].

Where it is turned on

EnvironmentReplayWhy
Staging (wss://staging.drazill.com)OnStaging turns DEVELOPER_SANDBOX_ENABLED on automatically when the flag is not pinned.
Production (wss://api.drazill.com)OffProduction keeps the flag at its False default; arming it is an explicit owner action.

So the flow on this page works on staging today. A live key is refused in both environments regardless — that is the design, not a limitation of the rollout.

Next