QTSurfer API (0.128.17)

Download OpenAPI specification:

QTSurfer backend services API

Auth

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:

  • Exchange an API key for a JWT carrying the caller's subscription tier.
  • Refresh the JWT before expiry without re-using the API key against any other endpoint.

Exchange API key for a short-lived JWT

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.

Authorizations:
apiKeyAuth

Responses

Response samples

Content type
application/json
{
  • "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI3NmI5MDIwMy0wM2MyLTQ2ZjYtYjM2Ni05OTQ0ZjE2N2U4MTgiLCJzY29wZXMiOltdLCJ0aWVyIjoiZnJlZSIsImlhdCI6MTc3OTczNTQ2MiwiZXhwIjoxNzc5NzM5MDYyfQ.signature",
  • "token_type": "Bearer",
  • "expires_in": 3600,
  • "scopes": [ ],
  • "tier": "free"
}

Account

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:

  • Get your tier and its limits (dataset count, dataset size, total storage).
  • Get your live storage usage — datasets, strategy-execution signals, and registered strategies all count against one shared total.

Get your account's identity and tier limits

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).

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "userId": "00000000-0000-0000-0000-000000000000",
  • "tier": "free",
  • "maxDatasets": 3,
  • "maxDatasetBytes": 52428800,
  • "maxTotalStorageBytes": 104857600,
  • "_links": {
    }
}

Get your live storage 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.

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "datasetsUsed": 2,
  • "datasetBytesUsed": 15728640,
  • "signalsUsed": 1,
  • "signalBytesUsed": 524288,
  • "strategiesUsed": 4,
  • "strategyBytesUsed": 40960,
  • "storageBytesUsed": 16293888,
  • "_links": {
    }
}

Exchange

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:

  • Retrieve the list of available exchanges on the platform.
  • Get the instruments available on a specific exchange (currency pairs, assets, etc.).

List the available exchanges

Responses

Response samples

Content type
application/json
[
  • {
    }
]

List an exchange's instruments (default spot segment)

"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.

path Parameters
exchangeId
required
string
Example: binance

ID of the exchange to retrieve instruments for

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "_links": {
    }
}

List an exchange segment's 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).

path Parameters
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

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "_links": {
    }
}

Download one hour of tickers for an instrument as a Lastra segment

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:

  • lastra-java — reference Java reader/writer with per-column codecs (ALP, Gorilla, delta-varint, ZSTD) and CRC32 integrity.
  • lastra-ts — TypeScript reader (~4 kB bundle, browser + Node.js).
  • duckdb-lastra — DuckDB extension for ad-hoc SQL over Lastra files.
  • lastra-convert — CLI + Java API for converting to/from Parquet, Reef, and CSV.
  • curl -OJ for offline dumps (the Content-Disposition header sets a descriptive filename).
path Parameters
exchangeId
required
string
Example: binance

ID of the exchange (e.g. binance).

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).

query Parameters
hour
required
string^\d{4}-\d{2}-\d{2}T\d{2}$
Example: hour=2026-01-15T10

Hour selector in YYYY-MM-DDTHH (UTC). The returned segment covers [HH:00:00Z, HH+1:00:00Z).

format
string
Default: "lastra"
Enum: "lastra" "parquet"
Example: format=lastra

Response wire format. lastra (default) returns raw Lastra bytes. parquet returns Parquet via on-the-fly conversion using lastra-convert.

Responses

Response samples

Content type
application/json
{
  • "code": 400,
  • "message": "Invalid request"
}

Download one hour of klines for an instrument as a Lastra segment

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.

path Parameters
exchangeId
required
string
Example: binance

ID of the exchange (e.g. binance).

base
required
string
Example: BTC

Base asset symbol.

quote
required
string
Example: USDT

Quote asset symbol.

query Parameters
hour
required
string^\d{4}-\d{2}-\d{2}T\d{2}$
Example: hour=2026-01-15T10

Hour selector in YYYY-MM-DDTHH (UTC). The returned segment covers [HH:00:00Z, HH+1:00:00Z).

format
string
Default: "lastra"
Enum: "lastra" "parquet"
Example: format=lastra

Response wire format. lastra (default) returns raw Lastra bytes. parquet returns Parquet via on-the-fly conversion.

Responses

Response samples

Content type
application/json
{
  • "code": 400,
  • "message": "Invalid request"
}

Backtesting

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:

  • Prepare historical market data for fast access.
  • Run simulations of trading strategies using prepared historical market data.
  • Assess performance and optimize strategies based on past results and metrics.

