Live execution

Run a strategy continuously against a live market feed — stream its signals and update parameters over WebSocket.

Run a strategy continuously against a live market feed, watch its signals as they happen, and change its parameters without restarting it.

MethodPathPurpose
POST/strategy/{strategyId}/liveStart a strategy live
GET/strategy/{strategyId}/liveRead this strategy’s current (or last) run
DELETE/strategy/{strategyId}/liveStop it
GET/liveList your own runs
GET/live/{runId}Read one of your runs by its id
GET/live/publicBrowse runs other users made public
PATCH/live/{runId}Change visibility, name, or description
PUT/live/{runId}/paramsChange parameters while it stays live
POST/live/{runId}/commandsTell it a command while it stays live
GET/live/{runId}/signalsRead the signals it has already produced
GET/live/{runId}/paperRead its paper trading — see Paper trading
GET/live/{runId}/paper/equityPage through its paper equity curve — see Paper trading
POST/live/tokenMint a WebSocket connection token

Lifecycle: sandbox, then live

Starting a run (POST /strategy/{strategyId}/live) never puts it in front of anyone but you. It begins in the sandbox stage, a trial of 24 hours. During it the platform runs an independent second execution of your strategy beside the first and checks four things: that the run is processing market data, that its memory use and per-tick time stay within the platform’s allowance, that it does not hang or fail repeatedly, and that the two executions produce the same signals. Only you can read a sandbox run: over the WebSocket channel from its first signal if you asked for relay, and through the read routes either way (see Visibility) — and through a stream URL if you create one for it, which lets whoever you give it to read the run’s signals from the sandbox on.

A run that passes is promoted to live automatically when the 24 hours are up. There is no separate “promote” call and nothing for you to do while you wait. A run that does not pass is not promoted, and keeps running in the sandbox.

What you can watch while it waits, on GET/PATCH .../live:

  • stage is SANDBOX until the promotion and LIVE after it.
  • state is the run’s health right now (see State of a run).
  • gate is absent for the whole trial and appears when it ends, holding the verdict. An absent gate therefore means “the trial has not finished”, never “nobody is evaluating the run”. Its passed field is the verdict; the rest is diagnostic detail whose shape may change.

State of a run

state says what the run is doing. It is a string that may gain values, so read an unknown one as “running, with something to look at”.

stateWhat it means
STARTINGAccepted; no runner has reported on it yet.
RUNNINGRunning normally.
LAGGINGRunning, but behind the market data: usual while it catches up after starting or after a platform restart. It clears by itself.
HUNGYour strategy is stuck inside one call for longer than the platform allows. It clears when that call returns.
DEGRADEDThe run’s independent executions produced different signals from the same market data. The run keeps publishing. In the sandbox this counts against the trial: the run is not promoted.
FAILEDThe platform refused the run, could not start it, or the run failed while running. reason says why (see Why a run failed or stopped).
STOPPEDStopped, by you or by the platform (reason says so when it was for exceeding its resource allowance).

LAGGING, HUNG and DEGRADED are flags on a run that is otherwise running: they come and go, and the run’s signals keep flowing throughout. desired is what you last asked for (RUNNING or STOPPED), and state can trail it briefly.

Only one run per strategy at a time. Starting again while one is RUNNING is 409 — stop it first with DELETE.

A stop (DELETE) is a request, not an instant kill: desired flips to STOPPED immediately, but state can stay RUNNING for a short window while the run winds down. Calling DELETE again on an already-stopped run is not an error.

A failed run is final, and still holds its place

FAILED is final for that run: it is not processing data and nothing restarts it. To try again, fix what reason names and start a new run. Usually desired stays RUNNING until you stop the run yourself, and a run counts as active by its desired, not its state: a FAILED run still answers 409 to a new start of the same strategy and still counts toward your plan’s live-run limit. Call DELETE on it, then start again.

The exception is a run that can never run because of what it was started with: when its strategy cannot consume its source type, the platform stops it itself (desired becomes STOPPED, state stays FAILED, reason says why), so it does not hold a place.

Why a run failed or stopped

GET /strategy/{strategyId}/live, and each entry of GET /live, carry a reason when there is one to give. It is absent otherwise, and it is never a stack trace or an internal message: it is one of a fixed set of sentences, so a client can match on it.

