Download OpenAPI specification:
QTSurfer backend services API
The auth endpoint exchanges a long-lived API key for a short-lived
JWT used by every other endpoint. Issue an API key via the web app, then
call POST /auth/token at the start of each session (and on 401
responses) to obtain a fresh JWT.
Key functionalities:
Exchanges a long-lived API key for a short-lived JWT used by every other
endpoint. This is the only endpoint that accepts an API key directly —
callers should obtain a JWT here, then send it as Authorization: Bearer <token> to all other operations.
The returned JWT carries the caller's subscription tier as a claim and
expires after expires_in seconds. Callers should refresh the token
before expiry (or on a 401 response) by calling this endpoint again.
{- "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI3NmI5MDIwMy0wM2MyLTQ2ZjYtYjM2Ni05OTQ0ZjE2N2U4MTgiLCJzY29wZXMiOltdLCJ0aWVyIjoiZnJlZSIsImlhdCI6MTc3OTczNTQ2MiwiZXhwIjoxNzc5NzM5MDYyfQ.signature",
- "token_type": "Bearer",
- "expires_in": 3600,
- "scopes": [ ],
- "tier": "free"
}The account endpoints report your identity, tier limits, and live usage against them. Split in two on purpose: tier limits never change mid-session and cost nothing to fetch, while usage is a live figure that changes on every upload or strategy execution.
Key functionalities:
Your userId, current tier, and that tier's limits — no database call, safe to fetch on
every page load. For live usage against these limits, see _links.usage
(GET /account/usage).
{- "userId": "00000000-0000-0000-0000-000000000000",
- "tier": "free",
- "maxDatasets": 3,
- "maxDatasetBytes": 52428800,
- "maxTotalStorageBytes": 104857600,
- "_links": {
- "self": {
- "href": "/v1/account"
}, - "usage": {
- "href": "/v1/account/usage"
}
}
}How much of your account's shared storage pool (GET /account's maxTotalStorageBytes)
you're currently using. Datasets, strategy-execution signals, and registered strategies
all count against the same total — they compete for the same underlying storage, so
there's one number to watch, not one per resource type. Not guaranteed real-time — a
just-completed upload or strategy execution may take a short moment to be reflected here.
{- "datasetsUsed": 2,
- "datasetBytesUsed": 15728640,
- "signalsUsed": 1,
- "signalBytesUsed": 524288,
- "strategiesUsed": 4,
- "strategyBytesUsed": 40960,
- "storageBytesUsed": 16293888,
- "_links": {
- "self": {
- "href": "/v1/account/usage"
}, - "account": {
- "href": "/v1/account"
}
}
}This set of endpoints allows interaction with various exchanges for cryptocurrencies and financial assets. With these endpoints, users can access information about available exchanges, retrieve the instruments (currency pairs or assets) offered by each exchange, and perform analyses on them. The data provided by these endpoints is crucial for strategic decision-making within the trading platform.
Key functionalities:
"Give me binance instruments" — returns the exchange's DEFAULT segment (spot)
in data, each instrument with per-data-type coverage and market info. meta
confirms the served segment (spot); HAL _links carry self plus the
spot / futures segment-discovery links.
| exchangeId required | string Example: binance ID of the exchange to retrieve instruments for |
{- "data": [
- {
- "id": "BTC/USDT",
- "base": "BTC",
- "quote": "USDT",
- "coverage": {
- "tickers": {
- "from": "2026-04-10T21:00:00Z",
- "to": "2026-07-09T20:29:05Z"
}, - "klines": {
- "from": "2026-04-22T17:00:00Z",
- "to": "2026-07-09T20:31:08Z"
}
}, - "lastPrice": 84250.5,
- "volume24h": 1234567.89
}
], - "meta": {
- "updatedAt": "2026-07-09T19:09:07Z",
- "exchange": "binance",
- "segment": "spot"
}, - "_links": {
- "self": {
- "href": "/v1/exchange/binance/instruments"
}, - "spot": {
- "href": "/v1/exchange/binance/spot/instruments"
}, - "futures": {
- "href": "/v1/exchange/binance/futures/instruments"
}
}
}Returns the instruments for one market segment of the exchange, each with
per-data-type coverage and market info. HAL _links carry self plus the
spot / futures segment-discovery links; the default-segment shortcut is
GET /exchange/{exchangeId}/instruments (spot).
| exchangeId required | string Example: binance ID of the exchange to retrieve instruments for |
| segment required | string Enum: "spot" "futures" Example: spot Market segment to list instruments for |
{- "data": [
- {
- "id": "BTC/USDT",
- "base": "BTC",
- "quote": "USDT",
- "coverage": {
- "tickers": {
- "from": "2026-04-10T21:00:00Z",
- "to": "2026-07-09T20:29:05Z"
}, - "klines": {
- "from": "2026-04-22T17:00:00Z",
- "to": "2026-07-09T20:31:08Z"
}
}, - "lastPrice": 84250.5,
- "volume24h": 1234567.89
}, - {
- "id": "ETH/USDT",
- "base": "ETH",
- "quote": "USDT",
- "coverage": {
- "tickers": {
- "from": "2026-04-10T21:00:00Z",
- "to": "2026-07-09T20:28:22Z",
- "inactiveSince": "2026-07-09T20:28:22Z"
}, - "klines": {
- "from": "2026-04-22T17:00:00Z",
- "to": "2026-07-09T20:31:09Z",
- "inactiveSince": "2026-07-09T20:31:09Z"
}
}, - "lastPrice": 3120.75,
- "volume24h": 456789.12
}
], - "meta": {
- "updatedAt": "2026-07-09T19:09:07Z",
- "exchange": "binance",
- "segment": "spot"
}, - "_links": {
- "self": {
- "href": "/v1/exchange/binance/spot/instruments"
}, - "spot": {
- "href": "/v1/exchange/binance/spot/instruments"
}, - "futures": {
- "href": "/v1/exchange/binance/futures/instruments"
}
}
}Serves exactly one hour of raw ticker data for the given instrument on the requested exchange. The payload is a native Lastra file — QTSurfer's columnar format for tick-precision timeseries — with no JSON envelope.
One segment = one hour, aligned to UTC. The hour query parameter selects
the segment and must match YYYY-MM-DDTHH (no minutes/seconds, no timezone
suffix). Example: hour=2026-01-15T10 returns h10.lastra for
2026-01-15, covering [10:00:00Z, 11:00:00Z). Hours not yet available
return 404.
A format=parquet query parameter switches the response to on-the-fly
Parquet conversion via lastra-convert
for clients that don't yet read Lastra. Lastra is the primary format
and cheaper when the client can consume it.
Clients:
curl -OJ for offline dumps (the Content-Disposition header sets a
descriptive filename).| exchangeId required | string Example: binance ID of the exchange (e.g. |
| base required | string Example: BTC Base asset symbol (first leg of the pair). |
| quote required | string Example: USDT Quote asset symbol (second leg of the pair). |
| hour required | string^\d{4}-\d{2}-\d{2}T\d{2}$ Example: hour=2026-01-15T10 Hour selector in |
| format | string Default: "lastra" Enum: "lastra" "parquet" Example: format=lastra Response wire format. |
{- "code": 400,
- "message": "Invalid request"
}Same shape and semantics as /exchange/{exchangeId}/tickers/{base}/{quote},
but serves klines (aggregated bars) instead of raw ticks. One
Lastra segment = one hour of
klines at the exchange's native kline cadence, aligned to UTC.
Klines use the same columnar layout as tickers — readers that handle one format read the other with the same code. Use this endpoint when a per-tick payload would be too large for the window of interest.
| exchangeId required | string Example: binance ID of the exchange (e.g. |
| base required | string Example: BTC Base asset symbol. |
| quote required | string Example: USDT Quote asset symbol. |
| hour required | string^\d{4}-\d{2}-\d{2}T\d{2}$ Example: hour=2026-01-15T10 Hour selector in |
| format | string Default: "lastra" Enum: "lastra" "parquet" Example: format=lastra Response wire format. |
{- "code": 400,
- "message": "Invalid request"
}The backtest endpoints enable users to test trading strategies based on historical data before applying them in real-time trading. This functionality is essential for traders who want to evaluate the performance of a strategy under past market conditions and optimize it before implementing it in a live trading environment.
Key functionalities:
Enqueues a prepare task over the requested date range. Returns immediately with a jobId;
poll GET /backtest/{exchangeId}/{type}/prepare/{jobId} for completion.
The same params always return the same jobId (idempotent). Repeated calls with identical
params do not enqueue duplicate work — they reuse the existing job.
Every source in DataSourceType can be prepared, but not every one can then be run: funding
data can be prepared and is rejected by execute and executeSweep. A kline prepare takes
the bar width from cadence — see PrepareRequest.
exchangeId: user is reserved for your own uploaded data. Instead of a managed
exchange, it prepares from a dataset you created via POST /datasets (see the Dataset
endpoints) — send datasetId in place of instrument. See PrepareRequest below for the
two request shapes.
| exchangeId required | string Example: binance ID of the exchange to prepare the backtesting for (e.g. |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker The type of data source to prepare from |
The required data to prepare a backtesting
| instrument | string Required unless |
| datasetId | string Only for |
| datasetVersionId | string Only for |
| from required | string Start date for the preparation process. Supports the following formats:
|
| to required | string End date for the preparation process. Supports the following formats:
|
| cadence | string Output bar cadence for the prepared range. Coarser cadences are produced on demand by
resampling the source and stored alongside the native blob in cache. A target finer
than the source, or not an exact multiple of it, returns
|
{- "instrument": "BTC/USDT",
- "from": "2024-12-13T00:00:00Z",
- "to": "2024-12-14T00:00:00Z",
- "cadence": "1m"
}{- "jobId": "13RBLGQlPnfDjO6wyKSX8i"
}Retrieves the current state of the prepare job identified by jobId.
Poll until status is Completed, Failed, or Aborted.
For a dataset prepare (exchangeId: user), coverage is reported against the dataset's own
cadence grid instead of hours — see cadence/gaps/largestGapSteps on PrepareJobState.
| exchangeId required | string Example: binance ID of the exchange for the backtesting process, or the reserved value |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker The type of data source to prepare from |
| jobId required | string Example: 13RBLGQlPnfDjO6wyKSX8i Job ID returned by |
{- "contextId": "ctx_0bjmoxd4vahkgc0hnvdldh",
- "status": "Completed",
- "statusDetail": null,
- "size": 0,
- "completed": 24,
- "startTime": "2026-04-14T15:00:00Z",
- "endTime": "2026-04-14T15:00:01Z",
- "dataFrom": "2026-04-14T13:00:00Z",
- "dataTo": "2026-04-14T15:30:05Z",
- "coverageRatio": 0.994,
- "totalHours": 168,
- "hoursWithData": 167,
- "hoursWithoutData": [
- {
- "hour": "2026-04-14T02:00:00Z",
- "expected": 0,
- "rationale": "low_activity"
}
]
}Runs a parameter matrix over the single immutable dataset identified by requestId.
The backend expands and executes the matrix internally; clients poll the returned
sweepId for incremental results.
type must be ticker or kline; a kline sweep, walk-forward included, runs over bars of
the cadence the request was prepared at. funding can be prepared but not swept yet: it is
rejected with 400 before anything is queued.
Supplying walkForward runs the sweep in a different mode entirely. Instead of scoring
every parameter vector once over the whole range, the data is split into F sequential
folds; each fold optimizes the full grid on its own window and then scores only its winner
on the window immediately after — data that winner was not chosen on. It answers a harder
question than a leaderboard: not "which parameters won", but "does re-optimizing this
periodically actually work". Omit the block and nothing changes, including the response.
The cost is the reason it is opt-in rather than always on: F folds × N vectors, so a
4-fold run over a 500-point grid is 2004 backtests where the plain sweep is 500. The
request is rejected when folds × totalRuns exceeds the server's sweep budget.
| exchangeId required | string Example: binance |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker Managed exchange data sources available for backtesting.
|
| requestId required | string Job ID returned by |
| strategyId required | string (strategyId) Unique identifier for a compiled strategy, derived from the source itself: the same code
always yields the same id, for every caller. How much formatting the id ignores depends on
the language — see |
required | object (SweepSpecRequest) |
object (SweepBaseConfig) | |
| storeSignals | boolean Default: false Store signals for every trial. Keep false for normal sweeps. |
| shards | integer >= 0 Default: 0 Requested horizontal shard count; 0 or omitted selects automatically. |
| minTradeFloor | integer >= 0 Default: 30 Trials below this trade count are flagged but remain in the results. |
object (WalkForwardRequest) Opt in to walk-forward validation. Present, the sweep runs as F sequential folds and the result gains a | |
object (EquityCurveRequest) Selection ( |
{- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "sweep": {
- "sampler": "lhs",
- "seed": 487221,
- "samples": 100,
- "objective": "sharpe",
- "params": {
- "rsiPeriod": {
- "from": 7,
- "to": 28,
- "step": 1
}, - "useTrendFilter": {
- "values": [
- true,
- false
]
}
}
}, - "baseConfig": {
- "initialFunding": 100,
- "feeRate": 0.001,
- "buyFeeRate": 0,
- "sellFeeRate": 0,
- "feeLeg": "RECEIVED",
- "percentAmountToLock": 1
}, - "storeSignals": false,
- "shards": 0,
- "minTradeFloor": 30,
- "walkForward": {
- "folds": 2,
- "inSamplePct": 66
}, - "equityCurve": {
- "resample": 2,
- "differential": false,
- "outMode": "ARRAY",
- "mode": "auto",
- "n": 1,
- "maxPct": 1
}
}{- "sweepId": "swp_95e47a7f0966ce11",
- "requestId": "string",
- "totalRuns": 1,
- "shards": 1,
- "seed": -9007199254740991,
- "queued": true,
- "walkForward": {
- "folds": 0,
- "inSamplePct": 0,
- "totalRuns": 0
}
}Returns incremental sweep progress. The default ranked view sorts and may truncate the
display leaderboard. order=natural returns every available row, untruncated, ordered by
deterministic runIx; use that view when materialising durable trial rows.
The ranked view is ordered by plateau score by default, not by the raw objective. A
plateau score is the objective of the worst run in a parameter point's immediate
neighbourhood, so a point scores well only if the region around it also does — the highest
raw score is frequently a spike that does not survive the parameters moving slightly. Pass
ranking=raw for the unadjusted objective order.
Rows in the ranked view carry plateauScore and neighbourCount when plateau ranking
applied. Read them together: neighbourCount: 0 means the point had no neighbours to
compare against, so its plateau score is unevidenced rather than confirmed. Sweeps
submitted before plateau ranking existed have no stored parameter grid to rebuild a
neighbourhood from and are always ranked raw; the response's ranking field says which
ordering was actually used.
A sweep submitted with walkForward answers in a different shape, and the walkForward
field on the response is what tells the two apart — it appears as soon as the sweep is
accepted, before any fold has finished, so it is safe to branch on while polling. There
the leaderboard is one row per completed fold: that fold's winner as it scored
out-of-sample, with runIx carrying the fold index rather than a grid position. The
in-sample runs behind those winners are not retained — they are an optimization's working
set, and only the winner survives its fold. ranking is always raw and no plateau, DSR
or PBO figure is reported: the out-of-sample scores are already the honest number, and
layering a certification computed over F observations on top of them would overstate what
was measured.
| exchangeId required | string Example: binance |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker Managed exchange data sources available for backtesting.
|
| requestId required | string The |
| sweepId required | string |
| objective | string Enum: "sharpe" "sortino" "pnl" "maxdd" |
| order | string Default: "ranked" Enum: "ranked" "natural"
|
| ranking | string Default: "plateau" Enum: "plateau" "raw" How the |
{- "sweepId": "string",
- "status": "RUNNING",
- "objective": "sharpe",
- "order": "ranked",
- "ranking": "plateau",
- "pbo": 0,
- "pboSplits": 0,
- "failReason": "Failed to load/configure strategy",
- "progress": {
- "done": 0,
- "total": 0,
- "aborted": 0,
- "shardCount": 0,
- "pendingShards": 0,
- "failedShards": 0,
- "retrying": 0,
- "notStarted": 0,
- "stalledSeconds": 0,
- "etaSeconds": 0
}, - "leaderboardSize": 0,
- "truncated": true,
- "leaderboard": [
- {
- "runIx": 0,
- "rank": 1,
- "plateauScore": 0.1,
- "neighbourCount": 0,
- "deflatedSharpe": 0,
- "params": { },
- "sharpe": 0.1,
- "sortino": 0.1,
- "pnl": 0.1,
- "pnlPct": 0.1,
- "cagr": 0.1,
- "maxDdPct": 0.1,
- "trades": 0,
- "winRate": 0.1,
- "belowTradeFloor": true,
- "aborted": true,
- "runtimeMs": 0,
- "equityCurve": {
- "meta": {
- "inputPointCount": 100000,
- "outputPointCount": 100,
- "resampled": true,
- "differential": true,
- "outMode": "ARRAY"
}, - "points": [
- {
- "timestamp": 1700000000000,
- "equity": 110.5
}
], - "timestamps": [
- 0
], - "equities": [
- 0.1
], - "url": "/v1/backtest/binance/ticker/executeSweep/req-1/swp_test/runs/3/equityCurve"
}
}
], - "walkForward": {
- "folds": 0,
- "inSamplePct": 0,
- "completedFolds": 0,
- "paramDrift": 0,
- "results": [
- {
- "foldIx": 0,
- "inSampleFrom": 0,
- "inSampleTo": 0,
- "outOfSampleTo": 0,
- "params": { },
- "inSampleSharpe": 0.1,
- "outOfSample": {
- "runIx": 0,
- "rank": 1,
- "plateauScore": 0.1,
- "neighbourCount": 0,
- "deflatedSharpe": 0,
- "params": { },
- "sharpe": 0.1,
- "sortino": 0.1,
- "pnl": 0.1,
- "pnlPct": 0.1,
- "cagr": 0.1,
- "maxDdPct": 0.1,
- "trades": 0,
- "winRate": 0.1,
- "belowTradeFloor": true,
- "aborted": true,
- "runtimeMs": 0,
- "equityCurve": {
- "meta": {
- "inputPointCount": 100000,
- "outputPointCount": 100,
- "resampled": true,
- "differential": true,
- "outMode": "ARRAY"
}, - "points": [
- {
- "timestamp": 1700000000000,
- "equity": 110.5
}
], - "timestamps": [
- 0
], - "equities": [
- 0.1
], - "url": "/v1/backtest/binance/ticker/executeSweep/req-1/swp_test/runs/3/equityCurve"
}
}, - "vectorsRun": 0
}
]
}, - "state": {
- "contextId": "jctx:ticker:00000000-0000-0000-0000-000000000000:binance:5ikyamio8b3v9wcnfxztzg:btc/usdt:0vicnz3thzhrqvfczks1pu",
- "status": "Completed",
- "statusDetail": "Job completed with error code 5001",
- "size": 100,
- "completed": 50,
- "startTime": "2025-01-04T14:00:00Z",
- "endTime": "2025-01-04T14:00:20Z"
}
}Requests cancellation between parameter vectors. Completed rows remain readable.
| exchangeId required | string Example: binance |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker Managed exchange data sources available for backtesting.
|
| requestId required | string The |
| sweepId required | string |
{- "status": "cancelling",
- "sweepId": "string"
}How the objective moves as each parameter moves — the question a leaderboard cannot answer. A leaderboard says which point won; a sweep can spend its entire budget on an axis that never moved the objective at all, and showing only the top rows hides that completely.
A marginal takes one axis and collapses every other one: for each value of that axis,
it aggregates every run that used it, whatever the rest of the parameters were. A flat
marginal means the axis is irrelevant over the range swept. best, mean and worst are
all reported because them disagreeing is itself the signal — a value with a high best and
a poor mean works only in specific company, which is an interaction between parameters
and would be invisible behind a single number.
A heatmap does the same over a pair of axes, where that interaction becomes visible directly.
Served from the sweep's stored rows: no re-run, no engine call, and it works on a sweep still in flight — the aggregates then describe the runs finished so far. Aborted runs are excluded throughout, since a run that threw measured nothing and counting it as a bad outcome would invent evidence against a parameter value that was never really tested.
This is a separate endpoint rather than extra fields on the result view because the two-dimensional half is quadratic in the axis count (N axes give N(N-1)/2 surfaces, each the product of two axes' value counts) and is not wanted on the poll that drives progress.
| exchangeId required | string Example: binance |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker Managed exchange data sources available for backtesting.
|
| requestId required | string The |
| sweepId required | string |
| objective | string Enum: "sharpe" "sortino" "pnl" "maxdd" Which metric to aggregate. Defaults to the objective the sweep was submitted with. |
{- "sweepId": "string",
- "status": "RUNNING",
- "objective": "sharpe",
- "rowsAnalysed": 0,
- "marginals": [
- {
- "param": "string",
- "points": [
- {
- "value": null,
- "count": 0,
- "best": 0.1,
- "mean": 0.1,
- "worst": 0.1
}
]
}
], - "heatmaps": [
- {
- "paramA": "string",
- "paramB": "string",
- "cells": [
- {
- "valueA": null,
- "valueB": null,
- "count": 0,
- "best": 0.1,
- "mean": 0.1
}
]
}
], - "heatmapsTruncated": true
}The resource a leaderboard row's equityCurve.url points at — only reachable when that
trial's curve was actually selected (equityCurve.mode: topN or topPct on the sweep
submission, and this trial ranked among the winners). Returns the exact same
{points|timestamps+equities, meta} shape a plain backtest's inline equityCurve carries.
Query params reshape the response the same way a plain backtest's equityCurve options do.
A param genuinely absent from the query string falls back to the equityCurve transform
preference the sweep was submitted with — a param
present but malformed does not fall back, it degrades the same way it always has. Above a
server-side size threshold, the shape is forced regardless of either — meta.outMode in
the response, not the query string or the submitted default, is the source of truth for
what shape actually came back.
| exchangeId required | string Example: binance |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker Managed exchange data sources available for backtesting.
|
| requestId required | string The |
| sweepId required | string |
| runIx required | integer >= 0 The trial's |
| outMode | string Default: "ARRAY" Enum: "ARRAY" "SHORT" Requested JSON shape. Omit to use the sweep's submitted default; may be overridden either way by the server's size guard. |
| resample | integer >= 2 Downsample to at most this many points (extrema-preserving — the global max/min and the exact first/last point are always kept). Omit to use the sweep's submitted default (itself omittable, for no downsampling). |
| differential | boolean Default: false Delta-encode both fields from the second point onward. Omit to use the sweep's submitted default. |
{- "meta": {
- "inputPointCount": 100000,
- "outputPointCount": 100,
- "resampled": true,
- "differential": true,
- "outMode": "ARRAY"
}, - "points": [
- {
- "timestamp": 1700000000000,
- "equity": 110.5
}
], - "timestamps": [
- 0
], - "equities": [
- 0.1
], - "url": "/v1/backtest/binance/ticker/executeSweep/req-1/swp_test/runs/3/equityCurve"
}Enqueues an execute task that runs the strategy identified by strategyId over the data
prepared by the prepare job identified by prepareJobId. The instrument and date range are
recovered from the prepare job — they do not need to be sent again.
Returns immediately with a jobId; poll GET /backtest/{exchangeId}/{type}/execute/{jobId}
for the result.
type must be a source that can be executed: ticker or kline. A kline strategy is fed
bars of the cadence its prepareJobId was prepared at — chosen by you when preparing, not by
the strategy. funding can be prepared but not executed yet: it is rejected with 400
before anything is queued.
Optionally takes params: strategy properties for this one run, applied without
recompiling. This is how a sweep leaderboard winner gets re-run for its equityCurve —
a sweep row carries the ten ranking metrics but never a curve, whatever its size. Compile
once, call this endpoint N times with different params, and each response is an ordinary
backtest result with the curve included.
The same request (same prepareJobId, strategyId, storeSignals, equityCurve,
baseConfig, params) always returns the same jobId (idempotent) — a request that omits
equityCurve, baseConfig or params dedupes exactly as it did before those fields
existed. Two different params vectors over one prepare are two different jobs, and 9
and 9.0 are the same one.
The re-run is an independent execution rather than a replay of the sweep trial — the two paths do not share a simulator — but they are pinned to agree: one vector run both ways matches on every leaderboard metric, asserted as a regression test. Treat a difference as a bug worth reporting, not as expected behaviour.
Optionally takes baseConfig, the same SweepBaseConfig shape executeSweep accepts —
initialFunding, feeRate, percentAmountToLock, etc. — so the same object can be reused
against either endpoint. This endpoint has one effective fee rate rather than a sweep's
independent buy/sell legs: a baseConfig that resolves to different buy/sell rates, or sets
a non-default feeLeg, is rejected with 400 rather than silently collapsed to one side.
Works unchanged for a dataset-backed prepare (exchangeId: user) — the request body is
identical either way, since the instrument and range are recovered from prepareJobId.
| exchangeId required | string Example: binance ID of the exchange for the backtesting process, or the reserved value |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker The type of data source to execute from |
Execute task parameters
| prepareJobId required | string Job ID returned by |
| strategyId required | string (strategyId) Unique identifier for a compiled strategy, derived from the source itself: the same code
always yields the same id, for every caller. How much formatting the id ignores depends on
the language — see |
| storeSignals | boolean Default: false When true, the worker uploads emitted signals to object storage and the
response includes |
object (EquityCurveOptions) Requested equity-curve transform, applied server-side in a fixed pipeline order: | |
object Capital/fee/position-size overrides for this one run — the same shape
| |
object <= 64 properties Strategy properties to apply to this run. Omit to run the strategy's declared defaults, which is exactly what a request without this field has always done. Each key is the Scalars only — number, string or boolean. Ranges and lists belong to
|
{- "prepareJobId": "13RBLGQlPnfDjO6wyKSX8i",
- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "storeSignals": false,
- "params": {
- "ema.fast.period": 9,
- "ema.slow.period": 21
}
}{- "jobId": "13RBLGQlPnfDjO6wyKSX8i"
}Requests cancellation of the specified execution. The execution
status will transition to Aborted once the cancellation is
processed. Cancellation is asynchronous — poll the GET endpoint
to confirm the final status.
| exchangeId required | string Example: binance |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker Managed exchange data sources available for backtesting.
|
| jobId required | string Example: 13RBLGQlPnfDjO6wyKSX8i Job ID returned by |
{- "status": "cancelling",
- "jobId": "13RBLGQlPnfDjO6wyKSX8i"
}Retrieves the current state and results of the execute job identified by jobId.
Poll until state.status is Completed, Failed, or Aborted.
A 202 means the result is not readable yet — keep polling. It is never a terminal
outcome, and it carries no state, so a poll loop that stops on a terminal status will
not stop on it.
| exchangeId required | string Example: binance ID of the exchange for the backtesting process |
| type required | string (DataSourceType) Enum: "ticker" "kline" "funding" Example: ticker The type of data source to execute from |
| jobId required | string Example: 13RBLGQlPnfDjO6wyKSX8i Job ID returned by |
{- "state": {
- "contextId": "ctx_2mnyblatqqw34kix2echpb",
- "status": "Completed",
- "statusDetail": null,
- "size": 73160,
- "completed": 73158,
- "startTime": "2026-03-18T13:21:28.958Z",
- "endTime": "2026-03-18T13:21:29.605Z"
}, - "results": {
- "hostName": "executor10",
- "iops": 123956.53,
- "strategyId": "strategy:00000000-0000-0000-0000-000000000000:2iyvtenlzh9dabqtxn7nbv",
- "instrument": "BTC/USDT",
- "notices": [
- {
- "level": "WARN",
- "code": "indicator.bar-data-on-ticker-path",
- "message": "Indicator requires bar data but is on the ticker path",
- "provenance": "execute"
}
], - "pnlTotal": 42.75,
- "pnlTotalPercent": 2.25,
- "totalTrades": 156,
- "winRate": 0.5833,
- "sharpeRatio": 1.245,
- "sortinoRatio": 1.872,
- "cagr": 0.1534,
- "maxDrawdown": 12.5,
- "maxDrawdownPercent": 8.75,
- "equityCurve": {
- "points": [
- {
- "timestamp": 1700000000000,
- "equity": 100
}, - {
- "timestamp": 1700000060000,
- "equity": 110.5
}, - {
- "timestamp": 1700000120000,
- "equity": 90.25
}
], - "meta": {
- "inputPointCount": 3,
- "outputPointCount": 3,
- "resampled": false,
- "differential": false,
- "outMode": "ARRAY"
}
}, - "signalCount": 100000,
- "signalsId": "00000000-0000-0000-0000-000000000000/exec/binance/3vsndwikcuaatjmb83fjtl",
- "signalsUpload": "Done",
- "signalsUploadedAt": "2026-03-18T13:21:48.170Z"
}
}The strategy endpoints allow users to submit, compile, and validate trading strategies within the platform. This feature is crucial for traders who develop automated trading systems and wish to ensure their strategy is both executable and effective before deploying it in a live/backtesting environment.
Key functionalities:
Every strategy you have registered and not deleted, most recently compiled first.
Each entry carries the same provenance GET /strategy/{strategyId} does — compiledAt,
requiredSources — but not its validation state, so listing stays cheap regardless of how
many strategies you have. Check a specific strategy's validation with GET /strategy/{strategyId}.
With includeDeleted=true, strategies you have deleted are listed too, each with the
deletedAt it was deleted at — useful to keep a copy of your list in sync, telling a
deleted strategy apart from one that never existed.
| includeDeleted | boolean Default: false
|
{- "strategies": [
- {
- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "compiledAt": "2026-08-19T10:15:00Z",
- "requiredSources": [
- "Ticker"
]
}, - {
- "strategyId": "2ul144qe9tlwzu5anhwvc6",
- "compiledAt": "2026-08-12T09:02:11Z"
}
]
}Compiles raw strategy source and registers it, returning its strategyId.
The source is either Java — a class extending a strategy base class — or QTScript
(beta), a compact strategy language whose braced bodies are plain Java. QTScript source
begins with the strategy keyword — whitespace and comments (// or /* */) before it
are ignored — and that is how the two are told apart: there is no separate endpoint and
no header to set. Once registered, a strategy is used the same way
whichever language it was written in.
This answers one question: is the source valid. It compiles, registers, and hands
back the id — nothing more. Whether the class can actually run is
POST /strategy/{strategyId}/validate, and everything known about a strategy, validation
included, is read from GET /strategy/{strategyId}. One place to ask, so there is no second
answer to keep in step.
A 200 means the source parsed and compiled, not that it will run. What only shows once the
strategy sets up its indicators is found by validate: for QTScript, a window on an indicator
name that is not registered (window nosuch m1 { ... }) registers, and validate ends failed
on that line.
For Java, the strategyId is derived from what the code means, not from how it is
written. Adding a comment, inserting a blank line, re-indenting, reordering imports, or
moving a method around all return the same id — you have not created a second strategy.
Renaming a variable, changing an identifier's case, reordering fields, or reordering
statements inside a method return a different one.
For QTScript, the id is derived from the text, because indentation is part of its grammar. Only differences that cannot change the strategy are ignored: a byte-order mark, the style of line endings, whitespace at the end of a line, and blank lines before the first and after the last line. Anything else — a comment, the indentation, a blank line in between — returns a different id.
Two rules follow, and they are worth designing around:
The response also lists declaredProperties — the sweep/execute param keys this strategy
is known to accept, so a caller can catch a typo'd key before submitting a sweep instead of
only learning it from a rejected one. See DeclaredProperty: best-effort, not exhaustive.
The raw strategy source code
Raw strategy source code, Java or QTScript
{- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "declaredProperties": [
- {
- "name": "rsi.period",
- "description": "RSI period",
- "defaultValue": "14",
- "reflected": true,
- "min": 2,
- "max": 50,
- "step": 1
}, - {
- "name": "enabled",
- "description": "Enabled",
- "reflected": true
}
]
}Instantiates the compiled class and drives it through a bounded synthetic series, so a wiring
fault surfaces here instead of at your first backtest. The verdict — pass or fail, plus any
engine notices — is recorded and served from GET /strategy/{strategyId}.
Idempotent. If a verdict already exists for the current compilation it comes straight
back with 200 and nothing is queued. Otherwise the check is queued and this returns 202;
poll GET /strategy/{strategyId} until validation is passed or failed.
Recompiling supersedes a verdict, which makes this callable again — the old answer described bytecode that is no longer what would run.
| strategyId required | string (strategyId) Example: 6bsh31ikwkuivhtgcoa6s4 The id returned by |
{- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "validation": "passed",
- "compiledAt": "2026-08-04T16:23:04Z",
- "requiredSources": [
- "Ticker"
], - "validatedAt": "2026-08-04T16:24:11Z",
- "notices": [
- {
- "level": "WARN",
- "code": "indicator.bar-data-on-ticker-path",
- "message": "Indicator requires bar data but is on the ticker path",
- "provenance": "compile-dry-run"
}
], - "_links": {
- "code": {
- "href": "/v1/strategy/6bsh31ikwkuivhtgcoa6s4/code"
}
}
}Reports that the strategy is registered — implied by a 200 at all — and what validating it
found.
A 404 means one thing: no such registered strategy for this user. It is never a stale or
expired answer; registration and verdict are stored durably, not cached.
| strategyId required | string (strategyId) Example: 6bsh31ikwkuivhtgcoa6s4 The id returned by |
{- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "validation": "passed",
- "compiledAt": "2026-08-04T16:23:04Z",
- "requiredSources": [
- "Ticker"
], - "validatedAt": "2026-08-04T16:24:11Z",
- "notices": [
- {
- "level": "WARN",
- "code": "indicator.bar-data-on-ticker-path",
- "message": "Indicator requires bar data but is on the ticker path",
- "provenance": "compile-dry-run"
}
], - "_links": {
- "code": {
- "href": "/v1/strategy/6bsh31ikwkuivhtgcoa6s4/code"
}
}
}Removes a strategy from GET /strategy/{strategyId} and GET /strategies. This is not
undone by re-submitting the same source to POST /strategy — that registers a new
strategy, with a new id.
Backtests you already ran against this strategy are unaffected. Deleting it stops it from counting against your account and stops you from validating or re-running it under this id — it does not erase what already happened.
Only removes a strategy you registered yourself. If you copied someone else's strategy (a shared/marketplace listing), deleting your copy never affects theirs, or anyone else's.
| strategyId required | string (strategyId) Example: 6bsh31ikwkuivhtgcoa6s4 The id returned by |
{- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "deleted": true
}The exact source you last submitted for this id — the same text POST /strategy derives
strategyId from, whitespace and comments included.
"If available", not "always". A strategy you resolve only through a shared/marketplace
listing you copied by reference carries no source of its own, and reads as a 404 here the
same as a strategyId you never registered — that is the honest answer either way, since
from this endpoint's point of view nothing is there to return.
| strategyId required | string (strategyId) Example: 6bsh31ikwkuivhtgcoa6s4 The id returned by |
{- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "code": "package strategy;\npublic class EmaCrossStrategy extends AbstractTickerStrategy { ... }\n"
}The dataset endpoints let you upload your own historical ticker data and backtest against
it the same way you would against a managed exchange — via the reserved exchangeId: user
value on the existing prepare/execute endpoints. This is for data QTSurfer doesn't
capture itself: your own exports, a venue not yet integrated, or a private feed.
Key functionalities:
Creates a dataset AND its first upload session in one call — a presigned URL your client
PUTs the file to directly, no API credentials involved in that PUT. Call
POST /datasets/{datasetId}/uploads/{uploadId}/finalize once the upload completes to kick
off ingest.
Losing this response loses nothing: calling this dataset's
POST /datasets/{datasetId}/uploads returns the very same upload session again rather than
opening a new one, as long as nothing has been finalized against it yet.
v1 is ticker data only — type is not a request field, it is always "ticker" in the
response. instrument must be a plain spot pair (BASE/QUOTE, exactly one /); derivative
forms (e.g. BTC/USDT:USDT) are rejected.
Upload format. A CSV with a header row, a parquet file with the same columns by
name, or a lastra file — our own native columnar format, the same one a dataset's
dataUrl hands back by default, so a downloaded dataset can be handed to another user to
upload with no conversion in between. For CSV/parquet, required: timestamp (ISO-8601, or
numeric epoch seconds/millis/micros — detected from the first row, then enforced for every
later row), close. Optional: open, high, low, volume, quoteVolume, bid,
bidSize, ask, askSize. A lastra upload carries its own fixed column set instead and
only needs a timestamp series and a close series present. Cadence and timestamp unit are
discovered from the data, not declared, for all three.
A CSV upload is converted to our native columnar format (lastra) for storage. A parquet
or lastra upload is stored as-is today. Either way, always check dataFormat on
GET /datasets/{datasetId} and GET /datasets/{datasetId}/uploads/{uploadId} for which
one dataUrl actually is, rather than assuming from how you uploaded it (a converted CSV
and an uploaded lastra file both report dataFormat: "lastra").
The bytes PUT to upload.url may be that file directly, gzipped (.gz), or zipped
(.zip, exactly one file inside — a dataset is one file regardless of how it travels).
Format is detected from the decompressed content itself: there is no filename or
Content-Type anywhere in this flow for a client to declare it with, so nothing needs to
be sent besides the bytes.
The dataset to create
| name required | string A name unique among your datasets. |
| instrument required | string (Instrument) Exchange instrument identifier (e.g. a currency pair) |
{- "name": "My BTC ticks",
- "instrument": "BTC/USDT"
}{- "datasetId": "ds_3f9a1c2e7b0d4a5f",
- "name": "My BTC ticks",
- "type": "ticker",
- "instrument": "BTC/USDT",
- "uploadId": "up_1a2b3c4d5e6f7a8b",
- "upload": {
- "expiresInMinutes": 15
}
}Every dataset you have created and not deleted, most recently created first. Never a 404
— an empty array if you have none, same convention as GET /strategies.
With includeDeleted=true, datasets you have deleted are listed too, each with the
deletedAt it was deleted at — useful to keep a copy of your list in sync, telling a
deleted dataset apart from one that never existed.
| includeDeleted | boolean Default: false
|
{- "datasets": [
- {
- "datasetId": "ds_3f9a1c2e7b0d4a5f",
- "name": "My BTC ticks",
- "type": "ticker",
- "instrument": "BTC/USDT",
- "createdAt": "2026-08-20T09:00:00Z",
- "currentVersionId": "dsv_8e2b4f19c6a03d7e",
- "updatedAt": "2026-08-20T09:04:12Z",
- "from": "2026-03-01T00:00:00Z",
- "to": "2026-03-08T00:00:00Z",
- "cadence": "1m",
- "timestampUnit": "iso",
- "status": "ready",
- "bytes": 4831022,
- "rows": 86400,
- "gaps": 0,
- "largestGapSteps": 0
}
]
}Detail for one dataset, plus a self link.
| datasetId required | string Example: ds_3f9a1c2e7b0d4a5f The id returned by |
{- "datasetId": "ds_3f9a1c2e7b0d4a5f",
- "name": "My BTC ticks",
- "type": "ticker",
- "instrument": "BTC/USDT",
- "createdAt": "2026-08-20T09:00:00Z",
- "currentVersionId": "dsv_8e2b4f19c6a03d7e",
- "updatedAt": "2026-08-20T09:04:12Z",
- "from": "2026-03-01T00:00:00Z",
- "to": "2026-03-08T00:00:00Z",
- "cadence": "1m",
- "timestampUnit": "iso",
- "status": "ready",
- "bytes": 4831022,
- "rows": 86400,
- "gaps": 0,
- "largestGapSteps": 0,
- "dataFormat": "lastra",
- "_links": {
- "self": {
- "href": "/v1/datasets/ds_3f9a1c2e7b0d4a5f"
}
}
}Soft-delete — the dataset stops appearing in GET /datasets/GET /datasets/{datasetId} and
can no longer be prepared from, but its object data is reclaimed later rather than purged
inline, so a backtest already running against one of its versions is not disrupted.
| datasetId required | string Example: ds_3f9a1c2e7b0d4a5f The id returned by |
{- "datasetId": "ds_3f9a1c2e7b0d4a5f",
- "deleted": true
}Get a fresh presigned URL to upload a new version into a dataset you already have — a
corrected file, or the next chunk of history. Behaves the same way POST /datasets does
for a brand-new dataset's own upload: at most one upload session is open per dataset at a
time, so calling this again before finalizing just hands back that same session rather
than opening a second one — safe to call repeatedly if a response gets lost.
Once a session has been finalized (successfully or not), the next call here opens a genuinely new one for that dataset's next version.
| datasetId required | string Example: ds_3f9a1c2e7b0d4a5f The id returned by |
{- "uploadId": "up_1a2b3c4d5e6f7a8b",
- "upload": {
- "expiresInMinutes": 15
}
}Call once the file has been PUT to the upload.url from POST /datasets (or from
POST /datasets/{datasetId}/uploads). Enqueues ingest and returns immediately; poll
GET /datasets/{datasetId}/uploads/{uploadId} for the result.
Idempotent while the upload is still open — a repeat finalize before it has produced a
version returns the same jobId rather than enqueueing a second ingest. Once it HAS
produced a version, uploadId is spent: finalizing it again is a 409, even with
different bytes freshly PUT to the same URL — open a new upload session instead
(POST /datasets/{datasetId}/uploads) rather than reusing a spent one.
| datasetId required | string Example: ds_3f9a1c2e7b0d4a5f The id returned by |
| uploadId required | string Example: up_1a2b3c4d5e6f7a8b The |
{- "jobId": "dataset-upload:00000000-.../ds_3f9a1c2e7b0d4a5f:up_1a2b3c4d5e6f7a8b"
}Poll after POST .../finalize until status is ready or failed. Also reports
uploading (finalize not called yet, but the file was PUT) before you finalize at all.
| datasetId required | string Example: ds_3f9a1c2e7b0d4a5f The id returned by |
| uploadId required | string Example: up_1a2b3c4d5e6f7a8b The |
{- "uploadId": "up_1a2b3c4d5e6f7a8b",
- "status": "ready",
- "version": {
- "datasetId": "ds_3f9a1c2e7b0d4a5f",
- "id": "dsv_8e2b4f19c6a03d7e",
- "bytes": 4831022,
- "rows": 86400,
- "cadence": "1s",
- "timestampUnit": "iso",
- "gaps": 0,
- "largestGapSteps": 0,
- "dataFormat": "lastra"
}
}A second way to get data into a dataset, alongside POST /datasets: instead of PUTting a
file yourself, ask the API to go fetch history on your behalf. Creates the dataset and
starts the fetch in the same call — there is no separate upload step, and the result lands
as a dataset version indistinguishable from an uploaded one once it's ready. Poll
GET /datasets/{datasetId}/imports/{importId} for progress.
type selects the source. dex — history over a pool/pair's own on-chain market — is the
only value today; other source types join this same endpoint later.
A dex import has two data shapes, chosen by the top-level cadence:
cadence is rt
unless the swaps happen to sit on a fixed grid (see DatasetVersion.cadence).1s / 1m / 5m — pre-aggregated candles at that width instead of raw trades.
The resulting dataset's type is klines, not ticker. Not every network supports every
cadence yet — an unsupported combination fails asynchronously, same as an unresolvable
pool (see the failed status on the poll endpoint below).What to fetch, and where from
| name required | string A name unique among your datasets. |
| instrument required | string (Instrument) Exchange instrument identifier (e.g. a currency pair) |
| from required | string <date-time> Start of the range to fetch, inclusive. Must be before |
| to required | string <date-time> End of the range to fetch, exclusive. The total span is capped by your tier — a
request wider than that ceiling is |
| cadence | string Enum: "1s" "1m" "5m" Optional. Omitted/blank keeps native per-trade event cadence — each swap at its own
timestamp, so the resulting version's |
| type required | string Value: "dex" The source to fetch from. |
object (DatasetImportDexRequest) The |
{- "name": "weth-usdc-week",
- "instrument": "WETH/USDC",
- "from": "2026-08-01T00:00:00Z",
- "to": "2026-08-08T00:00:00Z",
- "type": "dex",
- "dex": {
- "network": "ethereum",
- "id": "uniswap",
- "version": "v3",
- "contract": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"
}
}{- "datasetId": "ds_3f9a1c2e7b0d4a5f",
- "importId": "imp_01j9z1x2y3z4a5b6c7d8e9f0g1",
- "jobId": "dataset-import:00000000-.../ds_3f9a1c2e7b0d4a5f:imp_01j9z1x2y3z4a5b6c7d8e9f0g1",
- "status": "fetching"
}Poll after POST /datasets/imports until status is ready or failed. An import spends
real time fetching from its source before anything is even staged — fetching is the one
status only an import ever reports; ingesting/ready/failed mean exactly what they do
on GET /datasets/{datasetId}/uploads/{uploadId}, since an import re-enters that same
ingest chain once it has fetched and staged its data.
| datasetId required | string Example: ds_3f9a1c2e7b0d4a5f The id returned by |
| importId required | string Example: imp_01j9z1x2y3z4a5b6c7d8e9f0g1 The |
{- "importId": "imp_01j9z1x2y3z4a5b6c7d8e9f0g1",
- "status": "ready",
- "version": {
- "datasetId": "ds_3f9a1c2e7b0d4a5f",
- "id": "dsv_8e2b4f19c6a03d7e",
- "bytes": 4831022,
- "rows": 604800,
- "cadence": "rt",
- "timestampUnit": "us",
- "gaps": 0,
- "largestGapSteps": 0,
- "dataFormat": "lastra"
}
}The live execution endpoints run your strategy continuously against a live market feed instead of a fixed historical window, publishing its signals as they happen. A run always starts in a short sandbox trial before promotion to live.
These endpoints cover the run's lifecycle (start, inspect, stop, change visibility) and its
runtime parameters. Consuming the run's own signal stream and updating parameters over a live
connection instead of polling is a WebSocket protocol built on top of these same endpoints —
see Live execution for the full flow: minting a connection token
(POST /live/token), the signal channel, and the live.params RPC. A run can also be
paper-traded — its signals executed in simulation, with equity and KPIs — see
Paper trading.
Key functionalities:
Starts your strategy against a live market feed. A new run always begins in the sandbox
stage — a short trial that compares an independent second execution against the first for
agreement — before it is eligible for promotion to the live stage where it actually
publishes signals other systems can act on. Poll GET /strategy/{strategyId}/live (or
PATCH/DELETE /live/{runId} once you have the runId) to watch stage move from
SANDBOX to LIVE.
Only one run per strategy at a time — starting again while one is already running is 409;
stop the current one first.
sources takes exactly one entry today (multi-source strategies are not supported yet).
type is ticker or kline; anything else is rejected. Both connect to the lightest
(fastest) cadence available for the exchange — today that is 1 tick/second on every
supported exchange; choosing among several cadences is not offered yet.
params is passed straight through to the strategy at start — the same free-form object
POST /strategy/{strategyId}/validate and the backtest endpoints already accept. To change
a parameter while the run is live, use PUT /live/{runId}/params instead; this endpoint
only sets the values a run starts with.
Consuming a run's own output — its signals, and updating its parameters over a live connection instead of polling — is a WebSocket protocol on top of these REST endpoints; see the "Live execution" guide linked from this tag's description for the full flow (minting a connection token, the channel and RPC method).
Paper trading. Pass a paper block to have the run's hints executed in simulation from
its first tick, as a backtest would execute them: fills, closed trades, equity and KPIs,
with one simulated account per quote currency. Omitted, the run has no paper trading. It
takes the same economics as a backtest's baseConfig plus where its output goes; read it
back with GET /live/{runId}/paper. A strategy that listens to its own execution events
(it overrides getExecutionCallback()) has no other execution venue, so it cannot start
without a paper block.
A plain WebSocket stream. Pass stream: true to get a secret URL for this run's signals that a simple
client, or a service that passes them on to others, can open as an ordinary WebSocket, from the sandbox stage
on. The URL is in the response (streamUrl) and in GET /strategy/{strategyId}/live; treat it like a
password. See the "Live execution" guide.
| strategyId required | string (strategyId) Example: 6bsh31ikwkuivhtgcoa6s4 The id returned by |
required | Array of objects (LiveSource) = 1 items |
object Strategy parameters to start with. Opaque key/value pairs — see this strategy's own | |
| visibility | string Default: "private" Enum: "private" "public" A |
| relay | boolean Default: false Request that this run's signals be relayed over its WebSocket channel, from its first signal — in the |
| stream | boolean Default: false Ask for a stream URL: a secret address that a simple client, or a service that passes your signals
on to others, can open as a plain WebSocket to receive this run's signals as they are produced, one JSON
text frame per signal, with none of the live-execution protocol around it. The URL comes back as
|
| name | string |
| description | string |
object (LivePaperConfig) Paper trading for this run: the same economics as a backtest's |
{- "sources": [
- {
- "venueType": "cx",
- "exchange": "binance",
- "segment": "spot",
- "type": "ticker",
- "instruments": [
- "BTC/USDT"
]
}
], - "visibility": "private",
- "name": "EMA cross",
- "paper": {
- "initialFunding": 1000,
- "feeRate": 0.001,
- "percentAmountToLock": 20
}
}{- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "runId": "5t5oAmQ4PD0lQRoCU58uE0",
- "visibility": "private",
- "stage": "SANDBOX",
- "state": "STARTING",
- "desired": "RUNNING",
- "sources": [
- {
- "venueType": "cx",
- "exchange": "binance",
- "segment": "spot",
- "type": "ticker",
- "instruments": [
- "BTC/USDT"
]
}
], - "params": { },
- "paramsVersion": 0,
- "relay": false,
- "startedAtMs": 1758330000000
}The run you last started for this strategy — its most complete state, including params
and the promotion gate once the sandbox trial has one to report. Returns the run's last
known state even after it has stopped; this endpoint never disappears history. When the run was started
with a stream, streamUrl is here too while the run is running and your plan allows it.
While the run is being executed it also carries stats, the run's latest counters (updates
processed, rate, instruments seen), refreshed about once a minute. Starting or stopping a run
does not return them; read them here.
| strategyId required | string (strategyId) Example: 6bsh31ikwkuivhtgcoa6s4 The id returned by |
{- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "runId": "string",
- "name": "string",
- "description": "string",
- "visibility": "private",
- "stage": "SANDBOX",
- "state": "string",
- "desired": "RUNNING",
- "reason": "string",
- "sources": [
- {
- "venueType": "cx",
- "exchange": "binance",
- "segment": "spot",
- "type": "ticker",
- "instruments": [
- "BTC/USDT"
]
}
], - "params": { },
- "paramsVersion": 0,
- "relay": true,
- "startedAtMs": 0,
- "gate": { },
- "paper": {
- "initialFunding": 100,
- "feeRate": 0.001,
- "buyFeeRate": 0,
- "sellFeeRate": 0,
- "feeLeg": "RECEIVED",
- "percentAmountToLock": 1,
- "output": "separate"
}, - "stats": {
- "processed": 0,
- "opsPerSecond": 0,
- "instrumentsSeen": 0,
- "asOfMs": 0,
- "progressedAtMs": 0,
- "stale": true
},
}Requests a stop. The run winds down at its own next check-in rather than instantly —
poll GET/PATCH .../live and expect state to remain RUNNING for a short window
after desired flips to STOPPED. Calling this again on an already-stopped run is not an
error; it returns the same (unchanged) state. A stopped run has no stream: its streamUrl, if it had one,
stops working and is not in this response.
| strategyId required | string (strategyId) Example: 6bsh31ikwkuivhtgcoa6s4 The id returned by |
{- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "runId": "string",
- "name": "string",
- "description": "string",
- "visibility": "private",
- "stage": "SANDBOX",
- "state": "string",
- "desired": "RUNNING",
- "reason": "string",
- "sources": [
- {
- "venueType": "cx",
- "exchange": "binance",
- "segment": "spot",
- "type": "ticker",
- "instruments": [
- "BTC/USDT"
]
}
], - "params": { },
- "paramsVersion": 0,
- "relay": true,
- "startedAtMs": 0,
- "gate": { },
- "paper": {
- "initialFunding": 100,
- "feeRate": 0.001,
- "buyFeeRate": 0,
- "sellFeeRate": 0,
- "feeLeg": "RECEIVED",
- "percentAmountToLock": 1,
- "output": "separate"
}, - "stats": {
- "processed": 0,
- "opsPerSecond": 0,
- "instrumentsSeen": 0,
- "asOfMs": 0,
- "progressedAtMs": 0,
- "stale": true
}
}Every run you have started, in any stage, desired state, or visibility — newest
first. Unlike GET /live/public, this is not filtered to RUNNING public runs: it is
the complete list of runs you own, including sandbox trials and stopped ones.
| cursor | string <= 64 characters The |
| limit | integer <= 100 Default: 20 Page size. Larger values are capped, not rejected. |
{- "runs": [
- {
- "strategyId": "6bsh31ikwkuivhtgcoa6s4",
- "runId": "5t5oAmQ4PD0lQRoCU58uE0",
- "name": "EMA cross",
- "visibility": "private",
- "stage": "LIVE",
- "state": "RUNNING",
- "desired": "RUNNING",
- "sources": [
- {
- "venueType": "cx",
- "exchange": "binance",
- "segment": "spot",
- "type": "ticker",
- "instruments": [
- "BTC/USDT"
]
}
], - "createdAtMs": 1758330000000,
- "startedAtMs": 1758330015000
}
], - "_links": {
- "next": {
- "href": "/v1/live?limit=20&cursor=5t5oAmQ4PD0lQRoCU58uE0"
}
}
}Every run whose owner marked it public, that has been promoted to live and is running —
anyone's, yours included, and listed without revealing who owns it. A public run still in
its sandbox trial is not listed. Most recently started first.
This is the only Live Execution endpoint that needs no Authorization header.
| cursor | string <= 64 characters The |
| limit | integer <= 100 Default: 20 Page size. Larger values are capped, not rejected. |
{- "runs": [
- {
- "runId": "5t5oAmQ4PD0lQRoCU58uE0",
- "name": "EMA cross",
- "sources": [
- {
- "venueType": "cx",
- "exchange": "binance",
- "segment": "spot",
- "type": "ticker",
- "instruments": [
- "BTC/USDT"
]
}
], - "state": "RUNNING",
- "createdAtMs": 1758330000000
}
], - "_links": {
- "next": {
- "href": "/v1/live/public?limit=20&cursor=5t5oAmQ4PD0lQRoCU58uE0"
}
}
}Owner only, addressed by runId directly rather than through its strategy: the run's own
canonical identity, which is what POST/GET/DELETE .../live, GET /live and
GET /live/public all hand out. Use it to re-read one specific run — including one you
stopped long ago or that is no longer your strategy's most recent — without paging through
GET /live.
Returns the same state as GET /strategy/{strategyId}/live (strategyId is the strategy's
id, and stats is there too), plus updatedAtMs: when the run last changed, from any cause — a
state change, a promotion, a parameter update, a stop. A refresh of stats is not a change of
the run and does not move updatedAtMs. A run that is public is reachable by others through
GET /live/public, never through this route.
| runId required | string The |
{- "strategyId": "0dyfzg599rfwngikrosiw2",
- "runId": "5t5oAmQ4PD0lQRoCU58uE0",
- "name": "EMA cross",
- "visibility": "private",
- "stage": "SANDBOX",
- "state": "RUNNING",
- "desired": "RUNNING",
- "sources": [
- {
- "venueType": "cx",
- "exchange": "binance",
- "segment": "spot",
- "type": "ticker",
- "instruments": [
- "BTC/USDT"
]
}
], - "params": { },
- "paramsVersion": 1,
- "relay": false,
- "startedAtMs": 1790790087954,
- "updatedAtMs": 1790790092722
}Owner only, addressed by runId directly rather than through its strategy — this is a
run's own canonical identity, independent of which strategy started it.
Setting visibility from public back to private also evicts anyone currently connected
to the run's live signal channel who is not its owner — best-effort; the change to this
record is not rolled back if that eviction fails.
| runId required | string The |
| visibility | string Enum: "private" "public" |
| name | string |
| description | string |
{- "visibility": "public"
}{- "runId": "5t5oAmQ4PD0lQRoCU58uE0",
- "name": "EMA cross",
- "visibility": "public",
- "stage": "LIVE",
- "state": "RUNNING",
- "sources": [
- {
- "venueType": "cx",
- "exchange": "binance",
- "segment": "spot",
- "type": "ticker",
- "instruments": [
- "BTC/USDT"
]
}
]
}Updates one or more parameters of a run while it stays live — unlike params on
POST /strategy/{strategyId}/live, which only sets the starting values. Owner only.
Every key in params must be one your strategy declares (see declaredProperties on
POST /strategy) as of the compilation this run is executing — recompiling the strategy
later never changes what an already-running instance accepts; start a new run for that.
The change does not take effect the instant this call returns: effectiveAtMs is the
earliest moment it is guaranteed to apply, a few seconds out, so both the REST and
WebSocket paths to this same update (see the "Live execution" guide) land on the exact same
value at the exact same moment.
| runId required | string |
required | object Must be non-empty; every key must be a property your strategy declares. |
{- "params": {
- "emaFastPeriod": "12"
}
}{- "runId": "5t5oAmQ4PD0lQRoCU58uE0",
- "paramsVersion": 2,
- "effectiveAtMs": 1758330015000
}Tells a running strategy something while it stays live, without restarting it — for a strategy that
implements the engine's CommandRequestHandler. Owner only.
A command is an event, not a stored setting: it is delivered once to every execution behind the run, at
the same market position, and nothing about it is written to the run's state. It is transient — a
replica that restarts replays only its recent market history, so a command from before that only reaches
one that was already running when it arrived. Anything the strategy needs to remember across a restart
belongs in a parameter (PUT /live/{runId}/params), which does have a stored value; a command does not.
The command's text is a plain string; an optional properties object of your own choosing travels
alongside it. Each entry lands as a top-level entry on the CommandRequest the handler receives —
request.get("<key>") in Java, $command.<key> sugar in QTScript — no key is off limits, since the
command's own text is kept separately.
| runId required | string |
| command required | string The command's text — non-blank. |
object An optional map of your own choosing, alongside command. Absent means none; when given, it must be a JSON object, and |
{- "command": "flatten",
- "properties": {
- "instrument": "BTC/USDT"
}
}{- "runId": "5t5oAmQ4PD0lQRoCU58uE0",
- "commandId": "0e3f2f1a-9c4b-4d3e-8a2f-6b7c5d4e3f21",
- "effectiveAtMs": 1758330015000
}Gives a run a new stream URL and retires the old one: connections open on the old URL are closed
within about 15 seconds and the old URL answers 404 from then on. Use it when the URL may have leaked.
Owner only, and only for a run that is running and was started with stream: true (the URL cannot be added
to a run later, and a revoked one cannot be brought back). The plan is checked again: a plan that may not
broadcast gets 429.
See the "Live execution" guide, "A plain WebSocket stream of a run".
| runId required | string |
{
}Revokes a run's stream URL for good: connections open on it are closed within about 15 seconds and it
answers 404 from then on. The run itself is not touched, and a stream cannot be added to it again; start
the run again with stream: true for a new URL. Owner only, and always allowed, whatever your plan or the
run's state. Repeating it is not an error.
| runId required | string |
{- "runId": "string",
- "revoked": true
}Returns one page of the signals a run has already produced, newest-last, optionally from a given time and narrowed to one or more instruments.
This is the counterpart to the real-time WebSocket channel: that channel only carries what
happens while you are connected, and only for a run that asked for relay. A run's signals
are recorded either way, so this endpoint serves them whether or not relay was ever on,
in both the sandbox and live stages — use it to catch up after a disconnect, to read a
run you never relayed, or to page back over what has already happened.
The available window moves. Signals are kept for a limited span, and the oldest are
continuously discarded as new ones arrive, so how far back you can read is not a fixed
number of hours: on a busy run it can be a good deal shorter. Every response carries
availableSinceMs, the oldest moment that can still be answered for. Asking for a
sinceMs older than that is not an error — you get everything from availableSinceMs
onwards, and that field tells you it happened.
A cursor can expire, and on a busy run it expires quickly. If the position a cursor
points at has since been discarded, the next page answers 410 rather than silently
serving a shortened page that looks complete. Treat that as a normal outcome: read
availableSinceMs from the error and start again from there.
Readable by the run's owner, and by anyone if the run is public and has reached the live
stage — the same rule the signal channel applies to a subscription. A sandbox run is read
by its owner only, whatever visibility it asked for.
| runId required | string The run whose signals to read. |
| sinceMs | integer <int64> Start from this moment (epoch ms). Omitted, or older than the available window, starts at |
| instrument | string Narrow to one or more instruments. Omitted, or |
| type | string Narrow to one or more signal types, comma-separated ( |
| cursor | string Continue from a previous page's |
| limit | integer [ 1 .. 100 ] Default: 20 Page size. |
{- "signals": [
- {
- "v": 1,
- "signalId": "5t5oAmQ4PD0lQRoCU58uE0-000042",
- "runId": "6TzAPiPpsOWwBLdLBZCxwH",
- "stage": "live",
- "paramsVersion": 2,
- "type": "hint",
- "kind": "BUY",
- "eventTsMs": 1758330012000,
- "emittedAtMs": 1758330012040,
- "instrument": {
- "exchange": "binance",
- "segment": "spot",
- "symbol": "BTC/USDT"
}, - "order": {
- "orderKind": "MARKET",
- "price": null,
- "amount": null,
- "stopPrice": null,
- "trailPct": null
}, - "data": { },
- "regenerated": false,
- "digest": "9f2c…"
}
], - "availableSinceMs": 1757725212000,
- "_links": {
- "next": {
- "href": "/v1/live/6TzAPiPpsOWwBLdLBZCxwH/signals?cursor=eyJzZXEiOjQyfQ&limit=20"
}
}
}The run's paper trading as last recorded: one entry per simulated account (one per quote currency the run trades — never added together), with its starting capital, current equity, realised PnL, open positions and KPIs. The KPIs are the same a backtest reports, computed over the trades closed so far.
equity is the account's latest recorded value: at the last closed trade (equityKind: equity), or the last periodic mark-to-market while positions are open (equityKind: mark, taken every minute of market time). Until either exists the account holds its
starting capital.
Readable by the run's owner, and by anyone if the run is public and has reached the live
stage. A run started without a paper block answers 404.
| runId required | string |
{- "runId": "6TzAPiPpsOWwBLdLBZCxwH",
- "stage": "SANDBOX",
- "accounts": [
- {
- "currency": "USDT",
- "initialFunding": 1000,
- "equity": 996.4,
- "equityAtMs": 1758330060000,
- "equityKind": "mark",
- "realisedPnl": -2.1,
- "trades": 14,
- "gaps": 0,
- "openPositions": [
- {
- "instrument": "BTC/USDT",
- "base": 0.0023,
- "cost": 194.2
}
], - "kpi": {
- "totalTrades": 14,
- "winCount": 6,
- "lossCount": 8,
- "winRate": 0.4286,
- "pnlTotal": -2.1,
- "pnlTotalPercent": -0.21,
- "sharpeRatio": -0.08,
- "sortinoRatio": -0.11,
- "cagr": -0.41,
- "maxDrawdown": 3.4,
- "maxDrawdownPercent": 0.34
}
}
]
}The run's paper equity curve, oldest first, page by page. It is kept for the life of the
run, so unlike signals it has no moving window. Points are equity at every closed trade,
mark every minute of market time while positions are open, and gap where the run was
restarted with positions open: those positions are not carried over, so the curve has no
value there.
currency narrows to one account; without it, every account's points come interleaved by
time, each carrying its currency.
Readable by the run's owner, and by anyone if the run is public and has reached the live
stage.
| runId required | string |
| currency | string One account's quote currency, e.g. |
| sinceMs | integer <int64> Start from this market time (epoch ms). Ignored when |
| cursor | string Continue from a previous page's |
| limit | integer [ 1 .. 1000 ] Default: 100 |
{- "points": [
- {
- "currency": "USDT",
- "kind": "equity",
- "eventTsMs": 1758330012000,
- "equity": 999.2
}, - {
- "currency": "USDT",
- "kind": "mark",
- "eventTsMs": 1758330060000,
- "equity": 996.4
}
], - "_links": {
- "next": {
- "href": "/v1/live/6TzAPiPpsOWwBLdLBZCxwH/paper/equity?cursor=eyJ0cyI6MTc1ODMzMDA2MDAwMH0&limit=2¤cy=USDT"
}
}
}Mints a short-lived token for the WebSocket connection used to receive a run's signals in
real time and to call live.params (the WebSocket form of PUT /live/{runId}/params) —
see the "Live execution" guide linked from this tag's description for the full protocol.
Carries no request body.
{- "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YWViOTljYi01NjcyLTQ1ZmItYWJiNi02NDU4NTk1NzJkMDAiLCJpYXQiOjE3OTAwOTkxNTksImV4cCI6MTc5MDA5OTc1OX0.signature",
- "expiresAtMs": 1758502200000
}