Prepare backtest data

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.

Authorizations:
bearerAuth
path Parameters
exchangeId
required
string
Example: binance

ID of the exchange to prepare the backtesting for (e.g. binance), or the reserved value user to prepare from a dataset you uploaded instead of a managed exchange.

type
required
string (DataSourceType)
Enum: "ticker" "kline" "funding"
Example: ticker

The type of data source to prepare from

Request Body schema: application/json
required

The required data to prepare a backtesting

instrument
string

Required unless exchangeId is the reserved value user, in which case send datasetId instead.

datasetId
string

Only for exchangeId: user: the id of a dataset created via POST /datasets, in place of instrument. Ignored against a managed exchange.

datasetVersionId
string

Only for exchangeId: user, and optional even then: pins a specific past version of the dataset instead of its current one. Defaults to the dataset's current version.

from
required
string

Start date for the preparation process. Supports the following formats:

  • ISO-8601 (e.g. 2024-12-14T23:59:59Z)
  • ISO DATE (e.g. 2024-12-14)
  • BASIC ISO DATE (e.g., 20241214)
to
required
string

End date for the preparation process. Supports the following formats:

  • ISO-8601 (e.g. 2024-12-14T23:59:59Z)
  • ISO DATE (e.g. 2024-12-14)
  • BASIC ISO DATE (e.g., 20241214)
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 400. What's accepted, and what omitting it means, depends on the source:

  • Managed exchange, ticker or funding — one of 1s, 5s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 1d, 1w, 1q; any other label returns 400. Omitted = 1s, the publisher's native cadence.
  • Managed exchange, kline — one of 1s, 1m, 5m, 15m, 30m, 1h, 4h, 1d; any other label, including 5s, 3m and 8h, returns 400 and names the accepted ones. Omitted = 1s. This is the width of the bars a run reads: it is chosen here, once, and the same strategy can be run at several cadences by preparing the range at each.
  • Dataset (exchangeId: user) — omitted = the dataset version's own discovered cadence (see DatasetVersion.cadence), served as-is. Any cadence equal to or coarser than it and an exact multiple of it is accepted, including ones outside the managed-exchange list (e.g. 15s); an rt dataset can be resampled to any fixed cadence.

Responses

Request samples

Content type
application/json
{
  • "instrument": "BTC/USDT",
  • "from": "2024-12-13T00:00:00Z",
  • "to": "2024-12-14T00:00:00Z",
  • "cadence": "1m"
}

Response samples

Content type
application/json
{
  • "jobId": "13RBLGQlPnfDjO6wyKSX8i"
}

Get the status of a prepare job

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.

Authorizations:
bearerAuth
path Parameters
exchangeId
required
string
Example: binance

ID of the exchange for the backtesting process, or the reserved value user for a dataset-backed prepare.

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 POST /prepare

Responses

Response samples

Content type
application/json
{
  • "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": [
    ]
}

Execute a parameter sweep over prepared data

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.

Authorizations:
bearerAuth
path Parameters
exchangeId
required
string
Example: binance
type
required
string (DataSourceType)
Enum: "ticker" "kline" "funding"
Example: ticker

Managed exchange data sources available for backtesting.

  • ticker — trades. Can be prepared, executed and swept.
  • kline — aggregated bars (candlesticks). Can be prepared, executed and swept. A run reads bars of the cadence the data was prepared at: you choose the bar width when you prepare, and the strategy does not fix it. See PrepareRequest.cadence for the accepted values.
  • funding — funding rates. Can be prepared but not executed or swept yet: a funding request to execute or executeSweep is rejected with 400 before anything is queued, and the message names the sources that can be run.
requestId
required
string

Job ID returned by POST /backtest/{exchangeId}/{type}/prepare.

Request Body schema: application/json
required
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 POST /strategy for exactly which rewrites preserve it and which do not.

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 walkForward section; absent, nothing about the sweep changes. Two requests that differ only in this block are two different sweeps and do not deduplicate against each other.

object (EquityCurveRequest)

Selection (mode/n/maxPct) plus the transform preference (resample/differential/outMode) applied by GET .../equityCurve whenever ITS OWN query params are absent, for a curve this sweep retained. The transform half never affects retention or sweepId — a caller can always override it per-request at read time regardless of what was submitted here.

Responses

Request samples

Content type
application/json
{
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "sweep": {
    },
  • "baseConfig": {
    },
  • "storeSignals": false,
  • "shards": 0,
  • "minTradeFloor": 30,
  • "walkForward": {
    },
  • "equityCurve": {
    }
}

