Auto-generated Java client for the QTSurfer API, built from the OpenAPI 3.1 spec with openapi-generator and the JDK's java.net.http.HttpClient.
com.qtsurfer:api-client-java · com.qtsurfer:api-client-java
Intentionally thin: one method per endpoint, 1:1 with the spec. For workflow orchestration (polling, retries, domain objects, unified errors), use com.qtsurfer:sdk.
- Zero HTTP runtime deps —
java.net.http.HttpClient(JDK built-in) + Jackson for JSON. - Spec-driven — generated sources fetched from
QTSurfer/qtsurfer-apion every build. - JDK 17+ — modern language features, long-term support.
- Distributed via JitPack today; Maven Central later.
Add the JitPack repository and the dependency:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.qtsurfer</groupId>
<artifactId>api-client-java</artifactId>
<version>x.x.x</version>
</dependency>For Gradle:
repositories { maven { url 'https://jitpack.io' } }
dependencies { implementation 'com.qtsurfer:api-client-java:x.x.x' }Once published to Central, the coordinate will be com.qtsurfer:api-client-java:x.x.x.
import com.qtsurfer.api.client.api.ExchangeApi;
import com.qtsurfer.api.client.invoker.ApiClient;
import com.qtsurfer.api.client.model.Exchange;
import java.util.List;
ApiClient client = new ApiClient();
client.updateBaseUri("https://api.qtsurfer.net/v1"); // Staging beta server
client.setRequestInterceptor(builder ->
builder.header("Authorization", "Bearer " + System.getenv("QTSURFER_TOKEN")));
ExchangeApi exchanges = new ExchangeApi(client);
List<Exchange> result = exchanges.listExchanges();Every endpoint above expects a short-lived JWT in Authorization: Bearer ….
Exchange a long-lived API key for one via AuthApi.authenticate():
import com.qtsurfer.api.client.api.AuthApi;
import com.qtsurfer.api.client.invoker.ApiClient;
import com.qtsurfer.api.client.model.AuthTokenResponse;
ApiClient apikeyClient = new ApiClient();
apikeyClient.updateBaseUri("https://api.qtsurfer.net/v1");
apikeyClient.setRequestInterceptor(builder ->
builder.header("X-API-Key", System.getenv("QTSURFER_APIKEY")));
AuthTokenResponse token = new AuthApi(apikeyClient).authenticate();
String jwt = token.getAccessToken(); // feed to a Bearer-authed ApiClientAuthApi.authenticate() throws ApiException for rejected keys. 401 means an invalid,
revoked or expired key; 403 with apikey_without_scopes means the key is valid but carries no
scopes and the deployment requires them. No token is minted: create a key with at least one
resource/action scope before retrying. Deserialize the exception's response body as
AuthTokenError to inspect CodeEnum.APIKEY_WITHOUT_SCOPES.
Successful token responses carry a scopes object, for example {"exchanges":["read"]}.
A resource is listed only with at least one action; a key with no scopes has {} where such a
key may authenticate. This authentication requirement is separate from checking the scope of
each operation, which the API does not yet enforce.
For production use, prefer the com.qtsurfer:sdk
authenticate(apikey) helper — it returns an AuthenticatedClient that refreshes the
JWT transparently, reads QTSURFER_APIKEY from the environment, and supports
pluggable token stores so callers don't reinvent that plumbing.
| API class | Methods |
|---|---|
AuthApi |
authenticate() — exchange API key for a short-lived JWT |
AccountApi |
getAccount() — read account limits, including maxSweepCartesian; getAccountUsage() — read current usage |
ExchangeApi |
listExchanges(), listInstruments(exchangeId), listSegmentInstruments(exchangeId, segment) |
ExchangeBinaryDownloads |
getTickersHour(...), getKlinesHour(...) — Lastra/Parquet streams (manual; see note below) |
StrategyApi |
compileStrategy(body), validateStrategy(strategyId), getStrategy(strategyId), listStrategies(includeDeleted), deleteStrategy(strategyId), getStrategyCode(strategyId) |
BacktestingApi |
prepareBacktest, getPrepareStatus, executeBacktest, cancelBacktest, getBacktestResult, executeSweep, getSweepResult, cancelSweep, getSweepSensitivity, getSweepRunEquityCurve |
DatasetApi |
listDatasets(includeDeleted), createDataset, getDataset, deleteDataset, openDatasetUpload, finalizeDatasetUpload, getDatasetUpload, importDataset, getDatasetImport |
LiveExecutionApi |
startLive (including optional paper trading and plain stream), getLive, getLiveRun, listLive, listPublicLive, updateLive, updateLiveParams, rotateLiveStream, revokeLiveStream, sendLiveCommand, stopLive, getLiveRunSignals (including type filtering), getLiveRunPaper, getLiveRunPaperEquity |
StrategyApi.listStrategies(true) and DatasetApi.listDatasets(true) include deleted entries, marked by deletedAt; the default remains active entries only. Account.maxSweepCartesian reports the maximum Cartesian sweep grid size. LiveRun.reason explains why a run failed or stopped when the service provides a reason.
sendLiveCommand delivers an event to a live strategy without restarting it. The strategy must implement the engine's CommandRequestHandler, and only the run owner can send commands. Commands are transient: they are not stored on the run and are not replayed to replicas that start later. Put state that must survive restarts in live parameters instead.
import com.qtsurfer.api.client.api.LiveExecutionApi;
import com.qtsurfer.api.client.model.LiveCommandResult;
import com.qtsurfer.api.client.model.SendLiveCommandRequest;
import java.util.Map;
SendLiveCommandRequest request = new SendLiveCommandRequest()
.command("rebalance")
.properties(Map.of("targetWeight", 0.25));
LiveCommandResult accepted = new LiveExecutionApi(client).sendLiveCommand(runId, request);The 202 response contains the command ID and effective market position. A 503 guarantees the command was not sent and can be retried; the endpoint has no idempotency key, so do not blindly retry an ambiguous network failure.
listInstruments and listSegmentInstruments return InstrumentCatalogueResponse: inspect
getActualInstance() for InstrumentListResponse (CEX) or PoolListResponse (DEX). Both carry
data, meta.venueType and HAL links; empty lists still identify their venue. InstrumentDetail
has required symbol, optional settle and per-type coverage. PoolDetail includes the pool
address (id), symbol, token addresses, fees, liquidity and USD volume. Missing prices/quote volumes
and pool creation times are absent. Pool market data is refreshed hourly. Exchange supplies valid
venueType, segments and liveSegments; a DEX source is one pool address with type=kline.
DatasetImportRequest.type(CEX) selects a DatasetImportCexRequest block (exchange, segment=spot,
dataType=ticker|klines). CEX imports accept all fifteen cadences from 1s to 1q; DEX accepts
1s, 1m, 5m or omitted native events. Token and account scopes are a resource-to-actions map,
for example {"exchanges":["read"]}. Per-operation scope checks are not yet enforced; token
authentication can still require the key to carry at least one scope, as described above.
getStrategy(...) and validateStrategy(...)'s already-validated 200 response carry an optional
links (StrategyLinks → code: HalLink, .../strategy/{strategyId}/code) pointing at
getStrategyCode(strategyId); that same endpoint's 202 (a check still pending) omits it. Following
the link can still 404 — a strategy resolved only through a shared/marketplace reference carries no
source of its own, and that reads the same as an id never registered.
BacktestingApi.executeSweep(...) accepts an optional walkForward (WalkForwardRequest) to run the sweep as walk-forward validation instead of a flat parameter sweep; BacktestingApi.getSweepResult(...) accepts an optional ranking query param (plateau default, or raw) controlling how its ranked view is ordered; BacktestingApi.getSweepSensitivity(...) returns marginal/heatmap aggregates over a sweep's stored rows.
LiveExecutionApi.startLive(...) accepts an optional paper configuration for simulated fills, balances and positions; this never sends orders to an exchange. getLiveRunPaper(...) reads account snapshots and getLiveRunPaperEquity(...) pages through each account's equity history. Set output to mix to include paper events in retained signals, then filter with getLiveRunSignals(..., type: "paper", ...).
Set StartLiveRequest.stream(true) to receive a secret plain-WebSocket signal URL in the returned
LiveRunWithStream; getLive(...) returns it while the run is active. Treat that URL as a credential.
Set StartLiveRequest.warmFrom(seconds) only while starting a run to replay 0–3600 seconds of market
history; 0 disables warming and omitting it retains the platform's automatic 0–900-second warmup.
The effective value is returned as warmFrom on LiveRunWithStream and LiveRunDetail (it can be
absent only on historical runs). It is not a live parameter and is not present on LiveRun stop responses.
On a LiveSourceRequest, instruments can be omitted to use the selection declared by the compiled
strategy, or every instrument in its exchange/segment if the strategy declares none. Explicit lists
remain supported; matching is case-insensitive, and wildcard pairs such as */USDT are subject to
the account plan. Returned LiveSource.instruments reports the submitted list or resolved selection.
getLiveRun(runId) reads an owned run by canonical id with updatedAtMs and optional stats.
rotateLiveStream(runId) replaces the URL; revokeLiveStream(runId) permanently revokes it.
A compiled strategy starts directly in LIVE when an earlier run of the same compilation was
promoted and none of its runs was stopped for exceeding resource limits. Set
StartLiveRequest.sandbox(true) to repeat the trial; this is useful when debugging because recent
signal history is available over the sandbox WebSocket channel only.
All generated model types (Exchange, InstrumentDetail, InstrumentListResponse, InstrumentCoverage, CoverageWindow, JobState, PrepareJobState, BacktestJobResult, ResultMap, ResponseError, …) live under com.qtsurfer.api.client.model.
BacktestingApi.getPrepareStatus(exchangeId, type, jobId) returns PrepareJobState: the standard job status fields plus coverageRatio, totalHours, hoursWithData, and hoursWithoutData (per-hour rationale: pending_conversion / low_activity / unknown).
ResultMap.getEquityCurve() returns EquityCurveResult, not a bare point list. Its meta records the shape actually served; inline ARRAY data is in points, while SHORT data uses parallel timestamps and equities arrays. A selected sweep row can instead contain a pointer (url), resolved with getSweepRunEquityCurve(...).
These endpoints return raw Lastra bytes (default) or Parquet (format=parquet). The auto-generated ExchangeApi.downloadTickers / downloadKlines methods are unusable for binary payloads — openapi-generator's native library decodes the body as UTF-8 and feeds it to Jackson, which corrupts the bytes. Use ExchangeBinaryDownloads instead:
import com.qtsurfer.api.client.binary.ExchangeBinaryDownloads;
import com.qtsurfer.api.client.binary.ExchangeBinaryDownloads.Format;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
ExchangeBinaryDownloads downloads = new ExchangeBinaryDownloads(client);
String hour = ZonedDateTime.now(ZoneOffset.UTC).minusHours(2)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH"));
try (var in = downloads.getTickersHour("binance", "BTC", "USDT", hour)) {
Files.copy(in, Path.of("BTC_USDT.lastra"));
}
try (var in = downloads.getKlinesHour("binance", "BTC", "USDT", hour, Format.PARQUET)) {
// feed into Apache Parquet, DuckDB, etc.
}The class reuses the ApiClient's HttpClient and request interceptor, so any Authorization header set at the client level applies automatically.
Hourly downloads serve the last seven days. Use getTickersHourWithMetadata or
getKlinesHourWithMetadata when interpreting or caching the bytes: timestampUnit() preserves
X-Ts-Unit: micros for Lastra (application/vnd.qtsurfer.lastra); Parquet keeps millisecond
timestamps in its schema. dataStatus() can be provisional, and dataThrough() is the last row's
ISO-8601 UTC time. Recent hours may instead return 404 NOT_YET_AVAILABLE with Retry-After.
404 NO_DATA identifies an empty stored hour; 503 TEMPORARILY_UNAVAILABLE is retryable after
Retry-After. 400 OUTSIDE_WINDOW carries ResponseError.next, a CEX dataset import of the hour.
Use a recent UTC hour (YYYY-MM-DDTHH) rather than copying an example's calendar date.
ApiClient exposes the underlying HttpClient.Builder and an ObjectMapper, plus hooks for request/response interceptors.
client.updateBaseUri("https://api.qtsurfer.net/v1");
client.setRequestInterceptor(builder ->
builder.header("Authorization", "Bearer " + token)
.header("X-Request-Id", UUID.randomUUID().toString()));
client.setResponseInterceptor(response ->
log.debug("HTTP {} {}", response.statusCode(), response.uri()));Generated sources are produced by the openapi-generator-maven-plugin during the generate-sources phase and compiled from target/generated-sources/openapi. To regenerate:
mvn -B clean generate-sourcesThe input spec URL is configured in pom.xml (openapi.spec.url property). Point it to a tag or commit for reproducible builds.
| Command | Description |
|---|---|
mvn verify |
Fetch spec, generate, compile, run tests, build jar + sources + javadoc |
mvn clean |
Remove target/ |
Apache-2.0 — see LICENSE.