Skip to content

Commit 0b3f00b

Browse files
committed
fix: treat prepare Partial as terminal, honor real instrument coverage
- StatusNormalizer maps "partial" to COMPLETED (terminal) instead of IN_PROGRESS. Coverage windows can include hours with no trades; a prepare job resolving Partial means the available data is ready to execute on, not that more will arrive. Previously this polled until timeout. - BacktestIntegrationTest derives its backtest window from the instrument coverage API (tickers, falling back to klines) instead of a hardcoded full-day range, capped at 24h.
1 parent fe37d08 commit 0b3f00b

5 files changed

Lines changed: 56 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66

77
## [Unreleased]
88

9+
## [0.6.1] — 2026-07-10
10+
11+
### Fixed 🐛
12+
13+
- **Prepare is treated as terminal on `Partial`, not just `Completed`.** An instrument's coverage can span hours that hold no ticks — e.g. the tickers window `2026-07-10T13:00Z → 15:30Z`, where only the `14:00–15:00` hour traded — in which case the prepare reports partial coverage. The backtest workflow now proceeds to execute on the available data instead of polling that state until it times out.
14+
915
## [0.6.0] — 2026-07-10
1016

1117
### Changed 🔄

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.qtsurfer</groupId>
88
<artifactId>sdk-java</artifactId>
9-
<version>0.6.0</version>
9+
<version>0.6.1</version>
1010
<packaging>jar</packaging>
1111

1212
<name>QTSurfer SDK</name>

‎src/main/java/com/qtsurfer/api/sdk/internal/StatusNormalizer.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ public static Normalized normalize(Object raw) {
1616
if (raw == null) return Normalized.IN_PROGRESS;
1717
String value = raw.toString().toLowerCase(java.util.Locale.ROOT);
1818
return switch (value) {
19-
case "completed" -> Normalized.COMPLETED;
19+
// A prepare job is terminal as soon as it returns: {@code Completed}, or the
20+
// legacy {@code Partial} (some hours in the range legitimately hold no data —
21+
// e.g. low-activity hours — so those hours never arrive). Both mean "ready to
22+
// execute on the available coverage"; polling on Partial would hang forever.
23+
case "completed", "partial" -> Normalized.COMPLETED;
2024
case "failed" -> Normalized.FAILED;
2125
case "aborted", "cancelled", "canceled" -> Normalized.ABORTED;
2226
default -> Normalized.IN_PROGRESS;

‎src/test/java/com/qtsurfer/api/sdk/integration/BacktestIntegrationTest.java‎

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.qtsurfer.api.sdk.integration;
22

3+
import com.qtsurfer.api.client.model.CoverageWindow;
4+
import com.qtsurfer.api.client.model.InstrumentCoverage;
5+
import com.qtsurfer.api.client.model.InstrumentDetail;
36
import com.qtsurfer.api.client.model.ResultMap;
47
import com.qtsurfer.api.sdk.BacktestOptions;
58
import com.qtsurfer.api.sdk.BacktestRequest;
@@ -14,8 +17,7 @@
1417
import java.io.InputStream;
1518
import java.nio.charset.StandardCharsets;
1619
import java.time.Duration;
17-
import java.time.Instant;
18-
import java.time.temporal.ChronoUnit;
20+
import java.time.OffsetDateTime;
1921
import java.util.Objects;
2022
import java.util.Set;
2123
import java.util.concurrent.ConcurrentSkipListSet;
@@ -55,12 +57,26 @@ void completesCompilePrepareExecuteAgainstBinanceBtcUsdt() throws Exception {
5557
.token(token)
5658
.build();
5759

60+
// Honor the instrument's real data availability from the instruments API
61+
// instead of presupposing a fixed calendar day — the backend may hold only
62+
// a short recent window. Prefer ticker coverage (this is a TICKER backtest),
63+
// fall back to klines, and cap the span at 24h.
64+
CoverageWindow window = availableWindow(qts, "binance", "BTC/USDT");
65+
OffsetDateTime to = window.getTo();
66+
OffsetDateTime from = window.getFrom();
67+
if (Duration.between(from, to).compareTo(Duration.ofHours(24)) > 0) {
68+
from = to.minus(Duration.ofHours(24));
69+
}
70+
if (VERBOSE) {
71+
log.info("Backtest window (instrument coverage, capped at 24h): {} → {}", from, to);
72+
}
73+
5874
BacktestRequest req = BacktestRequest.builder()
5975
.strategy(loadFixture("fixtures/ForcedTradeStrategy.java"))
6076
.exchangeId("binance")
6177
.instrument("BTC/USDT")
62-
.from(dayStartIso(1))
63-
.to(dayStartIso(0))
78+
.from(from.toInstant().toString())
79+
.to(to.toInstant().toString())
6480
.build();
6581

6682
Set<BacktestStage> stages = new ConcurrentSkipListSet<>();
@@ -96,11 +112,22 @@ void completesCompilePrepareExecuteAgainstBinanceBtcUsdt() throws Exception {
96112
assertTrue(stages.contains(BacktestStage.EXECUTING), "executing stage fired");
97113
}
98114

99-
private static String dayStartIso(int offsetDays) {
100-
return Instant.now()
101-
.truncatedTo(ChronoUnit.DAYS)
102-
.minus(offsetDays, ChronoUnit.DAYS)
103-
.toString();
115+
/**
116+
* Resolves the instrument's currently-available data window from the
117+
* instruments API, preferring ticker coverage and falling back to klines.
118+
*/
119+
private static CoverageWindow availableWindow(QTSurfer qts, String exchangeId, String instrumentId) {
120+
InstrumentDetail instrument = qts.instruments(exchangeId).stream()
121+
.filter(i -> instrumentId.equals(i.getId()))
122+
.findFirst()
123+
.orElseThrow(() -> new AssertionError(instrumentId + " not listed on " + exchangeId));
124+
InstrumentCoverage coverage = instrument.getCoverage();
125+
assertNotNull(coverage, "coverage for " + instrumentId);
126+
CoverageWindow window = coverage.getTickers() != null ? coverage.getTickers() : coverage.getKlines();
127+
assertNotNull(window, "ticker/kline coverage window for " + instrumentId);
128+
assertNotNull(window.getFrom(), "coverage window from");
129+
assertNotNull(window.getTo(), "coverage window to");
130+
return window;
104131
}
105132

106133
private static String loadFixture(String path) throws IOException {

‎src/test/java/com/qtsurfer/api/sdk/internal/StatusNormalizerTest.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ void recognizesCompletedCaseInsensitive() {
1414
assertEquals(Normalized.COMPLETED, StatusNormalizer.normalize("COMPLETED"));
1515
}
1616

17+
@Test
18+
void treatsPartialAsTerminalCompleted() {
19+
// A prepare job is terminal on the legacy `Partial` status (low-activity hours
20+
// never arrive), so it must resolve like `Completed`, not keep polling.
21+
assertEquals(Normalized.COMPLETED, StatusNormalizer.normalize("partial"));
22+
assertEquals(Normalized.COMPLETED, StatusNormalizer.normalize("Partial"));
23+
}
24+
1725
@Test
1826
void recognizesFailed() {
1927
assertEquals(Normalized.FAILED, StatusNormalizer.normalize("failed"));

0 commit comments

Comments
 (0)