Response samples

Content type
application/json
{
  • "sweepId": "swp_95e47a7f0966ce11",
  • "requestId": "string",
  • "totalRuns": 1,
  • "shards": 1,
  • "seed": -9007199254740991,
  • "queued": true,
  • "walkForward": {
    }
}

Get sweep progress and results

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.

Authorizations:
bearerAuth
path Parameters
exchangeId
required
string
Example: binance
type
required
string (DataSourceType)
Enum: "ticker" "kline" "funding"
Example: ticker

Managed exchange data sources available for backtesting.

  • ticker — trades. Can be prepared, executed and swept.
  • kline — aggregated bars (candlesticks). Can be prepared, executed and swept. A run reads bars of the cadence the data was prepared at: you choose the bar width when you prepare, and the strategy does not fix it. See PrepareRequest.cadence for the accepted values.
  • funding — funding rates. Can be prepared but not executed or swept yet: a funding request to execute or executeSweep is rejected with 400 before anything is queued, and the message names the sources that can be run.
requestId
required
string

The jobId returned by POST /backtest/{exchangeId}/{type}/prepare.

sweepId
required
string
query Parameters
objective
string
Enum: "sharpe" "sortino" "pnl" "maxdd"
order
string
Default: "ranked"
Enum: "ranked" "natural"

natural is stable materialisation order; ranked is the display view.

ranking
string
Default: "plateau"
Enum: "plateau" "raw"

How the ranked view is ordered. plateau prefers points whose neighbourhood also scores well; raw uses the objective alone. Ignored when order=natural, which is always ordered by runIx.

Responses

Response samples

Content type
application/json
{
  • "sweepId": "string",
  • "status": "RUNNING",
  • "objective": "sharpe",
  • "order": "ranked",
  • "ranking": "plateau",
  • "pbo": 0,
  • "pboSplits": 0,
  • "failReason": "Failed to load/configure strategy",
  • "progress": {
    },
  • "leaderboardSize": 0,
  • "truncated": true,
  • "leaderboard": [
    ],
  • "walkForward": {
    },
  • "state": {
    }
}

Cancel a running parameter sweep

Requests cancellation between parameter vectors. Completed rows remain readable.

Authorizations:
bearerAuth
path Parameters
exchangeId
required
string
Example: binance
type
required
string (DataSourceType)
Enum: "ticker" "kline" "funding"
Example: ticker

Managed exchange data sources available for backtesting.

  • ticker — trades. Can be prepared, executed and swept.
  • kline — aggregated bars (candlesticks). Can be prepared, executed and swept. A run reads bars of the cadence the data was prepared at: you choose the bar width when you prepare, and the strategy does not fix it. See PrepareRequest.cadence for the accepted values.
  • funding — funding rates. Can be prepared but not executed or swept yet: a funding request to execute or executeSweep is rejected with 400 before anything is queued, and the message names the sources that can be run.
requestId
required
string

The jobId returned by POST /backtest/{exchangeId}/{type}/prepare.

sweepId
required
string

Responses

Response samples

Content type
application/json
{
  • "status": "cancelling",
  • "sweepId": "string"
}

Get sweep sensitivity surfaces

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.

Authorizations:
bearerAuth
path Parameters
exchangeId
required
string
Example: binance
type
required
string (DataSourceType)
Enum: "ticker" "kline" "funding"
Example: ticker

Managed exchange data sources available for backtesting.

  • ticker — trades. Can be prepared, executed and swept.
  • kline — aggregated bars (candlesticks). Can be prepared, executed and swept. A run reads bars of the cadence the data was prepared at: you choose the bar width when you prepare, and the strategy does not fix it. See PrepareRequest.cadence for the accepted values.
  • funding — funding rates. Can be prepared but not executed or swept yet: a funding request to execute or executeSweep is rejected with 400 before anything is queued, and the message names the sources that can be run.
requestId
required
string

The jobId returned by POST /backtest/{exchangeId}/{type}/prepare.

sweepId
required
string
query Parameters
objective
string
Enum: "sharpe" "sortino" "pnl" "maxdd"

Which metric to aggregate. Defaults to the objective the sweep was submitted with.

Responses

Response samples

Content type
application/json
{
  • "sweepId": "string",
  • "status": "RUNNING",
  • "objective": "sharpe",
  • "rowsAnalysed": 0,
  • "marginals": [
    ],
  • "heatmaps": [
    ],
  • "heatmapsTruncated": true
}

Get one sweep trial's equity curve

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.

