Skip to content
Back to App

Playground API

The Playground API powers the Algo Playground experience. It provides endpoints for browsing strategy templates, running backtests, controlling playback speed, and managing saved strategies with full CRUD operations.

Base path

https://api.test-max.com/api/playground/

Authentication

The Playground API uses cookie-based JWT authentication — the same session cookie set when you log into the TestMax web app. No API key is needed.

Response format

Unlike the TopStep Simulator API, Playground responses are plain JSON without the success/errorCode envelope, and errors use real HTTP status codes with a body of {"error": "message"}:

StatusMeaning
401Not logged in
403Pro plan required
400Invalid request (missing fields, bad dates, size limits)
404Strategy or execution not found
429Rate limited (includes Retry-After); a backtest already running or starting for your user ("Already running a backtest. Stop it first." or "Already starting a backtest." — no Retry-After); or server at capacity and the wait queue full (Retry-After: 30 plus maxRuns in the body)
500 / 503Server error / Playground temporarily unavailable

Access requirements and limits

EndpointAccess
GET /templatesPublic (no auth required)
All other endpointsPro plan required
LimitValue
Concurrent backtests per user1 (a queued run counts)
Concurrent backtests server-wide~20; further runs wait in a FIFO queue (~30 seats, up to 10 minutes) — 429 only when the queue is also full
Execution timeout30 minutes
Max script size200 KB
Max backtest date range2 years
Saved strategies per user100
Run history returned per strategymost recent 50 runs (a response limit on GET /strategies/:id/runs, not a retention policy)
Rate limit on CRUD/control endpoints100 requests/minute per user (/run and /stop are exempt — /run has the concurrency caps instead, and /stop must always be able to free a running backtest)

Templates

GET /api/playground/templates

List all available strategy templates. This endpoint is public and does not require authentication.

URL: GET /api/playground/templates

Authentication: None required.

Response

{
"templates": [
{
"id": "sma_crossover",
"name": "SMA Crossover",
"description": "Buy when fast SMA crosses above slow SMA, sell on cross below. Classic trend-following strategy.",
"params": [
{ "name": "FAST_PERIOD", "label": "Fast Period", "type": "number", "default": 10 },
{ "name": "SLOW_PERIOD", "label": "Slow Period", "type": "number", "default": 30 },
{ "name": "MAX_BARS", "label": "Max Bars (0=unlimited)", "type": "number", "default": 0 }
]
}
]
}

Available template IDs: sma_crossover, rsi_scalper, breakout, and custom (a bare scaffold for your own code).

Template fields

FieldTypeDescription
idstringUnique template identifier. Pass this as strategy when running a backtest.
namestringHuman-readable template name
descriptionstringWhat the strategy does
paramsarrayConfigurable parameters for the template

Parameter fields

FieldTypeDescription
namestringEnvironment variable name the strategy code reads (e.g., FAST_PERIOD)
labelstringDisplay label for the UI
typestringData type: "number" or "string"
defaultnumber or stringDefault value

Example

Terminal window
curl https://api.test-max.com/api/playground/templates

Execution

POST /api/playground/run

Run a backtest. You can run a template with custom parameters or provide custom Python code.

URL: POST /api/playground/run

Authentication: Cookie-based JWT. Pro plan required.

Request body

{
"strategy": "sma_crossover",
"script": null,
"instrument": "NQ",
"startDate": "2025-01-15",
"endDate": "2025-01-15",
"timeframe": "5m",
"speed": 25,
"savedStrategyId": null,
"params": {
"FAST_PERIOD": 10,
"SLOW_PERIOD": 25
}
}
FieldTypeRequiredDescription
strategystring or nullConditionalTemplate ID to run. Used when script is empty.
scriptstring or nullConditionalCustom Python strategy code. Takes precedence over strategy when non-empty.
instrumentstringYesInstrument symbol (e.g., "NQ", "ES", "GC")
startDatestringYesStart date in YYYY-MM-DD format (must not be in the future)
endDatestring or nullNoEnd date (inclusive). Defaults to the end of available data; the total span is capped at 2 years.
timeframestringNoBar timeframe (default "1s")
speednumberNoInitial playback speed (1–50)
savedStrategyIdstring or nullNoID of one of your saved strategies to link this run to for run history (see GET /strategies/:id/runs). When set, a run record is created when the backtest ends. Invalid or foreign IDs are silently ignored — the run proceeds, nothing is recorded.
paramsobject or nullNoParameter overrides, passed to the script as environment variables. Keys must match the template’s param names.

