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.
| Method | Path | Purpose |
|---|---|---|
POST | /strategy/{strategyId}/live | Start a strategy live |
GET | /strategy/{strategyId}/live | Read this strategy’s current (or last) run |
DELETE | /strategy/{strategyId}/live | Stop it |
GET | /live | List your own runs |
GET | /live/{runId} | Read one of your runs by its id |
GET | /live/public | Browse runs other users made public |
PATCH | /live/{runId} | Change visibility, name, or description |
PUT | /live/{runId}/params | Change parameters while it stays live |
POST | /live/{runId}/commands | Tell it a command while it stays live |
GET | /live/{runId}/signals | Read the signals it has already produced |
GET | /live/{runId}/paper | Read its paper trading — see Paper trading |
GET | /live/{runId}/paper/equity | Page through its paper equity curve — see Paper trading |
POST | /live/token | Mint 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:
stageisSANDBOXuntil the promotion andLIVEafter it.stateis the run’s health right now (see State of a run).gateis absent for the whole trial and appears when it ends, holding the verdict. An absentgatetherefore means “the trial has not finished”, never “nobody is evaluating the run”. Itspassedfield 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”.
state | What it means |
|---|---|
STARTING | Accepted; no runner has reported on it yet. |
RUNNING | Running normally. |
LAGGING | Running, but behind the market data: usual while it catches up after starting or after a platform restart. It clears by itself. |
HUNG | Your strategy is stuck inside one call for longer than the platform allows. It clears when that call returns. |
DEGRADED | The 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. |
FAILED | The platform refused the run, could not start it, or the run failed while running. reason says why (see Why a run failed or stopped). |
STOPPED | Stopped, 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.
reason | When |
|---|---|
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
} | Field | What it means |
|---|---|
processed | Updates of instruments the run has accepted since it started executing. It can start again from zero if the run is restarted. |
opsPerSecond | Updates 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. |
instrumentsSeen | Distinct instruments the run has received an update for. |
asOfMs | When these counters were last written. |
progressedAtMs | The last refresh in which processed had grown. Absent until the run has processed anything. |
stale | true when the run is meant to be running and its counters have not been refreshed for several refresh intervals. |
Three things to know:
statsis 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 twoGETs above.staleonly says the platform stopped updating the counters. Check it againststate. A run whoseprocessedstays 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.progressedAtMsis how to tell such a run from one that has stopped.- A refresh of
statsis not a change of the run. It does not moveupdatedAtMs.
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 run | Subscribe to its channel, and read .../signals, .../paper | Listed in GET /live/public |
|---|---|---|
| Any run of yours | You, always | — |
private | Only you | No |
public, still in the sandbox | Only you (public takes effect at the promotion) | No |
public, promoted to live | Anyone | Yes, 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.
- Mint a token.
POST /live/token(JWT bearer, same as any other endpoint) returns a short-livedtokenand itsexpiresAtMs. - Connect. Open a WebSocket to
wss://rt.qtsurfer.net/connection/websocketand send, as your first message:
A successful connect replies with your own{"id": 1, "connect": {"token": "<the token from step 1>"}}clientid, and how long the token has left:{"id": 1, "connect": {"client": "<client-id>", "expires": true, "ttl": 600, "ping": 25, "pong": true}}ttlandpingare in seconds. A token that is not accepted closes the socket with close code3500(invalid token). - Subscribe to the run’s signal channel, named
sig:<runId>— for examplesig: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{"id": 2, "subscribe": {"channel": "sig:6TzAPiPpsOWwBLdLBZCxwH"}}publicand has reached thelivestage — a foreign private run’s channel, and a public run that is still in thesandbox, refuse the subscription with{"id": 2, "error": {"code": 103, "message": "permission denied"}}. Each signal then arrives as apushframe, with noid; the signal itself is itspub.data, in the shape below:
If the run is made private while you are subscribed and it is not yours, the server removes you with{"push": {"channel": "sig:6TzAPiPpsOWwBLdLBZCxwH", "pub": {"data": {"v": 1, "signalId": "…", …}, "offset": 42}}}{"push": {"channel": "sig:…", "unsubscribe": {"code": 2000, "reason": "server unsubscribe"}}}, and you are not resubscribed. - Call
live.params(the WebSocket form ofPUT /live/{runId}/params, owner-only):
Success:{"id": 3, "rpc": {"method": "live.params", "data": {"runId": "6TzAPiPpsOWwBLdLBZCxwH", "params": {"emaFastPeriod": "12"}}}}
Failure (mirrors the REST endpoint’s own 400/404/409):{"id": 3, "rpc": {"data": {"runId": "6TzAPiPpsOWwBLdLBZCxwH", "paramsVersion": 2, "effectiveAtMs": 1758330015000}}}{"id": 3, "error": {"code": 404, "message": "no such run"}} - Keep the connection alive. The server sends an empty frame
{}as a ping; answer each one with{}(that is what"pong": truein the connect reply asks for). If nothing arrives for well overpingseconds, treat the connection as dead and reconnect. - Refresh the token before
ttlruns out, on the same connection — mint a new one withPOST /live/tokenand send it:
which answers{"id": 4, "refresh": {"token": "<a new token>"}}{"id": 4, "refresh": {"expires": true, "ttl": 600}}. Your subscriptions are untouched. A connection whose token is not refreshed in time is closed with close code3005(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": "…"
} | field | meaning |
|---|---|
signalId | Stable id for this exact signal — dedupe on it if your connection ever reconnects mid-stream. |
stage | sandbox 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. |
paramsVersion | The parameter set in force when this signal was produced. |
type | hint, info, marker, or command — plus paper when reading a mix run’s history (see Paper trading; paper items are never pushed on this channel). |
kind | BUY/SELL for a hint; the command name for a command; absent otherwise. |
eventTsMs | Market time the signal was produced. |
emittedAtMs | Time it was published — always ≥ eventTsMs. |
order | Present only for a hint. |
data | The 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. |
regenerated | true 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. |
digest | Content 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
1013once it is open, or turned away before it opens, when the handshake itself fails (with429, or with a gateway error such as502). - 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:
| Code | Meaning | What to do |
|---|---|---|
1008 | The 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. |
1013 | Too 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. |
4001 | The signal in after is no longer held. | Read the history, then connect without after. |
1001 | The service is restarting. | Reconnect right away, with after. |
1009 | You 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}/streamgives the run a new address and retires the old one: connections on the old address close with1008within about 15 seconds. Only for a running run that was started with a stream.DELETE /live/{runId}/streamrevokes it for good: connections close and the address answers404. The run itself keeps running, and a stream cannot be added to it again: start the run again withstream: truefor 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:
instrument | returns |
|---|---|
omitted, or * | every instrument the run covers |
BTC/USDT | just that pair |
*/USDT | any base against that quote |
BTC/* | that base against any quote |
BTC/USDT,ETH/EUR | each 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:
availableSinceMsin every response is the oldest moment still answerable. Asking for asinceMsolder than that is not an error: you are served fromavailableSinceMsonwards, 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
410rather 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
availableSinceMsit 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.