Compile a Java strategy, 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 |
POST /strategy — body is the raw Java source, Content-Type: text/plain.
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 Java. It compiles, registers, and hands back
the id — nothing more. Whether the class can actually run is validate;
everything known about a strategy, validation included, is read from GET
/strategy/{strategyId}.
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. Two consequences:
declaredProperties — DeclaredPropertyThe 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 Java — the message carries the compiler diagnostics, nothing is
registered · 429 too many compilations in flight, retry later.
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.
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.
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}.
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"}
]
}
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.
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.