Response — a live output stream

The response is not JSON — it is a streaming text/plain body carrying the strategy’s stdout/stderr in real time (stderr lines are prefixed with [STDERR]). The stream ends with an exit marker line:

[INFO] Bar 1/78: Close=21503.0
...
__EXIT__:0

If the server is at its concurrency cap, the run holds a FIFO queue seat instead of being rejected: the same stream first emits [QUEUE] lines with your live position, then starts the backtest automatically when a slot frees up ([QUEUE] Slot acquired — starting backtest.). Closing the connection or calling /stop gives the seat up; after 10 minutes without a slot the stream ends with __EXIT__:1.

[QUEUE] Server at capacity (20 backtests running) — position 2 of 3 in line.
[QUEUE] Position 1 of 2 — your backtest starts automatically when a slot frees up.
[QUEUE] Slot acquired — starting backtest.
[INFO] Bar 1/78: Close=21503.0
...

Two response headers identify the run:

HeaderDescription
X-Execution-IdExecution ID — pass to /stop and /speed
X-Session-IdThe replay session backing this run

Example

const response = await fetch("https://api.test-max.com/api/playground/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include", // Include session cookie
body: JSON.stringify({
strategy: "sma_crossover",
instrument: "NQ",
startDate: "2025-01-15",
timeframe: "5m",
params: { FAST_PERIOD: 8, SLOW_PERIOD: 21 },
}),
});
const executionId = response.headers.get("X-Execution-Id");
// Stream the output as it arrives
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(decoder.decode(value));
}

POST /api/playground/stop

Stop a running backtest execution.

URL: POST /api/playground/stop

Authentication: Cookie-based JWT. Pro plan required.

Request body

{
"executionId": "8b6f6a2e-..."
}
FieldTypeRequiredDescription
executionIdstringYesThe X-Execution-Id from the /run response

Response

{
"ok": true,
"stopped": true
}

Returns 404 {"error": "Execution not found"} if the execution does not exist or belongs to another user.


POST /api/playground/speed

Set the playback speed for a running backtest.

URL: POST /api/playground/speed

Authentication: Cookie-based JWT. Pro plan required.

Request body

{
"executionId": "8b6f6a2e-...",
"speed": 25
}
FieldTypeRequiredDescription
executionIdstringYesThe X-Execution-Id from the /run response
speednumberYesSpeed multiplier from 1 (slowest) to 50 (fastest)

Response

{
"ok": true,
"speed": 25,
"stepDelay": 0.02
}

stepDelay is the resulting per-bar delay in secondsmax(0.01, 0.5 / speed) — so speed 1 gives 0.5, speed 25 gives 0.02, and speed 50 and above hit the 0.01 floor.


Strategies

GET /api/playground/strategies

List all saved strategies for the authenticated user, most recently updated first.

URL: GET /api/playground/strategies

Authentication: Cookie-based JWT. Pro plan required.

Response

{
"strategies": [
{
"id": "d4c3b2a1-...",
"name": "My SMA Strategy",
"templateId": "sma_crossover",
"instrument": "NQ",
"timeframe": "5m",
"params": { "FAST_PERIOD": 10, "SLOW_PERIOD": 25 },
"runCount": 1,
"lastRun": {
"pnl": 1250.00,
"winRate": 62.5,
"trades": 8,
"date": "2025-01-15T10:02:34.000Z"
},
"updatedAt": "2025-01-14T16:45:00.000Z"
}
]
}
FieldTypeDescription
strategiesarrayList of saved strategy summaries
strategies[].idstringUnique strategy ID
strategies[].namestringStrategy name
strategies[].templateIdstring or nullTemplate the strategy was based on, if any
strategies[].instrumentstringDefault instrument
strategies[].timeframestringDefault timeframe
strategies[].paramsobjectSaved parameter values
strategies[].runCountnumber1 if the strategy has at least one run, else 0
strategies[].lastRunobject or nullMost recent run summary: pnl, winRate, trades, date
strategies[].updatedAtstringISO 8601 last-modified timestamp

