@qtsurfer/sdk 0.7.1 → 0.9.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/dist/index.js CHANGED
@@ -5,20 +5,8 @@ import { client as apiClient } from "@qtsurfer/api-client";
5
5
  import {
6
6
  cancelBacktest,
7
7
  executeBacktest,
8
- getBacktestResult,
9
- getPrepareStatus,
10
- compileStrategy as apiCompileStrategy,
11
- prepareBacktest
8
+ getBacktestResult
12
9
  } from "@qtsurfer/api-client";
13
- import {
14
- ExponentialBackoff,
15
- TaskCancelledError,
16
- TimeoutStrategy,
17
- handleWhenResult,
18
- retry,
19
- timeout,
20
- wrap
21
- } from "cockatiel";
22
10
 
23
11
  // src/errors.ts
24
12
  var QTSError = class extends Error {
@@ -75,27 +63,27 @@ var QTSAuthError = class extends QTSError {
75
63
  }
76
64
  };
77
65
 
78
- // src/workflows/backtest.ts
79
- var TICKER = "ticker";
66
+ // src/internal/polling.ts
67
+ import {
68
+ ExponentialBackoff,
69
+ TaskCancelledError,
70
+ TimeoutStrategy,
71
+ handleWhenResult,
72
+ retry,
73
+ timeout,
74
+ wrap
75
+ } from "cockatiel";
80
76
  function normalizeStatus(raw) {
81
77
  const value = typeof raw === "string" ? raw.toLowerCase() : "";
82
78
  if (value === "completed") return "completed";
79
+ if (value === "partial") return "completed";
83
80
  if (value === "failed") return "failed";
84
81
  if (value === "aborted" || value === "cancelled" || value === "canceled") {
85
82
  return "aborted";
86
83
  }
87
84
  return "in-progress";
88
85
  }
