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"}:
| Status | Meaning |
|---|---|
401 | Not logged in |
403 | Pro plan required |
400 | Invalid request (missing fields, bad dates, size limits) |
404 | Strategy or execution not found |
429 | Rate 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 / 503 | Server error / Playground temporarily unavailable |
Access requirements and limits
| Endpoint | Access |
|---|---|
GET /templates | Public (no auth required) |
| All other endpoints | Pro plan required |
| Limit | Value |
|---|---|
| Concurrent backtests per user | 1 (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 timeout | 30 minutes |
| Max script size | 200 KB |
| Max backtest date range | 2 years |
| Saved strategies per user | 100 |
| Run history returned per strategy | most recent 50 runs (a response limit on GET /strategies/:id/runs, not a retention policy) |
| Rate limit on CRUD/control endpoints | 100 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
| Field | Type | Description |
|---|---|---|
id | string | Unique template identifier. Pass this as strategy when running a backtest. |
name | string | Human-readable template name |
description | string | What the strategy does |
params | array | Configurable parameters for the template |
Parameter fields
| Field | Type | Description |
|---|---|---|
name | string | Environment variable name the strategy code reads (e.g., FAST_PERIOD) |
label | string | Display label for the UI |
type | string | Data type: "number" or "string" |
default | number or string | Default value |
Example
curl https://api.test-max.com/api/playground/templatesconst response = await fetch("https://api.test-max.com/api/playground/templates");const data = await response.json();
for (const template of data.templates) { console.log(`${template.name}: ${template.description}`);}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 }}| Field | Type | Required | Description |
|---|---|---|---|
strategy | string or null | Conditional | Template ID to run. Used when script is empty. |
script | string or null | Conditional | Custom Python strategy code. Takes precedence over strategy when non-empty. |
instrument | string | Yes | Instrument symbol (e.g., "NQ", "ES", "GC") |
startDate | string | Yes | Start date in YYYY-MM-DD format (must not be in the future) |
endDate | string or null | No | End date (inclusive). Defaults to the end of available data; the total span is capped at 2 years. |
timeframe | string | No | Bar timeframe (default "1s") |
speed | number | No | Initial playback speed (1–50) |
savedStrategyId | string or null | No | ID 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. |
params | object or null | No | Parameter 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__:0If 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:
| Header | Description |
|---|---|
X-Execution-Id | Execution ID — pass to /stop and /speed |
X-Session-Id | The 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 arrivesconst 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-..."}| Field | Type | Required | Description |
|---|---|---|---|
executionId | string | Yes | The 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}| Field | Type | Required | Description |
|---|---|---|---|
executionId | string | Yes | The X-Execution-Id from the /run response |
speed | number | Yes | Speed multiplier from 1 (slowest) to 50 (fastest) |
Response
{ "ok": true, "speed": 25, "stepDelay": 0.02}stepDelay is the resulting per-bar delay in seconds — max(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" } ]}| Field | Type | Description |
|---|---|---|
strategies | array | List of saved strategy summaries |
strategies[].id | string | Unique strategy ID |
strategies[].name | string | Strategy name |
strategies[].templateId | string or null | Template the strategy was based on, if any |
strategies[].instrument | string | Default instrument |
strategies[].timeframe | string | Default timeframe |
strategies[].params | object | Saved parameter values |
strategies[].runCount | number | 1 if the strategy has at least one run, else 0 |
strategies[].lastRun | object or null | Most recent run summary: pnl, winRate, trades, date |
strategies[].updatedAt | string | ISO 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 }}| Field | Type | Required | Description |
|---|---|---|---|
id | string or null | No | Existing strategy ID to update. Omit to create a new strategy. |
name | string | Yes | Strategy name (max 200 characters) |
code | string | Yes | Full Python strategy code (non-empty, max 200 KB) |
templateId | string or null | No | Template the code was based on |
instrument | string | No | Default instrument symbol (default "NQ") |
timeframe | string | No | Default timeframe (default "1m") |
params | object | No | Default 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" } ]}| Field | Type | Description |
|---|---|---|
strategy | object | The parent strategy’s id and name |
runs | array | List of past backtest runs, newest first |
runs[].id | string | Unique run ID |
runs[].startDate / endDate | string | Backtest date range |
runs[].instrument | string | Instrument traded |
runs[].timeframe | string | Bar timeframe used |
runs[].params | object | Parameters used for this run |
runs[].finalBalance | number | Account balance at the end of the run |
runs[].totalPnl | number | Net P&L in USD |
runs[].totalTrades | number | Number of trades |
runs[].winRate | number | Win percentage (0–100) |
runs[].maxDrawdown | number | Maximum drawdown in USD |
runs[].status | string | How the run ended: "completed", "error", or "stopped" (manual stop, timeout, or disconnect) |
runs[].createdAt | string | ISO 8601 timestamp of the run |