POST /api/playground/strategies/save

Save a new strategy, or update an existing one by including its id.

URL: POST /api/playground/strategies/save

Authentication: Cookie-based JWT. Pro plan required.

Request body

{
"id": null,
"name": "My Custom Strategy",
"templateId": "sma_crossover",
"code": "# Strategy code here\nfor i in range(MAX_BARS):\n bar = get_next_bar()\n ...",
"instrument": "NQ",
"timeframe": "5m",
"params": {
"FAST_PERIOD": 10,
"SLOW_PERIOD": 25
}
}
FieldTypeRequiredDescription
idstring or nullNoExisting strategy ID to update. Omit to create a new strategy.
namestringYesStrategy name (max 200 characters)
codestringYesFull Python strategy code (non-empty, max 200 KB)
templateIdstring or nullNoTemplate the code was based on
instrumentstringNoDefault instrument symbol (default "NQ")
timeframestringNoDefault timeframe (default "1m")
paramsobjectNoDefault parameter values (default {})

Response

{
"id": "d4c3b2a1-...",
"name": "My Custom Strategy"
}

Creating past the 100-strategy cap returns 400 {"error": "Saved strategy limit reached (100). Delete some first."}.


GET /api/playground/strategies/:id

Get a saved strategy by ID, including the full code and parameters.

URL: GET /api/playground/strategies/:id

Authentication: Cookie-based JWT. Pro plan required.

Response

{
"id": "d4c3b2a1-...",
"name": "My Custom Strategy",
"templateId": "sma_crossover",
"code": "# Strategy code here...",
"params": { "FAST_PERIOD": 10, "SLOW_PERIOD": 25 },
"instrument": "NQ",
"timeframe": "5m"
}

Returns 404 {"error": "Strategy not found"} for unknown IDs or strategies owned by another user.


DELETE /api/playground/strategies/:id

Delete a saved strategy.

URL: DELETE /api/playground/strategies/:id

Authentication: Cookie-based JWT. Pro plan required.

Response

{
"ok": true
}

GET /api/playground/strategies/:id/runs

Get the run history for a saved strategy (up to 50 most recent runs, newest first).

URL: GET /api/playground/strategies/:id/runs

Authentication: Cookie-based JWT. Pro plan required.

A run record is created when a backtest started with savedStrategyId ends — through any path: normal completion ("completed"), a script error ("error"), or a manual stop, the 30-minute timeout, or a client disconnect (all recorded as "stopped"). Runs started without savedStrategyId are not recorded. The response returns at most the 50 most recent runs — a response limit, not a retention policy.

Response

{
"strategy": { "id": "d4c3b2a1-...", "name": "My Custom Strategy" },
"runs": [
{
"id": "f7e6d5c4-...",
"startDate": "2025-01-15",
"endDate": "2025-01-15",
"instrument": "NQ",
"timeframe": "5m",
"params": { "FAST_PERIOD": 10, "SLOW_PERIOD": 25 },
"finalBalance": 51250.00,
"totalPnl": 1250.00,
"totalTrades": 8,
"winRate": 62.5,
"maxDrawdown": 325.00,
"status": "completed",
"createdAt": "2025-01-15T10:02:34.000Z"
}
]
}
FieldTypeDescription
strategyobjectThe parent strategy’s id and name
runsarrayList of past backtest runs, newest first
runs[].idstringUnique run ID
runs[].startDate / endDatestringBacktest date range
runs[].instrumentstringInstrument traded
runs[].timeframestringBar timeframe used
runs[].paramsobjectParameters used for this run
runs[].finalBalancenumberAccount balance at the end of the run
runs[].totalPnlnumberNet P&L in USD
runs[].totalTradesnumberNumber of trades
runs[].winRatenumberWin percentage (0–100)
runs[].maxDrawdownnumberMaximum drawdown in USD
runs[].statusstringHow the run ended: "completed", "error", or "stopped" (manual stop, timeout, or disconnect)
runs[].createdAtstringISO 8601 timestamp of the run