Opinionated Java SDK for QTSurfer, built on top of com.qtsurfer:api-client.
com.qtsurfer:sdk-java
Where com.qtsurfer:api-client-java gives you one method per endpoint, this package adds workflow orchestration, normalized errors, and cancellation — run a backtest with a single CompletableFuture.
- Powered by
java.net.http.HttpClient(JDK built-in) via the transitive client. - Retry/backoff/timeout delegated to Failsafe — no hand-rolled polling loops.
- SLF4J 2.x API (no binding shipped — consumers bring their own).
- JDK 17+.
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.qtsurfer</groupId>
<artifactId>sdk-java</artifactId>
<version>0.4.1</version>
</dependency>The transitive com.qtsurfer:api-client-java and dev.failsafe:failsafe come along automatically.
Once published to Central, the coordinate will be com.qtsurfer:sdk:0.4.1.
import com.qtsurfer.api.client.model.ResultMap;
import com.qtsurfer.api.sdk.BacktestOptions;
import com.qtsurfer.api.sdk.BacktestRequest;
import com.qtsurfer.api.sdk.QTSurfer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
QTSurfer qts = QTSurfer.builder()
.baseUrl("https://api.qtsurfer.com/v1")
.token(System.getenv("JWT_API_TOKEN"))
.build();
CompletableFuture<ResultMap> future = qts.backtest(
BacktestRequest.builder()
.strategy(Files.readString(Path.of("Strategy.java")))
.exchangeId("binance")
.instrument("BTC/USDT")
.from("2026-04-13T00:00:00Z")
.to("2026-04-14T00:00:00Z")
.storeSignals(true)
.build(),
BacktestOptions.builder()
.onProgress(p -> System.out.printf("[%s] %s%n",
p.stage(),
p.percent() != null ? String.format("%.1f%%", p.percent()) : ""))
.pollInterval(Duration.ofMillis(500))
.maxPollInterval(Duration.ofSeconds(5))
.timeout(Duration.ofMinutes(10))
.build());
ResultMap result = future.join();
System.out.println("PnL: " + result.getPnlTotal());
System.out.println("Trades: " + result.getTotalTrades());qts.backtest(req) is a shortcut for compile → backtest → await. When you want
the intermediate handles — to reuse a compiled strategy, subscribe to progress as
a reactive stream, or cancel mid-run — use them directly:
import com.qtsurfer.api.sdk.Backtest;
import com.qtsurfer.api.sdk.Strategy;
Strategy strategy = qts.compile(request).join();
Backtest job = strategy.backtest(request, options).join();
job.progress().subscribe(/* a Flow.Subscriber<BacktestProgress> */);
ResultMap result = job.await().join();Backtest exposes id(), state(), progress() (a Flow.Publisher<BacktestProgress>),
await(), and cancel() (best-effort server-side cancelExecution).
Orchestrates the four-step workflow exposed by the raw API:
- Compile the strategy (
POST /strategyin async mode) and pollGET /strategy/{jobId}until completed. - Prepare the data range (
POST /backtest/{exchange}/ticker/prepare) and pollGET …/prepare/{jobId}untilCompleted. - Execute the backtest (
POST /backtest/{exchange}/ticker/execute) and pollGET …/execute/{jobId}untilCompleted. - Resolve the returned
CompletableFuturewith theResultMap(pnlTotal,totalTrades,sharpeRatio,signalsUrl, …).
Polling uses Failsafe RetryPolicy with exponential backoff (initial → max, capped) plus an optional Timeout per stage.
Progress is emitted:
- On every stage transition (
percent == null). - After each poll where the backend reports
size > 0(percentin 0–100).
Stream one hour of raw ticker or kline data for an instrument. The default wire format is
Lastra (application/vnd.lastra); pass
DownloadFormat.PARQUET for on-the-fly Parquet conversion.
import com.qtsurfer.api.sdk.DownloadFormat;
// Lastra (default), streamed straight to disk
try (var in = qts.tickers("binance", "BTC", "USDT", "2026-01-15T10")) {
Files.copy(in, Path.of("BTC_USDT_2026-01-15_h10.lastra"));
}
// Parquet
try (var in = qts.klines("binance", "BTC", "USDT", "2026-01-15T10", DownloadFormat.PARQUET)) {
// feed into Apache Parquet, DuckDB, etc.
}The caller closes the stream. HTTP errors surface as QTSDownloadError (subclass of QTSError).
List available exchanges and the instruments (with data-availability windows) for a given exchange.
import com.qtsurfer.api.client.model.Exchange;
import com.qtsurfer.api.client.model.InstrumentDetail;
// List exchanges
List<Exchange> exchanges = qts.exchanges();
exchanges.forEach(e -> System.out.println(e.getId() + " — " + e.getName()));
// → binance — Binance
// → binancefutures — Binance Futures
// List instruments for an exchange
List<InstrumentDetail> instruments = qts.instruments("binance");
instruments.forEach(i -> System.out.printf(
"%s data: %s → %s last: %.2f%n",
i.getId(), i.getDataFrom(), i.getDataTo(), i.getLastPrice()));HTTP errors surface as QTSError. Responses reflect live platform state — no client-side cache.
All SDK errors extend QTSError (a RuntimeException) and surface as the cause of the CompletionException wrapping them when the future fails.
try {
qts.backtest(req).join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
switch (cause) {
case QTSStrategyCompileError x -> log.error("Compile failed: {}", x.getMessage());
case QTSPreparationError x -> log.error("Data prep failed: {}", x.getMessage());
case QTSExecutionError x -> log.error("Execution failed: {}", x.getMessage());
case QTSDownloadError x -> log.error("Download failed: {}", x.getMessage());
case QTSTimeoutError x -> log.error("Stage timed out: {}", x.getMessage());
case QTSCanceledError x -> log.error("Canceled");
default -> throw e;
}
}Two ways to cancel an in-flight backtest:
// 1. Cancel the future returned by the backtest() shortcut.
CompletableFuture<ResultMap> future = qts.backtest(req, opts);
future.cancel(true);
// 2. Cancel through the Backtest handle (decomposed API).
Backtest job = strategy.backtest(req, opts).join();
job.cancel();Both stop polling immediately and, if the execute stage has already started
server-side, best-effort call cancelExecution on the backend.
dev.failsafe:failsafe— retry policies with exponential backoff, optional per-stageTimeout,withInterrupt()so thread interruption fromCompletableFuture#cancel(true)propagates cleanly.com.qtsurfer:api-client— generated with openapi-generator'snativelibrary; usesjava.net.http.HttpClient, so no OkHttp/Apache HttpClient transitive dependency.StatusNormalizer— maps the backend's mixed-case status strings (queued,started,completed,failed, …) to a stable enum so the retry predicate and terminal checks work regardless of spec drift.
| Command | Description |
|---|---|
mvn verify |
Compile, run unit tests, build jar + sources + javadoc |
mvn -B -Dtest='*IntegrationTest' test |
Run the integration test — requires JWT_API_TOKEN |
mvn clean |
Remove target/ |
Hits the real backend with ForcedTradeStrategy on binance BTC/USDT for the previous UTC day. Controlled by env vars:
JWT_API_TOKEN— required; the test is skipped when absent.QTSURFER_API_URL— required; the test is skipped when absent.QTSURFER_TEST_VERBOSE=1— optional; stream progress events and the final result through SLF4J.
JWT_API_TOKEN=... QTSURFER_API_URL=... QTSURFER_TEST_VERBOSE=1 mvn -B -Dtest='*IntegrationTest' test-
QTSurferclient overcom.qtsurfer:api-client -
qts.backtest()orchestrating compile → prepare → execute - Backoff, timeout, and cancellation via Failsafe policies
-
QTSErrorhierarchy
-
Strategy+Backtesthandles withid(),state(),progress(),await(),cancel() - Progress exposed as
Flow.Publisher<BacktestProgress>(JDK reactive-streams) - Hourly tickers/klines downloads (
qts.tickers(...)/qts.klines(...)) withDownloadFormat(Lastra/Parquet)
-
qts.exchanges()→List<Exchange>(live, no cache) -
qts.instruments(exchangeId)→List<InstrumentDetail>with data-availability windows, last price, and 24 h volume
- TTL cache for
exchanges/instruments - Loaders for
signalsUrlParquet intoduckdb-java/lastra-java - Optional reactive adapters (Reactor / RxJava)
Apache-2.0 — see LICENSE.