reasonWhen
resource: ...The platform stopped the run for exceeding its resource allowance; the text says which limit.
The run could not start: its strategy cannot consume the source type it was started with.A ticker strategy started with a kline source, or the reverse. Start refuses this with a 400 (see Sources); it can only show up on a run created before that check existed.
The run could not start: its definition was refused.The run’s definition is not one the platform can run.
The run could not start after several attempts.A start that kept failing for a reason that was not yours. Start again.
The run stopped because its strategy failed while processing data.Your strategy’s own code brought the run down.
The run stopped because it lost its data feed.The run’s market data stream broke and the run stalled.
The run failed.Anything else.

The set may grow. Read an unrecognised sentence as “the run failed”, and do not parse it for detail: the text is for people.

What a run is doing: stats

While a run is being executed the platform keeps its latest counters and refreshes them about once a minute. GET /strategy/{strategyId}/live and GET /live/{runId} return them as stats:

"stats": {
  "processed": 18233,
  "opsPerSecond": 4.2,
  "instrumentsSeen": 12,
  "asOfMs": 1758330060000,
  "progressedAtMs": 1758330060000,
  "stale": false
}
FieldWhat it means
processedUpdates of instruments the run has accepted since it started executing. It can start again from zero if the run is restarted.
opsPerSecondUpdates accepted per second over the last refresh. An average over about a minute, so it does not jump from one update to the next. 0 when none arrived.
instrumentsSeenDistinct instruments the run has received an update for.
asOfMsWhen these counters were last written.
progressedAtMsThe last refresh in which processed had grown. Absent until the run has processed anything.
staletrue when the run is meant to be running and its counters have not been refreshed for several refresh intervals.

Three things to know:

  • stats is absent, not zero, when there is nothing yet: a run that has just started has no snapshot. Starting (POST) and stopping (DELETE) a run do not return it; read it with one of the two GETs above.
  • stale only says the platform stopped updating the counters. Check it against state. A run whose processed stays flat is not stale and is not broken: one fed by a source that updates rarely (a funding rate, for example) can stay flat for hours. progressedAtMs is how to tell such a run from one that has stopped.
  • A refresh of stats is not a change of the run. It does not move updatedAtMs.

Sources

sources takes exactly one entry (multi-source strategies are not supported yet):

{
  "sources": [
    {"venueType": "cx", "exchange": "binance", "segment": "spot", "type": "ticker", "instruments": ["BTC/USDT"]}
  ]
}

type has to match the kind of strategy: a ticker strategy runs on a ticker source and a kline strategy on a kline one, and a start with the other one is refused with 400, naming both. A QTScript strategy is a ticker strategy unless its header says otherwise (strategy "Name" kline); a Java one is whichever base class it extends (AbstractTickerStrategy or AbstractKlineStrategy).

type is ticker or kline. 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. instruments can be ["*"] for every instrument the exchange/segment offers, subject to your plan’s instrument-count limit.

Listing your runs

GET /live (needs a Bearer token) returns every run you have started — any stage, any desired state, any visibility — newest first, paged the same way as GET /live/public (cursor/limit, _links.next.href). It does not filter by state: a sandbox trial or a run you have already stopped still shows up, unlike GET /live/public, which needs no Authorization header but only ever lists other runs — anyone’s, yours included — that are public and currently RUNNING.

Reading one run

GET /live/{runId} (needs a Bearer token) returns one of your runs by its own id, whatever its stage, desired state or visibility and however long ago you stopped it: the same state GET /strategy/{strategyId}/live gives, plus updatedAtMs, when the run last changed. That value only moves forward, so when you keep your own copy of a run, apply a read only if its updatedAtMs is larger than the one you hold. A run that is not yours, or does not exist, answers 404; a run someone made public is found through GET /live/public, not here.

Visibility

A run is private by default — only you can read its state or receive its signals. Setting visibility: public (via PATCH /live/{runId}) does two things:

  • it appears in GET /live/public’s catalogue, listed without revealing who owns it or which strategy runs it;
  • its signal channel (see below) accepts a WebSocket subscription from anyone, not only you.

public is what you ask for, and it takes effect when the run is promoted to live. Until then — while it is a sandbox trial — only you can read it, over the channel and through the read routes, and it is not listed in the catalogue; nothing you did needs repeating at promotion.

