Replay Control
The Replay Control endpoints are the heart of TestMax’s backtesting engine. They let you start a historical replay session, step through bars one at a time (or in batches), control playback speed, jump to specific timestamps, and end the session to get final results.
Replay session lifecycle
A typical backtest session follows this flow:
- Start —
Replay/startinitializes a session with an instrument, date, and timeframe - Loop —
Replay/stepadvances bars one at a time; your strategy analyzes data and places orders - End —
Replay/endfinalizes the session and returns performance results
You can also use Replay/play for auto-playback, Replay/pause to stop auto-play, and Replay/jumpTo to skip to a specific point in time.
POST Replay/start
Initialize a new backtest replay session. This loads historical data for the specified instrument and date range and prepares the session for bar-by-bar replay.
URL: POST /api/topstep-sim/Replay/start
Authentication: Bearer token required.
Request body
{ "instrumentSymbol": "NQ", "startDate": "2025-01-15", "timeframe": "5m"}| Field | Type | Required | Description |
|---|---|---|---|
instrumentSymbol | string | Yes* | Instrument symbol (e.g., "NQ", "ES", "GC", "CL", "EURUSD") |
contractId | string | Yes* | Alternative to instrumentSymbol — a contract ID from Contract/search |
startDate | string | Yes | Session start date in YYYY-MM-DD format |
timeframe | string | No | Bar timeframe (default "1s") |
accountId | number | No | An existing prop-firm challenge account — the new session inherits its challenge rules and account size |
* Provide either instrumentSymbol or contractId.
Timeframe values
| Value | Description |
|---|---|
"1s" | 1-second bars (default) |
"5s" | 5-second bars |
"10s" | 10-second bars |
"15s" | 15-second bars |
"30s" | 30-second bars |
"1m" | 1-minute bars |
"5m" | 5-minute bars |
"15m" | 15-minute bars |
"30m" | 30-minute bars |
"1h" | 1-hour bars |
"4h" | 4-hour bars |
"1d" | Daily bars |
Bars are aggregated up from each instrument’s stored resolution, so a timeframe
is only available when it is an exact multiple of that resolution — "30s"
works on 10-second instruments such as Gold (GC), "15s" does not.
Response
{ "success": true, "errorCode": 0, "errorMessage": null, "accountId": 1000000001, "sessionId": "8b6f6a2e-...", "totalBars": 1440, "historyBars": 144, "instrument": { "name": "NQ", "contractId": "CON.F.US.ENQ.N26", "tickSize": 0.25, "tickValue": 5.00 }}| Field | Type | Description |
|---|---|---|
accountId | number | The account ID to use for orders and positions in this session |
sessionId | string | Internal session UUID (informational) |
totalBars | number | Total number of bars available in the session |
historyBars | number | Number of bars pre-loaded as chart history before the replay start |
instrument.name | string | Instrument symbol |
instrument.contractId | string | The resolved contract ID — use this as contractId in order endpoints |
instrument.tickSize | number | Minimum price increment |
instrument.tickValue | number | Dollar value per tick per contract |
The session starts with a $50,000 balance, unless it inherits a prop-firm challenge config via accountId (then the challenge’s account size is used).
Example
# Start a 5-minute replay session on NQresult = api("Replay/start", { "instrumentSymbol": "NQ", "startDate": "2025-01-15", "timeframe": "5m"})
if result["success"]: ACCOUNT_ID = result["accountId"] CONTRACT_ID = result["instrument"]["contractId"] TOTAL_BARS = result["totalBars"] print(f"Session started: {TOTAL_BARS} bars available") print(f"Account: {ACCOUNT_ID}, Contract: {CONTRACT_ID}")else: print(f"Failed to start: {result['errorMessage']}")curl -X POST https://api.test-max.com/api/topstep-sim/Replay/start \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "instrumentSymbol": "NQ", "startDate": "2025-01-15", "timeframe": "5m" }'Error responses
| Scenario | errorCode | errorMessage |
|---|---|---|
Neither instrumentSymbol nor contractId given | 1 | "instrumentSymbol or contractId required" |
| Unknown instrument | 1 | "Instrument not found" |
Missing startDate | 2 | "startDate required" |
| Session setup failed (e.g., no data for the date) | 7 | The failure reason |
POST Replay/step
Advance the replay by one or more bars. This is the primary way to move through historical data in your strategy’s main loop.
URL: POST /api/topstep-sim/Replay/step
Authentication: Bearer token required.
Request body
{ "accountId": 12345, "steps": 1}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
accountId | number | Yes | — | Account ID from Replay/start |
steps | number | No | 1 | Number of bars to advance (1–100 per call) |
Response
{ "success": true, "errorCode": 0, "errorMessage": null, "bars": [ { "t": "2025-01-15T14:30:00.000Z", "o": 21500.25, "h": 21505.50, "l": 21498.75, "c": 21503.00, "v": 150 } ], "barsProcessed": 1, "currentIndex": 42, "totalBars": 1440, "account": { "balance": 50000, "equity": 50120.5, "unrealizedPnl": 120.5, "marginUsed": 1500, "positions": 1, "pendingOrders": 0 }, "propFirm": null, "filledOrders": [], "violations": []}| Field | Type | Description |
|---|---|---|
bars | array | Array of bar objects, one per step. See Bar Data Format. |
bars[].t | string | ISO 8601 timestamp |
bars[].o / h / l / c | number | Open / high / low / close price |
bars[].v | number | Volume |
barsProcessed | number | How many bars actually advanced — 0 means the replay reached the end of the data |
currentIndex | number | Index of the next bar to be played |
totalBars | number | Total bars in the session |
account | object or null | Live account snapshot: balance, equity, unrealizedPnl, marginUsed, positions, pendingOrders |
propFirm | object or null | Prop-firm snapshot (status, isFunded, currentPhase, tradingDaysCount, drawdownLocked), or null for non-challenge sessions |
filledOrders | array | Orders filled during these bars: {orderId, fillPrice, fillQty} |
violations | array | Prop-firm violation reasons triggered during these bars (stepping halts on a violation or pass, and the session is finalized) |
Example
# Step through bars one at a time (typical strategy loop)for i in range(TOTAL_BARS): result = api("Replay/step", { "accountId": ACCOUNT_ID, "steps": 1 })
if not result["success"] or result["barsProcessed"] == 0: break
bar = result["bars"][0] close = bar["c"] timestamp = bar["t"]
# Your strategy logic here print(f"Bar {result['currentIndex']}/{result['totalBars']}: Close={close}")Stepping multiple bars at once
You can advance multiple bars in a single call. This is useful when you want to skip ahead quickly without processing each bar individually.
# Skip forward 10 barsresult = api("Replay/step", { "accountId": ACCOUNT_ID, "steps": 10})
# Process all returned barsfor bar in result["bars"]: # Each bar in the batch print(f"{bar['t']}: O={bar['o']} H={bar['h']} L={bar['l']} C={bar['c']}")POST Replay/jumpTo
Jump forward to a specific point in time within the replay session. Every bar between the current position and the target is processed instantly — pending orders fill and prop-firm rules are checked, you just don’t receive the bars in the response. Useful for skipping past the market open or fast-forwarding to a known event.
URL: POST /api/topstep-sim/Replay/jumpTo
Authentication: Bearer token required.
Request body
{ "accountId": 12345, "targetTime": "2025-01-15T15:00:00Z"}| Field | Type | Required | Description |
|---|---|---|---|
accountId | number | Yes | Account ID |
targetTime | string | Yes | ISO 8601 timestamp to jump to. Must be later than the current position — jumps are forward-only (a past target is a no-op with barsProcessed: 0). |
Response
{ "success": true, "errorCode": 0, "errorMessage": null, "barsProcessed": 138, "currentIndex": 180, "totalBars": 1440, "account": { "balance": 50000, "equity": 50000, "unrealizedPnl": 0, "marginUsed": 0, "positions": 0, "pendingOrders": 0 }, "propFirm": null, "filledOrders": [], "violations": []}Same response shape as Replay/step (minus bars): barsProcessed, currentIndex, totalBars, account, propFirm, filledOrders, and violations.
Example
# Skip to the US market open (9:30 AM ET = 14:30 UTC)result = api("Replay/jumpTo", { "accountId": ACCOUNT_ID, "targetTime": "2025-01-15T14:30:00Z"})
if result["success"]: print(f"Jumped to bar {result['currentIndex']}") for fill in result["filledOrders"]: print(f" Filled during jump: order {fill['orderId']} @ {fill['fillPrice']}")else: print(f"Jump failed: {result['errorMessage']}")POST Replay/play
Start auto-playing the replay at a specified speed multiplier. The session will advance automatically, simulating real-time market progression at the given speed.
URL: POST /api/topstep-sim/Replay/play
Authentication: Bearer token required.
Request body
{ "accountId": 12345, "speed": 10}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
accountId | number | Yes | — | Account ID |
speed | number | No | 1 | Playback speed multiplier (1 to 50). 1 = real-time, 50 = 50x faster. |
Response
{ "success": true, "errorCode": 0, "errorMessage": null, "playing": true, "speed": 10}During auto-play, bars are processed exactly like Replay/step — orders fill and prop-firm rules apply. Playback pauses automatically on a violation or pass.
Example
# Start auto-play at 10x speedresult = api("Replay/play", { "accountId": ACCOUNT_ID, "speed": 10})
print("Auto-play started at 10x speed")POST Replay/pause
Pause auto-playback. The session remains active and can be resumed with Replay/play or advanced manually with Replay/step.
URL: POST /api/topstep-sim/Replay/pause
Authentication: Bearer token required.
Request body
{ "accountId": 12345}| Field | Type | Required | Description |
|---|---|---|---|
accountId | number | Yes | Account ID |
Response
{ "success": true, "errorCode": 0, "errorMessage": null, "paused": true, "account": { "balance": 50000, "equity": 50000, "unrealizedPnl": 0, "marginUsed": 0, "positions": 0, "pendingOrders": 0 }, "propFirm": null}Example
# Pause auto-playresult = api("Replay/pause", {"accountId": ACCOUNT_ID})print(f"Paused — equity: {result['account']['equity']}")POST Replay/end
End the replay session. This is the last call in a backtest — it persists the final balance and tears down the session. The response carries the final account snapshot.
URL: POST /api/topstep-sim/Replay/end
Authentication: Bearer token required.
Request body
{ "accountId": 12345}| Field | Type | Required | Description |
|---|---|---|---|
accountId | number | Yes | Account ID |
Response
{ "success": true, "errorCode": 0, "errorMessage": null, "ended": true, "account": { "balance": 51250.00, "equity": 51250.00, "unrealizedPnl": 0, "marginUsed": 0, "positions": 0, "pendingOrders": 0 }}| Field | Type | Description |
|---|---|---|
ended | boolean | Always true on success |
account | object or null | Final account snapshot: balance, equity, unrealizedPnl, marginUsed, positions, pendingOrders |
Example
# Flatten, then end the session and print the final balanceapi("Position/closeContract", {"accountId": ACCOUNT_ID, "contractId": CONTRACT_ID})result = api("Replay/end", {"accountId": ACCOUNT_ID})
if result["success"]: acct = result["account"] print(f"\n=== Backtest Complete ===") print(f"Final Balance: ${acct['balance']:,.2f}") print(f"Net P&L: ${acct['balance'] - 50000:+,.2f}")curl -X POST https://api.test-max.com/api/topstep-sim/Replay/end \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{"accountId": 12345}'Complete backtest example
Here is a minimal end-to-end example that starts a replay, steps through bars, and ends the session:
import urllib.requestimport jsonimport os
API_URL = "https://api.test-max.com/api/topstep-sim"
def api(path, body=None): data = json.dumps(body).encode() if body else None req = urllib.request.Request( f"{API_URL}/{path}", data=data, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}" } ) return json.loads(urllib.request.urlopen(req).read())
# 1. Authenticatelogin = api("Auth/loginKey", { "userName": os.environ["TESTMAX_EMAIL"], "apiKey": os.environ["TESTMAX_API_KEY"]})TOKEN = login["token"]
# 2. Start replaysession = api("Replay/start", { "instrumentSymbol": "NQ", "startDate": "2025-01-15", "timeframe": "5m"})ACCOUNT_ID = session["accountId"]CONTRACT_ID = session["instrument"]["contractId"]
# 3. Step through barstry: for i in range(session["totalBars"]): result = api("Replay/step", {"accountId": ACCOUNT_ID, "steps": 1}) if not result["success"] or result["barsProcessed"] == 0: break
bar = result["bars"][0]
# Simple strategy: buy on first bar, sell on last if i == 0: api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 2, "side": 0, "size": 1 }) elif i == session["totalBars"] - 2: api("Position/closeContract", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID })
finally: # 4. Always end the session results = api("Replay/end", {"accountId": ACCOUNT_ID}) if results["success"] and results["account"]: print(f"P&L: ${results['account']['balance'] - 50000:+,.2f}")