@qtsurfer/sdk 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2026 QTSurfer
189
+ Copyright 2026 Wualabs LTD
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -35,7 +35,7 @@ const result = await qts.backtest(
35
35
  {
36
36
  strategy: readFileSync('./MyStrategy.java', 'utf8'),
37
37
  exchangeId: 'binance',
38
- instrument: 'BTCUSDT',
38
+ instrument: 'BTC/USDT',
39
39
  from: '2024-01-01',
40
40
  to: '2024-12-31',
41
41
  storeSignals: true,
@@ -67,6 +67,32 @@ Polling uses exponential backoff (`intervalMs * 1.5`, capped at `maxIntervalMs`)
67
67
 
68
68
  Progress is emitted on every stage transition and after each poll whose `size > 0`.
69
69
 
70
+ ## Hourly tickers/klines downloads
71
+
72
+ Stream one hour of raw ticker or kline data for an instrument. The default wire format is [Lastra](https://github.com/QTSurfer/lastra-ts) (`application/vnd.lastra`); pass `format: 'parquet'` for on-the-fly Parquet conversion.
73
+
74
+ ```ts
75
+ // Lastra (default)
76
+ const blob = await qts.tickers({
77
+ exchangeId: 'binance',
78
+ base: 'BTC',
79
+ quote: 'USDT',
80
+ hour: '2026-01-15T10',
81
+ });
82
+ await Bun.write('BTC_USDT_2026-01-15_h10.lastra', await blob.arrayBuffer());
83
+
84
+ // Parquet
85
+ const klines = await qts.klines({
86
+ exchangeId: 'binance',
87
+ base: 'BTC',
88
+ quote: 'USDT',
89
+ hour: '2026-01-15T10',
90
+ format: 'parquet',
91
+ });
92
+ ```
93
+
94
+ HTTP errors surface as `QTSDownloadError` (subclass of `QTSError`).
95
+
70
96
  ## Error hierarchy
71
97
 
72
98
  All SDK errors extend `QTSError` so you can catch them generically or match by subclass.
@@ -77,6 +103,7 @@ import {
77
103
  QTSStrategyCompileError,
78
104
  QTSPreparationError,
79
105
  QTSExecutionError,
106
+ QTSDownloadError,
80
107
  QTSTimeoutError,
81
108
  QTSCanceledError,
82
109
  } from '@qtsurfer/sdk';
@@ -90,6 +117,8 @@ try {
90
117
  console.error('Data prep failed:', e.message);
91
118
  } else if (e instanceof QTSExecutionError) {
92
119
  console.error('Execution failed:', e.message);
120
+ } else if (e instanceof QTSDownloadError) {
121
+ console.error('Download failed:', e.message);
93
122
  } else if (e instanceof QTSTimeoutError) {
94
123
  console.error('Stage timed out');
95
124
  } else if (e instanceof QTSCanceledError) {
@@ -145,7 +174,8 @@ src/
145
174
  ├── client.ts # QTSurfer class
146
175
  ├── errors.ts # QTSError hierarchy
147
176
  └── workflows/
148
- └── backtest.ts # compile → prepare → execute (cockatiel policies)
177
+ ├── backtest.ts # compile → prepare → execute (cockatiel policies)
178
+ └── downloads.ts # hourly tickers/klines as Lastra/Parquet blobs
149
179
  ```
150
180
 
151
181
  ## Development
@@ -153,7 +183,21 @@ src/
153
183
  | Script | Description |
154
184
  | ------ | ----------- |
155
185
  | `npm run lint` | Type-check without emitting |
156
- | `npm run build` | Compile to `dist/` |
186
+ | `npm run build` | Bundle to `dist/` via `tsup` |
187
+ | `npm test` | Run unit tests |
188
+ | `npm run test:integration` | Run the integration test (requires `JWT_API_TOKEN`). Set `QTSURFER_TEST_VERBOSE=1` to stream progress + final result |
189
+ | `npm run changeset` | Record a changeset for the next release |
190
+ | `npm run changeset:version` | Consume pending changesets: bump `package.json` and update `CHANGELOG.md` |
191
+ | `npm run changeset:publish` | Publish released packages to npm (used by CI) |
192
+
193
+ ### Releasing
194
+
195
+ Versioning and changelogs are managed with [changesets](https://github.com/changesets/changesets):
196
+
197
+ 1. Create a changeset describing your change: `npm run changeset`
198
+ 2. Commit the generated `.changeset/<slug>.md` with your PR.
199
+ 3. When ready to release, run `npm run changeset:version` locally. It bumps `package.json` and appends to `CHANGELOG.md`.
200
+ 4. Commit the version bump, tag `vX.Y.Z`, and push the tag; the `Publish to npm` workflow handles the rest.
157
201
 
158
202
  ## License
159
203
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,89 @@
1
- export { QTSurfer, type QTSurferOptions } from './client';
2
- export { QTSError, QTSStrategyCompileError, QTSPreparationError, QTSExecutionError, QTSTimeoutError, QTSCanceledError, } from './errors';
3
- export type { BacktestRequest, BacktestResult, BacktestProgress, BacktestStage, BacktestOptions, } from './workflows/backtest';
4
- //# sourceMappingURL=index.d.ts.map
1
+ import { ResultMap } from '@qtsurfer/api-client';
2
+
3
+ interface BacktestRequest {
4
+ /** Strategy source code (Java) */
5
+ strategy: string;
6
+ /** Exchange id, e.g. `binance` */
7
+ exchangeId: string;
8
+ /** Instrument symbol, e.g. `BTC/USDT` */
9
+ instrument: string;
10
+ /** Date range start (ISO-8601, ISO DATE or BASIC ISO DATE) */
11
+ from: string;
12
+ /** Date range end (same formats as `from`) */
13
+ to: string;
14
+ /** When true, the worker uploads emitted signals to object storage. */
15
+ storeSignals?: boolean;
16
+ }
17
+ type BacktestResult = ResultMap;
18
+ type BacktestStage = 'compiling' | 'preparing' | 'executing';
19
+ interface BacktestProgress {
20
+ stage: BacktestStage;
21
+ /** 0-100 when size is known. Undefined during stage start. */
22
+ percent?: number;
23
+ }
24
+ interface BacktestOptions {
25
+ /** Abort the workflow. Cancels the current poll and calls `cancelExecution` server-side if execution has started. */
26
+ signal?: AbortSignal;
27
+ /** Called on stage transitions and after each poll with updated progress. */
28
+ onProgress?: (p: BacktestProgress) => void;
29
+ /** Initial interval between polls. Default 500ms, backed off up to `maxPollIntervalMs`. */
30
+ pollIntervalMs?: number;
31
+ /** Upper bound for exponential backoff. Default 5000ms. */
32
+ maxPollIntervalMs?: number;
33
+ /** Per-stage timeout. Default none. */
34
+ timeoutMs?: number;
35
+ }
36
+
37
+ /** Wire format for hourly tickers/klines downloads. */
38
+ type DownloadFormat = 'lastra' | 'parquet';
39
+
40
+ interface QTSurferOptions {
41
+ baseUrl: string;
42
+ token?: string;
43
+ fetch?: typeof fetch;
44
+ }
45
+ interface DownloadHourArgs {
46
+ exchangeId: string;
47
+ base: string;
48
+ quote: string;
49
+ /** Hour selector in `YYYY-MM-DDTHH` (UTC). */
50
+ hour: string;
51
+ /** Wire format. Defaults to `'lastra'`. */
52
+ format?: DownloadFormat;
53
+ }
54
+ declare class QTSurfer {
55
+ constructor(options: QTSurferOptions);
56
+ backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult>;
57
+ /**
58
+ * Download one hour of raw tickers for an instrument as a {@link Blob}.
59
+ * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.
60
+ */
61
+ tickers(args: DownloadHourArgs): Promise<Blob>;
62
+ /** Download one hour of klines for an instrument as a {@link Blob}. */
63
+ klines(args: DownloadHourArgs): Promise<Blob>;
64
+ }
65
+
66
+ declare class QTSError extends Error {
67
+ readonly cause?: unknown | undefined;
68
+ constructor(message: string, cause?: unknown | undefined);
69
+ }
70
+ declare class QTSStrategyCompileError extends QTSError {
71
+ constructor(message: string, cause?: unknown);
72
+ }
73
+ declare class QTSPreparationError extends QTSError {
74
+ constructor(message: string, cause?: unknown);
75
+ }
76
+ declare class QTSExecutionError extends QTSError {
77
+ constructor(message: string, cause?: unknown);
78
+ }
79
+ declare class QTSTimeoutError extends QTSError {
80
+ constructor(message: string, cause?: unknown);
81
+ }
82
+ declare class QTSCanceledError extends QTSError {
83
+ constructor(message: string, cause?: unknown);
84
+ }
85
+ declare class QTSDownloadError extends QTSError {
86
+ constructor(message: string, cause?: unknown);
87
+ }
88
+
89
+ export { type BacktestOptions, type BacktestProgress, type BacktestRequest, type BacktestResult, type BacktestStage, type DownloadFormat, type DownloadHourArgs, QTSCanceledError, QTSDownloadError, QTSError, QTSExecutionError, QTSPreparationError, QTSStrategyCompileError, QTSTimeoutError, QTSurfer, type QTSurferOptions };
package/dist/index.js CHANGED
@@ -1,3 +1,331 @@
1
- export { QTSurfer } from './client';
2
- export { QTSError, QTSStrategyCompileError, QTSPreparationError, QTSExecutionError, QTSTimeoutError, QTSCanceledError, } from './errors';
1
+ // src/client.ts
2
+ import { client as apiClient } from "@qtsurfer/api-client";
3
+
4
+ // src/workflows/backtest.ts
5
+ import {
6
+ cancelExecution,
7
+ executeBacktesting,
8
+ getExecutionResult,
9
+ getPreparationStatus,
10
+ getStrategyStatus,
11
+ postStrategy,
12
+ prepareBacktesting
13
+ } from "@qtsurfer/api-client";
14
+ import {
15
+ ExponentialBackoff,
16
+ TaskCancelledError,
17
+ TimeoutStrategy,
18
+ handleWhenResult,
19
+ retry,
20
+ timeout,
21
+ wrap
22
+ } from "cockatiel";
23
+
24
+ // src/errors.ts
25
+ var QTSError = class extends Error {
26
+ constructor(message, cause) {
27
+ super(message);
28
+ this.cause = cause;
29
+ this.name = "QTSError";
30
+ }
31
+ cause;
32
+ };
33
+ var QTSStrategyCompileError = class extends QTSError {
34
+ constructor(message, cause) {
35
+ super(message, cause);
36
+ this.name = "QTSStrategyCompileError";
37
+ }
38
+ };
39
+ var QTSPreparationError = class extends QTSError {
40
+ constructor(message, cause) {
41
+ super(message, cause);
42
+ this.name = "QTSPreparationError";
43
+ }
44
+ };
45
+ var QTSExecutionError = class extends QTSError {
46
+ constructor(message, cause) {
47
+ super(message, cause);
48
+ this.name = "QTSExecutionError";
49
+ }
50
+ };
51
+ var QTSTimeoutError = class extends QTSError {
52
+ constructor(message, cause) {
53
+ super(message, cause);
54
+ this.name = "QTSTimeoutError";
55
+ }
56
+ };
57
+ var QTSCanceledError = class extends QTSError {
58
+ constructor(message, cause) {
59
+ super(message, cause);
60
+ this.name = "QTSCanceledError";
61
+ }
62
+ };
63
+ var QTSDownloadError = class extends QTSError {
64
+ constructor(message, cause) {
65
+ super(message, cause);
66
+ this.name = "QTSDownloadError";
67
+ }
68
+ };
69
+
70
+ // src/workflows/backtest.ts
71
+ var TICKER = "ticker";
72
+ function normalizeStatus(raw) {
73
+ const value = typeof raw === "string" ? raw.toLowerCase() : "";
74
+ if (value === "completed") return "completed";
75
+ if (value === "failed") return "failed";
76
+ if (value === "aborted" || value === "cancelled" || value === "canceled") {
77
+ return "aborted";
78
+ }
79
+ return "in-progress";
80
+ }
81
+ async function backtest(req, opts = {}) {
82
+ const policy = buildStagePolicy(opts);
83
+ opts.onProgress?.({ stage: "compiling" });
84
+ const strategyId = await compileStrategy(req.strategy, policy, opts);
85
+ opts.onProgress?.({ stage: "preparing" });
86
+ const prepareJobId = await prepareData(req, policy, opts);
87
+ opts.onProgress?.({ stage: "executing" });
88
+ return executeStrategy(req, prepareJobId, strategyId, policy, opts);
89
+ }
90
+ function buildStagePolicy(opts) {
91
+ const retryPolicy = retry(
92
+ handleWhenResult((r) => {
93
+ const status = r?.status;
94
+ return normalizeStatus(status) === "in-progress";
95
+ }),
96
+ {
97
+ maxAttempts: Number.MAX_SAFE_INTEGER,
98
+ backoff: new ExponentialBackoff({
99
+ initialDelay: opts.pollIntervalMs ?? 500,
100
+ maxDelay: opts.maxPollIntervalMs ?? 5e3
101
+ })
102
+ }
103
+ );
104
+ return opts.timeoutMs ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy) : retryPolicy;
105
+ }
106
+ async function compileStrategy(source, policy, opts) {
107
+ const { data, error } = await postStrategy({
108
+ body: source,
109
+ headers: { "X-Compile-Async": true },
110
+ ...opts.signal ? { signal: opts.signal } : {}
111
+ });
112
+ if (error) throw new QTSStrategyCompileError("Strategy submission failed", error);
113
+ if (!data) throw new QTSStrategyCompileError("Empty response from strategy endpoint");
114
+ if ("strategyId" in data && data.strategyId) {
115
+ return data.strategyId;
116
+ }
117
+ if (!("jobId" in data) || !data.jobId) {
118
+ throw new QTSStrategyCompileError("Missing jobId/strategyId in compile response");
119
+ }
120
+ const compileJobId = data.jobId;
121
+ const status = await runStage(
122
+ policy,
123
+ opts,
124
+ async ({ signal }) => {
125
+ const res = await getStrategyStatus({ path: { strategyId: compileJobId }, signal });
126
+ if (res.error) throw new QTSStrategyCompileError("Compile status request failed", res.error);
127
+ if (!res.data) throw new QTSStrategyCompileError("Empty compile status response");
128
+ return res.data;
129
+ }
130
+ );
131
+ const norm = normalizeStatus(status.status);
132
+ if (norm === "failed") {
133
+ throw new QTSStrategyCompileError(status.statusDetail ?? "Strategy compilation failed");
134
+ }
135
+ if (norm === "aborted") {
136
+ throw new QTSCanceledError("Strategy compilation aborted");
137
+ }
138
+ if (!status.strategyId) {
139
+ throw new QTSStrategyCompileError("Compile completed without a strategyId");
140
+ }
141
+ return status.strategyId;
142
+ }
143
+ async function prepareData(req, policy, opts) {
144
+ const { data, error } = await prepareBacktesting({
145
+ path: { exchangeId: req.exchangeId, type: TICKER },
146
+ body: { instrument: req.instrument, from: req.from, to: req.to },
147
+ ...opts.signal ? { signal: opts.signal } : {}
148
+ });
149
+ if (error) throw new QTSPreparationError("Prepare submission failed", error);
150
+ if (!data?.jobId) throw new QTSPreparationError("Missing jobId in prepare response");
151
+ const prepareJobId = data.jobId;
152
+ const state = await runStage(
153
+ policy,
154
+ opts,
155
+ async ({ signal }) => {
156
+ const res = await getPreparationStatus({
157
+ path: { exchangeId: req.exchangeId, type: TICKER, jobId: prepareJobId },
158
+ signal
159
+ });
160
+ if (res.error) throw new QTSPreparationError("Preparation status request failed", res.error);
161
+ if (!res.data) throw new QTSPreparationError("Empty preparation status response");
162
+ return res.data;
163
+ },
164
+ (r) => {
165
+ if (r.size > 0) {
166
+ opts.onProgress?.({ stage: "preparing", percent: r.completed / r.size * 100 });
167
+ }
168
+ }
169
+ );
170
+ const prepNorm = normalizeStatus(state.status);
171
+ if (prepNorm === "failed") {
172
+ throw new QTSPreparationError(state.statusDetail ?? "Data preparation failed");
173
+ }
174
+ if (prepNorm === "aborted") {
175
+ throw new QTSCanceledError("Data preparation aborted");
176
+ }
177
+ return prepareJobId;
178
+ }
179
+ async function executeStrategy(req, prepareJobId, strategyId, policy, opts) {
180
+ const { data, error } = await executeBacktesting({
181
+ path: { exchangeId: req.exchangeId, type: TICKER },
182
+ body: {
183
+ prepareJobId,
184
+ strategyId,
185
+ ...req.storeSignals !== void 0 ? { storeSignals: req.storeSignals } : {}
186
+ },
187
+ ...opts.signal ? { signal: opts.signal } : {}
188
+ });
189
+ if (error) throw new QTSExecutionError("Execute submission failed", error);
190
+ if (!data?.jobId) throw new QTSExecutionError("Missing jobId in execute response");
191
+ const executeJobId = data.jobId;
192
+ try {
193
+ const finalResult = await runStage(
194
+ policy,
195
+ opts,
196
+ async ({ signal }) => {
197
+ const res = await getExecutionResult({
198
+ path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },
199
+ signal
200
+ });
201
+ if (res.error) throw new QTSExecutionError("Execution result request failed", res.error);
202
+ if (!res.data) throw new QTSExecutionError("Empty execution result response");
203
+ return { ...res.data.state, __result: res.data.results };
204
+ },
205
+ (r) => {
206
+ if (r.size > 0) {
207
+ opts.onProgress?.({ stage: "executing", percent: r.completed / r.size * 100 });
208
+ }
209
+ }
210
+ );
211
+ const execNorm = normalizeStatus(finalResult.status);
212
+ if (execNorm === "failed") {
213
+ throw new QTSExecutionError(finalResult.statusDetail ?? "Execution failed");
214
+ }
215
+ if (execNorm === "aborted") {
216
+ throw new QTSCanceledError("Execution aborted");
217
+ }
218
+ return finalResult.__result;
219
+ } catch (err) {
220
+ if (err instanceof QTSCanceledError) {
221
+ await cancelExecution({
222
+ path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId }
223
+ }).catch(() => void 0);
224
+ }
225
+ throw err;
226
+ }
227
+ }
228
+ async function runStage(policy, opts, fetchFn, onEachAttempt) {
229
+ try {
230
+ return await policy.execute(async (ctx) => {
231
+ if (opts.signal?.aborted) throw new QTSCanceledError("Workflow aborted");
232
+ const result = await fetchFn(ctx);
233
+ onEachAttempt?.(result);
234
+ if (opts.signal?.aborted) throw new QTSCanceledError("Workflow aborted");
235
+ return result;
236
+ }, opts.signal);
237
+ } catch (err) {
238
+ if (err instanceof QTSCanceledError) throw err;
239
+ if (err instanceof TaskCancelledError) {
240
+ if (opts.signal?.aborted) throw new QTSCanceledError("Workflow aborted", err);
241
+ throw new QTSTimeoutError(`Stage exceeded ${opts.timeoutMs}ms`, err);
242
+ }
243
+ if (opts.signal?.aborted) throw new QTSCanceledError("Workflow aborted", err);
244
+ throw err;
245
+ }
246
+ }
247
+
248
+ // src/workflows/downloads.ts
249
+ import {
250
+ getExchangeKlinesHour,
251
+ getExchangeTickersHour
252
+ } from "@qtsurfer/api-client";
253
+ async function downloadTickers(params) {
254
+ const { exchangeId, base, quote, hour, format } = params;
255
+ const { data, error, response } = await getExchangeTickersHour({
256
+ path: { exchangeId, base, quote },
257
+ query: { hour, ...format ? { format } : {} }
258
+ });
259
+ if (error) {
260
+ throw new QTSDownloadError(
261
+ `tickers download failed: HTTP ${response.status} \u2014 ${describe(error)}`,
262
+ error
263
+ );
264
+ }
265
+ return data;
266
+ }
267
+ async function downloadKlines(params) {
268
+ const { exchangeId, base, quote, hour, format } = params;
269
+ const { data, error, response } = await getExchangeKlinesHour({
270
+ path: { exchangeId, base, quote },
271
+ query: { hour, ...format ? { format } : {} }
272
+ });
273
+ if (error) {
274
+ throw new QTSDownloadError(
275
+ `klines download failed: HTTP ${response.status} \u2014 ${describe(error)}`,
276
+ error
277
+ );
278
+ }
279
+ return data;
280
+ }
281
+ function describe(error) {
282
+ if (error && typeof error === "object") {
283
+ const e = error;
284
+ const code = typeof e.code === "string" ? e.code : void 0;
285
+ const message = typeof e.message === "string" ? e.message : void 0;
286
+ if (code && message) return `${code}: ${message}`;
287
+ if (message) return message;
288
+ if (code) return code;
289
+ }
290
+ return String(error);
291
+ }
292
+
293
+ // src/client.ts
294
+ var QTSurfer = class {
295
+ constructor(options) {
296
+ apiClient.setConfig({
297
+ baseUrl: options.baseUrl,
298
+ ...options.token ? { headers: { Authorization: `Bearer ${options.token}` } } : {},
299
+ ...options.fetch ? { fetch: options.fetch } : {}
300
+ });
301
+ }
302
+ backtest(req, opts) {
303
+ return backtest(req, opts);
304
+ }
305
+ /**
306
+ * Download one hour of raw tickers for an instrument as a {@link Blob}.
307
+ * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.
308
+ */
309
+ tickers(args) {
310
+ return downloadTickers(args);
311
+ }
312
+ /** Download one hour of klines for an instrument as a {@link Blob}. */
313
+ klines(args) {
314
+ return downloadKlines(args);
315
+ }
316
+ // Future surface:
317
+ // strategies: { compile, status, list }
318
+ // instruments: { list, get } with TTL cache
319
+ // jobs: { cancel, stream, result }
320
+ };
321
+ export {
322
+ QTSCanceledError,
323
+ QTSDownloadError,
324
+ QTSError,
325
+ QTSExecutionError,
326
+ QTSPreparationError,
327
+ QTSStrategyCompileError,
328
+ QTSTimeoutError,
329
+ QTSurfer
330
+ };
3
331
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAwB,MAAM,UAAU,CAAC;AAC1D,OAAO,EACL,QAAQ,EACR,uBAAuB,EACvB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,GACjB,MAAM,UAAU,CAAC"}
1
+ {"version":3,"sources":["../src/client.ts","../src/workflows/backtest.ts","../src/errors.ts","../src/workflows/downloads.ts"],"sourcesContent":["import { client as apiClient } from '@qtsurfer/api-client';\nimport {\n backtest,\n type BacktestOptions,\n type BacktestRequest,\n type BacktestResult,\n} from './workflows/backtest';\nimport {\n downloadKlines,\n downloadTickers,\n type DownloadFormat,\n} from './workflows/downloads';\n\nexport interface QTSurferOptions {\n baseUrl: string;\n token?: string;\n fetch?: typeof fetch;\n}\n\nexport interface DownloadHourArgs {\n exchangeId: string;\n base: string;\n quote: string;\n /** Hour selector in `YYYY-MM-DDTHH` (UTC). */\n hour: string;\n /** Wire format. Defaults to `'lastra'`. */\n format?: DownloadFormat;\n}\n\nexport class QTSurfer {\n constructor(options: QTSurferOptions) {\n apiClient.setConfig({\n baseUrl: options.baseUrl,\n ...(options.token\n ? { headers: { Authorization: `Bearer ${options.token}` } }\n : {}),\n ...(options.fetch ? { fetch: options.fetch } : {}),\n });\n }\n\n backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return backtest(req, opts);\n }\n\n /**\n * Download one hour of raw tickers for an instrument as a {@link Blob}.\n * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.\n */\n tickers(args: DownloadHourArgs): Promise<Blob> {\n return downloadTickers(args);\n }\n\n /** Download one hour of klines for an instrument as a {@link Blob}. */\n klines(args: DownloadHourArgs): Promise<Blob> {\n return downloadKlines(args);\n }\n\n // Future surface:\n // strategies: { compile, status, list }\n // instruments: { list, get } with TTL cache\n // jobs: { cancel, stream, result }\n}\n","import {\n cancelExecution,\n executeBacktesting,\n getExecutionResult,\n getPreparationStatus,\n getStrategyStatus,\n postStrategy,\n prepareBacktesting,\n type DataSourceType,\n type ResultMap,\n} from '@qtsurfer/api-client';\nimport {\n ExponentialBackoff,\n TaskCancelledError,\n TimeoutStrategy,\n handleWhenResult,\n retry,\n timeout,\n wrap,\n type IPolicy,\n type ICancellationContext,\n} from 'cockatiel';\nimport {\n QTSCanceledError,\n QTSExecutionError,\n QTSPreparationError,\n QTSStrategyCompileError,\n QTSTimeoutError,\n} from '../errors';\n\nexport interface BacktestRequest {\n /** Strategy source code (Java) */\n strategy: string;\n /** Exchange id, e.g. `binance` */\n exchangeId: string;\n /** Instrument symbol, e.g. `BTC/USDT` */\n instrument: string;\n /** Date range start (ISO-8601, ISO DATE or BASIC ISO DATE) */\n from: string;\n /** Date range end (same formats as `from`) */\n to: string;\n /** When true, the worker uploads emitted signals to object storage. */\n storeSignals?: boolean;\n}\n\nexport type BacktestResult = ResultMap;\n\nexport type BacktestStage = 'compiling' | 'preparing' | 'executing';\n\nexport interface BacktestProgress {\n stage: BacktestStage;\n /** 0-100 when size is known. Undefined during stage start. */\n percent?: number;\n}\n\nexport interface BacktestOptions {\n /** Abort the workflow. Cancels the current poll and calls `cancelExecution` server-side if execution has started. */\n signal?: AbortSignal;\n /** Called on stage transitions and after each poll with updated progress. */\n onProgress?: (p: BacktestProgress) => void;\n /** Initial interval between polls. Default 500ms, backed off up to `maxPollIntervalMs`. */\n pollIntervalMs?: number;\n /** Upper bound for exponential backoff. Default 5000ms. */\n maxPollIntervalMs?: number;\n /** Per-stage timeout. Default none. */\n timeoutMs?: number;\n}\n\nconst TICKER: DataSourceType = 'ticker';\n\ntype JobStatus = 'New' | 'Started' | 'Completed' | 'Aborted' | 'Failed' | 'Partial';\n\n/**\n * Normalize the backend job status to a stable lowercase form so we can\n * reason about it regardless of OpenAPI spec drift (the live API sometimes\n * returns lowercase values like `queued` / `completed` / `failed`).\n */\ntype NormalizedStatus = 'in-progress' | 'completed' | 'failed' | 'aborted';\n\nfunction normalizeStatus(raw: unknown): NormalizedStatus {\n const value = typeof raw === 'string' ? raw.toLowerCase() : '';\n if (value === 'completed') return 'completed';\n if (value === 'failed') return 'failed';\n if (value === 'aborted' || value === 'cancelled' || value === 'canceled') {\n return 'aborted';\n }\n // new / started / queued / running / anything else → still running\n return 'in-progress';\n}\n\nexport async function backtest(\n req: BacktestRequest,\n opts: BacktestOptions = {},\n): Promise<BacktestResult> {\n const policy = buildStagePolicy(opts);\n\n // 1. Compile strategy (async mode)\n opts.onProgress?.({ stage: 'compiling' });\n const strategyId = await compileStrategy(req.strategy, policy, opts);\n\n // 2. Prepare data\n opts.onProgress?.({ stage: 'preparing' });\n const prepareJobId = await prepareData(req, policy, opts);\n\n // 3. Execute\n opts.onProgress?.({ stage: 'executing' });\n return executeStrategy(req, prepareJobId, strategyId, policy, opts);\n}\n\nfunction buildStagePolicy(opts: BacktestOptions): IPolicy<ICancellationContext, never> {\n const retryPolicy = retry(\n handleWhenResult((r) => {\n const status = (r as { status?: unknown } | undefined)?.status;\n return normalizeStatus(status) === 'in-progress';\n }),\n {\n maxAttempts: Number.MAX_SAFE_INTEGER,\n backoff: new ExponentialBackoff({\n initialDelay: opts.pollIntervalMs ?? 500,\n maxDelay: opts.maxPollIntervalMs ?? 5000,\n }),\n },\n );\n\n return opts.timeoutMs\n ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy)\n : retryPolicy;\n}\n\nasync function compileStrategy(\n source: string,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<string> {\n const { data, error } = await postStrategy({\n body: source,\n headers: { 'X-Compile-Async': true },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSStrategyCompileError('Strategy submission failed', error);\n if (!data) throw new QTSStrategyCompileError('Empty response from strategy endpoint');\n\n // Sync mode returns { strategyId }; async mode returns { jobId }.\n if ('strategyId' in data && data.strategyId) {\n return data.strategyId;\n }\n if (!('jobId' in data) || !data.jobId) {\n throw new QTSStrategyCompileError('Missing jobId/strategyId in compile response');\n }\n\n const compileJobId = data.jobId;\n const status = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getStrategyStatus({ path: { strategyId: compileJobId }, signal });\n if (res.error) throw new QTSStrategyCompileError('Compile status request failed', res.error);\n if (!res.data) throw new QTSStrategyCompileError('Empty compile status response');\n return res.data;\n },\n );\n\n const norm = normalizeStatus(status.status);\n if (norm === 'failed') {\n throw new QTSStrategyCompileError(status.statusDetail ?? 'Strategy compilation failed');\n }\n if (norm === 'aborted') {\n throw new QTSCanceledError('Strategy compilation aborted');\n }\n if (!status.strategyId) {\n throw new QTSStrategyCompileError('Compile completed without a strategyId');\n }\n return status.strategyId;\n}\n\nasync function prepareData(\n req: BacktestRequest,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<string> {\n const { data, error } = await prepareBacktesting({\n path: { exchangeId: req.exchangeId, type: TICKER },\n body: { instrument: req.instrument, from: req.from, to: req.to },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSPreparationError('Prepare submission failed', error);\n if (!data?.jobId) throw new QTSPreparationError('Missing jobId in prepare response');\n\n const prepareJobId = data.jobId;\n const state = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getPreparationStatus({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: prepareJobId },\n signal,\n });\n if (res.error) throw new QTSPreparationError('Preparation status request failed', res.error);\n if (!res.data) throw new QTSPreparationError('Empty preparation status response');\n return res.data;\n },\n (r) => {\n if (r.size > 0) {\n opts.onProgress?.({ stage: 'preparing', percent: (r.completed / r.size) * 100 });\n }\n },\n );\n\n const prepNorm = normalizeStatus(state.status);\n if (prepNorm === 'failed') {\n throw new QTSPreparationError(state.statusDetail ?? 'Data preparation failed');\n }\n if (prepNorm === 'aborted') {\n throw new QTSCanceledError('Data preparation aborted');\n }\n return prepareJobId;\n}\n\nasync function executeStrategy(\n req: BacktestRequest,\n prepareJobId: string,\n strategyId: string,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<BacktestResult> {\n const { data, error } = await executeBacktesting({\n path: { exchangeId: req.exchangeId, type: TICKER },\n body: {\n prepareJobId,\n strategyId,\n ...(req.storeSignals !== undefined ? { storeSignals: req.storeSignals } : {}),\n },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSExecutionError('Execute submission failed', error);\n if (!data?.jobId) throw new QTSExecutionError('Missing jobId in execute response');\n\n const executeJobId = data.jobId;\n\n try {\n const finalResult = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getExecutionResult({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n signal,\n });\n if (res.error) throw new QTSExecutionError('Execution result request failed', res.error);\n if (!res.data) throw new QTSExecutionError('Empty execution result response');\n return { ...res.data.state, __result: res.data.results };\n },\n (r) => {\n if (r.size > 0) {\n opts.onProgress?.({ stage: 'executing', percent: (r.completed / r.size) * 100 });\n }\n },\n );\n\n const execNorm = normalizeStatus(finalResult.status);\n if (execNorm === 'failed') {\n throw new QTSExecutionError(finalResult.statusDetail ?? 'Execution failed');\n }\n if (execNorm === 'aborted') {\n throw new QTSCanceledError('Execution aborted');\n }\n return finalResult.__result;\n } catch (err) {\n if (err instanceof QTSCanceledError) {\n await cancelExecution({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n }).catch(() => undefined);\n }\n throw err;\n }\n}\n\nasync function runStage<T extends { status: JobStatus }>(\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n fetchFn: (ctx: ICancellationContext) => Promise<T>,\n onEachAttempt?: (r: T) => void,\n): Promise<T> {\n try {\n return await policy.execute(async (ctx) => {\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n const result = await fetchFn(ctx);\n onEachAttempt?.(result);\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n return result;\n }, opts.signal);\n } catch (err) {\n if (err instanceof QTSCanceledError) throw err;\n if (err instanceof TaskCancelledError) {\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw new QTSTimeoutError(`Stage exceeded ${opts.timeoutMs}ms`, err);\n }\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw err;\n }\n}\n","export class QTSError extends Error {\n constructor(message: string, readonly cause?: unknown) {\n super(message);\n this.name = 'QTSError';\n }\n}\n\nexport class QTSStrategyCompileError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSStrategyCompileError';\n }\n}\n\nexport class QTSPreparationError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSPreparationError';\n }\n}\n\nexport class QTSExecutionError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSExecutionError';\n }\n}\n\nexport class QTSTimeoutError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSTimeoutError';\n }\n}\n\nexport class QTSCanceledError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSCanceledError';\n }\n}\n\nexport class QTSDownloadError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSDownloadError';\n }\n}\n","import {\n getExchangeKlinesHour,\n getExchangeTickersHour,\n} from '@qtsurfer/api-client';\nimport { QTSDownloadError } from '../errors';\n\n/** Wire format for hourly tickers/klines downloads. */\nexport type DownloadFormat = 'lastra' | 'parquet';\n\nexport interface DownloadParams {\n exchangeId: string;\n base: string;\n quote: string;\n /** Hour selector in `YYYY-MM-DDTHH` (UTC). */\n hour: string;\n /** Defaults to {@code 'lastra'}. */\n format?: DownloadFormat;\n}\n\n/**\n * Download one hour of raw tickers as a {@link Blob}.\n *\n * The default wire format is Lastra (`application/vnd.lastra`); pass\n * `format: 'parquet'` for on-the-fly Parquet conversion.\n *\n * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.\n */\nexport async function downloadTickers(params: DownloadParams): Promise<Blob> {\n const { exchangeId, base, quote, hour, format } = params;\n const { data, error, response } = await getExchangeTickersHour({\n path: { exchangeId, base, quote },\n query: { hour, ...(format ? { format } : {}) },\n });\n if (error) {\n throw new QTSDownloadError(\n `tickers download failed: HTTP ${response.status} — ${describe(error)}`,\n error,\n );\n }\n return data as Blob;\n}\n\n/**\n * Download one hour of klines as a {@link Blob}. See {@link downloadTickers}\n * for semantics.\n *\n * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.\n */\nexport async function downloadKlines(params: DownloadParams): Promise<Blob> {\n const { exchangeId, base, quote, hour, format } = params;\n const { data, error, response } = await getExchangeKlinesHour({\n path: { exchangeId, base, quote },\n query: { hour, ...(format ? { format } : {}) },\n });\n if (error) {\n throw new QTSDownloadError(\n `klines download failed: HTTP ${response.status} — ${describe(error)}`,\n error,\n );\n }\n return data as Blob;\n}\n\nfunction describe(error: unknown): string {\n if (error && typeof error === 'object') {\n const e = error as { code?: unknown; message?: unknown };\n const code = typeof e.code === 'string' ? e.code : undefined;\n const message = typeof e.message === 'string' ? e.message : undefined;\n if (code && message) return `${code}: ${message}`;\n if (message) return message;\n if (code) return code;\n }\n return String(error);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qtsurfer/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Opinionated TypeScript SDK for QTSurfer: workflow orchestration, domain objects, normalized errors",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,8 +18,14 @@
18
18
  "LICENSE"
19
19
  ],
20
20
  "scripts": {
21
- "build": "tsc",
22
- "lint": "tsc --noEmit",
21
+ "build": "tsup",
22
+ "lint": "tsc --noEmit -p tsconfig.test.json",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "test:integration": "vitest run --config vitest.integration.config.ts",
26
+ "changeset": "changeset",
27
+ "changeset:version": "changeset version",
28
+ "changeset:publish": "changeset publish",
23
29
  "prepublishOnly": "npm run build"
24
30
  },
25
31
  "repository": {
@@ -44,11 +50,17 @@
44
50
  "access": "public"
45
51
  },
46
52
  "dependencies": {
47
- "@qtsurfer/api-client": "^0.1.0",
53
+ "@qtsurfer/api-client": "^0.1.2",
48
54
  "cockatiel": "^3.2.1"
49
55
  },
50
56
  "devDependencies": {
51
- "typescript": "^5.8.0"
57
+ "@changesets/changelog-github": "^0.6.0",
58
+ "@changesets/cli": "^2.30.0",
59
+ "@types/node": "^25.6.0",
60
+ "@vitest/coverage-v8": "^4.1.4",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^5.8.0",
63
+ "vitest": "^4.1.4"
52
64
  },
53
65
  "engines": {
54
66
  "node": ">=20"
package/src/client.ts CHANGED
@@ -5,6 +5,11 @@ import {
5
5
  type BacktestRequest,
6
6
  type BacktestResult,
7
7
  } from './workflows/backtest';
8
+ import {
9
+ downloadKlines,
10
+ downloadTickers,
11
+ type DownloadFormat,
12
+ } from './workflows/downloads';
8
13
 
9
14
  export interface QTSurferOptions {
10
15
  baseUrl: string;
@@ -12,6 +17,16 @@ export interface QTSurferOptions {
12
17
  fetch?: typeof fetch;
13
18
  }
14
19
 
20
+ export interface DownloadHourArgs {
21
+ exchangeId: string;
22
+ base: string;
23
+ quote: string;
24
+ /** Hour selector in `YYYY-MM-DDTHH` (UTC). */
25
+ hour: string;
26
+ /** Wire format. Defaults to `'lastra'`. */
27
+ format?: DownloadFormat;
28
+ }
29
+
15
30
  export class QTSurfer {
16
31
  constructor(options: QTSurferOptions) {
17
32
  apiClient.setConfig({
@@ -27,6 +42,19 @@ export class QTSurfer {
27
42
  return backtest(req, opts);
28
43
  }
29
44
 
45
+ /**
46
+ * Download one hour of raw tickers for an instrument as a {@link Blob}.
47
+ * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.
48
+ */
49
+ tickers(args: DownloadHourArgs): Promise<Blob> {
50
+ return downloadTickers(args);
51
+ }
52
+
53
+ /** Download one hour of klines for an instrument as a {@link Blob}. */
54
+ klines(args: DownloadHourArgs): Promise<Blob> {
55
+ return downloadKlines(args);
56
+ }
57
+
30
58
  // Future surface:
31
59
  // strategies: { compile, status, list }
32
60
  // instruments: { list, get } with TTL cache
package/src/errors.ts CHANGED
@@ -39,3 +39,10 @@ export class QTSCanceledError extends QTSError {
39
39
  this.name = 'QTSCanceledError';
40
40
  }
41
41
  }
42
+
43
+ export class QTSDownloadError extends QTSError {
44
+ constructor(message: string, cause?: unknown) {
45
+ super(message, cause);
46
+ this.name = 'QTSDownloadError';
47
+ }
48
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { QTSurfer, type QTSurferOptions } from './client';
1
+ export { QTSurfer, type QTSurferOptions, type DownloadHourArgs } from './client';
2
2
  export {
3
3
  QTSError,
4
4
  QTSStrategyCompileError,
@@ -6,6 +6,7 @@ export {
6
6
  QTSExecutionError,
7
7
  QTSTimeoutError,
8
8
  QTSCanceledError,
9
+ QTSDownloadError,
9
10
  } from './errors';
10
11
  export type {
11
12
  BacktestRequest,
@@ -14,3 +15,4 @@ export type {
14
15
  BacktestStage,
15
16
  BacktestOptions,
16
17
  } from './workflows/backtest';
18
+ export type { DownloadFormat } from './workflows/downloads';
@@ -33,7 +33,7 @@ export interface BacktestRequest {
33
33
  strategy: string;
34
34
  /** Exchange id, e.g. `binance` */
35
35
  exchangeId: string;
36
- /** Instrument symbol, e.g. `BTCUSDT` */
36
+ /** Instrument symbol, e.g. `BTC/USDT` */
37
37
  instrument: string;
38
38
  /** Date range start (ISO-8601, ISO DATE or BASIC ISO DATE) */
39
39
  from: string;
@@ -68,7 +68,25 @@ export interface BacktestOptions {
68
68
 
69
69
  const TICKER: DataSourceType = 'ticker';
70
70
 
71
- type JobStatus = 'New' | 'Started' | 'Completed' | 'Aborted' | 'Failed';
71
+ type JobStatus = 'New' | 'Started' | 'Completed' | 'Aborted' | 'Failed' | 'Partial';
72
+
73
+ /**
74
+ * Normalize the backend job status to a stable lowercase form so we can
75
+ * reason about it regardless of OpenAPI spec drift (the live API sometimes
76
+ * returns lowercase values like `queued` / `completed` / `failed`).
77
+ */
78
+ type NormalizedStatus = 'in-progress' | 'completed' | 'failed' | 'aborted';
79
+
80
+ function normalizeStatus(raw: unknown): NormalizedStatus {
81
+ const value = typeof raw === 'string' ? raw.toLowerCase() : '';
82
+ if (value === 'completed') return 'completed';
83
+ if (value === 'failed') return 'failed';
84
+ if (value === 'aborted' || value === 'cancelled' || value === 'canceled') {
85
+ return 'aborted';
86
+ }
87
+ // new / started / queued / running / anything else → still running
88
+ return 'in-progress';
89
+ }
72
90
 
73
91
  export async function backtest(
74
92
  req: BacktestRequest,
@@ -92,8 +110,8 @@ export async function backtest(
92
110
  function buildStagePolicy(opts: BacktestOptions): IPolicy<ICancellationContext, never> {
93
111
  const retryPolicy = retry(
94
112
  handleWhenResult((r) => {
95
- const status = (r as { status?: JobStatus } | undefined)?.status;
96
- return status === 'New' || status === 'Started';
113
+ const status = (r as { status?: unknown } | undefined)?.status;
114
+ return normalizeStatus(status) === 'in-progress';
97
115
  }),
98
116
  {
99
117
  maxAttempts: Number.MAX_SAFE_INTEGER,
@@ -142,10 +160,11 @@ async function compileStrategy(
142
160
  },
143
161
  );
144
162
 
145
- if (status.status === 'Failed') {
163
+ const norm = normalizeStatus(status.status);
164
+ if (norm === 'failed') {
146
165
  throw new QTSStrategyCompileError(status.statusDetail ?? 'Strategy compilation failed');
147
166
  }
148
- if (status.status === 'Aborted') {
167
+ if (norm === 'aborted') {
149
168
  throw new QTSCanceledError('Strategy compilation aborted');
150
169
  }
151
170
  if (!status.strategyId) {
@@ -187,10 +206,11 @@ async function prepareData(
187
206
  },
188
207
  );
189
208
 
190
- if (state.status === 'Failed') {
209
+ const prepNorm = normalizeStatus(state.status);
210
+ if (prepNorm === 'failed') {
191
211
  throw new QTSPreparationError(state.statusDetail ?? 'Data preparation failed');
192
212
  }
193
- if (state.status === 'Aborted') {
213
+ if (prepNorm === 'aborted') {
194
214
  throw new QTSCanceledError('Data preparation aborted');
195
215
  }
196
216
  return prepareJobId;
@@ -237,10 +257,11 @@ async function executeStrategy(
237
257
  },
238
258
  );
239
259
 
240
- if (finalResult.status === 'Failed') {
260
+ const execNorm = normalizeStatus(finalResult.status);
261
+ if (execNorm === 'failed') {
241
262
  throw new QTSExecutionError(finalResult.statusDetail ?? 'Execution failed');
242
263
  }
243
- if (finalResult.status === 'Aborted') {
264
+ if (execNorm === 'aborted') {
244
265
  throw new QTSCanceledError('Execution aborted');
245
266
  }
246
267
  return finalResult.__result;
@@ -262,15 +283,19 @@ async function runStage<T extends { status: JobStatus }>(
262
283
  ): Promise<T> {
263
284
  try {
264
285
  return await policy.execute(async (ctx) => {
286
+ if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');
265
287
  const result = await fetchFn(ctx);
266
288
  onEachAttempt?.(result);
289
+ if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');
267
290
  return result;
268
291
  }, opts.signal);
269
292
  } catch (err) {
293
+ if (err instanceof QTSCanceledError) throw err;
270
294
  if (err instanceof TaskCancelledError) {
271
295
  if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);
272
296
  throw new QTSTimeoutError(`Stage exceeded ${opts.timeoutMs}ms`, err);
273
297
  }
298
+ if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);
274
299
  throw err;
275
300
  }
276
301
  }
@@ -0,0 +1,74 @@
1
+ import {
2
+ getExchangeKlinesHour,
3
+ getExchangeTickersHour,
4
+ } from '@qtsurfer/api-client';
5
+ import { QTSDownloadError } from '../errors';
6
+
7
+ /** Wire format for hourly tickers/klines downloads. */
8
+ export type DownloadFormat = 'lastra' | 'parquet';
9
+
10
+ export interface DownloadParams {
11
+ exchangeId: string;
12
+ base: string;
13
+ quote: string;
14
+ /** Hour selector in `YYYY-MM-DDTHH` (UTC). */
15
+ hour: string;
16
+ /** Defaults to {@code 'lastra'}. */
17
+ format?: DownloadFormat;
18
+ }
19
+
20
+ /**
21
+ * Download one hour of raw tickers as a {@link Blob}.
22
+ *
23
+ * The default wire format is Lastra (`application/vnd.lastra`); pass
24
+ * `format: 'parquet'` for on-the-fly Parquet conversion.
25
+ *
26
+ * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.
27
+ */
28
+ export async function downloadTickers(params: DownloadParams): Promise<Blob> {
29
+ const { exchangeId, base, quote, hour, format } = params;
30
+ const { data, error, response } = await getExchangeTickersHour({
31
+ path: { exchangeId, base, quote },
32
+ query: { hour, ...(format ? { format } : {}) },
33
+ });
34
+ if (error) {
35
+ throw new QTSDownloadError(
36
+ `tickers download failed: HTTP ${response.status} — ${describe(error)}`,
37
+ error,
38
+ );
39
+ }
40
+ return data as Blob;
41
+ }
42
+
43
+ /**
44
+ * Download one hour of klines as a {@link Blob}. See {@link downloadTickers}
45
+ * for semantics.
46
+ *
47
+ * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.
48
+ */
49
+ export async function downloadKlines(params: DownloadParams): Promise<Blob> {
50
+ const { exchangeId, base, quote, hour, format } = params;
51
+ const { data, error, response } = await getExchangeKlinesHour({
52
+ path: { exchangeId, base, quote },
53
+ query: { hour, ...(format ? { format } : {}) },
54
+ });
55
+ if (error) {
56
+ throw new QTSDownloadError(
57
+ `klines download failed: HTTP ${response.status} — ${describe(error)}`,
58
+ error,
59
+ );
60
+ }
61
+ return data as Blob;
62
+ }
63
+
64
+ function describe(error: unknown): string {
65
+ if (error && typeof error === 'object') {
66
+ const e = error as { code?: unknown; message?: unknown };
67
+ const code = typeof e.code === 'string' ? e.code : undefined;
68
+ const message = typeof e.message === 'string' ? e.message : undefined;
69
+ if (code && message) return `${code}: ${message}`;
70
+ if (message) return message;
71
+ if (code) return code;
72
+ }
73
+ return String(error);
74
+ }
package/dist/client.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import { type BacktestOptions, type BacktestRequest, type BacktestResult } from './workflows/backtest';
2
- export interface QTSurferOptions {
3
- baseUrl: string;
4
- token?: string;
5
- fetch?: typeof fetch;
6
- }
7
- export declare class QTSurfer {
8
- constructor(options: QTSurferOptions);
9
- backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult>;
10
- }
11
- //# sourceMappingURL=client.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,qBAAa,QAAQ;gBACP,OAAO,EAAE,eAAe;IAUpC,QAAQ,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;CAQhF"}
package/dist/client.js DELETED
@@ -1,17 +0,0 @@
1
- import { client as apiClient } from '@qtsurfer/api-client';
2
- import { backtest, } from './workflows/backtest';
3
- export class QTSurfer {
4
- constructor(options) {
5
- apiClient.setConfig({
6
- baseUrl: options.baseUrl,
7
- ...(options.token
8
- ? { headers: { Authorization: `Bearer ${options.token}` } }
9
- : {}),
10
- ...(options.fetch ? { fetch: options.fetch } : {}),
11
- });
12
- }
13
- backtest(req, opts) {
14
- return backtest(req, opts);
15
- }
16
- }
17
- //# sourceMappingURL=client.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EACL,QAAQ,GAIT,MAAM,sBAAsB,CAAC;AAQ9B,MAAM,OAAO,QAAQ;IACnB,YAAY,OAAwB;QAClC,SAAS,CAAC,SAAS,CAAC;YAClB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,GAAG,CAAC,OAAO,CAAC,KAAK;gBACf,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,OAAO,CAAC,KAAK,EAAE,EAAE,EAAE;gBAC3D,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnD,CAAC,CAAC;IACL,CAAC;IAED,QAAQ,CAAC,GAAoB,EAAE,IAAsB;QACnD,OAAO,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC7B,CAAC;CAMF"}
package/dist/errors.d.ts DELETED
@@ -1,20 +0,0 @@
1
- export declare class QTSError extends Error {
2
- readonly cause?: unknown | undefined;
3
- constructor(message: string, cause?: unknown | undefined);
4
- }
5
- export declare class QTSStrategyCompileError extends QTSError {
6
- constructor(message: string, cause?: unknown);
7
- }
8
- export declare class QTSPreparationError extends QTSError {
9
- constructor(message: string, cause?: unknown);
10
- }
11
- export declare class QTSExecutionError extends QTSError {
12
- constructor(message: string, cause?: unknown);
13
- }
14
- export declare class QTSTimeoutError extends QTSError {
15
- constructor(message: string, cause?: unknown);
16
- }
17
- export declare class QTSCanceledError extends QTSError {
18
- constructor(message: string, cause?: unknown);
19
- }
20
- //# sourceMappingURL=errors.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,QAAS,SAAQ,KAAK;IACJ,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO;gBAAzC,OAAO,EAAE,MAAM,EAAW,KAAK,CAAC,EAAE,OAAO,YAAA;CAItD;AAED,qBAAa,uBAAwB,SAAQ,QAAQ;gBACvC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,mBAAoB,SAAQ,QAAQ;gBACnC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,iBAAkB,SAAQ,QAAQ;gBACjC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,eAAgB,SAAQ,QAAQ;gBAC/B,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,gBAAiB,SAAQ,QAAQ;gBAChC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C"}
package/dist/errors.js DELETED
@@ -1,39 +0,0 @@
1
- export class QTSError extends Error {
2
- cause;
3
- constructor(message, cause) {
4
- super(message);
5
- this.cause = cause;
6
- this.name = 'QTSError';
7
- }
8
- }
9
- export class QTSStrategyCompileError extends QTSError {
10
- constructor(message, cause) {
11
- super(message, cause);
12
- this.name = 'QTSStrategyCompileError';
13
- }
14
- }
15
- export class QTSPreparationError extends QTSError {
16
- constructor(message, cause) {
17
- super(message, cause);
18
- this.name = 'QTSPreparationError';
19
- }
20
- }
21
- export class QTSExecutionError extends QTSError {
22
- constructor(message, cause) {
23
- super(message, cause);
24
- this.name = 'QTSExecutionError';
25
- }
26
- }
27
- export class QTSTimeoutError extends QTSError {
28
- constructor(message, cause) {
29
- super(message, cause);
30
- this.name = 'QTSTimeoutError';
31
- }
32
- }
33
- export class QTSCanceledError extends QTSError {
34
- constructor(message, cause) {
35
- super(message, cause);
36
- this.name = 'QTSCanceledError';
37
- }
38
- }
39
- //# sourceMappingURL=errors.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,QAAS,SAAQ,KAAK;IACK;IAAtC,YAAY,OAAe,EAAW,KAAe;QACnD,KAAK,CAAC,OAAO,CAAC,CAAC;QADqB,UAAK,GAAL,KAAK,CAAU;QAEnD,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAED,MAAM,OAAO,uBAAwB,SAAQ,QAAQ;IACnD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,MAAM,OAAO,mBAAoB,SAAQ,QAAQ;IAC/C,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,QAAQ;IAC7C,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED,MAAM,OAAO,eAAgB,SAAQ,QAAQ;IAC3C,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,MAAM,OAAO,gBAAiB,SAAQ,QAAQ;IAC5C,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EACL,QAAQ,EACR,uBAAuB,EACvB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,GACjB,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,eAAe,GAChB,MAAM,sBAAsB,CAAC"}
@@ -1,36 +0,0 @@
1
- import { type ResultMap } from '@qtsurfer/api-client';
2
- export interface BacktestRequest {
3
- /** Strategy source code (Java) */
4
- strategy: string;
5
- /** Exchange id, e.g. `binance` */
6
- exchangeId: string;
7
- /** Instrument symbol, e.g. `BTCUSDT` */
8
- instrument: string;
9
- /** Date range start (ISO-8601, ISO DATE or BASIC ISO DATE) */
10
- from: string;
11
- /** Date range end (same formats as `from`) */
12
- to: string;
13
- /** When true, the worker uploads emitted signals to object storage. */
14
- storeSignals?: boolean;
15
- }
16
- export type BacktestResult = ResultMap;
17
- export type BacktestStage = 'compiling' | 'preparing' | 'executing';
18
- export interface BacktestProgress {
19
- stage: BacktestStage;
20
- /** 0-100 when size is known. Undefined during stage start. */
21
- percent?: number;
22
- }
23
- export interface BacktestOptions {
24
- /** Abort the workflow. Cancels the current poll and calls `cancelExecution` server-side if execution has started. */
25
- signal?: AbortSignal;
26
- /** Called on stage transitions and after each poll with updated progress. */
27
- onProgress?: (p: BacktestProgress) => void;
28
- /** Initial interval between polls. Default 500ms, backed off up to `maxPollIntervalMs`. */
29
- pollIntervalMs?: number;
30
- /** Upper bound for exponential backoff. Default 5000ms. */
31
- maxPollIntervalMs?: number;
32
- /** Per-stage timeout. Default none. */
33
- timeoutMs?: number;
34
- }
35
- export declare function backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult>;
36
- //# sourceMappingURL=backtest.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"backtest.d.ts","sourceRoot":"","sources":["../../src/workflows/backtest.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAoB9B,MAAM,WAAW,eAAe;IAC9B,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC;AAEvC,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,aAAa,CAAC;IACrB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,qHAAqH;IACrH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC3C,2FAA2F;IAC3F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,eAAe,EACpB,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,cAAc,CAAC,CAczB"}
@@ -1,168 +0,0 @@
1
- import { cancelExecution, executeBacktesting, getExecutionResult, getPreparationStatus, getStrategyStatus, postStrategy, prepareBacktesting, } from '@qtsurfer/api-client';
2
- import { ExponentialBackoff, TaskCancelledError, TimeoutStrategy, handleWhenResult, retry, timeout, wrap, } from 'cockatiel';
3
- import { QTSCanceledError, QTSExecutionError, QTSPreparationError, QTSStrategyCompileError, QTSTimeoutError, } from '../errors';
4
- const TICKER = 'ticker';
5
- export async function backtest(req, opts = {}) {
6
- const policy = buildStagePolicy(opts);
7
- // 1. Compile strategy (async mode)
8
- opts.onProgress?.({ stage: 'compiling' });
9
- const strategyId = await compileStrategy(req.strategy, policy, opts);
10
- // 2. Prepare data
11
- opts.onProgress?.({ stage: 'preparing' });
12
- const prepareJobId = await prepareData(req, policy, opts);
13
- // 3. Execute
14
- opts.onProgress?.({ stage: 'executing' });
15
- return executeStrategy(req, prepareJobId, strategyId, policy, opts);
16
- }
17
- function buildStagePolicy(opts) {
18
- const retryPolicy = retry(handleWhenResult((r) => {
19
- const status = r?.status;
20
- return status === 'New' || status === 'Started';
21
- }), {
22
- maxAttempts: Number.MAX_SAFE_INTEGER,
23
- backoff: new ExponentialBackoff({
24
- initialDelay: opts.pollIntervalMs ?? 500,
25
- maxDelay: opts.maxPollIntervalMs ?? 5000,
26
- }),
27
- });
28
- return opts.timeoutMs
29
- ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy)
30
- : retryPolicy;
31
- }
32
- async function compileStrategy(source, policy, opts) {
33
- const { data, error } = await postStrategy({
34
- body: source,
35
- headers: { 'X-Compile-Async': true },
36
- ...(opts.signal ? { signal: opts.signal } : {}),
37
- });
38
- if (error)
39
- throw new QTSStrategyCompileError('Strategy submission failed', error);
40
- if (!data)
41
- throw new QTSStrategyCompileError('Empty response from strategy endpoint');
42
- // Sync mode returns { strategyId }; async mode returns { jobId }.
43
- if ('strategyId' in data && data.strategyId) {
44
- return data.strategyId;
45
- }
46
- if (!('jobId' in data) || !data.jobId) {
47
- throw new QTSStrategyCompileError('Missing jobId/strategyId in compile response');
48
- }
49
- const compileJobId = data.jobId;
50
- const status = await runStage(policy, opts, async ({ signal }) => {
51
- const res = await getStrategyStatus({ path: { strategyId: compileJobId }, signal });
52
- if (res.error)
53
- throw new QTSStrategyCompileError('Compile status request failed', res.error);
54
- if (!res.data)
55
- throw new QTSStrategyCompileError('Empty compile status response');
56
- return res.data;
57
- });
58
- if (status.status === 'Failed') {
59
- throw new QTSStrategyCompileError(status.statusDetail ?? 'Strategy compilation failed');
60
- }
61
- if (status.status === 'Aborted') {
62
- throw new QTSCanceledError('Strategy compilation aborted');
63
- }
64
- if (!status.strategyId) {
65
- throw new QTSStrategyCompileError('Compile completed without a strategyId');
66
- }
67
- return status.strategyId;
68
- }
69
- async function prepareData(req, policy, opts) {
70
- const { data, error } = await prepareBacktesting({
71
- path: { exchangeId: req.exchangeId, type: TICKER },
72
- body: { instrument: req.instrument, from: req.from, to: req.to },
73
- ...(opts.signal ? { signal: opts.signal } : {}),
74
- });
75
- if (error)
76
- throw new QTSPreparationError('Prepare submission failed', error);
77
- if (!data?.jobId)
78
- throw new QTSPreparationError('Missing jobId in prepare response');
79
- const prepareJobId = data.jobId;
80
- const state = await runStage(policy, opts, async ({ signal }) => {
81
- const res = await getPreparationStatus({
82
- path: { exchangeId: req.exchangeId, type: TICKER, jobId: prepareJobId },
83
- signal,
84
- });
85
- if (res.error)
86
- throw new QTSPreparationError('Preparation status request failed', res.error);
87
- if (!res.data)
88
- throw new QTSPreparationError('Empty preparation status response');
89
- return res.data;
90
- }, (r) => {
91
- if (r.size > 0) {
92
- opts.onProgress?.({ stage: 'preparing', percent: (r.completed / r.size) * 100 });
93
- }
94
- });
95
- if (state.status === 'Failed') {
96
- throw new QTSPreparationError(state.statusDetail ?? 'Data preparation failed');
97
- }
98
- if (state.status === 'Aborted') {
99
- throw new QTSCanceledError('Data preparation aborted');
100
- }
101
- return prepareJobId;
102
- }
103
- async function executeStrategy(req, prepareJobId, strategyId, policy, opts) {
104
- const { data, error } = await executeBacktesting({
105
- path: { exchangeId: req.exchangeId, type: TICKER },
106
- body: {
107
- prepareJobId,
108
- strategyId,
109
- ...(req.storeSignals !== undefined ? { storeSignals: req.storeSignals } : {}),
110
- },
111
- ...(opts.signal ? { signal: opts.signal } : {}),
112
- });
113
- if (error)
114
- throw new QTSExecutionError('Execute submission failed', error);
115
- if (!data?.jobId)
116
- throw new QTSExecutionError('Missing jobId in execute response');
117
- const executeJobId = data.jobId;
118
- try {
119
- const finalResult = await runStage(policy, opts, async ({ signal }) => {
120
- const res = await getExecutionResult({
121
- path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },
122
- signal,
123
- });
124
- if (res.error)
125
- throw new QTSExecutionError('Execution result request failed', res.error);
126
- if (!res.data)
127
- throw new QTSExecutionError('Empty execution result response');
128
- return { ...res.data.state, __result: res.data.results };
129
- }, (r) => {
130
- if (r.size > 0) {
131
- opts.onProgress?.({ stage: 'executing', percent: (r.completed / r.size) * 100 });
132
- }
133
- });
134
- if (finalResult.status === 'Failed') {
135
- throw new QTSExecutionError(finalResult.statusDetail ?? 'Execution failed');
136
- }
137
- if (finalResult.status === 'Aborted') {
138
- throw new QTSCanceledError('Execution aborted');
139
- }
140
- return finalResult.__result;
141
- }
142
- catch (err) {
143
- if (err instanceof QTSCanceledError) {
144
- await cancelExecution({
145
- path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },
146
- }).catch(() => undefined);
147
- }
148
- throw err;
149
- }
150
- }
151
- async function runStage(policy, opts, fetchFn, onEachAttempt) {
152
- try {
153
- return await policy.execute(async (ctx) => {
154
- const result = await fetchFn(ctx);
155
- onEachAttempt?.(result);
156
- return result;
157
- }, opts.signal);
158
- }
159
- catch (err) {
160
- if (err instanceof TaskCancelledError) {
161
- if (opts.signal?.aborted)
162
- throw new QTSCanceledError('Workflow aborted', err);
163
- throw new QTSTimeoutError(`Stage exceeded ${opts.timeoutMs}ms`, err);
164
- }
165
- throw err;
166
- }
167
- }
168
- //# sourceMappingURL=backtest.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"backtest.js","sourceRoot":"","sources":["../../src/workflows/backtest.ts"],"names":[],"mappings":"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"}