Who can read what, by run:

The runSubscribe to its channel, and read .../signals, .../paperListed in GET /live/public
Any run of yoursYou, always—
privateOnly youNo
public, still in the sandboxOnly you (public takes effect at the promotion)No
public, promoted to liveAnyoneYes, while it is running

A stream URL is separate from all of this: it is a secret you create for one run, and whoever holds it can read that run’s signals in either stage, whatever its visibility says. You decide who holds it.

relay is separate: it only decides whether a run’s signals are pushed over the WebSocket channel (opt-in, in either stage) and never who may read them. GET /live/{runId}/signals serves a run’s signals whether or not you asked for relay.

Switching back to private also disconnects anyone else currently subscribed to that channel — best-effort, and it does not undo the visibility change if the disconnect itself fails.

Runtime parameters

params on POST /strategy/{strategyId}/live only sets the values a run starts with. To change one while the run keeps running, call PUT /live/{runId}/params (or the equivalent live.params WebSocket call below — both go through the same validation and land on the identical value at the identical moment). Every key must be one your strategy declares; an undeclared key is 400.

A 409 means this run’s compiled strategy has no record of the parameters it declares, so they cannot be changed while it runs. A run keeps the compiled version it started with: register the strategy again (POST /strategy with the same source, which compiles it afresh) and start a new run.

The response’s effectiveAtMs is not “now” — it is a few seconds out, the earliest moment the new value is guaranteed to be applied. This margin exists so that if a run has more than one execution worker behind it, they all pick up the change at the same point rather than one applying it a few events before the other.

PUT /live/6TzAPiPpsOWwBLdLBZCxwH/params
{"params": {"emaFastPeriod": "12"}}

200
{"runId": "6TzAPiPpsOWwBLdLBZCxwH", "paramsVersion": 2, "effectiveAtMs": 1758330015000}

Commands

POST /live/{runId}/commands tells a running strategy something without restarting it, for a strategy that implements the engine’s CommandRequestHandler — a Java strategy directly (see Coding Java strategies), or a QTScript strategy through onCommand { } (see QTScript). It takes {"command": "<text>"} — a plain string — and an optional properties object of your own choosing alongside it, which travels unchanged to the strategy’s own handler; command and properties are the only keys the body may carry. It answers 202 with commandId and effectiveAtMs, the market position every execution behind the run applies it at.

A command is transient, unlike a parameter: it is an event, not a stored value, and nothing about it is written to the run. A replica that restarts replays only its recent market history, so a command from before that window never reaches it — a peer that was already running when it arrived applies it, one that starts later does not. Anything the strategy needs to remember across a restart belongs in a parameter (PUT /live/{runId}/params), which does have a stored value.

A 409 means one of three things, each its own message: the run is not running; this run’s compiled strategy has no record of whether it handles commands (register the strategy again and start a new run, same as the 409 on params); or the strategy does not implement CommandRequestHandler at all. A 503 means the command could not be delivered right now and was not sent — there is no fallback path for an event the way there is for a parameter row, so retry the request itself.

POST /live/6TzAPiPpsOWwBLdLBZCxwH/commands
{"command": "flatten", "properties": {"instrument": "BTC/USDT"}}

202
{"runId": "6TzAPiPpsOWwBLdLBZCxwH", "commandId": "0e3f2f1a-9c4b-4d3e-8a2f-6b7c5d4e3f21", "effectiveAtMs": 1758330015000}

Receiving signals and updating parameters live: the WebSocket connection

Polling GET .../live tells you the run’s state; it does not stream its output. To receive a run’s signals as they happen, or to send a parameter update over the same connection instead of a separate REST call, open a WebSocket connection.

The connection speaks the Centrifugo v6 client protocol (JSON). Its machine-readable contract is asyncapi.yaml, next to the OpenAPI spec: the URL, every frame, the channel names, the live.params call and the error codes, with the signal payload shared with the REST schema LiveSignal.

The QTSurfer SDKs already wrap this connection — see Clients and SDKs — so you may not need to speak the protocol directly at all. Going direct, the easiest client is an official Centrifugo library — centrifuge (JavaScript/TypeScript), centrifuge-java, centrifuge-python and others — since it already does the pings, token refresh and reconnection described below. With one, you only supply the URL, a function that mints a token, the channel name and the RPC method.