89
- async function backtest(req, opts = {}) {
90
- const policy = buildStagePolicy(opts);
91
- opts.onProgress?.({ stage: "compiling" });
92
- const strategyId = await compileStrategy(req.strategy, opts);
93
- opts.onProgress?.({ stage: "preparing" });
94
- const prepareJobId = await prepareData(req, policy, opts);
95
- opts.onProgress?.({ stage: "executing" });
96
- return executeStrategy(req, prepareJobId, strategyId, policy, opts);
97
- }
98
- function buildStagePolicy(opts) {
86
+ function buildStagePolicy(opts, defaultPollMs, defaultMaxPollMs) {
99
87
  const retryPolicy = retry(
100
88
  handleWhenResult((r) => {
101
89
  const status = r?.status;
@@ -104,17 +92,44 @@ function buildStagePolicy(opts) {
104
92
  {
105
93
  maxAttempts: Number.MAX_SAFE_INTEGER,
106
94
  backoff: new ExponentialBackoff({
107
- initialDelay: opts.pollIntervalMs ?? 500,
108
- maxDelay: opts.maxPollIntervalMs ?? 5e3
95
+ initialDelay: opts.pollIntervalMs ?? defaultPollMs,
96
+ maxDelay: opts.maxPollIntervalMs ?? defaultMaxPollMs
109
97
  })
110
98
  }
111
99
  );
112
100
  return opts.timeoutMs ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy) : retryPolicy;
113
101
  }
114
- async function compileStrategy(source, opts) {
102
+ async function runStage(policy, fetchFn, run = {}) {
103
+ try {
104
+ return await policy.execute(async (ctx) => {
105
+ if (run.signal?.aborted) throw new QTSCanceledError("Workflow aborted");
106
+ const result = await fetchFn(ctx);
107
+ run.onEachAttempt?.(result);
108
+ if (run.signal?.aborted) throw new QTSCanceledError("Workflow aborted");
109
+ return result;
110
+ }, run.signal);
111
+ } catch (err) {
112
+ if (err instanceof QTSCanceledError) throw err;
113
+ if (err instanceof TaskCancelledError) {
114
+ if (run.signal?.aborted) throw new QTSCanceledError("Workflow aborted", err);
115
+ throw new QTSTimeoutError(`Stage exceeded ${run.timeoutMs}ms`, err);
116
+ }
117
+ if (run.signal?.aborted) throw new QTSCanceledError("Workflow aborted", err);
118
+ throw err;
119
+ }
120
+ }
121
+
122
+ // src/internal/preparation.ts
123
+ import {
124
+ getPrepareStatus,
125
+ compileStrategy as apiCompileStrategy,
126
+ prepareBacktest
127
+ } from "@qtsurfer/api-client";
128
+ var TICKER = "ticker";
129
+ async function compileStrategySource(source, signal) {
115
130
  const { data, error, response } = await apiCompileStrategy({
116
131
  body: source,
117
- ...opts.signal ? { signal: opts.signal } : {}
132
+ ...signal ? { signal } : {}
118
133
  });
119
134
  if (error) {
120
135
  if (response?.status === 429) {
@@ -130,30 +145,31 @@ async function compileStrategy(source, opts) {
130
145
  }
131
146
  return data.strategyId;
132
147
  }
133
- async function prepareData(req, policy, opts) {
148
+ async function prepareDataset(target, policy, run = {}) {
134
149
  const { data, error } = await prepareBacktest({
135
- path: { exchangeId: req.exchangeId, type: TICKER },
136
- body: { instrument: req.instrument, from: req.from, to: req.to },
137
- ...opts.signal ? { signal: opts.signal } : {}
150
+ path: { exchangeId: target.exchangeId, type: TICKER },
151
+ body: { instrument: target.instrument, from: target.from, to: target.to },
152
+ ...run.signal ? { signal: run.signal } : {}
138
153
  });
139
154
  if (error) throw new QTSPreparationError("Prepare submission failed", error);
140
155
  if (!data?.jobId) throw new QTSPreparationError("Missing jobId in prepare response");
141
156
  const prepareJobId = data.jobId;
142
157
  const state = await runStage(
143
158
  policy,
144
- opts,
145
159
  async ({ signal }) => {
146
160
  const res = await getPrepareStatus({
147
- path: { exchangeId: req.exchangeId, type: TICKER, jobId: prepareJobId },
161
+ path: { exchangeId: target.exchangeId, type: TICKER, jobId: prepareJobId },
148
162
  signal
149
163
  });
150
164
  if (res.error) throw new QTSPreparationError("Preparation status request failed", res.error);
151
165
  if (!res.data) throw new QTSPreparationError("Empty preparation status response");
152
166
  return res.data;
153
167
  },
154
- (r) => {
155
- if (r.size > 0) {
156
- opts.onProgress?.({ stage: "preparing", percent: r.completed / r.size * 100 });
168
+ {
169
+ ...run.signal ? { signal: run.signal } : {},
170
+ ...run.timeoutMs !== void 0 ? { timeoutMs: run.timeoutMs } : {},
171
+ onEachAttempt: (r) => {
172
+ if (r.size > 0) run.onPercent?.(r.completed / r.size * 100);
157
173
  }
158
174
  }
159
175
  );
@@ -164,9 +180,45 @@ async function prepareData(req, policy, opts) {
164
180
  if (prepNorm === "aborted") {
165
181
  throw new QTSCanceledError("Data preparation aborted");
166
182
  }
167
- opts.onProgress?.({ stage: "preparing", percent: 100, coverageRatio: state.coverageRatio });
183
+ run.onPrepared?.(state);
168
184
  return prepareJobId;
169
185
  }
186
+
187
+ // src/workflows/backtest.ts
188
+ var DEFAULT_POLL_INTERVAL_MS = 500;
189
+ var DEFAULT_MAX_POLL_INTERVAL_MS = 5e3;
190
+ async function backtest(req, opts = {}) {
191
+ const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS, DEFAULT_MAX_POLL_INTERVAL_MS);
192
+ opts.onProgress?.({ stage: "compiling" });
193
+ const strategyId = await compileStrategySource(req.strategy, opts.signal);
194
+ opts.onProgress?.({ stage: "preparing" });
195
+ const prepareJobId = await prepareData(req, policy, opts);
196
+ opts.onProgress?.({ stage: "executing" });
197
+ return executeStrategy(req, prepareJobId, strategyId, policy, opts);
198
+ }
199
+ function prepareData(req, policy, opts) {
200
+ return prepareDataset(
201
+ {
202
+ exchangeId: req.exchangeId,
203
+ instrument: req.instrument,
204
+ from: req.from,
205
+ to: req.to
206
+ },
207
+ policy,
208
+ {
209
+ ...opts.signal ? { signal: opts.signal } : {},
210
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
211
+ onPercent: (percent) => opts.onProgress?.({ stage: "preparing", percent }),
212
+ // Surface the backend's coverage ratio for the prepared window (spec 0.98.0) on the
213
+ // final preparing event, so callers can react to a partially-covered range.
214
+ onPrepared: (state) => opts.onProgress?.({
215
+ stage: "preparing",
216
+ percent: 100,
217
+ coverageRatio: state.coverageRatio
218
+ })
219
+ }
220
+ );
221
+ }
170
222
  async function executeStrategy(req, prepareJobId, strategyId, policy, opts) {
171
223
  const { data, error } = await executeBacktest({
172
224
  path: { exchangeId: req.exchangeId, type: TICKER },
@@ -183,7 +235,6 @@ async function executeStrategy(req, prepareJobId, strategyId, policy, opts) {
183
235
  try {
184
236
  const finalResult = await runStage(
185
237
  policy,
186
- opts,
187
238
  async ({ signal }) => {
188
239
  const res = await getBacktestResult({
189
240
  path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },
@@ -193,9 +244,13 @@ async function executeStrategy(req, prepareJobId, strategyId, policy, opts) {
193
244
  if (!res.data) throw new QTSExecutionError("Empty execution result response");
194
245
  return { ...res.data.state, __result: res.data.results };
195
246
  },
196
- (r) => {
197
- if (r.size > 0) {
198
- opts.onProgress?.({ stage: "executing", percent: r.completed / r.size * 100 });
247
+ {
248
+ ...opts.signal ? { signal: opts.signal } : {},
249
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
250
+ onEachAttempt: (r) => {
251
+ if (r.size > 0) {
252
+ opts.onProgress?.({ stage: "executing", percent: r.completed / r.size * 100 });
253
+ }
199
254
  }
200
255
  }
201
256
  );
@@ -216,24 +271,43 @@ async function executeStrategy(req, prepareJobId, strategyId, policy, opts) {
216
271
  throw err;
217
272
  }
218
273
  }
219
- async function runStage(policy, opts, fetchFn, onEachAttempt) {
220
- try {
221
- return await policy.execute(async (ctx) => {
222
- if (opts.signal?.aborted) throw new QTSCanceledError("Workflow aborted");
223
- const result = await fetchFn(ctx);
224
- onEachAttempt?.(result);
225
- if (opts.signal?.aborted) throw new QTSCanceledError("Workflow aborted");
226
- return result;
227
- }, opts.signal);
228
- } catch (err) {
229
- if (err instanceof QTSCanceledError) throw err;
230
- if (err instanceof TaskCancelledError) {
231
- if (opts.signal?.aborted) throw new QTSCanceledError("Workflow aborted", err);
232
- throw new QTSTimeoutError(`Stage exceeded ${opts.timeoutMs}ms`, err);
233
- }
234
- if (opts.signal?.aborted) throw new QTSCanceledError("Workflow aborted", err);
235
- throw err;
274
+
275
+ // src/workflows/catalog.ts
276
+ import {
277
+ listExchanges as apiListExchanges,
278
+ listInstruments as apiListInstruments,
279
+ listSegmentInstruments as apiListSegmentInstruments
280
+ } from "@qtsurfer/api-client";
281
+
282
+ // src/internal/requestError.ts
283
+ function requestFailed(what, error, status) {
284
+ const prefix = status === void 0 ? "" : `HTTP ${status} \u2014 `;
285
+ return new QTSError(`${what} failed: ${prefix}${describe(error)}`, error, status);
286
+ }
287
+ function describe(error) {
288
+ if (error && typeof error === "object") {
289
+ const e = error;
290
+ const code = typeof e.code === "string" || typeof e.code === "number" ? e.code : void 0;
291
+ const message = typeof e.message === "string" ? e.message : void 0;
292
+ if (code !== void 0 && message) return `${code}: ${message}`;
293
+ if (message) return message;
294
+ if (code !== void 0) return String(code);
236
295
  }
296
+ return String(error);
297
+ }
298
+
299
+ // src/workflows/catalog.ts
300
+ async function listExchanges() {
301
+ const { data, error, response } = await apiListExchanges();
302
+ if (error) throw requestFailed("exchanges call", error, response?.status);
303
+ if (!data) throw new QTSError("Empty exchanges response");
304
+ return data;
305
+ }
306
+ async function listInstruments(exchangeId, segment) {
307
+ const { data, error, response } = segment ? await apiListSegmentInstruments({ path: { exchangeId, segment } }) : await apiListInstruments({ path: { exchangeId } });
308
+ if (error) throw requestFailed("instruments call", error, response?.status);
309
+ if (!data) throw new QTSError("Empty instruments response");
310
+ return data.data;
237
311
  }
238
312
 
239
313
  // src/workflows/downloads.ts
@@ -249,7 +323,7 @@ async function downloadTickers(params) {
249
323
  });
250
324
  if (error) {
251
325
  throw new QTSDownloadError(
252
- `tickers download failed: HTTP ${response.status} \u2014 ${describe(error)}`,
326
+ `tickers download failed: HTTP ${response.status} \u2014 ${describe2(error)}`,
253
327
  error,
254
328
  response.status
255
329
  );
@@ -264,14 +338,14 @@ async function downloadKlines(params) {
264
338
  });
265
339
  if (error) {
266
340
  throw new QTSDownloadError(
267
- `klines download failed: HTTP ${response.status} \u2014 ${describe(error)}`,
341
+ `klines download failed: HTTP ${response.status} \u2014 ${describe2(error)}`,
268
342
  error,
269
343
  response.status
270
344
  );
271
345
  }
272
346
  return data;
273
347
  }
274
- function describe(error) {
348
+ function describe2(error) {
275
349
  if (error && typeof error === "object") {
276
350
  const e = error;
277
351
  const code = typeof e.code === "string" ? e.code : void 0;
@@ -283,6 +357,225 @@ function describe(error) {
283
357
  return String(error);
284
358
  }
285
359
 
360
+ // src/workflows/strategies.ts
361
+ import {
362
+ getStrategy as apiGetStrategy,
363
+ validateStrategy as apiValidateStrategy,
364
+ listStrategies as apiListStrategies,
365
+ deleteStrategy as apiDeleteStrategy,
366
+ getStrategyCode as apiGetStrategyCode
367
+ } from "@qtsurfer/api-client";
368
+ async function validateStrategy(strategyId) {
369
+ const { data, error, response } = await apiValidateStrategy({ path: { strategyId } });
370
+ if (error) throw requestFailed("strategy validation request", error, response?.status);
371
+ if (response?.status === 202) return { queued: true, strategyId };
372
+ if (!data) throw new QTSError("Empty strategy validation response");
373
+ return { queued: false, strategyId, state: data };
374
+ }
375
+ async function getStrategy(strategyId) {
376
+ const { data, error, response } = await apiGetStrategy({ path: { strategyId } });
377
+ if (error) throw requestFailed("strategy lookup", error, response?.status);
378
+ if (!data) throw new QTSError("Empty strategy response");
379
+ return data;
380
+ }
381
+ async function listStrategies() {
382
+ const { data, error, response } = await apiListStrategies();
383
+ if (error) throw requestFailed("strategies list", error, response?.status);
384
+ if (!data) throw new QTSError("Empty strategies response");
385
+ return data.strategies;
386
+ }
387
+ async function deleteStrategy(strategyId) {
388
+ const { error, response } = await apiDeleteStrategy({ path: { strategyId } });
389
+ if (error) throw requestFailed("strategy delete", error, response?.status);
390
+ }
391
+ async function getStrategyCode(strategyId) {
392
+ const { data, error, response } = await apiGetStrategyCode({ path: { strategyId } });
393
+ if (error) throw requestFailed("strategy code lookup", error, response?.status);
394
+ if (!data) throw new QTSError("Empty strategy code response");
395
+ return data.code;
396
+ }
397
+
398
+ // src/workflows/sweep.ts
399
+ import {
400
+ cancelSweep,
401
+ executeSweep,
402
+ getSweepResult,
403
+ getSweepSensitivity
404
+ } from "@qtsurfer/api-client";
405
+ var DEFAULT_POLL_INTERVAL_MS2 = 2e3;
406
+ var DEFAULT_MAX_POLL_INTERVAL_MS2 = 15e3;
407
+ async function sweep(req, opts = {}) {
408
+ validateRequest(req);
409
+ const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS2, DEFAULT_MAX_POLL_INTERVAL_MS2);
410
+ opts.onProgress?.({ stage: "compiling" });
411
+ const strategyId = await compileStrategySource(req.strategy, opts.signal);
412
+ opts.onProgress?.({ stage: "preparing" });
413
+ const requestId = await prepareData2(req, policy, opts);
414
+ opts.onProgress?.({ stage: "executing" });
415
+ const { data, error } = await executeSweep({
416
+ path: { exchangeId: req.exchangeId, type: TICKER, requestId },
417
+ body: buildSweepBody(req, strategyId),
418
+ ...opts.signal ? { signal: opts.signal } : {}
419
+ });
420
+ if (error) throw new QTSExecutionError("Sweep submission failed", error);
421
+ if (!data?.sweepId) throw new QTSExecutionError("Missing sweepId in executeSweep response");
422
+ return createHandle(req, opts, policy, data, requestId, strategyId);
423
+ }
424
+ function validateRequest(req) {
425
+ const names = Object.keys(req.params ?? {});
426
+ if (names.length === 0) {
427
+ throw new QTSError("sweep: params must hold at least one axis");
428
+ }
429
+ for (const name of names) {
430
+ const axis = req.params[name];
431
+ if ("values" in axis) {
432
+ if (axis.values.length === 0) {
433
+ throw new QTSError(`sweep: axis "${name}" must hold at least one value`);
434
+ }
435
+ } else if (!(axis.step > 0)) {
436
+ throw new QTSError(`sweep: axis "${name}" needs step > 0, got ${axis.step}`);
437
+ }
438
+ }
439
+ const wf = req.walkForward;
440
+ if (!wf) return;
441
+ if (wf.folds < 2) {
442
+ throw new QTSError(`sweep: walkForward.folds must be >= 2, got ${wf.folds}`);
443
+ }
444
+ if (wf.inSamplePct !== void 0 && (wf.inSamplePct < 10 || wf.inSamplePct > 90)) {
445
+ throw new QTSError(
446
+ `sweep: walkForward.inSamplePct must be within 10..90, got ${wf.inSamplePct}`
447
+ );
448
+ }
449
+ }
450
+ function prepareData2(req, policy, opts) {
451
+ return prepareDataset(
452
+ {
453
+ exchangeId: req.exchangeId,
454
+ instrument: req.instrument,
455
+ from: req.from,
456
+ to: req.to
457
+ },
458
+ policy,
459
+ {
460
+ ...opts.signal ? { signal: opts.signal } : {},
461
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
462
+ onPercent: (percent) => opts.onProgress?.({ stage: "preparing", percent }),
463
+ // A thinly covered window is about to be scored once per parameter vector, so the
464
+ // coverage ratio is worth at least as much here as on a single backtest.
465
+ onPrepared: (state) => opts.onProgress?.({
466
+ stage: "preparing",
467
+ percent: 100,
468
+ coverageRatio: state.coverageRatio
469
+ })
470
+ }
471
+ );
472
+ }
473
+ function buildSweepBody(req, strategyId) {
474
+ const spec = { params: req.params };
475
+ if (req.sampler !== void 0) spec.sampler = req.sampler;
476
+ if (req.samples !== void 0) spec.samples = req.samples;
477
+ if (req.seed !== void 0) spec.seed = req.seed;
478
+ if (req.objective !== void 0) spec.objective = req.objective;
479
+ const body = { strategyId, sweep: spec };
480
+ if (req.walkForward) {
481
+ body.walkForward = {
482
+ folds: req.walkForward.folds,
483
+ ...req.walkForward.inSamplePct !== void 0 ? { inSamplePct: req.walkForward.inSamplePct } : {}
484
+ };
485
+ }
486
+ return body;
487
+ }
488
+ function createHandle(req, opts, policy, accepted, requestId, strategyId) {
489
+ const sweepId = accepted.sweepId;
490
+ const path = { exchangeId: req.exchangeId, type: TICKER, requestId, sweepId };
491
+ let state = "executing";
492
+ const withQuery = viewQuery(opts);
493
+ const result = (async () => {
494
+ try {
495
+ const finalResult = await runStage(
496
+ policy,
497
+ async ({ signal: signal2 }) => {
498
+ const res = await getSweepResult({ path, ...withQuery, signal: signal2 });
499
+ if (res.error) throw new QTSExecutionError("Sweep result request failed", res.error);
500
+ if (!res.data) throw new QTSExecutionError("Empty sweep result response");
501
+ return res.data;
502
+ },
503
+ {
504
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
505
+ onEachAttempt: (r) => {
506
+ const percent = percentOf(r);
507
+ opts.onProgress?.({
508
+ stage: "executing",
509
+ ...percent !== void 0 ? { percent } : {},
510
+ snapshot: r.progress
511
+ });
512
+ }
513
+ }
514
+ );
515
+ state = normalizeStatus(finalResult.status) === "aborted" ? "canceled" : "completed";
516
+ return finalResult;
517
+ } catch (err) {
518
+ state = err instanceof QTSCanceledError ? "canceled" : "failed";
519
+ throw err;
520
+ }
521
+ })();
522
+ void result.catch(() => void 0);
523
+ const { signal } = opts;
524
+ if (signal) {
525
+ const requestCancel = () => {
526
+ if (state === "executing") state = "canceled";
527
+ void cancelSweep({ path }).catch(() => void 0);
528
+ };
529
+ if (signal.aborted) {
530
+ requestCancel();
531
+ } else {
532
+ signal.addEventListener("abort", requestCancel, { once: true });
533
+ const detach = () => signal.removeEventListener("abort", requestCancel);
534
+ void result.then(detach, detach);
535
+ }
536
+ }
537
+ return {
538
+ sweepId,
539
+ requestId,
540
+ strategyId,
541
+ accepted,
542
+ get state() {
543
+ return state;
544
+ },
545
+ result,
546
+ results: async (view = {}) => {
547
+ const res = await getSweepResult({ path, ...viewQuery(view) });
548
+ if (res.error) {
549
+ throw requestFailed("sweep results call", res.error, res.response?.status);
550
+ }
551
+ if (!res.data) throw new QTSError("Empty sweep result response");
552
+ return res.data;
553
+ },
554
+ sensitivity: async (objective) => {
555
+ const res = await getSweepSensitivity({
556
+ path,
557
+ ...objective !== void 0 ? { query: { objective } } : {}
558
+ });
559
+ if (res.error) {
560
+ throw requestFailed("sweep sensitivity call", res.error, res.response?.status);
561
+ }
562
+ if (!res.data) throw new QTSError("Empty sweep sensitivity response");
563
+ return res.data;
564
+ }
565
+ };
566
+ }
567
+ function viewQuery(view) {
568
+ const query = {};
569
+ if (view.order !== void 0) query.order = view.order;
570
+ if (view.ranking !== void 0) query.ranking = view.ranking;
571
+ return Object.keys(query).length > 0 ? { query } : {};
572
+ }
573
+ function percentOf(result) {
574
+ const p = result.progress;
575
+ if (!p || !(p.total > 0)) return void 0;
576
+ return p.done / p.total * 100;
577
+ }
578
+
286
579
  // src/client.ts
287
580
  var QTSurfer = class {
288
581
  constructor(options) {
@@ -301,6 +594,47 @@ var QTSurfer = class {
301
594
  backtest(req, opts) {
302
595
  return backtest(req, opts);
303
596
  }
597
+ /**
598
+ * Run the full compile → prepare → executeSweep pipeline and resolve once the
599
+ * platform has accepted the sweep, handing back a {@link Sweep} that keeps
600
+ * polling the leaderboard in the background.
601
+ *
602
+ * The whole sweep is one call because the execute-sweep endpoint is addressed
603
+ * by the id of an already-prepared dataset: exposing the stages separately
604
+ * would hand dataset lifecycle to the caller and buy nothing. Preparing is
605
+ * idempotent, so sweeping the same window twice prepares it once.
606
+ *
607
+ * The returned promise rejects with {@link QTSStrategyCompileError} if
608
+ * compilation fails, {@link QTSPreparationError} if data preparation fails,
609
+ * {@link QTSExecutionError} if the platform rejects the sweep — an expanded
610
+ * grid over the server limit, or a walk-forward request whose fold count
611
+ * multiplies past the sweep budget, both answer `400` — {@link QTSTimeoutError}
612
+ * if a stage exceeds `timeoutMs`, or {@link QTSCanceledError} if the caller's
613
+ * signal fires before the sweep is accepted. A plain {@link QTSError} means
614
+ * the request itself is malformed (an empty grid, a non-positive `step`, a
615
+ * walk-forward block with fewer than two folds) and never reached the network.
616
+ *
617
+ * What the sweep *found* arrives through {@link Sweep.result}, which is also
618
+ * where the semantics of the leaderboard are documented. Acceptance already
619
+ * answers three things worth reading before any result exists — the effective
620
+ * seed, whether this submission enqueued anything, and whether this is a
621
+ * walk-forward sweep — see {@link Sweep.accepted}.
622
+ *
623
+ * ```ts
624
+ * const handle = await qts.sweep({
625
+ * strategy: source,
626
+ * exchangeId: 'binance',
627
+ * instrument: 'BTC/USDT',
628
+ * from: '2026-01-01T00:00:00Z',
629
+ * to: '2026-02-01T00:00:00Z',
630
+ * params: { rsiPeriod: { from: 7, to: 28, step: 1 } },
631
+ * });
632
+ * const leaderboard = await handle.result;
633
+ * ```
634
+ */
635
+ sweep(req, opts) {
636
+ return sweep(req, opts);
637
+ }
304
638
  /**
305
639
  * Download one hour of raw tickers for an instrument as a {@link Blob}.
306
640
  * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.
@@ -312,9 +646,131 @@ var QTSurfer = class {
312
646
  klines(args) {
313
647
  return downloadKlines(args);
314
648
  }
649
+ /**
650
+ * List the exchanges the platform serves. Each `id` is what every other
651
+ * method takes as `exchangeId`.
652
+ *
653
+ * @throws QTSError on any non-2xx response, with the HTTP status on
654
+ * `status`.
655
+ */
656
+ exchanges() {
657
+ return listExchanges();
658
+ }
659
+ /**
660
+ * List an exchange's instruments, each with the per-data-type `coverage`
661
+ * that says which date windows are actually downloadable.
662
+ *
663
+ * Omitting `segment` asks for the exchange's **default** segment, which is
664
+ * `'spot'` today. The API answers with a HAL envelope that this method
665
+ * unwraps to the instrument array, so the envelope's `meta.segment`,
666
+ * `meta.updatedAt` and segment-discovery `_links` do not reach you: if you
667
+ * need certainty about which segment you are looking at, pass `segment`
668
+ * explicitly rather than relying on the default.
669
+ *
670
+ * @param exchangeId exchange identifier, e.g. `binance`
671
+ * @throws QTSError on any non-2xx response, with the HTTP status on
672
+ * `status`.
673
+ */
674
+ instruments(exchangeId, segment) {
675
+ return listInstruments(exchangeId, segment);
676
+ }
677
+ /**
678
+ * Ask the platform to check that a registered strategy can actually run:
679
+ * it instantiates the compiled class and drives it through a bounded
680
+ * synthetic series, so a wiring fault surfaces here instead of at the first
681
+ * backtest.
682
+ *
683
+ * **Idempotent, and two-outcome.** `queued: false` means a verdict already
684
+ * existed for the current compilation and came back unchanged in `state` —
685
+ * nothing was queued. `queued: true` means a check was just started, and is
686
+ * **not** terminal: poll {@link QTSurfer.strategy} until `validation`
687
+ * leaves `'pending'`. The discriminant reports whether work was *started*,
688
+ * not whether a verdict *exists*, because a `queued: false` answer can
689
+ * itself carry `validation: 'pending'` from a check an earlier call queued;
690
+ * `state.validation` is what tells you that.
691
+ *
692
+ * **Poll with a deadline of your own.** `'pending'` is not guaranteed to
693
+ * resolve — a queued check can go unreported for far longer than one takes,
694
+ * which the platform eventually flags as `validationStalled`. Nothing about
695
+ * the strategy is disproved when that happens, but a caller that waits for
696
+ * a terminal verdict without a timeout can wait forever. This SDK ships no
697
+ * polling helper for that reason: the timeout is the caller's policy.
698
+ *
699
+ * Whatever the verdict, it is a floor rather than a guarantee — see
700
+ * {@link StrategyState}.
701
+ *
702
+ * @param strategyId the id returned when the strategy was compiled
703
+ * @throws QTSError on any non-2xx response; a `404` (carried on `status`)
704
+ * means no such registered strategy for this caller.
705
+ */
706
+ validateStrategy(strategyId) {
707
+ return validateStrategy(strategyId);
708
+ }
709
+ /**
710
+ * Read everything the platform records about a strategy: whether it is
711
+ * registered at all, its validation verdict, the market data its compiled
712
+ * class requires, and any engine notices the check raised. This is what to
713
+ * poll after {@link QTSurfer.validateStrategy} returns `queued: true`, and
714
+ * the only place a verdict is read from.
715
+ *
716
+ * Check `compiledAt` against `validatedAt` before trusting a verdict: the
717
+ * strategy may have been recompiled since it was recorded, in which case
718
+ * the verdict describes bytecode that is no longer what would run.
719
+ * See {@link StrategyState} for why even a fresh `'passed'` is a floor
720
+ * rather than a guarantee.
721
+ *
722
+ * @param strategyId the id returned when the strategy was compiled
723
+ * @throws QTSError on any non-2xx response. A `404` (carried on `status`)
724
+ * means exactly one thing — no such registered strategy for this caller.
725
+ * It is never a stale or expired answer.
726
+ */
727
+ strategy(strategyId) {
728
+ return getStrategy(strategyId);
729
+ }
730
+ /**
731
+ * List every strategy you have registered and not deleted, most recently
732
+ * compiled first. Never `404`s — an empty array means you have none.
733
+ * Each entry deliberately omits `validation`; check a specific strategy's
734
+ * verdict with {@link QTSurfer.strategy}. See {@link StrategySummary}.
735
+ *
736
+ * @throws QTSError on any non-2xx response, with the HTTP status on
737
+ * `status`.
738
+ */
739
+ strategies() {
740
+ return listStrategies();
741
+ }
742
+ /**
743
+ * Release a registered strategy: removes it from both {@link
744
+ * QTSurfer.strategy} and {@link QTSurfer.strategies}.
745
+ *
746
+ * Backtests already run against this strategy are unaffected, and
747
+ * re-submitting the same source afterwards registers a **new** strategy
748
+ * with a **new** id rather than undeleting this one. Deleting your own
749
+ * copy of a strategy never affects anyone else's copy of the same source
750
+ * (e.g. a shared/marketplace listing).
751
+ *
752
+ * @param strategyId the id returned when the strategy was compiled
753
+ * @throws QTSError on any non-2xx response; a `404` (carried on `status`)
754
+ * means no such registered strategy for this caller.
755
+ */
756
+ deleteStrategy(strategyId) {
757
+ return deleteStrategy(strategyId);
758
+ }
759
+ /**
760
+ * Read back the exact source last submitted for a strategy id, whitespace
761
+ * and comments included.
762
+ *
763
+ * A `404` (carried on `status`) covers two indistinguishable cases: the id
764
+ * was never registered by you, or it resolves only through a shared/
765
+ * marketplace reference that carries no source of its own.
766
+ *
767
+ * @param strategyId the id returned when the strategy was compiled
768
+ */
769
+ strategyCode(strategyId) {
770
+ return getStrategyCode(strategyId);
771
+ }
315
772
  // Future surface:
316
- // strategies: { compile, status, list }
317
- // instruments: { list, get } with TTL cache
773
+ // TTL cache for exchanges / instruments
318
774
  // jobs: { cancel, stream, result }
319
775
  };
320
776
 
@@ -437,6 +893,27 @@ var AuthenticatedClient = class {
437
893
  backtest(req, opts) {
438
894
  return this.withRefreshOn401(() => backtest(req, opts));
439
895
  }
896
+ /**
897
+ * Run the full compile → prepare → executeSweep pipeline and resolve once the
898
+ * platform has accepted the sweep, handing back a {@link Sweep} that keeps
899
+ * polling the leaderboard in the background. See {@link QTSurfer.sweep} for
900
+ * why the sweep is one call rather than composable stages, and
901
+ * {@link Sweep.result} for how to read what it found.
902
+ *
903
+ * The currently cached token is sent (minting one first if none is cached),
904
+ * but as with `backtest()` a `401` here is **not** auto-retried: the
905
+ * underlying stage errors carry no HTTP status, so a token that expires
906
+ * mid-pipeline surfaces as `QTSPreparationError`/`QTSExecutionError` rather
907
+ * than triggering a refresh.
908
+ *
909
+ * The background leaderboard poll and {@link Sweep.sensitivity} sit outside
910
+ * the policy for a second, independent reason: both run after this promise
911
+ * has already resolved, so a token that expires while a sweep is in flight
912
+ * surfaces on {@link Sweep.result} whatever the stage errors carry.
913
+ */
914
+ sweep(req, opts) {
915
+ return this.withRefreshOn401(() => sweep(req, opts));
916
+ }
440
917
  /** Download one hour of raw tickers. Refreshes the token once on `401` before retrying. */
441
918
  tickers(args) {
442
919
  return this.withRefreshOn401(() => downloadTickers(args));
@@ -445,6 +922,65 @@ var AuthenticatedClient = class {
445
922
  klines(args) {
446
923
  return this.withRefreshOn401(() => downloadKlines(args));
447
924
  }
925
+ /**
926
+ * List the exchanges the platform serves. Refreshes the token once on `401`
927
+ * before retrying.
928
+ */
929
+ exchanges() {
930
+ return this.withRefreshOn401(() => listExchanges());
931
+ }
932
+ /**
933
+ * List an exchange's instruments, optionally for a specific segment.
934
+ * Refreshes the token once on `401` before retrying. See
935
+ * {@link QTSurfer.instruments} for what the unwrapped HAL envelope leaves
936
+ * out.
937
+ */
938
+ instruments(exchangeId, segment) {
939
+ return this.withRefreshOn401(() => listInstruments(exchangeId, segment));
940
+ }
941
+ /**
942
+ * Ask the platform to check that a registered strategy can actually run.
943
+ * Refreshes the token once on `401` before retrying. Two-outcome — see
944
+ * {@link QTSurfer.validateStrategy}; `queued: true` is not terminal and
945
+ * must be followed by polling {@link AuthenticatedClient.strategy} under a
946
+ * deadline of your own.
947
+ */
948
+ validateStrategy(strategyId) {
949
+ return this.withRefreshOn401(() => validateStrategy(strategyId));
950
+ }
951
+ /**
952
+ * Read a strategy's recorded state, including its validation verdict.
953
+ * Refreshes the token once on `401` before retrying. See
954
+ * {@link StrategyState} for why a `'passed'` verdict is a floor rather than
955
+ * a guarantee.
956
+ */
957
+ strategy(strategyId) {
958
+ return this.withRefreshOn401(() => getStrategy(strategyId));
959
+ }
960
+ /**
961
+ * List every strategy you have registered and not deleted, most recently
962
+ * compiled first. Refreshes the token once on `401` before retrying. See
963
+ * {@link QTSurfer.strategies}.
964
+ */
965
+ strategies() {
966
+ return this.withRefreshOn401(() => listStrategies());
967
+ }
968
+ /**
969
+ * Release a registered strategy. Refreshes the token once on `401` before
970
+ * retrying. See {@link QTSurfer.deleteStrategy} for what this does and
971
+ * does not undo.
972
+ */
973
+ deleteStrategy(strategyId) {
974
+ return this.withRefreshOn401(() => deleteStrategy(strategyId));
975
+ }
976
+ /**
977
+ * Read back a strategy's exact registered source. Refreshes the token
978
+ * once on `401` before retrying. See {@link QTSurfer.strategyCode} for
979
+ * what its `404` covers.
980
+ */
981
+ strategyCode(strategyId) {
982
+ return this.withRefreshOn401(() => getStrategyCode(strategyId));
983
+ }
448
984
  };
449
985
  async function authenticate(apikey, opts = {}) {
450
986
  const resolved = apikey ?? readEnvApikey();