Authorizations:
bearerAuth
path Parameters
exchangeId
required
string
Example: binance
type
required
string (DataSourceType)
Enum: "ticker" "kline" "funding"
Example: ticker

Managed exchange data sources available for backtesting.

  • ticker — trades. Can be prepared, executed and swept.
  • kline — aggregated bars (candlesticks). Can be prepared, executed and swept. A run reads bars of the cadence the data was prepared at: you choose the bar width when you prepare, and the strategy does not fix it. See PrepareRequest.cadence for the accepted values.
  • funding — funding rates. Can be prepared but not executed or swept yet: a funding request to execute or executeSweep is rejected with 400 before anything is queued, and the message names the sources that can be run.
requestId
required
string

The jobId returned by POST /backtest/{exchangeId}/{type}/prepare.

sweepId
required
string
runIx
required
integer >= 0

The trial's runIx, as it appears on its leaderboard row.

query Parameters
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.

Responses

Response samples

Content type
application/json
{
  • "meta": {
    },
  • "points": [
    ],
  • "timestamps": [
    ],
  • "equities": [
    ],
  • "url": "/v1/backtest/binance/ticker/executeSweep/req-1/swp_test/runs/3/equityCurve"
}

Execute a compiled strategy against a prepared dataset

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.

Authorizations:
bearerAuth
path Parameters
exchangeId
required
string
Example: binance

ID of the exchange for the backtesting process, or the reserved value user if prepareJobId came from a dataset-backed prepare.

type
required
string (DataSourceType)
Enum: "ticker" "kline" "funding"
Example: ticker

The type of data source to execute from

Request Body schema: application/json
required

Execute task parameters

prepareJobId
required
string

Job ID returned by POST /prepare (must be in Completed state)

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 POST /strategy for exactly which rewrites preserve it and which do not.

storeSignals
boolean
Default: false

When true, the worker uploads emitted signals to object storage and the response includes signalsUrl / signalsId fields. Defaults to false.

object (EquityCurveOptions)

Requested equity-curve transform, applied server-side in a fixed pipeline order: resample (point count) then differential (encoding) then outMode (JSON shape) — each stage assumes the previous one already ran. A server-side size guard can still force a smaller/deflated shape above its thresholds regardless of what is requested here — see EquityCurveMeta for what actually happened.

object

Capital/fee/position-size overrides for this one run — the same shape executeSweep's baseConfig accepts. Omit to run at the platform defaults (initialFunding: 100, feeRate: 0.001). buyFeeRate/sellFeeRate/feeLeg are accepted for shape compatibility with a sweep's baseConfig, but this endpoint has one fee-rate slot: a value that implies asymmetric buy/sell fees, or a non-default feeLeg, is rejected with 400.

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 name declared on the strategy's @StrategyProperty, which need NOT match the Java field it annotates — GET/POST /strategy returns declaredProperties for precisely this. A key naming no declared property is rejected: the job fails with the list of names the strategy does declare, rather than completing at the defaults and handing back a plausible result for parameters nobody chose.

Scalars only — number, string or boolean. Ranges and lists belong to executeSweep; one request here is one run. null is not a value: leave the key out to keep a property at its default. Keys are made of letters, digits, _, - and dots, and may not be strategyId, storeSignals, equityCurve, backtestEnabled or backtestFakeExecution — those configure the job rather than the strategy.

Responses

Request samples

Content type
application/json
{
  • "prepareJobId": "13RBLGQlPnfDjO6wyKSX8i",
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "storeSignals": false,
  • "params": {
    }
}

Response samples

Content type
application/json
{
  • "jobId": "13RBLGQlPnfDjO6wyKSX8i"
}

Cancel a running backtest execution

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.

Authorizations:
bearerAuth
path Parameters
exchangeId
required
string
Example: binance
type
required
string (DataSourceType)
Enum: "ticker" "kline" "funding"
Example: ticker

Managed exchange data sources available for backtesting.

  • ticker — trades. Can be prepared, executed and swept.
  • kline — aggregated bars (candlesticks). Can be prepared, executed and swept. A run reads bars of the cadence the data was prepared at: you choose the bar width when you prepare, and the strategy does not fix it. See PrepareRequest.cadence for the accepted values.
  • funding — funding rates. Can be prepared but not executed or swept yet: a funding request to execute or executeSweep is rejected with 400 before anything is queued, and the message names the sources that can be run.
jobId
required
string
Example: 13RBLGQlPnfDjO6wyKSX8i

Job ID returned by POST /execute

Responses

Response samples