Signals only reach this channel for a run started with relay: true (POST .../live’s own field, default false). They reach it from the run’s first signal, in the sandbox stage too, where only you can subscribe to it; the same channel carries on unchanged once the run is promoted to live, on the same subscription: stage flips from sandbox to live and nothing needs redoing. The run takes a while to start in the live stage, so the channel can stay quiet for several minutes around the promotion; what the run produced meanwhile then arrives in order, and each signal arrives once. GET/PATCH .../live echo back what was requested as the run’s own relay field.

  1. Mint a token. POST /live/token (JWT bearer, same as any other endpoint) returns a short-lived token and its expiresAtMs.
  2. Connect. Open a WebSocket to wss://rt.qtsurfer.net/connection/websocket and send, as your first message:
    {"id": 1, "connect": {"token": "<the token from step 1>"}}
    A successful connect replies with your own client id, and how long the token has left:
    {"id": 1, "connect": {"client": "<client-id>", "expires": true, "ttl": 600, "ping": 25, "pong": true}}
    ttl and ping are in seconds. A token that is not accepted closes the socket with close code 3500 (invalid token).
  3. Subscribe to the run’s signal channel, named sig:<runId> — for example sig:6TzAPiPpsOWwBLdLBZCxwH:
    {"id": 2, "subscribe": {"channel": "sig:6TzAPiPpsOWwBLdLBZCxwH"}}
    You may subscribe to any run’s channel this way, but the connection is only actually allowed onto it if you own that run, or it is public and has reached the live stage — a foreign private run’s channel, and a public run that is still in the sandbox, refuse the subscription with {"id": 2, "error": {"code": 103, "message": "permission denied"}}. Each signal then arrives as a push frame, with no id; the signal itself is its pub.data, in the shape below:
    {"push": {"channel": "sig:6TzAPiPpsOWwBLdLBZCxwH", "pub": {"data": {"v": 1, "signalId": "…", …}, "offset": 42}}}
    If the run is made private while you are subscribed and it is not yours, the server removes you with {"push": {"channel": "sig:…", "unsubscribe": {"code": 2000, "reason": "server unsubscribe"}}}, and you are not resubscribed.
  4. Call live.params (the WebSocket form of PUT /live/{runId}/params, owner-only):
    {"id": 3, "rpc": {"method": "live.params", "data": {"runId": "6TzAPiPpsOWwBLdLBZCxwH", "params": {"emaFastPeriod": "12"}}}}
    Success:
    {"id": 3, "rpc": {"data": {"runId": "6TzAPiPpsOWwBLdLBZCxwH", "paramsVersion": 2, "effectiveAtMs": 1758330015000}}}
    Failure (mirrors the REST endpoint’s own 400/404/409):
    {"id": 3, "error": {"code": 404, "message": "no such run"}}
  5. Keep the connection alive. The server sends an empty frame {} as a ping; answer each one with {} (that is what "pong": true in the connect reply asks for). If nothing arrives for well over ping seconds, treat the connection as dead and reconnect.
  6. Refresh the token before ttl runs out, on the same connection — mint a new one with POST /live/token and send it:
    {"id": 4, "refresh": {"token": "<a new token>"}}
    which answers {"id": 4, "refresh": {"expires": true, "ttl": 600}}. Your subscriptions are untouched. A connection whose token is not refreshed in time is closed with close code 3005 (connection expired); reconnect with a new token.

Some protocol details to know if you write the client yourself: every reply carries the id of the command it answers, frames the server sends on its own (pushes, pings) carry none, and one WebSocket frame may hold several replies, one JSON object per line. A browser page served from another site’s origin is refused at the WebSocket upgrade (403); a client that sends no Origin header, such as a server-side program or an SDK, is not affected.

After a disconnect, the channel does not replay what you missed: read it back with GET /live/{runId}/signals (below), deduplicating on signalId.

Signal shape

Each signal pushed on a sig:<runId> channel (the pub.data of the push frame):

