Skip to content

About

Java API client for the QTSurfer API, generated from the OpenAPI 3.1 spec. JDK 17+.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

46 Commits

Folders and files

Repository files navigation

QTSurfer API Client · Java

CI JitPack Javadoc JDK 17+ License

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-api on every build.
  • JDK 17+ — modern language features, long-term support.
  • Distributed via JitPack today; Maven Central later.

Installation

Via JitPack

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' }

Via Maven Central (future)

Once published to Central, the coordinate will be com.qtsurfer:api-client-java:x.x.x.

Quick start

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();

API key → JWT

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 ApiClient

AuthApi.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 surface

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.

Send a command to a running strategy

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(...).

Binary downloads (/exchange/{ex}/tickers|klines/{base}/{quote})

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.

Configuring the client

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()));

Regenerating the client

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-sources

The input spec URL is configured in pom.xml (openapi.spec.url property). Point it to a tag or commit for reproducible builds.

Development

Command Description
mvn verify Fetch spec, generate, compile, run tests, build jar + sources + javadoc
mvn clean Remove target/

License

Apache-2.0 — see LICENSE.

About

Java API client for the QTSurfer API, generated from the OpenAPI 3.1 spec. JDK 17+.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages