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 ApiClientFor 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), getLive, listLive, listPublicLive, updateLive, updateLiveParams, 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 both return an InstrumentListResponse HAL envelope: data (List<InstrumentDetail>), meta (InstrumentListMeta), links (InstrumentLinks). Each InstrumentDetail exposes data coverage per data type via coverage (InstrumentCoverage → tickers/klines CoverageWindow, each with from/to/inactiveSince) instead of the old flat dataFrom/dataTo fields.
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", ...).
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;
ExchangeBinaryDownloads downloads = new ExchangeBinaryDownloads(client);
try (var in = downloads.getTickersHour("binance", "BTC", "USDT", "2026-01-15T10")) {
Files.copy(in, Path.of("BTC_USDT_2026-01-15_h10.lastra"));
}
try (var in = downloads.getKlinesHour("binance", "BTC", "USDT", "2026-01-15T10", 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.
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.