{
  "v": 1,
  "signalId": "…",
  "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": "…"
}
fieldmeaning
signalIdStable id for this exact signal — dedupe on it if your connection ever reconnects mid-stream.
stagesandbox or live: the stage the run was in when it produced the signal. Only you receive a sandbox signal on this channel; everyone allowed onto the channel receives live ones.
paramsVersionThe parameter set in force when this signal was produced.
typehint, info, marker, or command — plus paper when reading a mix run’s history (see Paper trading; paper items are never pushed on this channel).
kindBUY/SELL for a hint; the command name for a command; absent otherwise.
eventTsMsMarket time the signal was produced.
emittedAtMsTime it was published — always ≥ eventTsMs.
orderPresent only for a hint.
dataThe signal’s own free-form payload: what the strategy put there with signal.set(...). Whoever may read the run may read it, so on a public run it is public. A signal whose data is over 8 KiB (8,192 bytes of its JSON) is not pushed on this channel; the history route returns it whole.
regeneratedtrue only for a signal republished to fill a gap in the historical record — always false for a signal you are seeing for the first time.
digestContent hash, for verifying two independent deliveries of the same signal agree.

Who owns the run, which strategy or compilation produced a signal, and the exact market-data position behind it are never included on this channel, whether the run is public or private.

A plain WebSocket stream of a run

The connection above is a protocol: a token, a subscription, frames of its own. A stream URL is the simple alternative. It is one address that you open as an ordinary WebSocket — from a script, a command-line tool such as websocat, or a service of your own that passes your signals on to others — and each signal of the run arrives as one JSON text frame. There is no token to mint, nothing to subscribe to and nothing to send.

It is meant for your own tests, for simple clients, and for services that pass your signals on to many connections themselves: this address is limited in how many connections it accepts (see below), so a service that serves many readers holds one connection and fans the signals out itself.

It is available from the sandbox stage on, so you can try it before the run is promoted, on the plans that may broadcast (the Pro and Elite plans: your plan is the tier that GET /account returns). Any other plan is refused with 429, naming the plan.

Getting one

Ask for it when you start the run, with stream: true in POST /strategy/{strategyId}/live. It turns relay on, and it cannot be added to a run later. The response carries the address as streamUrl:

{
  "runId": "5t5oAmQ4PD0lQRoCU58uE0",
  "stage": "SANDBOX",
  "relay": true,
  "streamUrl": "wss://…"
}

GET /strategy/{strategyId}/live returns the same streamUrl for as long as the run is running and your plan allows it. It is in no other response: not when the run is stopped, not in GET /live/{runId}, and not in the public catalogue. Use the address exactly as it is returned; it is opaque.

Treat it like a password. Anyone who holds it can read the run’s signals, sandbox ones included. Do not put it in a repository, a log, a screenshot or a shared chat. If it may have leaked, rotate it. You are responsible for who you give it to and for what is done with the signals you pass on.

What arrives

Each text frame is exactly one signal, the same object as the pub.data of a sig:<runId> channel push (see Signal shape), stage included, so a receiver can tell a sandbox trial from the real thing. There is no connect message and no wrapper around it, and nothing to answer: your WebSocket library answers the pings the service sends. A signal whose data is over 8 KiB is not sent, as on the channel.

websocat "$STREAM_URL"

Anything you send is ignored, and a frame from you over 1 KiB closes the connection.

Reconnecting

Connections do end (a restart of the service, a network blip), so a client reconnects. To resume without gaps, add the signalId of the last signal you received as a query parameter, ?after=<signalId>: the service sends the signals that came after that one, then carries on live, each signal once.

It keeps only the most recent signals of a run, about the last few minutes and fewer for a run whose signals are large. If the signal you name is no longer held, the connection is closed with 4001 before any frame is sent: read what you missed with GET /live/{runId}/signals, then connect again without after. Whatever you do, de-duplicate by signalId.

Limits, and why a connection closes

  • Plan on 2 connections open at once on one URL (the second covers the overlap while you reconnect), and 10 from one client address. They are the numbers to design for, not exact walls: the service counts connections in more than one place, so one past them is sometimes accepted. A connection that is refused is closed with 1013 once it is open, or turned away before it opens, when the handshake itself fails (with 429, or with a gateway error such as 502).
  • A reader that does not keep up is disconnected; it never slows anyone else down.
  • A client address that keeps presenting URLs that do not work is refused for a while (429).

An address that is not a valid stream URL, or one that has stopped working, answers 404 to the connection attempt, never saying which; 503 means try again in a moment. Once open, a connection can be closed with:

