Strategy API
Compile, validate, inspect, retrieve, and delete Java or QTScript (beta) strategies through the REST API.
Compile a strategy (Java, or QTScript in beta), check it can actually run, list/inspect/delete what you’ve registered, and read back its source.
This page documents the strategy REST resources. For the Java source itself — base classes, execution and information signals, advanced order parameters, and chart metadata — see Coding Java strategies.
| Method | Path | Purpose |
|---|---|---|
POST | /strategy | Compile and register |
GET | /strategies | List your registered strategies |
GET | /strategy/{strategyId} | Get one, including validation state |
POST | /strategy/{strategyId}/validate | Check it actually runs |
GET | /strategy/{strategyId}/code | Read back the registered source |
DELETE | /strategy/{strategyId} | Release it |
Compiling a strategy
POST /strategy — body is the raw source, Content-Type: text/plain. Java is the established
route; QTScript is a newer, compact language in beta (see QTScript below, and
the QTScript guide).
curl -X POST https://api.qtsurfer.net/v1/strategy
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: text/plain"
--data-binary @MyStrategy.java {
"strategyId": "2ul144qe9tlwzu5anhwvc6",
"declaredProperties": [
{"name": "rsi.period", "description": "RSI period", "defaultValue": "14",
"reflected": true, "min": 2, "max": 50, "step": 1},
{"name": "enabled", "description": "Enabled", "reflected": true}
]
} This answers one question: is the source valid. It compiles, registers, and hands back
the id — nothing more. A 200 means the source parsed and compiled, not that it will run: what only
shows once the strategy sets up its indicators (for QTScript, a window on an indicator that is not
registered) is found by validate. Whether the class can actually run is validate;
everything known about a strategy, validation included, is read from GET /strategy/{strategyId}.
For Java, strategyId is derived from what the code means, not from how it’s written. A
comment, 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, or reordering fields/statements returns a different one. (For QTScript the rules differ —
see below.) Two consequences:
- re-submitting a Java strategy you only reformatted is free — you get back the id you already had, along with any validation already recorded against it;
- the id says nothing about behaviour. Two sources computing the same thing by different means are two strategies, since deciding otherwise would mean deciding program equivalence.
QTScript (beta)
The QTScript guide covers the language in one page, with examples. In short:
QTScript removes the ceremony around a strategy — package, imports, class, base class, property
annotations — and keeps every { } body as plain Java. It is told apart from Java by its first
token: a QTScript file begins with strategy. Whitespace and comments (// or /* */) before it
are ignored, so a description can sit on top of the file. The same endpoint takes it, and a registered
QTScript strategy is used like any other.
strategy MiniKline kline
param buyBelow = 30
param sellAbove = 70
setup:
rsi(14) window m1 {
if (actual < buyBelow) emitBuy(price);
if (actual > sellAbove) emitSell(price);
} strategy Name [kline|funding]picks the data source (ticker by default) — see Data sources for what each can run.- A
400carriesLine N, Column M:entries against your own source. strategyIdcomes from the text, because indentation is part of the grammar. A byte-order mark, the style of line endings, trailing whitespace and blank lines before the first and after the last line are ignored; anything else — a comment, the indentation, a blank line in between — gives a different id.- A failure while a QTScript strategy runs is reported against your source, as
QTScript line 6: Index 2 out of bounds for length 1.
declaredProperties — DeclaredProperty
The sweep/execute param-key vocabulary this strategy is known to accept — established without
constructing the strategy, so a caller can catch a typo’d key before submitting a sweep instead
of only learning it from a rejected one. Best-effort, not exhaustive: a property registered
imperatively (e.g. through an attached RiskConfig) needs a live instance to discover and won’t
appear here — a name absent from this list may still be valid.
| Field | Notes |
|---|---|
name | the key a sweep or execute param map uses for this property |
description | human-readable label, as declared |
defaultValue | the declared default, as a string. Absent, not null, when none was declared |
reflected | true — a value is injected into the strategy’s field; false — only available through the property map |
min, max, step | suggested sweep/range bounds, if declared. Advisory only, never validated |
Errors: 400 not valid — the message carries the diagnostics, nothing is
registered · 429 too many compilations in flight, retry later.
Request size limits
Every endpoint that reads a request body caps it, and a strategy’s source is the biggest body the
API takes. A body over the cap is refused with 413 and the API’s usual JSON error, before it is
read or compiled, and the message names the cap:
{"code": 413, "message": "The request body is larger than this endpoint accepts (32768 bytes at most)."} | Request | Body cap |
|---|---|
POST /strategy — the source | 32 KiB |
POST /backtest/{exchangeId}/{type}/execute, POST /backtest/{exchangeId}/{type}/executeSweep/{requestId} | 8 KiB |
POST /strategy/{strategyId}/live | 8 KiB |
POST /backtest/{exchangeId}/{type}/prepare, PATCH /live/{runId}, PUT /live/{runId}/params | 4 KiB |
POST /datasets, POST /datasets/imports | 1 KiB |
A dataset’s data does not travel in a request body: it goes to a presigned URL, see Datasets. The caps are the same for everyone (they are not a plan limit), so a strategy source over 32 KiB cannot be registered: shorten it.
Checking it actually runs
POST /strategy/{strategyId}/validate
Instantiates the compiled class and drives it through a bounded synthetic series, so a wiring
fault surfaces here instead of at your first real 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. The
status code, not the body, is what tells the two apart — a 200 can also carry validation: pending, left by a check an earlier call queued. 202 means this call started a check; pending means only a check is outstanding. Poll GET /strategy/{strategyId} until validation leaves pending.
Recompiling supersedes a verdict — the old answer described bytecode that would no longer run — which is what makes this callable again after an edit.
curl -X POST https://api.qtsurfer.net/v1/strategy/2ul144qe9tlwzu5anhwvc6/validate
-H "Authorization: Bearer $TOKEN"
# → 202 {"strategyId": "2ul144qe9tlwzu5anhwvc6", "validation": "pending"} Errors: 404 no such registered strategy for this user.
Getting a strategy
GET /strategy/{strategyId} — response is StrategyState, the same shape validate’s
already-validated 200 returns.
validation: passed does not mean the strategy is correct. It means the class loaded and
survived the first event of a short synthetic run — a floor, not a guarantee. When dryRunIncomplete is true it’s a lower floor still, since the run didn’t finish.
| Field | Notes |
|---|---|
validation | not_validated | pending | passed | failed |
compiledAt | when the live compilation was produced |
requiredSources | market data the strategy needs (Ticker, KLine, FundingRate), read off the compiled class. Absent is not “needs nothing” — absent means the platform couldn’t establish the answer without constructing the strategy (a MultiSourceStrategy, a class overriding getMarketDataSource(), or anything registered before this field existed). Re-registering the source fills it in |
validatedAt | when the verdict was recorded; absent until there is one |
detail | why validation failed, or why a queued check hasn’t reported. Present on failed, and alongside validationStalled |
notices | what the run surfaced. An empty/absent list is not a clean bill of health when dryRunIncomplete is true |
noticesTruncated | notices dropped past the cap; absent when none were |
dryRunIncomplete | the check didn’t finish its budget — ran out of time, was refused (too many unfinishable runs already in flight), or hit a failure attributable to the synthetic instrument rather than the strategy. The verdict stands as far as it went |
validationStalled | a queued check hasn’t reported for far longer than one takes. Nothing is disproved — the check just hasn’t run. Stop waiting and re-request later |
_links.code | present on a full body (200 here, and validate’s already-validated 200), absent from validate’s 202 stub. Points at GET .../code — following it can still 404 for a strategy with no source of its own (see below) |
{
"strategyId": "6bsh31ikwkuivhtgcoa6s4",
"validation": "passed",
"compiledAt": "2026-08-04T16:23:04Z",
"requiredSources": ["Ticker"],
"validatedAt": "2026-08-04T16:24:11Z",
"notices": [
{"level": "WARN", "code": "indicator.bar-data-on-ticker-path",
"message": "Indicator requires bar data but is on the ticker path",
"provenance": "compile-dry-run"}
],
"_links": {"code": {"href": "/v1/strategy/6bsh31ikwkuivhtgcoa6s4/code"}}
} Errors: 404 no such registered strategy for this user — never stale/expired, registration and
verdict are stored durably, not cached.
Listing your strategies
GET /strategies — every strategy you’ve registered and not deleted, most recently compiled
first. Never a 404 — an empty array if you have none.
Each entry (StrategySummary) carries the same compiledAt/requiredSources provenance as StrategyState, but not validation state, so listing stays cheap regardless of how many
strategies you have. Check a specific one’s validation with GET /strategy/{strategyId}.
GET /strategies?includeDeleted=true also lists the strategies you’ve deleted, each with the deletedAt it was deleted at — handy when keeping your own copy of the list in sync, so a
deleted strategy shows up as deleted instead of just disappearing.
curl https://api.qtsurfer.net/v1/strategies -H "Authorization: Bearer $TOKEN" {
"strategies": [
{"strategyId": "6bsh31ikwkuivhtgcoa6s4", "compiledAt": "2026-08-19T10:15:00Z", "requiredSources": ["Ticker"]},
{"strategyId": "2ul144qe9tlwzu5anhwvc6", "compiledAt": "2026-08-12T09:02:11Z"}
]
} Reading back the source
GET /strategy/{strategyId}/code — the exact source last submitted for this id, whitespace and
comments included: the same text strategyId was derived from.
“If available”, not “always”. A strategy resolved only through a shared/marketplace listing
you copied by reference carries no source of its own, and reads as 404 here — the same as a strategyId you never registered. That’s the honest answer either way: nothing is there to
return.
curl https://api.qtsurfer.net/v1/strategy/2ul144qe9tlwzu5anhwvc6/code
-H "Authorization: Bearer $TOKEN"
# → {"strategyId": "2ul144qe9tlwzu5anhwvc6", "code": "package strategy;\npublic class..."} Errors: 404 no such registered strategy for this user, or nothing to read for this id.
Deleting a strategy
DELETE /strategy/{strategyId} — frees up the slot on a plan capped at a strategy count.
Removes it from GET /strategy/{strategyId} and GET /strategies. Not undone by
re-submitting the same source — that registers a new strategy, with a new id. Backtests
already run against it are unaffected: deleting stops it from counting against your account and
stops you validating/re-running it under this id, but doesn’t erase what already happened. Only
removes a strategy you registered yourself — deleting your copy of a shared/marketplace listing
never affects the original.
curl -X DELETE https://api.qtsurfer.net/v1/strategy/2ul144qe9tlwzu5anhwvc6
-H "Authorization: Bearer $TOKEN"
# → {"strategyId": "2ul144qe9tlwzu5anhwvc6", "deleted": true} Errors: 404 no such registered strategy for this user.