Content type
application/json
{
  • "status": "cancelling",
  • "jobId": "13RBLGQlPnfDjO6wyKSX8i"
}

Get the result of a backtest execution job

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.

Authorizations:
bearerAuth
path Parameters
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 POST /execute

Responses

Response samples

Content type
application/json
{
  • "state": {
    },
  • "results": {
    }
}

Strategy

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:

  • Submit rich Java™ based trading strategies — or QTScript (beta) ones — for validation and compilation.
  • Ensure strategies meet the required syntax and operational constraints.
  • Validate strategies to identify potential issues or errors before execution.
  • Integrate strategies with specific exchanges and market instruments for tailored use.

List your registered strategies

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.

Authorizations:
bearerAuth
query Parameters
includeDeleted
boolean
Default: false

true also lists the strategies you have deleted, each with its deletedAt. Any other value, or leaving it out, lists only the ones you have not deleted.

Responses

Response samples

Content type
application/json
{
  • "strategies": [
    ]
}

Compile and register a strategy

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:

  • re-submitting a Java strategy you have only reformatted is free, and gives you back the id you already had, along with any validation already recorded against it;
  • the id says nothing about behaviour. Two sources that compute the same thing by different means are two strategies, because deciding otherwise would mean deciding program equivalence.

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.

Authorizations:
bearerAuth
Request Body schema: text/plain
required

The raw strategy source code

string

Raw strategy source code, Java or QTScript

Responses

Response samples

Content type
application/json
{
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "declaredProperties": [
    ]
}

Check that a registered strategy can actually run

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.

Authorizations:
bearerAuth
path Parameters
strategyId
required
string (strategyId)
Example: 6bsh31ikwkuivhtgcoa6s4

The id returned by POST /strategy

Responses

Response samples

Content type
application/json
{
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "validation": "passed",
  • "compiledAt": "2026-08-04T16:23:04Z",
  • "requiredSources": [
    ],
  • "validatedAt": "2026-08-04T16:24:11Z",
  • "notices": [
    ],
  • "_links": {
    }
}

Get a strategy by id, including its validation state

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.

Authorizations:
bearerAuth
path Parameters
strategyId
required
string (strategyId)
Example: 6bsh31ikwkuivhtgcoa6s4

The id returned by POST /strategy

Responses

Response samples

Content type
application/json
{
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "validation": "passed",
  • "compiledAt": "2026-08-04T16:23:04Z",
  • "requiredSources": [
    ],
  • "validatedAt": "2026-08-04T16:24:11Z",
  • "notices": [
    ],
  • "_links": {
    }
}

Release a registered strategy

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.

Authorizations:
bearerAuth
path Parameters
strategyId
required
string (strategyId)
Example: 6bsh31ikwkuivhtgcoa6s4

The id returned by POST /strategy

Responses

Response samples

Content type
application/json
{
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "deleted": true
}

Get a registered strategy's source, if you still have one to read

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.

Authorizations:
bearerAuth
path Parameters
strategyId
required
string (strategyId)
Example: 6bsh31ikwkuivhtgcoa6s4

The id returned by POST /strategy

Responses

Response samples

Content type
application/json
{
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "code": "package strategy;\npublic class EmaCrossStrategy extends AbstractTickerStrategy { ... }\n"
}

Dataset

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:

  • Create a dataset and get a presigned URL to upload a CSV or parquet file directly to object storage.
  • Finalize an upload to trigger validated ingest (column contract check, cadence and timestamp unit discovery, gap detection) and poll its result.
  • List and inspect your datasets, including their current ingested version.
  • Prepare and execute backtests against a dataset exactly like against a managed exchange.
  • Delete a dataset you no longer need.

Create a dataset and get a URL to upload it to

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required

The dataset to create

name
required
string

A name unique among your datasets. 409 if already taken.

instrument
required
string (Instrument)

Exchange instrument identifier (e.g. a currency pair)

Responses

Request samples

Content type
application/json
{
  • "name": "My BTC ticks",
  • "instrument": "BTC/USDT"
}

Response samples

Content type
application/json
{}

List your datasets

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.

Authorizations:
bearerAuth
query Parameters
includeDeleted
boolean
Default: false

true also lists the datasets you have deleted, each with its deletedAt. Any other value, or leaving it out, lists only the ones you have not deleted.

Responses

Response samples

Content type
application/json
{
  • "datasets": [
    ]
}

Get a dataset by id

Detail for one dataset, plus a self link.

Authorizations:
bearerAuth
path Parameters
datasetId
required
string
Example: ds_3f9a1c2e7b0d4a5f

The id returned by POST /datasets