CodeMeaningWhat to do
1008The URL no longer works: it was rotated or revoked, the run stopped, or your plan no longer lets you broadcast.Do not retry the same URL. Read GET /strategy/{strategyId}/live for the current one.
1013Too many connections on this URL or from this address, the connection could not keep up, or the service could not confirm the URL for a moment.Wait and reconnect, with after.
4001The signal in after is no longer held.Read the history, then connect without after.
1001The service is restarting.Reconnect right away, with after.
1009You sent a frame over 1 KiB.Do not send frames.

If your plan stops letting you broadcast, the URL is no longer shown and open connections are closed with 1008, typically within a minute; if the plan lets you broadcast again, the same URL works again.

Rotating and revoking it

  • POST /live/{runId}/stream gives the run a new address and retires the old one: connections on the old address close with 1008 within about 15 seconds. Only for a running run that was started with a stream.
  • DELETE /live/{runId}/stream revokes it for good: connections close and the address answers 404. The run itself keeps running, and a stream cannot be added to it again: start the run again with stream: true for a new one. It is always allowed, whatever your plan, and repeating it is not an error.

Reading signals a run already produced

The channel above is live only: it carries what happens while you are connected, and only for a run that asked for relay. GET /live/{runId}/signals serves the record instead — a run’s signals are kept either way, so this works whether or not relay was ever on, and in both stages. Use it to catch up after a disconnect, to read a run you never relayed, or simply to page back over what has already happened.

GET /v1/live/6TzAPiPpsOWwBLdLBZCxwH/signals?sinceMs=1758330000000&limit=20

200
{
  "signals": [ { "signalId": "…", "eventTsMs": 1758330012000, … } ],
  "availableSinceMs": 1757725212000,
  "_links": {"next": {"href": "/v1/live/6TzAPiPpsOWwBLdLBZCxwH/signals?cursor=eyJzZXEiOjQyfQ&limit=20"}}
}

Each entry is the same shape the channel pushes — the table above applies unchanged, except that stage here is whichever stage the run was in when the signal was produced, so a sandbox run’s signals read back as sandbox. Page with _links.next while it is present; limit defaults to 20 and caps at 100. Readable by the run’s owner, and by anyone if the run is public — the same rule the channel applies to a subscription.

Filtering by instrument

instrument is optional and narrows the page without changing anything else:

instrumentreturns
omitted, or *every instrument the run covers
BTC/USDTjust that pair
*/USDTany base against that quote
BTC/*that base against any quote
BTC/USDT,ETH/EUReach pair in the list

Symbols match exactly, case included — pass them as this API reports them (as they appear in the run’s own sources, or in a signal’s instrument.symbol).

Filtering by type

type narrows to one or more signal types, comma-separated: hint, info, marker, command, paper. It combines with instrument — ?type=hint&instrument=*/USDT is every hint on a USDT pair. Both filters are carried over into _links.next, so following it keeps the same selection.

The window moves, and cursors expire

Signals are kept for a limited span, and the oldest are discarded continuously as new ones arrive. How far back you can read is therefore not a fixed number of hours: a run producing a lot of signals consumes that span faster, and so do other runs sharing it. Two consequences worth designing for:

  • availableSinceMs in every response is the oldest moment still answerable. Asking for a sinceMs older than that is not an error: you are served from availableSinceMs onwards, and the field tells you that is what happened.

  • A cursor can expire, and on a busy run it can expire within minutes. When the position it points at has already been discarded, the next page answers 410 rather than quietly serving a shortened page that looks complete:

    {"code": 410, "message": "the cursor's position is no longer retained by the signal stream (it now starts at availableSinceMs=1757725212000)"}

    Treat it as an ordinary outcome of paging a live system, not as a failure: read the availableSinceMs it names and start again from there. If you are paging to display a long history, fetch the pages you need in one pass rather than holding a cursor across a user’s think-time.

Paper trading

Start a run with a paper block and its hints are executed in simulation from its first tick, as a backtest would execute them — fills, closed trades, equity and the same KPIs a backtest reports:

"paper": {"initialFunding": 1000, "feeRate": 0.001, "percentAmountToLock": 20}

Read it back with GET /live/{runId}/paper and GET /live/{runId}/paper/equity. Everything about it — the configuration, one account per quote currency, the equity curve, mix output and gaps — is in Paper trading on live runs.