Opinionated Java SDK for QTSurfer, built on top of net.qtsurfer:api-client.
net.qtsurfer:sdk · com.github.QTSurfer:sdk-java
Where net.qtsurfer:api-client 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.github.QTSurfer</groupId>
<artifactId>sdk-java</artifactId>
<version>v0.2.0</version>
</dependency>The transitive com.github.QTSurfer:api-client-java and dev.failsafe:failsafe come along automatically.
Once published to Central, the coordinate will be net.qtsurfer:sdk:0.2.0.
import net.qtsurfer.api.client.model.ResultMap;
import net.qtsurfer.api.sdk.BacktestOptions;
import net.qtsurfer.api.sdk.BacktestRequest;
import net.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 net.qtsurfer.api.sdk.Backtest;
import net.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 net.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).
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.net.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 overnet.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) - TTL cache for
exchanges/instruments
- Loaders for
signalsUrlParquet intoduckdb-java/lastra-java - Optional reactive adapters (Reactor / RxJava)
Apache-2.0 — see LICENSE.