Responses

Response samples

Content type
application/json
{}

Delete a dataset

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.

Authorizations:
bearerAuth
path Parameters
datasetId
required
string
Example: ds_3f9a1c2e7b0d4a5f

The id returned by POST /datasets

Responses

Response samples

Content type
application/json
{
  • "datasetId": "ds_3f9a1c2e7b0d4a5f",
  • "deleted": true
}

Open a new upload session for an existing dataset

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.

Authorizations:
bearerAuth
path Parameters
datasetId
required
string
Example: ds_3f9a1c2e7b0d4a5f

The id returned by POST /datasets

Responses

Response samples

Content type
application/json
{}

Finalize an uploaded file and start ingest

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.

Authorizations:
bearerAuth
path Parameters
datasetId
required
string
Example: ds_3f9a1c2e7b0d4a5f

The id returned by POST /datasets

uploadId
required
string
Example: up_1a2b3c4d5e6f7a8b

The uploadId returned by POST /datasets

Responses

Response samples

Content type
application/json
{
  • "jobId": "dataset-upload:00000000-.../ds_3f9a1c2e7b0d4a5f:up_1a2b3c4d5e6f7a8b"
}

Get the state of an upload/ingest

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.

Authorizations:
bearerAuth
path Parameters
datasetId
required
string
Example: ds_3f9a1c2e7b0d4a5f

The id returned by POST /datasets

uploadId
required
string
Example: up_1a2b3c4d5e6f7a8b

The uploadId returned by POST /datasets

Responses

Response samples

Content type
application/json
{}

Create a dataset by importing history instead of uploading it

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:

  • Omitted/blank (default) — on-chain swap history, replayed directly from the pool/pair's own chain. Cadence is native, not resampled: each swap keeps the timestamp it happened at rather than being bucketed into candles, so the resulting version's cadence is rt unless the swaps happen to sit on a fixed grid (see DatasetVersion.cadence).
  • One of 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).
Authorizations:
bearerAuth
Request Body schema: application/json
required

What to fetch, and where from

name
required
string

