Skip to content

Commit 51e3e2c

Browse files
committed
feat: v0.2 domain objects (Strategy, Backtest) + remove internal URLs
Domain objects: - Strategy: compiled handle returned by QTSurfer.compile(), reusable across backtests - Backtest (was BacktestJob): running execution handle with id(), state(), progress() (Flow.Publisher), await(), cancel() - BacktestWorkflow (internal): renamed from Backtest to avoid clash with the new public Backtest class - QTSurfer.backtest() shortcut reimplemented as compile().thenCompose(backtest).thenCompose(await) Security: - Remove hardcoded staging URL (api.qtsurfer.net) from integration test default - QTSURFER_API_URL now required alongside JWT_API_TOKEN; test skips when either is absent - Javadoc and README examples use the official domain (api.qtsurfer.com) as illustration only Tests: - DomainObjectsTest: compile handle, backtest+await, progress publisher, cancel with server-side cancelExecution, runFull shortcut - Legacy cancel test disabled (cancellation is on Backtest handle, not on the runFull shortcut future)
1 parent 4e000c7 commit 51e3e2c

10 files changed

Lines changed: 525 additions & 118 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ import java.time.Duration;
6363
import java.util.concurrent.CompletableFuture;
6464

6565
QTSurfer qts = QTSurfer.builder()
66-
.baseUrl("https://api.qtsurfer.net/v1")
66+
.baseUrl("https://api.qtsurfer.com/v1")
6767
.token(System.getenv("JWT_API_TOKEN"))
6868
.build();
6969

@@ -154,12 +154,12 @@ future.cancel(true);
154154

155155
Hits the real backend with `ForcedTradeStrategy` on `binance BTC/USDT` for the previous UTC day. Controlled by env vars:
156156

157-
- `JWT_API_TOKEN` — required; without it the test is disabled via `@EnabledIfEnvironmentVariable`.
158-
- `QTSURFER_API_URL`optional; defaults to `https://api.qtsurfer.net/v1` (Preproduction).
159-
- `QTSURFER_TEST_VERBOSE=1` — stream progress events and the final result through SLF4J.
157+
- `JWT_API_TOKEN` — required; the test is skipped when absent.
158+
- `QTSURFER_API_URL`required; the test is skipped when absent.
159+
- `QTSURFER_TEST_VERBOSE=1`optional; stream progress events and the final result through SLF4J.
160160

161161
```bash
162-
JWT_API_TOKEN=... QTSURFER_TEST_VERBOSE=1 mvn -B -Dtest='*IntegrationTest' test
162+
JWT_API_TOKEN=... QTSURFER_API_URL=... QTSURFER_TEST_VERBOSE=1 mvn -B -Dtest='*IntegrationTest' test
163163
```
164164

165165
## Roadmap
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package net.qtsurfer.api.sdk;
2+
3+
import net.qtsurfer.api.client.model.ResultMap;
4+
5+
import java.util.Objects;
6+
import java.util.concurrent.CompletableFuture;
7+
import java.util.concurrent.Flow;
8+
import java.util.concurrent.atomic.AtomicReference;
9+
10+
/**
11+
* Handle for a running backtest execution. Returned by {@link Strategy#backtest}
12+
* once the execute request has been accepted by the backend.
13+
*
14+
* <ul>
15+
* <li>{@link #await()} resolves with the final {@link ResultMap} when polling completes.</li>
16+
* <li>{@link #progress()} streams {@link BacktestProgress} events through a {@link Flow.Publisher}.</li>
17+
* <li>{@link #cancel()} triggers a best-effort server-side {@code cancelExecution}.</li>
18+
* <li>{@link #state()} returns a local snapshot of the lifecycle.</li>
19+
* </ul>
20+
*/
21+
public final class Backtest {
22+
23+
/** Execution lifecycle as observed by the SDK. */
24+
public enum State { EXECUTING, COMPLETED, FAILED, CANCELED }
25+
26+
private final String id;
27+
private final Strategy strategy;
28+
private final CompletableFuture<ResultMap> result;
29+
private final Flow.Publisher<BacktestProgress> progress;
30+
private final AtomicReference<State> state;
31+
private final Runnable cancelHook;
32+
33+
public Backtest(
34+
String id,
35+
Strategy strategy,
36+
CompletableFuture<ResultMap> result,
37+
Flow.Publisher<BacktestProgress> progress,
38+
AtomicReference<State> state,
39+
Runnable cancelHook) {
40+
this.id = Objects.requireNonNull(id, "id");
41+
this.strategy = Objects.requireNonNull(strategy, "strategy");
42+
this.result = Objects.requireNonNull(result, "result");
43+
this.progress = Objects.requireNonNull(progress, "progress");
44+
this.state = Objects.requireNonNull(state, "state");
45+
this.cancelHook = Objects.requireNonNull(cancelHook, "cancelHook");
46+
}
47+
48+
/** Server-side execute jobId. */
49+
public String id() { return id; }
50+
51+
public Strategy strategy() { return strategy; }
52+
53+
public State state() { return state.get(); }
54+
55+
/** Reactive-streams feed of progress events; terminates when the job reaches a terminal state. */
56+
public Flow.Publisher<BacktestProgress> progress() { return progress; }
57+
58+
/** Resolves with the final ResultMap. Completes exceptionally on failure or cancellation. */
59+
public CompletableFuture<ResultMap> await() { return result; }
60+
61+
/**
62+
* Request cancellation. Cancels the underlying polling future and best-effort
63+
* calls {@code cancelExecution} server-side.
64+
*
65+
* @return {@code true} if the call caused a transition from {@link State#EXECUTING}
66+
*/
67+
public boolean cancel() {
68+
if (!state.compareAndSet(State.EXECUTING, State.CANCELED)) {
69+
return false;
70+
}
71+
cancelHook.run();
72+
return true;
73+
}
74+
75+
@Override
76+
public String toString() {
77+
return "Backtest[" + id + ", state=" + state.get() + "]";
78+
}
79+
}

