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.1.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.1.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());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).
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 QTSTimeoutError x -> log.error("Stage timed out: {}", x.getMessage());
case QTSCanceledError x -> log.error("Canceled");
default -> throw e;
}
}Cancel the returned CompletableFuture to stop polling. If the execute stage has already started server-side, the SDK best-effort calls cancelExecution on the backend.
CompletableFuture<ResultMap> future = qts.backtest(req, opts);
// elsewhere:
future.cancel(true);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+BacktestJobclasses with methods likewait(),cancel(),stream() - TTL cache for
exchanges/instruments
- Progress exposed as
Flow.Publisher<BacktestProgress>(JDK reactive-streams)
- Loaders for
signalsUrlParquet intoduckdb-java/lastra-java - Optional reactive adapters (Reactor / RxJava)
Apache-2.0 — see LICENSE.