A name unique among your datasets. 409 if already taken.

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.

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 400, regardless of source type.

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 cadence is rt unless the swaps happen to sit on a fixed grid (see DatasetVersion.cadence). Set to 1s, 1m or 5m instead to get pre-aggregated candles at that width rather than raw trades (the resulting dataset's type becomes klines); any other value is 400. Not every network supports every cadence — an unsupported combination fails asynchronously, not at request time (see DatasetImportState.error).

type
required
string
Value: "dex"

The source to fetch from. dex is the only value today.

object (DatasetImportDexRequest)

The dex source's own fields — required when type is dex. id/version are required for a plain (native-cadence) import; both are ignored if the top-level cadence requested pre-aggregated candles instead, since that path needs neither a protocol nor a version distinction.

Responses

Request samples

Content type
application/json
Example
{
  • "name": "weth-usdc-week",
  • "instrument": "WETH/USDC",
  • "from": "2026-08-01T00:00:00Z",
  • "to": "2026-08-08T00:00:00Z",
  • "type": "dex",
  • "dex": {
    }
}

Response samples

Content type
application/json
{
  • "datasetId": "ds_3f9a1c2e7b0d4a5f",
  • "importId": "imp_01j9z1x2y3z4a5b6c7d8e9f0g1",
  • "jobId": "dataset-import:00000000-.../ds_3f9a1c2e7b0d4a5f:imp_01j9z1x2y3z4a5b6c7d8e9f0g1",
  • "status": "fetching"
}

Get the state of an import/ingest

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.

Authorizations:
bearerAuth
path Parameters
datasetId
required
string
Example: ds_3f9a1c2e7b0d4a5f

The id returned by POST /datasets/imports

importId
required
string
Example: imp_01j9z1x2y3z4a5b6c7d8e9f0g1

The importId returned by POST /datasets/imports

Responses

Response samples

Content type
application/json
{}

Live Execution

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:

  • Start, inspect, and stop a live run for one of your strategies.
  • Browse other users' runs they have made public.
  • Change a run's own visibility, name, or description.
  • Update a running strategy's parameters without restarting it.
  • Mint a token to receive a run's signals and updates in real time over WebSocket.

Start a strategy on a live market feed

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.

Authorizations:
bearerAuth
path Parameters
strategyId
required
string (strategyId)
Example: 6bsh31ikwkuivhtgcoa6s4

The id returned by POST /strategy

Request Body schema: application/json
required
required
Array of objects (LiveSource) = 1 items
object

Strategy parameters to start with. Opaque key/value pairs — see this strategy's own declaredProperties (from POST /strategy) for the keys it accepts.

visibility
string
Default: "private"
Enum: "private" "public"

A public run appears in GET /live/public and its signal channel accepts subscriptions from anyone, not only you — from the moment it is promoted to live. While it is a sandbox trial, public is only what you asked for, and only you can read it.

relay
boolean
Default: false

Request that this run's signals be relayed over its WebSocket channel, from its first signal — in the sandbox stage too, where only you can subscribe to it. See the "Live execution" guide.

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 streamUrl in the response to this call. Available from the sandbox stage on, on the plans that may broadcast; any other plan gets 429. It can be asked for only when the run is started (not added later), and it also turns relay on. See the "Live execution" guide, "A plain WebSocket stream of a run".

name
string
description
string
object (LivePaperConfig)

Paper trading for this run: the same economics as a backtest's baseConfig (same fields, defaults and limits), plus output. An empty object takes every default. Each quote currency the run trades gets its own simulated account, opened with initialFunding in that currency; accounts are never added together. As returned on a run, the block is normalised: feeRate is resolved into buyFeeRate/sellFeeRate and defaults are filled in.

Responses

Request samples

Content type
application/json
{
  • "sources": [
    ],
  • "visibility": "private",
  • "name": "EMA cross",
  • "paper": {
    }
}

Response samples

Content type
application/json
{
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "runId": "5t5oAmQ4PD0lQRoCU58uE0",
  • "visibility": "private",
  • "stage": "SANDBOX",
  • "state": "STARTING",
  • "desired": "RUNNING",
  • "sources": [
    ],
  • "params": { },
  • "paramsVersion": 0,
  • "relay": false,
  • "startedAtMs": 1758330000000
}

Get this strategy's current (or most recent) live run

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.

Authorizations:
bearerAuth
path Parameters
strategyId
required
string (strategyId)
Example: 6bsh31ikwkuivhtgcoa6s4

The id returned by POST /strategy

Responses

Response samples

Content type
application/json
{
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "runId": "string",
  • "name": "string",
  • "description": "string",
  • "visibility": "private",
  • "stage": "SANDBOX",
  • "state": "string",
  • "desired": "RUNNING",
  • "reason": "string",
  • "sources": [
    ],
  • "params": { },
  • "paramsVersion": 0,
  • "relay": true,
  • "startedAtMs": 0,
  • "gate": { },
  • "paper": {
    },
  • "stats": {
    },
  • "streamUrl": "http://example.com"
}

Stop this strategy's live run

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.

Authorizations:
bearerAuth
path Parameters
strategyId
required
string (strategyId)
Example: 6bsh31ikwkuivhtgcoa6s4

The id returned by POST /strategy

Responses

Response samples

Content type
application/json
{
  • "strategyId": "6bsh31ikwkuivhtgcoa6s4",
  • "runId": "string",
  • "name": "string",
  • "description": "string",
  • "visibility": "private",
  • "stage": "SANDBOX",
  • "state": "string",
  • "desired": "RUNNING",
  • "reason": "string",
  • "sources": [
    ],
  • "params": { },
  • "paramsVersion": 0,
  • "relay": true,
  • "startedAtMs": 0,
  • "gate": { },
  • "paper": {
    },
  • "stats": {
    }
}

List your own live runs

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.

Authorizations:
bearerAuth
query Parameters
cursor
string <= 64 characters

The runId from a previous page's _links.next.href. Omit for the first page.

limit
integer <= 100
Default: 20

Page size. Larger values are capped, not rejected.

Responses

Response samples

Content type
application/json
{
  • "runs": [
    ],
  • "_links": {
    }
}

Browse public live runs

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.

query Parameters
cursor
string <= 64 characters

The runId from a previous page's _links.next.href. Omit for the first page.

limit
integer <= 100
Default: 20

Page size. Larger values are capped, not rejected.

Responses

Response samples

Content type
application/json
{
  • "runs": [
    ],
  • "_links": {
    }
}

Read one of your runs by its id

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.

Authorizations:
bearerAuth
path Parameters
runId
required
string

The runId from POST/GET/DELETE .../live, from GET /live or from GET /live/public

Responses

Response samples

Content type
application/json
{
  • "strategyId": "0dyfzg599rfwngikrosiw2",
  • "runId": "5t5oAmQ4PD0lQRoCU58uE0",
  • "name": "EMA cross",
  • "visibility": "private",
  • "stage": "SANDBOX",
  • "state": "RUNNING",
  • "desired": "RUNNING",
  • "sources": [
    ],
  • "params": { },
  • "paramsVersion": 1,
  • "relay": false,
  • "startedAtMs": 1790790087954,
  • "updatedAtMs": 1790790092722
}

Change a run's visibility, name, or description

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.

Authorizations:
bearerAuth
path Parameters
runId
required
string

The runId from POST/GET/DELETE .../live or from GET /live/public

Request Body schema: application/json
visibility
string
Enum: "private" "public"
name
string
description
string

Responses

Request samples

Content type
application/json
{
  • "visibility": "public"
}

Response samples

Content type
application/json
{
  • "runId": "5t5oAmQ4PD0lQRoCU58uE0",
  • "name": "EMA cross",
  • "visibility": "public",
  • "stage": "LIVE",
  • "state": "RUNNING",
  • "sources": [
    ]
}

Change a running strategy's parameters

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.

Authorizations:
bearerAuth
path Parameters
runId
required
string
Request Body schema: application/json
required
required
object

Must be non-empty; every key must be a property your strategy declares.

Responses

Request samples

Content type
application/json
{
  • "params": {
    }
}

Response samples

Content type
application/json
{
  • "runId": "5t5oAmQ4PD0lQRoCU58uE0",
  • "paramsVersion": 2,
  • "effectiveAtMs": 1758330015000
}

Tell a running strategy a command

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.

Authorizations:
bearerAuth
path Parameters
runId
required
string
Request Body schema: application/json
required
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 and properties are the only keys the body may carry. Each entry lands as a top-level entry on the strategy's CommandRequest — no key is off limits, since the command's own text is kept separately.

Responses

Request samples

Content type
application/json
{
  • "command": "flatten",
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "runId": "5t5oAmQ4PD0lQRoCU58uE0",
  • "commandId": "0e3f2f1a-9c4b-4d3e-8a2f-6b7c5d4e3f21",
  • "effectiveAtMs": 1758330015000
}

Rotate a run's stream URL

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".

Authorizations:
bearerAuth
path Parameters
runId
required
string

Responses

Response samples

Content type
application/json
{}

Revoke a run's stream URL

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.

Authorizations:
bearerAuth
path Parameters
runId
required
string

Responses

Response samples

Content type
application/json
{
  • "runId": "string",
  • "revoked": true
}

Read a run's signals

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.

Authorizations:
bearerAuth
path Parameters
runId
required
string

The run whose signals to read.

query Parameters
sinceMs
integer <int64>

Start from this moment (epoch ms). Omitted, or older than the available window, starts at availableSinceMs. Ignored when cursor is given.

instrument
string

Narrow to one or more instruments. Omitted, or *, returns every instrument the run covers. Accepts a single pair (BTC/USDT), either half as a wildcard (*/USDT for any base against that quote, BTC/* for that base against any quote), or a comma-separated list of pairs (BTC/USDT,ETH/EUR). Symbols are matched exactly, case included — pass them as this API reports them.

type
string

Narrow to one or more signal types, comma-separated (hint, info, marker, command, paper). Combines with instrument.

cursor
string

Continue from a previous page's _links.next. Takes precedence over sinceMs.

limit
integer [ 1 .. 100 ]
Default: 20

Page size.

Responses

Response samples

Content type
application/json
{
  • "signals": [
    ],
  • "availableSinceMs": 1757725212000,
  • "_links": {
    }
}

Read a run's paper trading

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.

Authorizations:
bearerAuth
path Parameters
runId
required
string

Responses

Response samples

Content type
application/json
{
  • "runId": "6TzAPiPpsOWwBLdLBZCxwH",
  • "stage": "SANDBOX",
  • "accounts": [
    ]
}

Read a run's paper equity curve

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.

Authorizations:
bearerAuth
path Parameters
runId
required
string
query Parameters
currency
string

One account's quote currency, e.g. USDT.

sinceMs
integer <int64>

Start from this market time (epoch ms). Ignored when cursor is given.

cursor
string

Continue from a previous page's _links.next.

limit
integer [ 1 .. 1000 ]
Default: 100

Responses

Response samples

Content type
application/json
{
  • "points": [
    ],
  • "_links": {
    }
}

Mint a WebSocket connection token

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.

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YWViOTljYi01NjcyLTQ1ZmItYWJiNi02NDU4NTk1NzJkMDAiLCJpYXQiOjE3OTAwOTkxNTksImV4cCI6MTc5MDA5OTc1OX0.signature",
  • "expiresAtMs": 1758502200000
}