Skip to content
Back to App

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:

  1. StartReplay/start initializes a session with an instrument, date, and timeframe
  2. LoopReplay/step advances bars one at a time; your strategy analyzes data and places orders
  3. EndReplay/end finalizes 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"
}
FieldTypeRequiredDescription
instrumentSymbolstringYes*Instrument symbol (e.g., "NQ", "ES", "GC", "CL", "EURUSD")
contractIdstringYes*Alternative to instrumentSymbol — a contract ID from Contract/search
startDatestringYesSession start date in YYYY-MM-DD format
timeframestringNoBar timeframe (default "1s")
accountIdnumberNoAn existing prop-firm challenge account — the new session inherits its challenge rules and account size

* Provide either instrumentSymbol or contractId.

Timeframe values

ValueDescription
"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
}
}
FieldTypeDescription
accountIdnumberThe account ID to use for orders and positions in this session
sessionIdstringInternal session UUID (informational)
totalBarsnumberTotal number of bars available in the session
historyBarsnumberNumber of bars pre-loaded as chart history before the replay start
instrument.namestringInstrument symbol
instrument.contractIdstringThe resolved contract ID — use this as contractId in order endpoints
instrument.tickSizenumberMinimum price increment
instrument.tickValuenumberDollar 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 NQ
result = 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']}")

Error responses

ScenarioerrorCodeerrorMessage
Neither instrumentSymbol nor contractId given1"instrumentSymbol or contractId required"
Unknown instrument1"Instrument not found"
Missing startDate2"startDate required"
Session setup failed (e.g., no data for the date)7The 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
}
FieldTypeRequiredDefaultDescription
accountIdnumberYesAccount ID from Replay/start
stepsnumberNo1Number 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": []
}
FieldTypeDescription
barsarrayArray of bar objects, one per step. See Bar Data Format.
bars[].tstringISO 8601 timestamp
bars[].o / h / l / cnumberOpen / high / low / close price
bars[].vnumberVolume
barsProcessednumberHow many bars actually advanced — 0 means the replay reached the end of the data
currentIndexnumberIndex of the next bar to be played
totalBarsnumberTotal bars in the session
accountobject or nullLive account snapshot: balance, equity, unrealizedPnl, marginUsed, positions, pendingOrders
propFirmobject or nullProp-firm snapshot (status, isFunded, currentPhase, tradingDaysCount, drawdownLocked), or null for non-challenge sessions
filledOrdersarrayOrders filled during these bars: {orderId, fillPrice, fillQty}
violationsarrayProp-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 bars
result = api("Replay/step", {
"accountId": ACCOUNT_ID,
"steps": 10
})
# Process all returned bars
for 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"
}
FieldTypeRequiredDescription
accountIdnumberYesAccount ID
targetTimestringYesISO 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
}
FieldTypeRequiredDefaultDescription
accountIdnumberYesAccount ID
speednumberNo1Playback 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 speed
result = 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
}
FieldTypeRequiredDescription
accountIdnumberYesAccount 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-play
result = 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
}
FieldTypeRequiredDescription
accountIdnumberYesAccount 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
}
}
FieldTypeDescription
endedbooleanAlways true on success
accountobject or nullFinal account snapshot: balance, equity, unrealizedPnl, marginUsed, positions, pendingOrders

Example

# Flatten, then end the session and print the final balance
api("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}")

Complete backtest example

Here is a minimal end-to-end example that starts a replay, steps through bars, and ends the session:

import urllib.request
import json
import 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. Authenticate
login = api("Auth/loginKey", {
"userName": os.environ["TESTMAX_EMAIL"],
"apiKey": os.environ["TESTMAX_API_KEY"]
})
TOKEN = login["token"]
# 2. Start replay
session = api("Replay/start", {
"instrumentSymbol": "NQ",
"startDate": "2025-01-15",
"timeframe": "5m"
})
ACCOUNT_ID = session["accountId"]
CONTRACT_ID = session["instrument"]["contractId"]
# 3. Step through bars
try:
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}")