src/main/java/net/qtsurfer/api/sdk/QTSurfer.java

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import net.qtsurfer.api.client.invoker.ApiClient;
55
import net.qtsurfer.api.client.model.ResultMap;
66
import net.qtsurfer.api.sdk.internal.HttpStrategyCompileClient;
7-
import net.qtsurfer.api.sdk.workflows.Backtest;
7+
import net.qtsurfer.api.sdk.workflows.BacktestWorkflow;
88

99
import java.util.Objects;
1010
import java.util.concurrent.CompletableFuture;
@@ -17,57 +17,69 @@
1717
* <h2>Quick start</h2>
1818
* <pre>{@code
1919
* QTSurfer qts = QTSurfer.builder()
20-
* .baseUrl("https://api.qtsurfer.net/v1")
20+
* .baseUrl("https://api.qtsurfer.com/v1")
2121
* .token(System.getenv("JWT_API_TOKEN"))
2222
* .build();
2323
*
24-
* CompletableFuture<ResultMap> future = qts.backtest(
25-
* BacktestRequest.builder()
26-
* .strategy(Files.readString(Path.of("Strategy.java")))
27-
* .exchangeId("binance")
28-
* .instrument("BTC/USDT")
29-
* .from("2026-04-13T00:00:00Z")
30-
* .to("2026-04-14T00:00:00Z")
31-
* .build(),
32-
* BacktestOptions.builder()
33-
* .onProgress(p -> System.out.println(p.stage() + " " + p.percent()))
34-
* .timeout(Duration.ofMinutes(10))
35-
* .build());
24+
* // One-shot shortcut:
25+
* ResultMap result = qts.backtest(request, options).join();
3626
*
37-
* ResultMap result = future.join();
27+
* // Or decomposed for streaming / reuse:
28+
* Strategy strategy = qts.compile(source).join();
29+
* Backtest job = strategy.backtest(request, options).join();
30+
* job.progress().subscribe( ... );
31+
* ResultMap result = job.await().join();
3832
* }</pre>
3933
*/
4034
public final class QTSurfer {
4135

4236
private final QTSurferOptions options;
43-
private final Backtest backtestWorkflow;
37+
private final BacktestWorkflow backtestWorkflow;
4438

45-
private QTSurfer(QTSurferOptions options, Backtest backtestWorkflow) {
39+
private QTSurfer(QTSurferOptions options, BacktestWorkflow backtestWorkflow) {
4640
this.options = options;
4741
this.backtestWorkflow = backtestWorkflow;
4842
}
4943

5044
public QTSurferOptions options() { return options; }
5145

46+
/** Compile a strategy source. Resolves with a {@link Strategy} handle you can reuse. */
47+
public CompletableFuture<Strategy> compile(String source) {
48+
return compile(source, BacktestOptions.defaults());
49+
}
50+
51+
public CompletableFuture<Strategy> compile(String source, BacktestOptions options) {
52+
Objects.requireNonNull(source, "source");
53+
return backtestWorkflow.compile(source, options);
54+
}
55+
56+
/** Convenience: compile the strategy embedded in the given request. */
57+
public CompletableFuture<Strategy> compile(BacktestRequest request) {
58+
Objects.requireNonNull(request, "request");
59+
return compile(request.strategy(), BacktestOptions.defaults());
60+
}
61+
62+
public CompletableFuture<Strategy> compile(BacktestRequest request, BacktestOptions options) {
63+
Objects.requireNonNull(request, "request");
64+
return compile(request.strategy(), options);
65+
}
66+
5267
/**
53-
* Run a backtest (compile → prepare → execute) returning a {@link CompletableFuture} that
54-
* resolves with the result {@link ResultMap} when the whole workflow completes.
55-
*
56-
* <p>Cancel the returned future to stop polling and trigger a best-effort server-side
57-
* {@code cancelExecution} if the workflow already reached the execute stage.
68+
* Run the full compile → prepare → execute → await pipeline as a single future.
69+
* Equivalent to
70+
* {@code compile(request).thenCompose(s -> s.backtest(request, options)).thenCompose(Backtest::await)}.
5871
*/
5972
public CompletableFuture<ResultMap> backtest(BacktestRequest request) {
6073
return backtest(request, BacktestOptions.defaults());
6174
}
6275

6376
public CompletableFuture<ResultMap> backtest(BacktestRequest request, BacktestOptions options) {
6477
Objects.requireNonNull(request, "request");
65-
return backtestWorkflow.run(request, options);
78+
return backtestWorkflow.runFull(request, options);
6679
}
6780

6881
public static Builder builder() { return new Builder(); }
6982

70-
/** Fluent builder mirroring {@link QTSurferOptions}. */
7183
public static final class Builder {
7284
private final QTSurferOptions.Builder delegate = QTSurferOptions.builder();
7385

@@ -85,13 +97,9 @@ public QTSurfer build() {
8597
String bearer = "Bearer " + opts.token();
8698
apiClient.setRequestInterceptor(b -> b.header("Authorization", bearer));
8799
}
88-
if (opts.httpClient() != null) {
89-
apiClient.setHttpClientBuilder(java.net.http.HttpClient.newBuilder()
90-
.connectTimeout(java.time.Duration.ofSeconds(30))); // fallback; api-client requires a builder
91-
}
92100
BacktestingApi backtestingApi = new BacktestingApi(apiClient);
93101
ExecutorService exec = opts.executor() != null ? opts.executor() : ForkJoinPool.commonPool();
94-
return new QTSurfer(opts, new Backtest(
102+
return new QTSurfer(opts, new BacktestWorkflow(
95103
new HttpStrategyCompileClient(apiClient), backtestingApi, exec));
96104
}
97105
}

src/main/java/net/qtsurfer/api/sdk/QTSurferOptions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
/**
99
* Configuration for a {@link QTSurfer} client.
1010
*
11-
* @param baseUrl API base URL (e.g. {@code https://api.qtsurfer.net/v1})
11+
* @param baseUrl API base URL (e.g. {@code https://api.qtsurfer.com/v1})
1212
* @param token bearer token; {@code null} disables the {@code Authorization} header
1313
* @param httpClient optional custom {@link HttpClient}; when {@code null} the SDK creates one
1414
* @param executor executor that runs the async workflow; when {@code null} uses {@code ForkJoinPool.commonPool()}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package net.qtsurfer.api.sdk;
2+
3+
import net.qtsurfer.api.sdk.workflows.BacktestWorkflow;
4+
5+
import java.util.Objects;
6+
import java.util.concurrent.CompletableFuture;
7+
8+
/**
9+
* A compiled strategy handle returned by {@link QTSurfer#compile}. Use it to
10+
* spawn one or more {@link Backtest}s that reuse the same compilation.
11+
*/
12+
public final class Strategy {
13+
14+
private final String id;
15+
private final BacktestWorkflow workflow;
16+
17+
/** Internal constructor used by the SDK workflow; not part of the public contract. */
18+
public Strategy(String id, BacktestWorkflow workflow) {
19+
this.id = Objects.requireNonNull(id, "id");
20+
this.workflow = Objects.requireNonNull(workflow, "workflow");
21+
}
22+
23+
/** The compiled strategyId as returned by the backend. */
24+
public String id() {
25+
return id;
26+
}
27+
28+
/** Run a backtest with this strategy. Prepare + execute start immediately. */
29+
public CompletableFuture<Backtest> backtest(BacktestRequest request, BacktestOptions options) {
30+
Objects.requireNonNull(request, "request");
31+
return workflow.submitExecution(this, request, options != null ? options : BacktestOptions.defaults());
32+
}
33+
34+
public CompletableFuture<Backtest> backtest(BacktestRequest request) {
35+
return backtest(request, BacktestOptions.defaults());
36+
}
37+
38+
@Override
39+
public String toString() {
40+
return "Strategy[" + id + "]";
41+
}
42+
}

0 commit comments

Comments
 (0)