@qtsurfer/sdk 0.9.0 → 0.10.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.d.ts +60 -7
- package/dist/index.js +32 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +2 -1
- package/src/internal/preparation.ts +50 -4
- package/src/workflows/backtest.ts +39 -4
- package/src/workflows/sweep.ts +35 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
import { ResultMap, Exchange as Exchange$1, InstrumentDetail as InstrumentDetail$1, StrategyState as StrategyState$1, StrategySummary as StrategySummary$1, SweepProgress as SweepProgress$1, ExecuteSweepAccepted, ExecuteSweepResult, SweepSensitivity as SweepSensitivity$1, SweepHeatmap as SweepHeatmap$1, SweepHeatmapCell as SweepHeatmapCell$1, SweepMarginal as SweepMarginal$1, SweepMarginalPoint as SweepMarginalPoint$1, SweepRunRow as SweepRunRow$1, WalkForwardFold as WalkForwardFold$1, WalkForwardResult as WalkForwardResult$1, AuthTokenResponse } from '@qtsurfer/api-client';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* ```ts
|
|
5
|
+
* const request: BacktestRequest = {
|
|
6
|
+
* strategy: source,
|
|
7
|
+
* exchangeId: 'binance',
|
|
8
|
+
* instrument: 'BTC/USDT',
|
|
9
|
+
* from: '2026-01-01T00:00:00Z',
|
|
10
|
+
* to: '2026-02-01T00:00:00Z',
|
|
11
|
+
* };
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Backtest against a dataset you uploaded instead of an exchange instrument by
|
|
15
|
+
* replacing `instrument` with `datasetId` (and `exchangeId: 'user'`):
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const request: BacktestRequest = {
|
|
19
|
+
* strategy: source,
|
|
20
|
+
* exchangeId: 'user',
|
|
21
|
+
* datasetId: 'ds_123',
|
|
22
|
+
* from: '2026-01-01T00:00:00Z',
|
|
23
|
+
* to: '2026-02-01T00:00:00Z',
|
|
24
|
+
* };
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
3
27
|
interface BacktestRequest {
|
|
4
28
|
/** Strategy source code (Java) */
|
|
5
29
|
strategy: string;
|
|
6
|
-
/** Exchange id, e.g. `binance` */
|
|
30
|
+
/** Exchange id, e.g. `binance`, or the reserved value `user` when backtesting against `datasetId`. */
|
|
7
31
|
exchangeId: string;
|
|
8
|
-
/** Instrument symbol, e.g. `BTC/USDT` */
|
|
9
|
-
instrument
|
|
32
|
+
/** Instrument symbol, e.g. `BTC/USDT`. Exactly one of `instrument`/`datasetId` is required. */
|
|
33
|
+
instrument?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Id of a dataset you uploaded, in place of `instrument`. Exactly one of
|
|
36
|
+
* `instrument`/`datasetId` is required. Pairs with `exchangeId: 'user'`.
|
|
37
|
+
*/
|
|
38
|
+
datasetId?: string;
|
|
39
|
+
/** Optional specific version of `datasetId`; omit to use its current version. Requires `datasetId`. */
|
|
40
|
+
datasetVersionId?: string;
|
|
10
41
|
/** Date range start (ISO-8601, ISO DATE or BASIC ISO DATE) */
|
|
11
42
|
from: string;
|
|
12
43
|
/** Date range end (same formats as `from`) */
|
|
@@ -373,14 +404,35 @@ type WalkForwardFold = WalkForwardFold$1;
|
|
|
373
404
|
* objective: 'sharpe',
|
|
374
405
|
* };
|
|
375
406
|
* ```
|
|
407
|
+
*
|
|
408
|
+
* Sweep against a dataset you uploaded instead of an exchange instrument by
|
|
409
|
+
* replacing `instrument` with `datasetId` (and `exchangeId: 'user'`):
|
|
410
|
+
*
|
|
411
|
+
* ```ts
|
|
412
|
+
* const request: SweepRequest = {
|
|
413
|
+
* strategy: source,
|
|
414
|
+
* exchangeId: 'user',
|
|
415
|
+
* datasetId: 'ds_123',
|
|
416
|
+
* from: '2026-01-01T00:00:00Z',
|
|
417
|
+
* to: '2026-02-01T00:00:00Z',
|
|
418
|
+
* params: { rsiPeriod: { from: 7, to: 28, step: 1 } },
|
|
419
|
+
* };
|
|
420
|
+
* ```
|
|
376
421
|
*/
|
|
377
422
|
interface SweepRequest {
|
|
378
423
|
/** Strategy source code (Java), compiled once and reused by every trial. */
|
|
379
424
|
strategy: string;
|
|
380
|
-
/** Exchange id, e.g. `binance`. */
|
|
425
|
+
/** Exchange id, e.g. `binance`, or the reserved value `user` when sweeping against `datasetId`. */
|
|
381
426
|
exchangeId: string;
|
|
382
|
-
/** Instrument symbol, e.g. `BTC/USDT`. */
|
|
383
|
-
instrument
|
|
427
|
+
/** Instrument symbol, e.g. `BTC/USDT`. Exactly one of `instrument`/`datasetId` is required. */
|
|
428
|
+
instrument?: string;
|
|
429
|
+
/**
|
|
430
|
+
* Id of a dataset you uploaded, in place of `instrument`. Exactly one of
|
|
431
|
+
* `instrument`/`datasetId` is required. Pairs with `exchangeId: 'user'`.
|
|
432
|
+
*/
|
|
433
|
+
datasetId?: string;
|
|
434
|
+
/** Optional specific version of `datasetId`; omit to use its current version. Requires `datasetId`. */
|
|
435
|
+
datasetVersionId?: string;
|
|
384
436
|
/** Range start (ISO-8601, ISO DATE, or BASIC ISO DATE). */
|
|
385
437
|
from: string;
|
|
386
438
|
/** Range end (same formats as `from`; must be later than `from`). */
|
|
@@ -746,7 +798,8 @@ declare class QTSurfer {
|
|
|
746
798
|
* if a stage exceeds `timeoutMs`, or {@link QTSCanceledError} if the caller's
|
|
747
799
|
* signal fires before the sweep is accepted. A plain {@link QTSError} means
|
|
748
800
|
* the request itself is malformed (an empty grid, a non-positive `step`, a
|
|
749
|
-
* walk-forward block with fewer than two folds
|
|
801
|
+
* walk-forward block with fewer than two folds, or naming both/neither of
|
|
802
|
+
* `instrument`/`datasetId`) and never reached the network.
|
|
750
803
|
*
|
|
751
804
|
* What the sweep *found* arrives through {@link Sweep.result}, which is also
|
|
752
805
|
* where the semantics of the leaderboard are documented. Acceptance already
|
package/dist/index.js
CHANGED
|
@@ -145,10 +145,31 @@ async function compileStrategySource(source, signal) {
|
|
|
145
145
|
}
|
|
146
146
|
return data.strategyId;
|
|
147
147
|
}
|
|
148
|
+
function validatePrepareTarget(workflow, target) {
|
|
149
|
+
const hasInstrument = target.instrument !== void 0;
|
|
150
|
+
const hasDataset = target.datasetId !== void 0;
|
|
151
|
+
if (hasInstrument && hasDataset) {
|
|
152
|
+
throw new QTSError(`${workflow}: exactly one of instrument/datasetId is required, got both`);
|
|
153
|
+
}
|
|
154
|
+
if (!hasInstrument && !hasDataset) {
|
|
155
|
+
throw new QTSError(
|
|
156
|
+
`${workflow}: exactly one of instrument/datasetId is required, got neither`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (target.datasetVersionId !== void 0 && !hasDataset) {
|
|
160
|
+
throw new QTSError(`${workflow}: datasetVersionId requires datasetId`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
148
163
|
async function prepareDataset(target, policy, run = {}) {
|
|
149
164
|
const { data, error } = await prepareBacktest({
|
|
150
165
|
path: { exchangeId: target.exchangeId, type: TICKER },
|
|
151
|
-
body: {
|
|
166
|
+
body: {
|
|
167
|
+
...target.instrument !== void 0 ? { instrument: target.instrument } : {},
|
|
168
|
+
...target.datasetId !== void 0 ? { datasetId: target.datasetId } : {},
|
|
169
|
+
...target.datasetVersionId !== void 0 ? { datasetVersionId: target.datasetVersionId } : {},
|
|
170
|
+
from: target.from,
|
|
171
|
+
to: target.to
|
|
172
|
+
},
|
|
152
173
|
...run.signal ? { signal: run.signal } : {}
|
|
153
174
|
});
|
|
154
175
|
if (error) throw new QTSPreparationError("Prepare submission failed", error);
|
|
@@ -188,6 +209,7 @@ async function prepareDataset(target, policy, run = {}) {
|
|
|
188
209
|
var DEFAULT_POLL_INTERVAL_MS = 500;
|
|
189
210
|
var DEFAULT_MAX_POLL_INTERVAL_MS = 5e3;
|
|
190
211
|
async function backtest(req, opts = {}) {
|
|
212
|
+
validatePrepareTarget("backtest", req);
|
|
191
213
|
const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS, DEFAULT_MAX_POLL_INTERVAL_MS);
|
|
192
214
|
opts.onProgress?.({ stage: "compiling" });
|
|
193
215
|
const strategyId = await compileStrategySource(req.strategy, opts.signal);
|
|
@@ -200,7 +222,9 @@ function prepareData(req, policy, opts) {
|
|
|
200
222
|
return prepareDataset(
|
|
201
223
|
{
|
|
202
224
|
exchangeId: req.exchangeId,
|
|
203
|
-
instrument: req.instrument,
|
|
225
|
+
...req.instrument !== void 0 ? { instrument: req.instrument } : {},
|
|
226
|
+
...req.datasetId !== void 0 ? { datasetId: req.datasetId } : {},
|
|
227
|
+
...req.datasetVersionId !== void 0 ? { datasetVersionId: req.datasetVersionId } : {},
|
|
204
228
|
from: req.from,
|
|
205
229
|
to: req.to
|
|
206
230
|
},
|
|
@@ -422,6 +446,7 @@ async function sweep(req, opts = {}) {
|
|
|
422
446
|
return createHandle(req, opts, policy, data, requestId, strategyId);
|
|
423
447
|
}
|
|
424
448
|
function validateRequest(req) {
|
|
449
|
+
validatePrepareTarget("sweep", req);
|
|
425
450
|
const names = Object.keys(req.params ?? {});
|
|
426
451
|
if (names.length === 0) {
|
|
427
452
|
throw new QTSError("sweep: params must hold at least one axis");
|
|
@@ -451,7 +476,9 @@ function prepareData2(req, policy, opts) {
|
|
|
451
476
|
return prepareDataset(
|
|
452
477
|
{
|
|
453
478
|
exchangeId: req.exchangeId,
|
|
454
|
-
instrument: req.instrument,
|
|
479
|
+
...req.instrument !== void 0 ? { instrument: req.instrument } : {},
|
|
480
|
+
...req.datasetId !== void 0 ? { datasetId: req.datasetId } : {},
|
|
481
|
+
...req.datasetVersionId !== void 0 ? { datasetVersionId: req.datasetVersionId } : {},
|
|
455
482
|
from: req.from,
|
|
456
483
|
to: req.to
|
|
457
484
|
},
|
|
@@ -612,7 +639,8 @@ var QTSurfer = class {
|
|
|
612
639
|
* if a stage exceeds `timeoutMs`, or {@link QTSCanceledError} if the caller's
|
|
613
640
|
* signal fires before the sweep is accepted. A plain {@link QTSError} means
|
|
614
641
|
* the request itself is malformed (an empty grid, a non-positive `step`, a
|
|
615
|
-
* walk-forward block with fewer than two folds
|
|
642
|
+
* walk-forward block with fewer than two folds, or naming both/neither of
|
|
643
|
+
* `instrument`/`datasetId`) and never reached the network.
|
|
616
644
|
*
|
|
617
645
|
* What the sweep *found* arrives through {@link Sweep.result}, which is also
|
|
618
646
|
* where the semantics of the leaderboard are documented. Acceptance already
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/workflows/backtest.ts","../src/errors.ts","../src/internal/polling.ts","../src/internal/preparation.ts","../src/workflows/catalog.ts","../src/internal/requestError.ts","../src/workflows/downloads.ts","../src/workflows/strategies.ts","../src/workflows/sweep.ts","../src/auth/session.ts","../src/auth/tokenStore.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 listExchanges,\n listInstruments,\n type Exchange,\n type InstrumentDetail,\n type InstrumentSegment,\n} from './workflows/catalog';\nimport {\n downloadKlines,\n downloadTickers,\n type DownloadFormat,\n} from './workflows/downloads';\nimport {\n getStrategy,\n validateStrategy as runValidateStrategy,\n listStrategies,\n deleteStrategy as runDeleteStrategy,\n getStrategyCode,\n type StrategyState,\n type StrategyValidation,\n type StrategySummary,\n} from './workflows/strategies';\nimport {\n sweep as runSweep,\n type Sweep,\n type SweepOptions,\n type SweepRequest,\n} from './workflows/sweep';\n\n/** Configuration for {@link QTSurfer}. */\nexport interface QTSurferOptions {\n /** Base URL of the QTSurfer API, e.g. `https://api.qtsurfer.com/v1`. */\n baseUrl: string;\n /**\n * Pre-obtained bearer token. When omitted, requests go out unauthenticated.\n * Use the `authenticate()` helper instead of this constructor if you want\n * the SDK to exchange an apikey for a JWT and refresh it on `401` for you.\n */\n token?: string;\n /** Inject a custom `fetch` (Node 20+, browser, or test mock). */\n fetch?: typeof fetch;\n}\n\n/** Selects one hour of tickers or klines for a single instrument. */\nexport interface DownloadHourArgs {\n /** Exchange id, e.g. `binance`. */\n exchangeId: string;\n /** Base asset of the instrument, e.g. `BTC`. */\n base: string;\n /** Quote asset of the instrument, e.g. `USDT`. */\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\n/**\n * Thin, stateless wrapper over `@qtsurfer/api-client` that exposes the SDK's\n * workflow methods (`backtest`, `sweep`, `tickers`, `klines`), the platform catalog\n * (`exchanges`, `instruments`) and the strategy surface (`validateStrategy`,\n * `strategy`, `strategies`, `deleteStrategy`, `strategyCode`). Constructing an\n * instance reconfigures the underlying api-client singleton, so avoid\n * holding two `QTSurfer`s with different `baseUrl`s or tokens alive in the\n * same process — they will race. Prefer the `authenticate()` helper over\n * this constructor unless you already manage the JWT lifecycle yourself.\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 /**\n * Run a backtest end-to-end: compile the strategy, prepare the requested\n * data range, execute it, and resolve with the result once execution\n * completes. See the underlying `backtest` workflow for the\n * stage-by-stage error and retry semantics.\n */\n backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return backtest(req, opts);\n }\n\n /**\n * Run the full compile → prepare → executeSweep pipeline and resolve once the\n * platform has accepted the sweep, handing back a {@link Sweep} that keeps\n * polling the leaderboard in the background.\n *\n * The whole sweep is one call because the execute-sweep endpoint is addressed\n * by the id of an already-prepared dataset: exposing the stages separately\n * would hand dataset lifecycle to the caller and buy nothing. Preparing is\n * idempotent, so sweeping the same window twice prepares it once.\n *\n * The returned promise rejects with {@link QTSStrategyCompileError} if\n * compilation fails, {@link QTSPreparationError} if data preparation fails,\n * {@link QTSExecutionError} if the platform rejects the sweep — an expanded\n * grid over the server limit, or a walk-forward request whose fold count\n * multiplies past the sweep budget, both answer `400` — {@link QTSTimeoutError}\n * if a stage exceeds `timeoutMs`, or {@link QTSCanceledError} if the caller's\n * signal fires before the sweep is accepted. A plain {@link QTSError} means\n * the request itself is malformed (an empty grid, a non-positive `step`, a\n * walk-forward block with fewer than two folds) and never reached the network.\n *\n * What the sweep *found* arrives through {@link Sweep.result}, which is also\n * where the semantics of the leaderboard are documented. Acceptance already\n * answers three things worth reading before any result exists — the effective\n * seed, whether this submission enqueued anything, and whether this is a\n * walk-forward sweep — see {@link Sweep.accepted}.\n *\n * ```ts\n * const handle = await qts.sweep({\n * strategy: source,\n * exchangeId: 'binance',\n * instrument: 'BTC/USDT',\n * from: '2026-01-01T00:00:00Z',\n * to: '2026-02-01T00:00:00Z',\n * params: { rsiPeriod: { from: 7, to: 28, step: 1 } },\n * });\n * const leaderboard = await handle.result;\n * ```\n */\n sweep(req: SweepRequest, opts?: SweepOptions): Promise<Sweep> {\n return runSweep(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 /**\n * List the exchanges the platform serves. Each `id` is what every other\n * method takes as `exchangeId`.\n *\n * @throws QTSError on any non-2xx response, with the HTTP status on\n * `status`.\n */\n exchanges(): Promise<Exchange[]> {\n return listExchanges();\n }\n\n /**\n * List an exchange's instruments, each with the per-data-type `coverage`\n * that says which date windows are actually downloadable.\n *\n * Omitting `segment` asks for the exchange's **default** segment, which is\n * `'spot'` today. The API answers with a HAL envelope that this method\n * unwraps to the instrument array, so the envelope's `meta.segment`,\n * `meta.updatedAt` and segment-discovery `_links` do not reach you: if you\n * need certainty about which segment you are looking at, pass `segment`\n * explicitly rather than relying on the default.\n *\n * @param exchangeId exchange identifier, e.g. `binance`\n * @throws QTSError on any non-2xx response, with the HTTP status on\n * `status`.\n */\n instruments(\n exchangeId: string,\n segment?: InstrumentSegment,\n ): Promise<InstrumentDetail[]> {\n return listInstruments(exchangeId, segment);\n }\n\n /**\n * Ask the platform to check that a registered strategy can actually run:\n * it instantiates the compiled class and drives it through a bounded\n * synthetic series, so a wiring fault surfaces here instead of at the first\n * backtest.\n *\n * **Idempotent, and two-outcome.** `queued: false` means a verdict already\n * existed for the current compilation and came back unchanged in `state` —\n * nothing was queued. `queued: true` means a check was just started, and is\n * **not** terminal: poll {@link QTSurfer.strategy} until `validation`\n * leaves `'pending'`. The discriminant reports whether work was *started*,\n * not whether a verdict *exists*, because a `queued: false` answer can\n * itself carry `validation: 'pending'` from a check an earlier call queued;\n * `state.validation` is what tells you that.\n *\n * **Poll with a deadline of your own.** `'pending'` is not guaranteed to\n * resolve — a queued check can go unreported for far longer than one takes,\n * which the platform eventually flags as `validationStalled`. Nothing about\n * the strategy is disproved when that happens, but a caller that waits for\n * a terminal verdict without a timeout can wait forever. This SDK ships no\n * polling helper for that reason: the timeout is the caller's policy.\n *\n * Whatever the verdict, it is a floor rather than a guarantee — see\n * {@link StrategyState}.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * means no such registered strategy for this caller.\n */\n validateStrategy(strategyId: string): Promise<StrategyValidation> {\n return runValidateStrategy(strategyId);\n }\n\n /**\n * Read everything the platform records about a strategy: whether it is\n * registered at all, its validation verdict, the market data its compiled\n * class requires, and any engine notices the check raised. This is what to\n * poll after {@link QTSurfer.validateStrategy} returns `queued: true`, and\n * the only place a verdict is read from.\n *\n * Check `compiledAt` against `validatedAt` before trusting a verdict: the\n * strategy may have been recompiled since it was recorded, in which case\n * the verdict describes bytecode that is no longer what would run.\n * See {@link StrategyState} for why even a fresh `'passed'` is a floor\n * rather than a guarantee.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response. A `404` (carried on `status`)\n * means exactly one thing — no such registered strategy for this caller.\n * It is never a stale or expired answer.\n */\n strategy(strategyId: string): Promise<StrategyState> {\n return getStrategy(strategyId);\n }\n\n /**\n * List every strategy you have registered and not deleted, most recently\n * compiled first. Never `404`s — an empty array means you have none.\n * Each entry deliberately omits `validation`; check a specific strategy's\n * verdict with {@link QTSurfer.strategy}. See {@link StrategySummary}.\n *\n * @throws QTSError on any non-2xx response, with the HTTP status on\n * `status`.\n */\n strategies(): Promise<StrategySummary[]> {\n return listStrategies();\n }\n\n /**\n * Release a registered strategy: removes it from both {@link\n * QTSurfer.strategy} and {@link QTSurfer.strategies}.\n *\n * Backtests already run against this strategy are unaffected, and\n * re-submitting the same source afterwards registers a **new** strategy\n * with a **new** id rather than undeleting this one. Deleting your own\n * copy of a strategy never affects anyone else's copy of the same source\n * (e.g. a shared/marketplace listing).\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * means no such registered strategy for this caller.\n */\n deleteStrategy(strategyId: string): Promise<void> {\n return runDeleteStrategy(strategyId);\n }\n\n /**\n * Read back the exact source last submitted for a strategy id, whitespace\n * and comments included.\n *\n * A `404` (carried on `status`) covers two indistinguishable cases: the id\n * was never registered by you, or it resolves only through a shared/\n * marketplace reference that carries no source of its own.\n *\n * @param strategyId the id returned when the strategy was compiled\n */\n strategyCode(strategyId: string): Promise<string> {\n return getStrategyCode(strategyId);\n }\n\n // Future surface:\n // TTL cache for exchanges / instruments\n // jobs: { cancel, stream, result }\n}\n","import {\n cancelBacktest,\n executeBacktest,\n getBacktestResult,\n type ResultMap,\n} from '@qtsurfer/api-client';\nimport { QTSCanceledError, QTSExecutionError } from '../errors';\nimport {\n buildStagePolicy,\n normalizeStatus,\n runStage,\n type StagePolicy,\n} from '../internal/polling';\nimport {\n TICKER,\n compileStrategySource,\n prepareDataset,\n} from '../internal/preparation';\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\n/**\n * Resolved value of the backtest workflow (see {@link QTSurfer.backtest}).\n * Alias for api-client's `ResultMap` — always includes core fields\n * (`hostName`, `iops`, `instrument`); yield metrics (`pnlTotal`,\n * `totalTrades`, `equityCurve`, etc.) are only present once the strategy\n * has emitted at least one trade.\n */\nexport type BacktestResult = ResultMap;\n\n/** The three sequential stages {@link QTSurfer.backtest} moves through, in order. */\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 * Fraction (0-1) of the requested prepare window that actually holds data,\n * reported by the backend once preparation completes. Present only on the\n * final `preparing` event.\n */\n coverageRatio?: number;\n}\n\nexport interface BacktestOptions {\n /** Abort the workflow. Cancels the current poll and calls `cancelBacktest` 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\n/** Initial interval between polls of a single backtest. */\nconst DEFAULT_POLL_INTERVAL_MS = 500;\n/** Backoff ceiling for a single backtest. */\nconst DEFAULT_MAX_POLL_INTERVAL_MS = 5000;\n\nexport async function backtest(\n req: BacktestRequest,\n opts: BacktestOptions = {},\n): Promise<BacktestResult> {\n const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS, DEFAULT_MAX_POLL_INTERVAL_MS);\n\n // 1. Compile strategy (single synchronous request)\n opts.onProgress?.({ stage: 'compiling' });\n const strategyId = await compileStrategySource(req.strategy, opts.signal);\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 prepareData(\n req: BacktestRequest,\n policy: StagePolicy,\n opts: BacktestOptions,\n): Promise<string> {\n return prepareDataset(\n {\n exchangeId: req.exchangeId,\n instrument: req.instrument,\n from: req.from,\n to: req.to,\n },\n policy,\n {\n ...(opts.signal ? { signal: opts.signal } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n onPercent: (percent) => opts.onProgress?.({ stage: 'preparing', percent }),\n // Surface the backend's coverage ratio for the prepared window (spec 0.98.0) on the\n // final preparing event, so callers can react to a partially-covered range.\n onPrepared: (state) =>\n opts.onProgress?.({\n stage: 'preparing',\n percent: 100,\n coverageRatio: state.coverageRatio,\n }),\n },\n );\n}\n\nasync function executeStrategy(\n req: BacktestRequest,\n prepareJobId: string,\n strategyId: string,\n policy: StagePolicy,\n opts: BacktestOptions,\n): Promise<BacktestResult> {\n const { data, error } = await executeBacktest({\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 async ({ signal }) => {\n const res = await getBacktestResult({\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 // A 202 carries an empty body: no `state`, so the spread yields an undefined status and\n // the retry predicate keeps polling. That is the intended handling, not a coincidence —\n // see normalizeStatus. Do not \"fix\" this into a throw or an early return of the result.\n return { ...res.data.state, __result: res.data.results };\n },\n {\n ...(opts.signal ? { signal: opts.signal } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n onEachAttempt: (r) => {\n if (r.size > 0) {\n opts.onProgress?.({ stage: 'executing', percent: (r.completed / r.size) * 100 });\n }\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 cancelBacktest({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n }).catch(() => undefined);\n }\n throw err;\n }\n}\n","/**\n * Base class for every error the SDK throws. Catch this to handle all SDK\n * failures generically, or catch a specific subclass below to tell which\n * stage failed. `status` is only set when the throw site had an HTTP status\n * to attach: {@link QTSDownloadError} always carries one, and so does the\n * plain `QTSError` thrown by the single-request calls (`exchanges`,\n * `instruments`, `validateStrategy`, `strategy`). The workflow-stage errors\n * carry `cause` instead and encode retryability in their message.\n */\nexport class QTSError extends Error {\n /** HTTP status code, when the underlying transport surfaced one. */\n readonly status?: number;\n constructor(message: string, readonly cause?: unknown, status?: number) {\n super(message);\n this.name = 'QTSError';\n if (status !== undefined) this.status = status;\n }\n}\n\n/**\n * Thrown when strategy compilation fails. A `429` means the source was never\n * judged — too many compilations were already in flight — and is safe to\n * retry. Any other status (typically `400`) means the source itself does\n * not compile, so retrying with the same input fails again.\n */\nexport class QTSStrategyCompileError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSStrategyCompileError';\n }\n}\n\n/**\n * Thrown when the data-preparation stage fails: submitting the prepare\n * request, polling its status, or a backend-reported preparation failure\n * (e.g. no data available for the requested range) all surface here.\n */\nexport class QTSPreparationError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSPreparationError';\n }\n}\n\n/**\n * Thrown when the execute stage fails: submitting the execute request,\n * polling its result, or a backend-reported execution failure all surface\n * here.\n */\nexport class QTSExecutionError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSExecutionError';\n }\n}\n\n/**\n * Thrown when a stage (prepare or execute) exceeds `timeoutMs`. The stage\n * may still be running server-side — this only means the SDK stopped\n * waiting locally — so it is fine to retry, optionally with a larger\n * `timeoutMs`.\n */\nexport class QTSTimeoutError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSTimeoutError';\n }\n}\n\n/**\n * Thrown when a stage is aborted — either because the caller's\n * `AbortSignal` fired, or because the backend itself reported the\n * prepare/execute job as aborted. Either way this reflects a deliberate\n * stop, not a failure, and is not something to retry automatically.\n */\nexport class QTSCanceledError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSCanceledError';\n }\n}\n\n/**\n * Thrown by the tickers/klines download functions on any non-2xx response or\n * transport failure. Carries the HTTP `status` when one was received: a\n * `4xx` means the request itself is wrong (bad hour or instrument), while a\n * `5xx` or a missing status (transport failure) is generally safe to retry.\n */\nexport class QTSDownloadError extends QTSError {\n constructor(message: string, cause?: unknown, status?: number) {\n super(message, cause, status);\n this.name = 'QTSDownloadError';\n }\n}\n\n/**\n * Thrown by the `authenticate()` helper when the apikey is missing or the JWT\n * exchange fails (HTTP 401 from `POST /v1/auth/token`, etc.).\n */\nexport class QTSAuthError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSAuthError';\n }\n}\n","import {\n ExponentialBackoff,\n TaskCancelledError,\n TimeoutStrategy,\n handleWhenResult,\n retry,\n timeout,\n wrap,\n type ICancellationContext,\n type IPolicy,\n} from 'cockatiel';\nimport { QTSCanceledError, QTSTimeoutError } from '../errors';\n\n/**\n * The stable form of a backend job/sweep status, so the rest of the SDK can\n * reason about it regardless of OpenAPI spec drift (the live API sometimes\n * returns lowercase values like `queued` / `completed` / `failed`).\n *\n * @internal\n */\nexport type NormalizedStatus = 'in-progress' | 'completed' | 'failed' | 'aborted';\n\n/**\n * Normalize a raw status value.\n *\n * Only the terminal statuses end a poll loop. Everything else — including a\n * **missing** status — means \"keep asking\": the API answers `202` with an empty body when a\n * job is known but its result is not readable yet, and that response carries no state at all.\n * Mapping absent to in-progress is what makes a 202 continue the loop under its timeout\n * instead of being mistaken for a finished job with no data.\n *\n * @internal\n */\nexport function normalizeStatus(raw: unknown): NormalizedStatus {\n const value = typeof raw === 'string' ? raw.toLowerCase() : '';\n if (value === 'completed') return 'completed';\n // A sweep that finishes with at least one shard dead reports `partial`, and that is\n // terminal: the rows it did produce are readable and nothing more is coming, so treating it\n // as in-progress would poll a finished sweep forever. `PARTIAL` exists only on the two sweep\n // schemas (`ExecuteSweepResult.status` and `SweepSensitivity.status`) and is absent from\n // `JobState`, so mapping it here cannot reach the prepare or execute paths. It folds into\n // `completed` because this enum drives the poll loop — stop asking, hand the response back —\n // and the caller reads `partial` off the response itself, where the distinction survives.\n if (value === 'partial') 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 / absent (202) / anything else → still running\n return 'in-progress';\n}\n\n/** The retry-with-backoff policy one workflow stage polls under. @internal */\nexport type StagePolicy = IPolicy<ICancellationContext, never>;\n\n/** Poll tuning shared by every workflow's options object. @internal */\nexport interface PollTuning {\n pollIntervalMs?: number;\n maxPollIntervalMs?: number;\n timeoutMs?: number;\n}\n\n/**\n * Build the poll policy for a workflow's stages.\n *\n * The defaults are a per-workflow argument rather than a constant, because how\n * often it is worth asking depends on what is being watched: a single backtest\n * advances tick by tick, while a sweep's leaderboard changes on the timescale\n * of shards finishing.\n *\n * @param opts caller overrides\n * @param defaultPollMs initial interval when the caller sets none\n * @param defaultMaxPollMs backoff ceiling when the caller sets none\n *\n * @internal\n */\nexport function buildStagePolicy(\n opts: PollTuning,\n defaultPollMs: number,\n defaultMaxPollMs: number,\n): StagePolicy {\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 ?? defaultPollMs,\n maxDelay: opts.maxPollIntervalMs ?? defaultMaxPollMs,\n }),\n },\n );\n\n return opts.timeoutMs\n ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy)\n : retryPolicy;\n}\n\n/** How one call to {@link runStage} is cancelled and reported. @internal */\nexport interface StageRun<T> {\n /**\n * Aborts the stage, surfacing as {@link QTSCanceledError}. Omit it to run a\n * stage the caller's signal must **not** interrupt.\n */\n signal?: AbortSignal;\n /** Only used to render the timeout message. */\n timeoutMs?: number;\n /** Called with every attempt's result, terminal or not. */\n onEachAttempt?: (r: T) => void;\n}\n\n/**\n * Poll `fetchFn` under `policy` until its result stops normalizing to\n * `in-progress`, then return that result.\n *\n * @internal\n */\nexport async function runStage<T>(\n policy: StagePolicy,\n fetchFn: (ctx: ICancellationContext) => Promise<T>,\n run: StageRun<T> = {},\n): Promise<T> {\n try {\n return await policy.execute(async (ctx) => {\n if (run.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n const result = await fetchFn(ctx);\n run.onEachAttempt?.(result);\n if (run.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n return result;\n }, run.signal);\n } catch (err) {\n if (err instanceof QTSCanceledError) throw err;\n if (err instanceof TaskCancelledError) {\n if (run.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw new QTSTimeoutError(`Stage exceeded ${run.timeoutMs}ms`, err);\n }\n if (run.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw err;\n }\n}\n","import {\n getPrepareStatus,\n compileStrategy as apiCompileStrategy,\n prepareBacktest,\n type DataSourceType,\n type PrepareJobState,\n} from '@qtsurfer/api-client';\nimport { QTSCanceledError, QTSPreparationError, QTSStrategyCompileError } from '../errors';\nimport { normalizeStatus, runStage, type StagePolicy } from './polling';\n\n/** The only data source the workflows prepare against today. @internal */\nexport const TICKER: DataSourceType = 'ticker';\n\n/**\n * Compile in a single request: the API answers synchronously with the `strategyId`,\n * so there is no job to poll. A compile error arrives here as a `400`, not on a later poll.\n *\n * @internal\n */\nexport async function compileStrategySource(\n source: string,\n signal?: AbortSignal,\n): Promise<string> {\n const { data, error, response } = await apiCompileStrategy({\n body: source,\n ...(signal ? { signal } : {}),\n });\n\n if (error) {\n // A 429 means the platform is holding too many compilations at once and the source was never\n // judged — worth separating from the 400 that says the source itself does not compile.\n // Read the status inside this branch and optionally: a transport failure carries no response,\n // and dereferencing one would raise a TypeError that buries the error actually being reported.\n if (response?.status === 429) {\n throw new QTSStrategyCompileError(\n 'Strategy was not compiled, too many compilations in flight; retry later',\n error,\n );\n }\n throw new QTSStrategyCompileError('Strategy compilation failed', error);\n }\n if (!data?.strategyId) {\n throw new QTSStrategyCompileError('Compile response missing strategyId');\n }\n return data.strategyId;\n}\n\n/** The instrument and window one prepare covers. @internal */\nexport interface PrepareTarget {\n exchangeId: string;\n instrument: string;\n from: string;\n to: string;\n}\n\n/** Reporting and cancellation for {@link prepareDataset}. @internal */\nexport interface PrepareRun {\n signal?: AbortSignal;\n timeoutMs?: number;\n /** Called after each poll that reports size information. */\n onPercent?: (percent: number) => void;\n /** Called once with the terminal state, so the caller can read its coverage. */\n onPrepared?: (state: PrepareJobState) => void;\n}\n\n/**\n * Submit a prepare and poll it to a terminal state.\n *\n * One implementation on purpose. Preparing is idempotent — the same instrument\n * and window always resolve to the same job — so a workflow that prepares on\n * every call duplicates no work, and that argument only holds while there is a\n * single place where it is true.\n *\n * @returns the prepare jobId, which is what identifies the prepared dataset\n *\n * @internal\n */\nexport async function prepareDataset(\n target: PrepareTarget,\n policy: StagePolicy,\n run: PrepareRun = {},\n): Promise<string> {\n const { data, error } = await prepareBacktest({\n path: { exchangeId: target.exchangeId, type: TICKER },\n body: { instrument: target.instrument, from: target.from, to: target.to },\n ...(run.signal ? { signal: run.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 async ({ signal }) => {\n const res = await getPrepareStatus({\n path: { exchangeId: target.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 {\n ...(run.signal ? { signal: run.signal } : {}),\n ...(run.timeoutMs !== undefined ? { timeoutMs: run.timeoutMs } : {}),\n onEachAttempt: (r) => {\n if (r.size > 0) run.onPercent?.((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 run.onPrepared?.(state);\n return prepareJobId;\n}\n","import {\n listExchanges as apiListExchanges,\n listInstruments as apiListInstruments,\n listSegmentInstruments as apiListSegmentInstruments,\n type Exchange as ApiExchange,\n type InstrumentDetail as ApiInstrumentDetail,\n} from '@qtsurfer/api-client';\nimport { QTSError } from '../errors';\nimport { requestFailed } from '../internal/requestError';\n\n/**\n * One exchange the platform serves. Alias for api-client's `Exchange`:\n * `id` (what every other call takes as `exchangeId`), `name`, and an\n * optional `description`.\n */\nexport type Exchange = ApiExchange;\n\n/**\n * One instrument on an exchange. Alias for api-client's `InstrumentDetail`:\n * `id` / `base` / `quote`, plus optional `coverage` (the date windows for\n * which tickers and klines actually exist, per data type), `lastPrice` and\n * `volume24h`.\n *\n * `coverage` is what tells you whether a backtest range is downloadable at\n * all; it is optional, and absent means the platform did not report one, not\n * that there is no data.\n */\nexport type InstrumentDetail = ApiInstrumentDetail;\n\n/**\n * A market segment of an exchange. `'spot'` is the default segment served\n * when {@link QTSurfer.instruments} is called without one.\n */\nexport type InstrumentSegment = 'spot' | 'futures';\n\n/**\n * List the exchanges the platform serves.\n *\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\nexport async function listExchanges(): Promise<Exchange[]> {\n const { data, error, response } = await apiListExchanges();\n if (error) throw requestFailed('exchanges call', error, response?.status);\n if (!data) throw new QTSError('Empty exchanges response');\n return data;\n}\n\n/**\n * List an exchange's instruments, each with its per-data-type coverage.\n *\n * Omitting `segment` asks for the exchange's **default** segment, which is\n * `'spot'` today. The API answers both routes with a HAL envelope\n * (`data` / `meta` / `_links`) that this function unwraps to the instrument\n * array, so `meta.segment`, `meta.updatedAt` and the `_links`\n * segment-discovery links do not reach the caller: if you need certainty\n * about which segment you are looking at, pass `segment` explicitly rather\n * than relying on the default.\n *\n * @param exchangeId exchange identifier, e.g. `binance`\n * @param segment market segment to list; defaults to the exchange's default\n * segment\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\nexport async function listInstruments(\n exchangeId: string,\n segment?: InstrumentSegment,\n): Promise<InstrumentDetail[]> {\n const { data, error, response } = segment\n ? await apiListSegmentInstruments({ path: { exchangeId, segment } })\n : await apiListInstruments({ path: { exchangeId } });\n if (error) throw requestFailed('instruments call', error, response?.status);\n if (!data) throw new QTSError('Empty instruments response');\n return data.data;\n}\n","import { QTSError } from '../errors';\n\n/**\n * Build the {@link QTSError} for a failed single-request call.\n *\n * The HTTP status is attached to the error rather than only rendered into the\n * message, because callers branch on it — a `4xx` means the request itself was\n * wrong, a `5xx` is generally worth retrying, and the authenticated session\n * re-mints its JWT on a `401`.\n *\n * @param what short description of the call, e.g. `'exchanges call'`\n * @param error the api-client error payload\n * @param status HTTP status of the failing response\n *\n * @internal\n */\nexport function requestFailed(\n what: string,\n error: unknown,\n status?: number,\n): QTSError {\n const prefix = status === undefined ? '' : `HTTP ${status} — `;\n return new QTSError(`${what} failed: ${prefix}${describe(error)}`, error, status);\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' || typeof e.code === 'number' ? e.code : undefined;\n const message = typeof e.message === 'string' ? e.message : undefined;\n if (code !== undefined && message) return `${code}: ${message}`;\n if (message) return message;\n if (code !== undefined) return String(code);\n }\n return String(error);\n}\n","import {\n downloadKlines as apiDownloadKlines,\n downloadTickers as apiDownloadTickers,\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 /** Exchange id, e.g. `binance`. */\n exchangeId: string;\n /** Base asset of the instrument, e.g. `BTC`. */\n base: string;\n /** Quote asset of the instrument, e.g. `USDT`. */\n quote: string;\n /** Hour selector in `YYYY-MM-DDTHH` (UTC). */\n hour: string;\n /** Defaults to `'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 apiDownloadTickers({\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 response.status,\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 apiDownloadKlines({\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 response.status,\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","import {\n getStrategy as apiGetStrategy,\n validateStrategy as apiValidateStrategy,\n listStrategies as apiListStrategies,\n deleteStrategy as apiDeleteStrategy,\n getStrategyCode as apiGetStrategyCode,\n type StrategyState as ApiStrategyState,\n type StrategySummary as ApiStrategySummary,\n} from '@qtsurfer/api-client';\nimport { QTSError } from '../errors';\nimport { requestFailed } from '../internal/requestError';\n\n/**\n * Everything the platform records about a registered strategy. Alias for\n * api-client's `StrategyState`.\n *\n * `validation` is the verdict and is one of:\n *\n * - `'not_validated'` — registered, never checked.\n * - `'pending'` — a check was asked for and has not answered yet.\n * - `'passed'` — the class loaded and survived its first event.\n * - `'failed'` — it did not; `detail` says how.\n *\n * **`'passed'` is a floor, not a guarantee.** It means the compiled class\n * could be instantiated and got through the first event of a short synthetic\n * run — not the caller's instrument, not the caller's window, and not the\n * rest of the run. It says nothing about whether the strategy is correct,\n * profitable, or safe to run at scale. `dryRunIncomplete` marks a check that\n * ran out of its budget, which makes a `'passed'` verdict a lower floor\n * still, and makes an empty `notices` list no longer a clean bill of health.\n *\n * A verdict describes the bytecode that existed when it was recorded:\n * `compiledAt` newer than `validatedAt` means the strategy was recompiled\n * afterwards and the verdict no longer describes what would run.\n *\n * `_links.code`, when present, is a discovery link to this strategy's raw\n * source (`GET /strategy/{strategyId}/code` — the same thing\n * {@link QTSurfer.strategyCode} fetches by id, so there is no need to follow\n * the link yourself). It is present on a full `StrategyState` body — this\n * function's result, and {@link QTSurfer.validateStrategy}'s already-validated\n * `200` — and **absent** from that same operation's `queued: true` (`202`)\n * outcome, which is a deliberately partial stub. This field passes through\n * unmodified from api-client, so it needs no unwrapping on the SDK's part.\n */\nexport type StrategyState = ApiStrategyState;\n\n/**\n * Outcome of {@link QTSurfer.validateStrategy} — the SDK's rendering of the\n * two answers that operation has, which the response body alone cannot tell\n * apart.\n *\n * - `queued: false` — a verdict already existed for the current compilation\n * and comes back in `state` unchanged; nothing new was queued. This is\n * **not** the same as \"terminal\": a check queued by an earlier call can\n * still be running, so read `state.validation` rather than treating\n * `queued: false` as \"there is an answer\".\n * - `queued: true` — a check was just queued. Nothing is known yet; poll\n * {@link QTSurfer.strategy} until `validation` leaves `'pending'`.\n */\nexport type StrategyValidation =\n | { queued: false; strategyId: string; state: StrategyState }\n | { queued: true; strategyId: string; state?: undefined };\n\n/**\n * Ask the platform to check that a registered strategy can actually run: it\n * instantiates the compiled class and drives it through a bounded synthetic\n * series, so a wiring fault surfaces here instead of at the first backtest.\n *\n * **Idempotent, and two-outcome.** If a verdict already exists for the\n * current compilation it is returned unchanged and nothing is queued\n * (`queued: false`); otherwise a check is queued (`queued: true`) and this\n * call is *not* terminal — poll {@link QTSurfer.strategy} until `validation`\n * is `'passed'` or `'failed'`. Because a `queued: false` answer can itself carry\n * `validation: 'pending'` (a check an earlier call queued), the discriminant\n * tells you whether work was *started*, not whether a verdict *exists*;\n * `state.validation` is what tells you that.\n *\n * **Poll with a deadline of your own.** `'pending'` is not guaranteed to\n * resolve: a queued check can go unreported for far longer than one takes,\n * which the platform eventually flags as `validationStalled` on the strategy.\n * Nothing about the strategy is disproved when that happens — the check\n * simply did not run — but a caller that waits for a terminal verdict without\n * a timeout can wait forever. This SDK deliberately ships no polling helper\n * for that reason; the timeout is the caller's policy to set.\n *\n * Whatever the verdict, remember it is a floor rather than a guarantee — see\n * {@link StrategyState}.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * means no such registered strategy for this caller.\n */\nexport async function validateStrategy(strategyId: string): Promise<StrategyValidation> {\n const { data, error, response } = await apiValidateStrategy({ path: { strategyId } });\n if (error) throw requestFailed('strategy validation request', error, response?.status);\n // The two outcomes are distinguishable only by status: a `200` body is a\n // full StrategyState whose `validation` may itself be `'pending'`, so the\n // payload cannot be used to tell \"queued just now\" from \"already queued\".\n // The queued branch echoes back the caller's own id rather than reading one\n // out of the body, because an accepted-but-not-done response on this API is\n // not guaranteed to carry a body at all.\n if (response?.status === 202) return { queued: true, strategyId };\n if (!data) throw new QTSError('Empty strategy validation response');\n return { queued: false, strategyId, state: data as StrategyState };\n}\n\n/**\n * Read everything the platform records about a strategy: whether it is\n * registered at all, its validation verdict, the market data its compiled\n * class requires, and any engine notices the check raised.\n *\n * This is the endpoint to poll after {@link QTSurfer.validateStrategy}\n * returns `queued: true`, and the only place a verdict is read from.\n *\n * Check `compiledAt` against `validatedAt` before trusting a verdict: the\n * strategy may have been recompiled since the verdict was recorded, in which\n * case the verdict describes bytecode that is no longer what would run.\n * Re-request validation to get an answer about the current compilation.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response. A `404` (carried on `status`)\n * means exactly one thing — no such registered strategy for this caller. It is\n * never a stale or expired answer: registration and verdict are stored\n * durably, not cached.\n */\nexport async function getStrategy(strategyId: string): Promise<StrategyState> {\n const { data, error, response } = await apiGetStrategy({ path: { strategyId } });\n if (error) throw requestFailed('strategy lookup', error, response?.status);\n if (!data) throw new QTSError('Empty strategy response');\n return data;\n}\n\n/**\n * One entry in {@link QTSurfer.strategies}'s result: the same provenance\n * {@link QTSurfer.strategy} reports — `compiledAt`, `requiredSources` — but\n * never `validation`, which is what keeps listing cheap no matter how many\n * strategies you have registered. Check a specific strategy's verdict with\n * {@link QTSurfer.strategy}.\n *\n * Note: the spec types this endpoint's `requiredSources` as a plain\n * `string[]`, not the `'Ticker' | 'KLine' | 'FundingRate'` union that\n * {@link StrategyState}'s own `requiredSources` carries — narrow it yourself\n * if you need the literal type. Alias for api-client's `StrategySummary`.\n */\nexport type StrategySummary = ApiStrategySummary;\n\n/**\n * List every strategy you have registered and not deleted, most recently\n * compiled first.\n *\n * **Never `404`.** An empty array means you have none registered — not an\n * error. Each entry omits `validation` on purpose (see {@link\n * StrategySummary}); check a specific strategy's verdict with {@link\n * QTSurfer.strategy}.\n *\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\nexport async function listStrategies(): Promise<StrategySummary[]> {\n const { data, error, response } = await apiListStrategies();\n if (error) throw requestFailed('strategies list', error, response?.status);\n if (!data) throw new QTSError('Empty strategies response');\n return data.strategies;\n}\n\n/**\n * Release a registered strategy: removes it from both {@link\n * QTSurfer.strategy} and {@link QTSurfer.strategies}.\n *\n * **Does not undo anything already run.** Backtests you ran against this\n * strategy before deleting it are completely unaffected — deleting only\n * stops you from validating or re-running it under this id going forward.\n * Re-submitting the exact same source afterwards registers a **new**\n * strategy with a **new** id; it does not \"undelete\" this one, because\n * nothing about the id itself is restored.\n *\n * **Scoped to your own registration.** If you copied someone else's strategy\n * (a shared/marketplace listing), deleting your copy never affects theirs,\n * or anyone else's, regardless of how many callers registered the same\n * source independently.\n *\n * Resolves with nothing: the response body is `{ strategyId, deleted: true }`,\n * and both fields are things the caller already knows before calling this —\n * `strategyId` is the argument just passed in, and `deleted` is always `true`\n * on a `200`. There is nothing in it a `void` return would lose.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * means no such registered strategy for this caller.\n */\nexport async function deleteStrategy(strategyId: string): Promise<void> {\n const { error, response } = await apiDeleteStrategy({ path: { strategyId } });\n if (error) throw requestFailed('strategy delete', error, response?.status);\n}\n\n/**\n * Read back the exact source last submitted for a strategy id — the same\n * text `strategyId` was derived from, whitespace and comments included.\n *\n * **A `404` here covers two cases the response cannot tell apart:** the id\n * was never registered by you, or it resolves only through a shared/\n * marketplace reference that carries no source of its own (a strategy you\n * copied by reference rather than by resubmitting its code). Both read as\n * \"nothing to return\" from this endpoint's point of view, and the SDK does\n * not attempt to distinguish them — there is nothing in the response to tell\n * them apart with.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * is the two-case ambiguity described above.\n */\nexport async function getStrategyCode(strategyId: string): Promise<string> {\n const { data, error, response } = await apiGetStrategyCode({ path: { strategyId } });\n if (error) throw requestFailed('strategy code lookup', error, response?.status);\n if (!data) throw new QTSError('Empty strategy code response');\n return data.code;\n}\n","import {\n cancelSweep,\n executeSweep,\n getSweepResult,\n getSweepSensitivity,\n type ExecuteSweepAccepted,\n type ExecuteSweepRequest,\n type ExecuteSweepResult,\n type GetSweepResultData,\n type SweepHeatmap as ApiSweepHeatmap,\n type SweepHeatmapCell as ApiSweepHeatmapCell,\n type SweepMarginal as ApiSweepMarginal,\n type SweepMarginalPoint as ApiSweepMarginalPoint,\n type SweepProgress as ApiSweepProgress,\n type SweepRunRow as ApiSweepRunRow,\n type SweepSensitivity as ApiSweepSensitivity,\n type SweepSpecRequest,\n type WalkForwardFold as ApiWalkForwardFold,\n type WalkForwardResult as ApiWalkForwardResult,\n} from '@qtsurfer/api-client';\nimport { QTSCanceledError, QTSError, QTSExecutionError } from '../errors';\nimport {\n buildStagePolicy,\n normalizeStatus,\n runStage,\n type StagePolicy,\n} from '../internal/polling';\nimport { TICKER, compileStrategySource, prepareDataset } from '../internal/preparation';\nimport { requestFailed } from '../internal/requestError';\nimport type { BacktestStage } from './backtest';\n\n// ---------------------------------------------------------------------------\n// Vocabulary\n// ---------------------------------------------------------------------------\n\n/**\n * The metric a sweep optimizes, and the one its leaderboard and its\n * sensitivity surfaces are read against.\n *\n * One vocabulary throughout: the objective a {@link SweepRequest} is submitted\n * with is the objective the leaderboard is ranked by, and the one\n * {@link Sweep.sensitivity} aggregates unless a different one is asked for.\n *\n * - `'sharpe'` — risk-adjusted return; the platform default when a request names none.\n * - `'sortino'` — downside-risk-adjusted return.\n * - `'pnl'` — absolute net profit and loss.\n * - `'maxdd'` — maximum drawdown.\n */\nexport type SweepObjective = 'sharpe' | 'sortino' | 'pnl' | 'maxdd';\n\n/**\n * How the parameter grid is turned into the list of vectors that actually run.\n *\n * - `'grid'` — every combination of every axis value. The platform default;\n * cost is the product of the axis sizes.\n * - `'random'` — uniformly random draws from the grid, capped at\n * {@link SweepRequest.samples}.\n * - `'lhs'` — Latin hypercube draws, which spread the sample more evenly than\n * uniform random.\n *\n * `samples` is required by `'random'` and `'lhs'` and ignored by `'grid'`.\n */\nexport type SweepSampler = 'grid' | 'random' | 'lhs';\n\n/**\n * How the ranked leaderboard is ordered.\n *\n * The **platform default is `'plateau'`**, so the order a sweep answers with is\n * *not* raw objective order unless you ask for it. A plateau score is the\n * objective of the worst run in a point's immediate neighbourhood, so a point\n * only ranks well when the region around it does too — which exists because\n * the highest raw score is very often a spike that does not survive the\n * parameters moving slightly.\n *\n * **What you request is not always what you get.** Read `ranking` on the\n * result to find out which ordering was actually applied: a sweep with no\n * stored parameter grid has no neighbourhood to score against and falls back\n * to `'raw'`, and a walk-forward sweep is always `'raw'` because its\n * leaderboard is one out-of-sample row per fold rather than a grid.\n *\n * This applies to the ranked view only. Alongside `order: 'natural'` it is\n * **ignored** — that view is always ordered by `runIx`.\n */\nexport type SweepRanking = 'plateau' | 'raw';\n\n/**\n * Which view of a sweep's rows to read: the display leaderboard, or every row\n * in a stable order.\n *\n * - `'ranked'` — the platform default. Sorted, and capped at a display limit;\n * `truncated` on the result is `true` when the cap actually bit, in which\n * case rows exist that this view does not carry. This is the view\n * {@link SweepRanking} applies to.\n * - `'natural'` — every available row, untruncated, in deterministic `runIx`\n * order. The view to read when materialising durable trial rows rather than\n * showing a top-N, and the only way to reach rows the ranked view dropped —\n * {@link Sweep.results} reads it off an existing sweep without re-running it.\n * {@link SweepRanking} is **ignored** here and the response reports `'raw'`;\n * rank, plateau score and neighbour count belong to the ranked view and are\n * not part of this one.\n */\nexport type SweepOrder = 'ranked' | 'natural';\n\n/**\n * Sweep lifecycle as observed by the SDK, readable off {@link Sweep.state}.\n *\n * - `'executing'` — submitted and being polled.\n * - `'completed'` — finished. The platform's own status may still be `'PARTIAL'`.\n * - `'failed'` — the poll itself failed: transport, HTTP, or a stage timeout.\n * - `'canceled'` — the sweep was aborted and the platform reported it cancelled.\n */\nexport type SweepState = 'executing' | 'completed' | 'failed' | 'canceled';\n\n/**\n * One strategy property and the values a sweep should try for it: either a\n * numeric range walked in fixed steps, or an explicit list.\n *\n * ```ts\n * const params: Record<string, ParamAxis> = {\n * rsiPeriod: { from: 7, to: 28, step: 1 },\n * useTrendFilter: { values: [true, false] },\n * };\n * ```\n *\n * The two shapes are mutually exclusive on the wire, and mixing them is a\n * request the platform rejects rather than reconciles. A list entry is a number\n * or a boolean — the axis of a boolean flag is `{ values: [true, false] }`, not\n * a range.\n */\nexport type ParamAxis =\n | {\n /** First value. */\n from: number;\n /** Last value the walk may reach. */\n to: number;\n /** Increment; must be greater than zero. */\n step: number;\n }\n | {\n /** The values to try; at least one. */\n values: Array<number | boolean>;\n };\n\n/**\n * Opt a sweep into walk-forward validation.\n *\n * Attaching this changes what the sweep does, not just how much of it runs.\n * Instead of scoring every parameter vector once over the whole range, the data\n * is cut into sequential folds; each fold optimizes the whole grid on its own\n * window and then scores only its winner on the window immediately after — data\n * that winner was never chosen on. The question it answers is not \"which\n * parameters won\" but \"does re-optimizing this periodically actually work\".\n *\n * Omit it and nothing about the sweep changes, including the shape of the\n * response.\n *\n * **It costs folds × grid.** Four folds over a 500-point grid is roughly 2000\n * backtests where the plain sweep is 500, which is why it is opt-in. The\n * platform rejects the request outright when that product exceeds its sweep\n * budget.\n *\n * **It is a different sweep, not a variant of one.** Two requests that differ\n * only in this block do not deduplicate against each other.\n *\n * The answer arrives as `walkForward` on the {@link SweepResult} — see\n * {@link Sweep.result} for how to read it.\n */\nexport interface SweepWalkForward {\n /**\n * How many sequential optimize-then-score windows to run. Two is the floor\n * and the reason is structural rather than a tuning preference: parameter\n * drift is measured between consecutive fold winners, and a single fold has\n * no consecutive pair, so it would report the strongest possible stability\n * having measured nothing. The ceiling is a platform setting; exceeding it is\n * rejected.\n */\n folds: number;\n /**\n * Share of each fold's window spent optimizing, the rest being where its\n * winner is scored. Omit to take the platform default. Lower values leave\n * more data to score on and, on short sessions, are what let the requested\n * fold count tile the data at all. Must be within 10..90.\n */\n inSamplePct?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Platform models, re-exported so callers never have to reach for api-client\n// ---------------------------------------------------------------------------\n\n/**\n * What the platform answered when it accepted the sweep, exactly as it sent it\n * — see {@link Sweep.accepted} for the three fields that make it worth reading\n * before any result exists.\n */\nexport type SweepAccepted = ExecuteSweepAccepted;\n\n/**\n * A sweep snapshot: status, progress, and the rows available for the selected\n * view. Resolved by {@link Sweep.result}, which is also where the semantics of\n * every field are documented.\n */\nexport type SweepResult = ExecuteSweepResult;\n\n/**\n * The platform's own progress record for a running sweep, carried on\n * {@link SweepProgressEvent.snapshot}. Distinct from the event that wraps it:\n * this is what the server reported, the event is what the SDK emitted.\n *\n * Two of its fields are easy to add together by mistake. `aborted` counts\n * individual runs that executed and aborted — a row-level count. `failedShards`\n * counts whole units of work (shards, or folds on a walk-forward sweep) that\n * failed and will not be retried, having never reported anything. A shard that\n * dies before producing a single row leaves `aborted` at zero, which is exactly\n * why the second count exists; **summing them double-counts nothing and\n * describes nothing**.\n *\n * `retrying` is not a failure count either — those units failed on something\n * transient and are queued to be attempted again, so a sweep with a non-zero\n * value there is still expected to finish.\n *\n * `etaSeconds` is **omitted, never zero** when it cannot be computed: a sweep\n * with nothing finished has no observed rate to extrapolate from, and a zero\n * would read as \"about to finish\". When present it runs conservative — it\n * excludes queue wait entirely, and a sweep that spent part of its life being\n * retried will have diluted the rate it is derived from.\n */\nexport type SweepProgress = ApiSweepProgress;\n\n/** One trial on a sweep's leaderboard. See {@link Sweep.result} for how to read it. */\nexport type SweepRunRow = ApiSweepRunRow;\n\n/**\n * Sensitivity aggregates over a sweep's stored rows, returned by\n * {@link Sweep.sensitivity}. Marginals are always complete; the pair surfaces\n * may be capped, in which case `heatmapsTruncated` is `true`.\n */\nexport type SweepSensitivity = ApiSweepSensitivity;\n\n/** One axis, with every other axis collapsed away. */\nexport type SweepMarginal = ApiSweepMarginal;\n\n/** How the objective behaved at one value of one axis. */\nexport type SweepMarginalPoint = ApiSweepMarginalPoint;\n\n/** The surface for one pair of axes, with all others collapsed away. */\nexport type SweepHeatmap = ApiSweepHeatmap;\n\n/** One cell of a {@link SweepHeatmap}. */\nexport type SweepHeatmapCell = ApiSweepHeatmapCell;\n\n/**\n * The walk-forward section of a {@link SweepResult}, present exactly when the\n * sweep was submitted with {@link SweepWalkForward}.\n *\n * **`paramDrift` absent is not zero.** The field is omitted whenever the figure\n * could not be computed — fewer than two folds finished, or no stored grid to\n * place the winners on — and zero is itself a meaningful reading there (winners\n * that never moved), so a placeholder would be indistinguishable from perfect\n * stability.\n */\nexport type WalkForwardResult = ApiWalkForwardResult;\n\n/**\n * What one fold concluded. The out-of-sample row is the answer; the in-sample\n * figure is only there to be compared against it, since any grid produces a\n * flattering in-sample winner — that is what optimizing does. The gap between\n * them is the whole reading.\n */\nexport type WalkForwardFold = ApiWalkForwardFold;\n\n// ---------------------------------------------------------------------------\n// Request, options, progress\n// ---------------------------------------------------------------------------\n\n/**\n * A parameter sweep over one instrument and one window: the same strategy run\n * once per parameter vector, scored and ranked against a single objective.\n *\n * ```ts\n * const request: SweepRequest = {\n * strategy: source,\n * exchangeId: 'binance',\n * instrument: 'BTC/USDT',\n * from: '2026-01-01T00:00:00Z',\n * to: '2026-02-01T00:00:00Z',\n * params: {\n * rsiPeriod: { from: 7, to: 28, step: 1 },\n * useTrendFilter: { values: [true, false] },\n * },\n * objective: 'sharpe',\n * };\n * ```\n */\nexport interface SweepRequest {\n /** Strategy source code (Java), compiled once and reused by every trial. */\n strategy: string;\n /** Exchange id, e.g. `binance`. */\n exchangeId: string;\n /** Instrument symbol, e.g. `BTC/USDT`. */\n instrument: string;\n /** Range start (ISO-8601, ISO DATE, or BASIC ISO DATE). */\n from: string;\n /** Range end (same formats as `from`; must be later than `from`). */\n to: string;\n /** The grid: one {@link ParamAxis} per strategy property to vary. At least one. */\n params: Record<string, ParamAxis>;\n /**\n * How the grid becomes the list of vectors actually run. Omit to keep the\n * platform default, the full cross product.\n */\n sampler?: SweepSampler;\n /**\n * How many vectors to draw for `'random'` and `'lhs'`; ignored by `'grid'`.\n */\n samples?: number;\n /**\n * Reproducibility seed. Omit to let the platform generate one and report it\n * back on {@link Sweep.accepted}, so a randomly sampled sweep can be replayed\n * exactly by submitting the same seed again.\n */\n seed?: number;\n /**\n * The metric to optimize and rank by; omit to keep the platform default\n * (`'sharpe'`). It is also what {@link Sweep.sensitivity} aggregates unless\n * told otherwise.\n */\n objective?: SweepObjective;\n /**\n * Opt into walk-forward validation, which changes both what runs and the\n * shape of the answer. Omit to run an ordinary sweep.\n */\n walkForward?: SweepWalkForward;\n}\n\n/**\n * Emitted at stage transitions and after each poll of a running sweep.\n *\n * The `snapshot` is where the detail lives — see {@link SweepProgress}, whose\n * counts measure different things and must not be added together.\n */\nexport interface SweepProgressEvent {\n /** Current workflow stage. */\n stage: BacktestStage;\n /**\n * 0-100, computed from the runs finished out of the runs expected (`done` /\n * `total` on the snapshot, both run-level counts — not the shard counts,\n * which partition units of work rather than runs). Absent on stage\n * transitions before the first poll.\n */\n percent?: number;\n /**\n * Fraction (0-1) of the requested window that actually holds data, as\n * reported once preparation completes. Present only on the final `preparing`\n * event. Worth reading on a sweep in particular: a thinly covered window is\n * about to be scored once per parameter vector.\n */\n coverageRatio?: number;\n /**\n * The platform's progress record for the sweep. Present only on `executing`\n * events.\n */\n snapshot?: SweepProgress;\n}\n\n/** Tuning knobs for a {@link QTSurfer.sweep} invocation. */\nexport interface SweepOptions {\n /**\n * Ask the platform to stop the sweep between parameter vectors.\n *\n * **Aborting does not reject {@link Sweep.result}** — it resolves with\n * whatever was scored before the stop. That is a deliberate divergence from\n * `backtest()`, which rejects with `QTSCanceledError` when its run is\n * aborted; see {@link Sweep.result} for why. Aborting *before* the platform\n * has accepted the sweep — during compile, prepare or submission — rejects\n * the {@link QTSurfer.sweep} call itself with `QTSCanceledError`, since there\n * is no sweep yet and so no rows to keep.\n */\n signal?: AbortSignal;\n /** Called on stage transitions and after each poll with updated progress. */\n onProgress?: (p: SweepProgressEvent) => void;\n /**\n * Initial interval between polls. Default 2000ms, backed off up to\n * `maxPollIntervalMs`. Longer than a single backtest's, because a sweep is\n * many backtests and its leaderboard changes on the timescale of shards\n * finishing rather than ticks.\n */\n pollIntervalMs?: number;\n /** Upper bound for exponential backoff. Default 15000ms. */\n maxPollIntervalMs?: number;\n /**\n * Per-stage timeout. Default none. A sweep is many backtests, so the execute\n * stage legitimately outlasts anything a single run would take.\n */\n timeoutMs?: number;\n /**\n * How to order the leaderboard the poll reads. Omit to send no preference and\n * take the platform default, which is `'plateau'` — so leaving this unset\n * does **not** give you raw objective order. What was actually applied is\n * reported on the result. **Ignored entirely when `order` is `'natural'`**,\n * which is always ordered by `runIx`; the platform accepts both and answers\n * with the ordering it applied.\n */\n ranking?: SweepRanking;\n /**\n * Which view of the rows the background poll reads. Omit to take the platform\n * default, `'ranked'` — the sorted, display-capped leaderboard. `'natural'`\n * returns every available row untruncated.\n *\n * This only decides what {@link Sweep.result} resolves with. Reading the same\n * sweep another way afterwards — to reach the rows a `truncated` ranked view\n * dropped, say — is {@link Sweep.results}, which re-reads rather than\n * re-running.\n */\n order?: SweepOrder;\n}\n\n// ---------------------------------------------------------------------------\n// Handle\n// ---------------------------------------------------------------------------\n\n/**\n * Handle for a running parameter sweep, returned by {@link QTSurfer.sweep} once\n * the platform has accepted it. The leaderboard keeps being polled in the\n * background.\n */\nexport interface Sweep {\n /** Server-side sweep identifier. */\n readonly sweepId: string;\n /**\n * The prepared dataset every trial ran against — the prepare jobId the\n * workflow resolved before submitting, which is also what addresses this\n * sweep on the wire.\n *\n * This is the value the workflow prepared with, not the acceptance echo of\n * it. {@link Sweep.accepted} carries the echo, unmodified, for anyone who\n * wants to compare the two.\n */\n readonly requestId: string;\n /** The compiled strategy every trial shares. */\n readonly strategyId: string;\n /**\n * What acceptance already answered, before a single trial has run, exactly as\n * the platform sent it.\n *\n * Three of its fields are the reason this is exposed rather than folded away:\n *\n * - `seed` — the effective seed, generated platform-side when the request\n * omitted one. Submitting it again is what makes a randomly sampled sweep\n * replayable.\n * - `queued` — `false` means an identical sweep already existed and nothing\n * new was enqueued. The handle is still valid and still resolves; it is\n * just reading a sweep this call did not start.\n * - `walkForward` — present exactly when this is a walk-forward sweep. It is\n * the discriminator, and it is available here immediately, so code watching\n * progress can branch on the answer's shape without waiting for\n * {@link Sweep.result}.\n */\n readonly accepted: SweepAccepted;\n /**\n * Local snapshot of the sweep lifecycle; reading it does not contact the\n * server.\n */\n readonly state: SweepState;\n /**\n * Resolves with the final leaderboard once the sweep stops advancing.\n *\n * **This resolves on every terminal status, cancellation included** —\n * `'COMPLETED'`, `'PARTIAL'` and `'CANCELLED'` all hand back the result\n * rather than raising. That is a deliberate divergence from `backtest()`,\n * which rejects with `QTSCanceledError` when its run is aborted: cancelling a\n * sweep is documented as leaving completed rows readable, and throwing them\n * away would lose the only reason to cancel a sweep late rather than early.\n * Read `status` to find out which of the three you got. The promise rejects\n * only for transport failures, HTTP errors, and stage timeouts.\n *\n * `'PARTIAL'` means at least one unit of work died and its runs are simply\n * missing. There is no failed status for a sweep as a whole, so a sweep whose\n * every shard died is `'PARTIAL'` with an empty leaderboard — check\n * `leaderboardSize` before reading anything into a top row.\n *\n * ### Reading the leaderboard\n *\n * **The default order is not the raw objective order.** It is plateau order,\n * and `ranking` on the result says which was actually applied — not always\n * the one requested, because a sweep with no stored parameter grid cannot be\n * plateau-ranked and falls back to raw. See {@link SweepRanking}.\n *\n * **The default view is capped.** When `truncated` is `true`, rows exist that\n * the leaderboard does not carry — `leaderboardSize` counts what is\n * available. {@link Sweep.results} with `order: 'natural'` is what returns\n * all of them, in `runIx` order and with no ranking applied. That is a\n * re-read of this same sweep, not a second one.\n *\n * **`plateauScore` and `neighbourCount` are read together.** A neighbour\n * count of `0` means the point had no neighbours in the grid to compare\n * against, so its plateau score is unevidenced rather than confirmed — on its\n * own it is indistinguishable from a genuinely robust one.\n *\n * **`deflatedSharpe`** is the probability that a row's Sharpe reflects real\n * edge rather than the best draw from however many vectors were tried. Around\n * 0.95 and up it survives the multiple-testing correction; near 0.5 or below\n * it is not distinguishable from the best of a pile of coin flips. It is\n * absent on aborted runs, and on sweeps with too few trials to establish any\n * dispersion to deflate against.\n *\n * **`pbo`** is the probability of backtest overfitting for the sweep as a\n * whole: how often the configuration that won in-sample lands below median\n * out-of-sample. Above roughly 0.5 the sweep is selecting noise, and that\n * verdict is about the search, not about any one row — a high value\n * discredits the top row however good it looks. It is computed once the last\n * unit of work finishes, so it is absent while the sweep is still running and\n * on sweeps too small for the statistic to mean anything.\n *\n * ### A walk-forward sweep answers in a different shape\n *\n * `walkForward` is the discriminator, and it appears as soon as the sweep is\n * accepted — before any fold has finished — so it is safe to branch on while\n * polling (it is also on {@link Sweep.accepted}). When it is present, the\n * leaderboard is one row per *completed fold*: that fold's winner as it\n * scored out-of-sample, with **`runIx` carrying the fold index rather than a\n * position in the grid**. No plateau score, deflated Sharpe or PBO figure is\n * reported for one — the out-of-sample numbers are already the honest\n * measurement. See {@link WalkForwardResult} for why an absent `paramDrift`\n * is not a zero.\n *\n * ### An empty leaderboard is not always an empty answer\n *\n * A sweep can finish having scored nothing, because every shard failed before\n * producing a row. When that happens `failReason` carries the cause reported\n * by the *first* shard to fail — typically something the whole grid would have\n * hit, such as a strategy that could not be loaded. Read it before concluding\n * that a sweep with no rows simply found nothing: those are different\n * outcomes and the leaderboard alone cannot tell them apart. Only the first\n * failure is recorded, so where several shards failed for different reasons\n * this names one of them rather than summarising all — pair it with\n * `progress.failedShards` for the count.\n */\n readonly result: Promise<SweepResult>;\n /**\n * Re-read this sweep's rows under a different view.\n *\n * **This is a read, not a re-run.** It compiles nothing, prepares nothing and\n * submits nothing: the same sweep is asked for its rows again with different\n * query parameters, so no second sweep is created and nothing is enqueued.\n * The view a {@link SweepOptions} chose applies to the background poll behind\n * {@link Sweep.result}; this is how to look at the same sweep another way\n * afterwards.\n *\n * **It is the route to rows the ranked view dropped.** When `truncated` is\n * `true` on a result, rows exist that the leaderboard does not carry;\n * `order: 'natural'` returns every available row untruncated, in\n * deterministic `runIx` order.\n *\n * **`ranking` is ignored when `order` is `'natural'`** — that view is always\n * ordered by `runIx`, and the response reports `'raw'`. The platform accepts\n * both rather than rejecting the pair, and answers with the ordering it\n * actually applied.\n *\n * Readable while the sweep is still running, in which case it returns the\n * rows finished so far — exactly like {@link Sweep.sensitivity}. Like every\n * handle-scoped call, it does not take part in an\n * {@link AuthenticatedClient}'s refresh-on-401 policy.\n *\n * @param view which view to read; an absent property takes the platform\n * default (`order: 'ranked'`, `ranking: 'plateau'`)\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\n results(view?: { order?: SweepOrder; ranking?: SweepRanking }): Promise<SweepResult>;\n /**\n * How the objective moves as each parameter moves — the question a\n * leaderboard cannot answer. A leaderboard says which point won; a sweep can\n * spend its whole budget on an axis that never moved the objective at all,\n * and the top rows hide that completely.\n *\n * A *marginal* takes one axis and collapses every other one: for each value\n * of that axis it aggregates every run that used it, whatever the rest of the\n * parameters were. A flat marginal means the axis did not matter over the\n * range swept. `best`, `mean` and `worst` are all reported because them\n * disagreeing is the signal — a value with a high best and a poor mean only\n * works in specific company, which is an interaction, and a single number\n * would hide it. A *heatmap* does the same over a pair of axes, where that\n * interaction is visible directly.\n *\n * **Check `heatmapsTruncated`.** Marginals are always complete; the pair\n * surfaces are quadratic in the axis count and may be capped to stay inside\n * the response budget. When the flag is `true`, at least one pair was left\n * out, so the list you have is not the full set of interactions. This method\n * hands back the whole {@link SweepSensitivity} rather than just its surfaces\n * precisely so that flag cannot be lost on the way out.\n *\n * Readable while the sweep is still running, in which case the aggregates\n * describe the runs finished so far and `rowsAnalysed` says how many that\n * was. Aborted runs are excluded throughout: a run that threw measured\n * nothing, and counting it as a bad outcome would invent evidence against a\n * parameter value that was never really tested.\n *\n * Like every handle-scoped call, this does not take part in an\n * {@link AuthenticatedClient}'s refresh-on-401 policy.\n *\n * @param objective which metric to aggregate; omit to use the objective the\n * sweep was submitted with\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\n sensitivity(objective?: SweepObjective): Promise<SweepSensitivity>;\n}\n\n// ---------------------------------------------------------------------------\n// Workflow\n// ---------------------------------------------------------------------------\n\n/** Initial interval between polls of a sweep's leaderboard. */\nconst DEFAULT_POLL_INTERVAL_MS = 2000;\n/** Backoff ceiling for a sweep's leaderboard poll. */\nconst DEFAULT_MAX_POLL_INTERVAL_MS = 15000;\n\n/**\n * Orchestrate compile → prepare → executeSweep, then poll the leaderboard until\n * the sweep stops advancing.\n *\n * One call, not composable stages, and deliberately so: the execute-sweep\n * endpoint is addressed by the id of an already-prepared dataset, so a\n * stage-level API would hand dataset lifecycle to the caller for no gain.\n * Preparing is idempotent — the same instrument and window always resolve to\n * the same job — so preparing on every sweep duplicates no work.\n */\nexport async function sweep(req: SweepRequest, opts: SweepOptions = {}): Promise<Sweep> {\n validateRequest(req);\n const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS, DEFAULT_MAX_POLL_INTERVAL_MS);\n\n opts.onProgress?.({ stage: 'compiling' });\n const strategyId = await compileStrategySource(req.strategy, opts.signal);\n\n opts.onProgress?.({ stage: 'preparing' });\n const requestId = await prepareData(req, policy, opts);\n\n opts.onProgress?.({ stage: 'executing' });\n const { data, error } = await executeSweep({\n path: { exchangeId: req.exchangeId, type: TICKER, requestId },\n body: buildSweepBody(req, strategyId),\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSExecutionError('Sweep submission failed', error);\n if (!data?.sweepId) throw new QTSExecutionError('Missing sweepId in executeSweep response');\n\n // The handle addresses the sweep with the requestId of the dataset just prepared — the value\n // this workflow already knows — rather than the acceptance echo of it. `data` is stored as the\n // server sent it; nothing is written back onto a generated response model.\n return createHandle(req, opts, policy, data, requestId, strategyId);\n}\n\n/**\n * Reject the request shapes the platform would only reject over the network.\n * Same set as the sibling Java SDK validates, so both answer the same question\n * the same way.\n */\nfunction validateRequest(req: SweepRequest): void {\n const names = Object.keys(req.params ?? {});\n if (names.length === 0) {\n throw new QTSError('sweep: params must hold at least one axis');\n }\n for (const name of names) {\n const axis = req.params[name];\n if ('values' in axis) {\n if (axis.values.length === 0) {\n throw new QTSError(`sweep: axis \"${name}\" must hold at least one value`);\n }\n } else if (!(axis.step > 0)) {\n throw new QTSError(`sweep: axis \"${name}\" needs step > 0, got ${axis.step}`);\n }\n }\n\n const wf = req.walkForward;\n if (!wf) return;\n if (wf.folds < 2) {\n throw new QTSError(`sweep: walkForward.folds must be >= 2, got ${wf.folds}`);\n }\n if (wf.inSamplePct !== undefined && (wf.inSamplePct < 10 || wf.inSamplePct > 90)) {\n throw new QTSError(\n `sweep: walkForward.inSamplePct must be within 10..90, got ${wf.inSamplePct}`,\n );\n }\n}\n\nfunction prepareData(\n req: SweepRequest,\n policy: StagePolicy,\n opts: SweepOptions,\n): Promise<string> {\n return prepareDataset(\n {\n exchangeId: req.exchangeId,\n instrument: req.instrument,\n from: req.from,\n to: req.to,\n },\n policy,\n {\n ...(opts.signal ? { signal: opts.signal } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n onPercent: (percent) => opts.onProgress?.({ stage: 'preparing', percent }),\n // A thinly covered window is about to be scored once per parameter vector, so the\n // coverage ratio is worth at least as much here as on a single backtest.\n onPrepared: (state) =>\n opts.onProgress?.({\n stage: 'preparing',\n percent: 100,\n coverageRatio: state.coverageRatio,\n }),\n },\n );\n}\n\nfunction buildSweepBody(req: SweepRequest, strategyId: string): ExecuteSweepRequest {\n const spec: SweepSpecRequest = { params: req.params };\n if (req.sampler !== undefined) spec.sampler = req.sampler;\n if (req.samples !== undefined) spec.samples = req.samples;\n if (req.seed !== undefined) spec.seed = req.seed;\n if (req.objective !== undefined) spec.objective = req.objective;\n\n const body: ExecuteSweepRequest = { strategyId, sweep: spec };\n if (req.walkForward) {\n body.walkForward = {\n folds: req.walkForward.folds,\n ...(req.walkForward.inSamplePct !== undefined\n ? { inSamplePct: req.walkForward.inSamplePct }\n : {}),\n };\n }\n return body;\n}\n\nfunction createHandle(\n req: SweepRequest,\n opts: SweepOptions,\n policy: StagePolicy,\n accepted: SweepAccepted,\n requestId: string,\n strategyId: string,\n): Sweep {\n const sweepId = accepted.sweepId;\n const path = { exchangeId: req.exchangeId, type: TICKER, requestId, sweepId };\n let state: SweepState = 'executing';\n\n const withQuery = viewQuery(opts);\n\n const result = (async () => {\n try {\n // Deliberately runs without `opts.signal`: aborting a sweep must not stop the poll, because\n // the rows already scored are only reachable by polling on until the platform reports the\n // sweep CANCELLED. The abort listener below asks the platform to stop instead.\n const finalResult = await runStage(\n policy,\n async ({ signal }) => {\n const res = await getSweepResult({ path, ...withQuery, signal });\n if (res.error) throw new QTSExecutionError('Sweep result request failed', res.error);\n if (!res.data) throw new QTSExecutionError('Empty sweep result response');\n return res.data;\n },\n {\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n onEachAttempt: (r) => {\n const percent = percentOf(r);\n opts.onProgress?.({\n stage: 'executing',\n ...(percent !== undefined ? { percent } : {}),\n snapshot: r.progress,\n });\n },\n },\n );\n state = normalizeStatus(finalResult.status) === 'aborted' ? 'canceled' : 'completed';\n return finalResult;\n } catch (err) {\n state = err instanceof QTSCanceledError ? 'canceled' : 'failed';\n throw err;\n }\n })();\n // A handle whose result is never awaited must not take the process down with an unhandled\n // rejection; the caller still sees the failure on their own `await`.\n void result.catch(() => undefined);\n\n const { signal } = opts;\n if (signal) {\n const requestCancel = (): void => {\n // Only ever 'executing' → 'canceled'; a cancel that lands after the last unit of work has\n // finished changes nothing, and the poll's own completion settles the state either way.\n if (state === 'executing') state = 'canceled';\n void cancelSweep({ path }).catch(() => undefined);\n };\n if (signal.aborted) {\n requestCancel();\n } else {\n signal.addEventListener('abort', requestCancel, { once: true });\n const detach = (): void => signal.removeEventListener('abort', requestCancel);\n void result.then(detach, detach);\n }\n }\n\n return {\n sweepId,\n requestId,\n strategyId,\n accepted,\n get state() {\n return state;\n },\n result,\n results: async (view = {}): Promise<SweepResult> => {\n // A read of the sweep that already exists: same path, different query. Nothing here\n // compiles, prepares or submits, so asking for the natural view costs one request rather\n // than a second pipeline.\n const res = await getSweepResult({ path, ...viewQuery(view) });\n if (res.error) {\n throw requestFailed('sweep results call', res.error, res.response?.status);\n }\n if (!res.data) throw new QTSError('Empty sweep result response');\n return res.data;\n },\n sensitivity: async (objective?: SweepObjective): Promise<SweepSensitivity> => {\n const res = await getSweepSensitivity({\n path,\n ...(objective !== undefined ? { query: { objective } } : {}),\n });\n if (res.error) {\n throw requestFailed('sweep sensitivity call', res.error, res.response?.status);\n }\n if (!res.data) throw new QTSError('Empty sweep sensitivity response');\n return res.data;\n },\n };\n}\n\n/**\n * The `order` / `ranking` query for one view of a sweep's rows.\n *\n * `ranking` is sent as asked even alongside `order: 'natural'`, which ignores\n * it: the platform accepts both and answers with the ordering it actually\n * applied, which is more informative than the SDK silently dropping the\n * preference. Left off entirely when neither is set, so a default read carries\n * no query string and takes the platform's own defaults.\n */\nfunction viewQuery(view: { order?: SweepOrder; ranking?: SweepRanking }): {\n query?: NonNullable<GetSweepResultData['query']>;\n} {\n const query: NonNullable<GetSweepResultData['query']> = {};\n if (view.order !== undefined) query.order = view.order;\n if (view.ranking !== undefined) query.ranking = view.ranking;\n return Object.keys(query).length > 0 ? { query } : {};\n}\n\n/**\n * Percentage of the sweep's runs that have finished. Deliberately computed from\n * the run-level counts: the shard counts alongside them partition units of\n * work, not runs, and mixing the two reports a percentage of neither.\n */\nfunction percentOf(result: SweepResult): number | undefined {\n const p = result.progress;\n if (!p || !(p.total > 0)) return undefined;\n return (p.done / p.total) * 100;\n}\n","import {\n authenticate as apiAuth,\n client as apiClient,\n type AuthTokenResponse,\n} from '@qtsurfer/api-client';\nimport { QTSAuthError } from '../errors';\nimport {\n backtest as runBacktest,\n type BacktestOptions,\n type BacktestRequest,\n type BacktestResult,\n} from '../workflows/backtest';\nimport {\n listExchanges,\n listInstruments,\n type Exchange,\n type InstrumentDetail,\n type InstrumentSegment,\n} from '../workflows/catalog';\nimport {\n downloadKlines,\n downloadTickers,\n} from '../workflows/downloads';\nimport {\n getStrategy,\n validateStrategy as runValidateStrategy,\n listStrategies,\n deleteStrategy as runDeleteStrategy,\n getStrategyCode,\n type StrategyState,\n type StrategyValidation,\n type StrategySummary,\n} from '../workflows/strategies';\nimport {\n sweep as runSweep,\n type Sweep,\n type SweepOptions,\n type SweepRequest,\n} from '../workflows/sweep';\nimport type { DownloadHourArgs } from '../client';\nimport { InMemoryTokenStore, type TokenStore } from './tokenStore';\n\nconst APIKEY_ENV_VAR = 'QTSURFER_APIKEY';\nconst DEFAULT_BASE_URL = 'https://api.qtsurfer.com/v1';\n\nexport interface AuthOptions {\n /** Base URL of the QTSurfer API. Defaults to the public production endpoint. */\n baseUrl?: string;\n /** Custom token store. Defaults to {@link InMemoryTokenStore}. */\n store?: TokenStore;\n /** Inject a custom `fetch` (Node 20+, browser, or test mock). */\n fetch?: typeof fetch;\n}\n\n/**\n * Authenticated SDK session.\n *\n * Returned by {@link authenticate}. Wraps the underlying api-client, owns a JWT\n * (in memory by default, or in the provided {@link TokenStore}), and\n * transparently re-exchanges the apikey for a fresh JWT on 401.\n *\n * Multi-session note: the session mutates the api-client singleton config\n * on every call. Concurrent sessions in the same process will race; today\n * the SDK targets the one-session-per-process pattern.\n */\nexport class AuthenticatedClient {\n readonly baseUrl: string;\n private readonly apikey: string;\n private readonly store: TokenStore;\n private readonly fetchImpl: typeof fetch | undefined;\n private cached: AuthTokenResponse | null = null;\n private refreshing: Promise<AuthTokenResponse> | null = null;\n\n constructor(apikey: string, opts: AuthOptions = {}) {\n this.apikey = apikey;\n this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;\n this.store = opts.store ?? new InMemoryTokenStore();\n this.fetchImpl = opts.fetch;\n }\n\n /** Currently cached token, if any. */\n get token(): AuthTokenResponse | null {\n return this.cached;\n }\n\n /** Force a fresh JWT exchange. Bypasses the cache. */\n async refresh(): Promise<AuthTokenResponse> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = (async () => {\n const { data, error, response } = await apiAuth({\n baseUrl: this.baseUrl,\n headers: { 'X-API-Key': this.apikey },\n ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),\n });\n if (error || !data) {\n throw new QTSAuthError(\n `authenticate() failed: HTTP ${response.status}`,\n error,\n );\n }\n this.cached = data;\n await this.store.save(data);\n return data;\n })();\n try {\n return await this.refreshing;\n } finally {\n this.refreshing = null;\n }\n }\n\n /**\n * Load a previously-persisted token from the store. If none, mint one.\n * Called automatically by every workflow method.\n */\n async ensureToken(): Promise<AuthTokenResponse> {\n if (this.cached) return this.cached;\n const stored = await this.store.load();\n if (stored) {\n this.cached = stored;\n return stored;\n }\n return this.refresh();\n }\n\n /** Drop the cached token (in memory and in the store). */\n async clear(): Promise<void> {\n this.cached = null;\n await this.store.clear();\n }\n\n /**\n * Run a call with the Bearer header pre-set; if it returns 401, refresh\n * once and retry. A second 401 surfaces to the caller.\n */\n private async withRefreshOn401<T>(call: () => Promise<T>): Promise<T> {\n await this.applyConfig();\n try {\n return await call();\n } catch (err) {\n if (!isUnauthorized(err)) throw err;\n this.cached = null;\n await this.refresh();\n await this.applyConfig();\n return call();\n }\n }\n\n private async applyConfig(): Promise<void> {\n const token = await this.ensureToken();\n apiClient.setConfig({\n baseUrl: this.baseUrl,\n headers: { Authorization: `Bearer ${token.access_token}` },\n ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),\n });\n }\n\n // ---- Workflow surface (mirrors QTSurfer) ----\n\n /**\n * Run a backtest end-to-end (compile → prepare → execute), sending the\n * currently cached token (minting one first if none is cached). Unlike\n * `tickers()`/`klines()`, a `401` here is not auto-retried: the underlying\n * stage errors carry no HTTP status, so a token that expires mid-backtest\n * surfaces as `QTSPreparationError`/`QTSExecutionError` rather than\n * triggering a refresh.\n */\n backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return this.withRefreshOn401(() => runBacktest(req, opts));\n }\n\n /**\n * Run the full compile → prepare → executeSweep pipeline and resolve once the\n * platform has accepted the sweep, handing back a {@link Sweep} that keeps\n * polling the leaderboard in the background. See {@link QTSurfer.sweep} for\n * why the sweep is one call rather than composable stages, and\n * {@link Sweep.result} for how to read what it found.\n *\n * The currently cached token is sent (minting one first if none is cached),\n * but as with `backtest()` a `401` here is **not** auto-retried: the\n * underlying stage errors carry no HTTP status, so a token that expires\n * mid-pipeline surfaces as `QTSPreparationError`/`QTSExecutionError` rather\n * than triggering a refresh.\n *\n * The background leaderboard poll and {@link Sweep.sensitivity} sit outside\n * the policy for a second, independent reason: both run after this promise\n * has already resolved, so a token that expires while a sweep is in flight\n * surfaces on {@link Sweep.result} whatever the stage errors carry.\n */\n sweep(req: SweepRequest, opts?: SweepOptions): Promise<Sweep> {\n return this.withRefreshOn401(() => runSweep(req, opts));\n }\n\n /** Download one hour of raw tickers. Refreshes the token once on `401` before retrying. */\n tickers(args: DownloadHourArgs): Promise<Blob> {\n return this.withRefreshOn401(() => downloadTickers(args));\n }\n\n /** Download one hour of klines. Refreshes the token once on `401` before retrying. */\n klines(args: DownloadHourArgs): Promise<Blob> {\n return this.withRefreshOn401(() => downloadKlines(args));\n }\n\n /**\n * List the exchanges the platform serves. Refreshes the token once on `401`\n * before retrying.\n */\n exchanges(): Promise<Exchange[]> {\n return this.withRefreshOn401(() => listExchanges());\n }\n\n /**\n * List an exchange's instruments, optionally for a specific segment.\n * Refreshes the token once on `401` before retrying. See\n * {@link QTSurfer.instruments} for what the unwrapped HAL envelope leaves\n * out.\n */\n instruments(\n exchangeId: string,\n segment?: InstrumentSegment,\n ): Promise<InstrumentDetail[]> {\n return this.withRefreshOn401(() => listInstruments(exchangeId, segment));\n }\n\n /**\n * Ask the platform to check that a registered strategy can actually run.\n * Refreshes the token once on `401` before retrying. Two-outcome — see\n * {@link QTSurfer.validateStrategy}; `queued: true` is not terminal and\n * must be followed by polling {@link AuthenticatedClient.strategy} under a\n * deadline of your own.\n */\n validateStrategy(strategyId: string): Promise<StrategyValidation> {\n return this.withRefreshOn401(() => runValidateStrategy(strategyId));\n }\n\n /**\n * Read a strategy's recorded state, including its validation verdict.\n * Refreshes the token once on `401` before retrying. See\n * {@link StrategyState} for why a `'passed'` verdict is a floor rather than\n * a guarantee.\n */\n strategy(strategyId: string): Promise<StrategyState> {\n return this.withRefreshOn401(() => getStrategy(strategyId));\n }\n\n /**\n * List every strategy you have registered and not deleted, most recently\n * compiled first. Refreshes the token once on `401` before retrying. See\n * {@link QTSurfer.strategies}.\n */\n strategies(): Promise<StrategySummary[]> {\n return this.withRefreshOn401(() => listStrategies());\n }\n\n /**\n * Release a registered strategy. Refreshes the token once on `401` before\n * retrying. See {@link QTSurfer.deleteStrategy} for what this does and\n * does not undo.\n */\n deleteStrategy(strategyId: string): Promise<void> {\n return this.withRefreshOn401(() => runDeleteStrategy(strategyId));\n }\n\n /**\n * Read back a strategy's exact registered source. Refreshes the token\n * once on `401` before retrying. See {@link QTSurfer.strategyCode} for\n * what its `404` covers.\n */\n strategyCode(strategyId: string): Promise<string> {\n return this.withRefreshOn401(() => getStrategyCode(strategyId));\n }\n}\n\n/**\n * Exchange a long-lived API key for an authenticated session.\n *\n * If `apikey` is omitted, the SDK reads `QTSURFER_APIKEY` from the\n * environment. The returned {@link AuthenticatedClient} caches the JWT,\n * refreshes it on 401, and exposes the same surface as `QTSurfer`\n * (`backtest`, `sweep`, `tickers`, `klines`, `exchanges`, `instruments`,\n * `validateStrategy`, `strategy`, `strategies`, `deleteStrategy`,\n * `strategyCode`).\n *\n * @throws {QTSAuthError} if no apikey is supplied or available in env.\n */\nexport async function authenticate(\n apikey?: string,\n opts: AuthOptions = {},\n): Promise<AuthenticatedClient> {\n const resolved = apikey ?? readEnvApikey();\n if (!resolved) {\n throw new QTSAuthError(\n `authenticate() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`,\n );\n }\n const session = new AuthenticatedClient(resolved, opts);\n await session.ensureToken();\n return session;\n}\n\nfunction readEnvApikey(): string | undefined {\n // `process` is undefined in browser bundlers; guard explicitly.\n if (typeof process === 'undefined' || !process.env) return undefined;\n const value = process.env[APIKEY_ENV_VAR];\n return value && value.length > 0 ? value : undefined;\n}\n\nfunction isUnauthorized(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n // SDK-thrown errors (QTSDownloadError, etc.) carry the HTTP status on\n // a top-level `status` field. Workflow errors that don't yet expose\n // status default to non-401.\n const maybeStatus = (err as { status?: unknown }).status;\n if (typeof maybeStatus === 'number' && maybeStatus === 401) return true;\n return false;\n}\n","import type { AuthTokenResponse } from '@qtsurfer/api-client';\n\n/**\n * Pluggable token persistence interface.\n *\n * The SDK ships an {@link InMemoryTokenStore} by default. Adopters can\n * implement this contract to back tokens by browser `localStorage`, an\n * on-disk file, a secret manager, etc.\n *\n * The SDK calls {@link load} once per session-startup to seed a cached\n * token (if any), {@link save} after every successful `authenticate()` / refresh,\n * and {@link clear} when the session is explicitly invalidated.\n */\nexport interface TokenStore {\n /** Return the previously persisted token, or `null` if none. */\n load(): AuthTokenResponse | null | Promise<AuthTokenResponse | null>;\n /** Persist the token returned by `POST /v1/auth/token`. */\n save(token: AuthTokenResponse): void | Promise<void>;\n /** Drop any persisted token. */\n clear(): void | Promise<void>;\n}\n\n/**\n * Default {@link TokenStore} — holds the token in a single in-memory slot.\n * Lost on process exit. Sufficient for short-lived scripts and tests.\n */\nexport class InMemoryTokenStore implements TokenStore {\n private token: AuthTokenResponse | null = null;\n\n load(): AuthTokenResponse | null {\n return this.token;\n }\n\n save(token: AuthTokenResponse): void {\n this.token = token;\n }\n\n clear(): void {\n this.token = null;\n }\n}\n"],"mappings":"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","names":["describe","DEFAULT_POLL_INTERVAL_MS","DEFAULT_MAX_POLL_INTERVAL_MS","prepareData","signal","apiClient","apiClient"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/workflows/backtest.ts","../src/errors.ts","../src/internal/polling.ts","../src/internal/preparation.ts","../src/workflows/catalog.ts","../src/internal/requestError.ts","../src/workflows/downloads.ts","../src/workflows/strategies.ts","../src/workflows/sweep.ts","../src/auth/session.ts","../src/auth/tokenStore.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 listExchanges,\n listInstruments,\n type Exchange,\n type InstrumentDetail,\n type InstrumentSegment,\n} from './workflows/catalog';\nimport {\n downloadKlines,\n downloadTickers,\n type DownloadFormat,\n} from './workflows/downloads';\nimport {\n getStrategy,\n validateStrategy as runValidateStrategy,\n listStrategies,\n deleteStrategy as runDeleteStrategy,\n getStrategyCode,\n type StrategyState,\n type StrategyValidation,\n type StrategySummary,\n} from './workflows/strategies';\nimport {\n sweep as runSweep,\n type Sweep,\n type SweepOptions,\n type SweepRequest,\n} from './workflows/sweep';\n\n/** Configuration for {@link QTSurfer}. */\nexport interface QTSurferOptions {\n /** Base URL of the QTSurfer API, e.g. `https://api.qtsurfer.com/v1`. */\n baseUrl: string;\n /**\n * Pre-obtained bearer token. When omitted, requests go out unauthenticated.\n * Use the `authenticate()` helper instead of this constructor if you want\n * the SDK to exchange an apikey for a JWT and refresh it on `401` for you.\n */\n token?: string;\n /** Inject a custom `fetch` (Node 20+, browser, or test mock). */\n fetch?: typeof fetch;\n}\n\n/** Selects one hour of tickers or klines for a single instrument. */\nexport interface DownloadHourArgs {\n /** Exchange id, e.g. `binance`. */\n exchangeId: string;\n /** Base asset of the instrument, e.g. `BTC`. */\n base: string;\n /** Quote asset of the instrument, e.g. `USDT`. */\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\n/**\n * Thin, stateless wrapper over `@qtsurfer/api-client` that exposes the SDK's\n * workflow methods (`backtest`, `sweep`, `tickers`, `klines`), the platform catalog\n * (`exchanges`, `instruments`) and the strategy surface (`validateStrategy`,\n * `strategy`, `strategies`, `deleteStrategy`, `strategyCode`). Constructing an\n * instance reconfigures the underlying api-client singleton, so avoid\n * holding two `QTSurfer`s with different `baseUrl`s or tokens alive in the\n * same process — they will race. Prefer the `authenticate()` helper over\n * this constructor unless you already manage the JWT lifecycle yourself.\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 /**\n * Run a backtest end-to-end: compile the strategy, prepare the requested\n * data range, execute it, and resolve with the result once execution\n * completes. See the underlying `backtest` workflow for the\n * stage-by-stage error and retry semantics.\n */\n backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return backtest(req, opts);\n }\n\n /**\n * Run the full compile → prepare → executeSweep pipeline and resolve once the\n * platform has accepted the sweep, handing back a {@link Sweep} that keeps\n * polling the leaderboard in the background.\n *\n * The whole sweep is one call because the execute-sweep endpoint is addressed\n * by the id of an already-prepared dataset: exposing the stages separately\n * would hand dataset lifecycle to the caller and buy nothing. Preparing is\n * idempotent, so sweeping the same window twice prepares it once.\n *\n * The returned promise rejects with {@link QTSStrategyCompileError} if\n * compilation fails, {@link QTSPreparationError} if data preparation fails,\n * {@link QTSExecutionError} if the platform rejects the sweep — an expanded\n * grid over the server limit, or a walk-forward request whose fold count\n * multiplies past the sweep budget, both answer `400` — {@link QTSTimeoutError}\n * if a stage exceeds `timeoutMs`, or {@link QTSCanceledError} if the caller's\n * signal fires before the sweep is accepted. A plain {@link QTSError} means\n * the request itself is malformed (an empty grid, a non-positive `step`, a\n * walk-forward block with fewer than two folds, or naming both/neither of\n * `instrument`/`datasetId`) and never reached the network.\n *\n * What the sweep *found* arrives through {@link Sweep.result}, which is also\n * where the semantics of the leaderboard are documented. Acceptance already\n * answers three things worth reading before any result exists — the effective\n * seed, whether this submission enqueued anything, and whether this is a\n * walk-forward sweep — see {@link Sweep.accepted}.\n *\n * ```ts\n * const handle = await qts.sweep({\n * strategy: source,\n * exchangeId: 'binance',\n * instrument: 'BTC/USDT',\n * from: '2026-01-01T00:00:00Z',\n * to: '2026-02-01T00:00:00Z',\n * params: { rsiPeriod: { from: 7, to: 28, step: 1 } },\n * });\n * const leaderboard = await handle.result;\n * ```\n */\n sweep(req: SweepRequest, opts?: SweepOptions): Promise<Sweep> {\n return runSweep(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 /**\n * List the exchanges the platform serves. Each `id` is what every other\n * method takes as `exchangeId`.\n *\n * @throws QTSError on any non-2xx response, with the HTTP status on\n * `status`.\n */\n exchanges(): Promise<Exchange[]> {\n return listExchanges();\n }\n\n /**\n * List an exchange's instruments, each with the per-data-type `coverage`\n * that says which date windows are actually downloadable.\n *\n * Omitting `segment` asks for the exchange's **default** segment, which is\n * `'spot'` today. The API answers with a HAL envelope that this method\n * unwraps to the instrument array, so the envelope's `meta.segment`,\n * `meta.updatedAt` and segment-discovery `_links` do not reach you: if you\n * need certainty about which segment you are looking at, pass `segment`\n * explicitly rather than relying on the default.\n *\n * @param exchangeId exchange identifier, e.g. `binance`\n * @throws QTSError on any non-2xx response, with the HTTP status on\n * `status`.\n */\n instruments(\n exchangeId: string,\n segment?: InstrumentSegment,\n ): Promise<InstrumentDetail[]> {\n return listInstruments(exchangeId, segment);\n }\n\n /**\n * Ask the platform to check that a registered strategy can actually run:\n * it instantiates the compiled class and drives it through a bounded\n * synthetic series, so a wiring fault surfaces here instead of at the first\n * backtest.\n *\n * **Idempotent, and two-outcome.** `queued: false` means a verdict already\n * existed for the current compilation and came back unchanged in `state` —\n * nothing was queued. `queued: true` means a check was just started, and is\n * **not** terminal: poll {@link QTSurfer.strategy} until `validation`\n * leaves `'pending'`. The discriminant reports whether work was *started*,\n * not whether a verdict *exists*, because a `queued: false` answer can\n * itself carry `validation: 'pending'` from a check an earlier call queued;\n * `state.validation` is what tells you that.\n *\n * **Poll with a deadline of your own.** `'pending'` is not guaranteed to\n * resolve — a queued check can go unreported for far longer than one takes,\n * which the platform eventually flags as `validationStalled`. Nothing about\n * the strategy is disproved when that happens, but a caller that waits for\n * a terminal verdict without a timeout can wait forever. This SDK ships no\n * polling helper for that reason: the timeout is the caller's policy.\n *\n * Whatever the verdict, it is a floor rather than a guarantee — see\n * {@link StrategyState}.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * means no such registered strategy for this caller.\n */\n validateStrategy(strategyId: string): Promise<StrategyValidation> {\n return runValidateStrategy(strategyId);\n }\n\n /**\n * Read everything the platform records about a strategy: whether it is\n * registered at all, its validation verdict, the market data its compiled\n * class requires, and any engine notices the check raised. This is what to\n * poll after {@link QTSurfer.validateStrategy} returns `queued: true`, and\n * the only place a verdict is read from.\n *\n * Check `compiledAt` against `validatedAt` before trusting a verdict: the\n * strategy may have been recompiled since it was recorded, in which case\n * the verdict describes bytecode that is no longer what would run.\n * See {@link StrategyState} for why even a fresh `'passed'` is a floor\n * rather than a guarantee.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response. A `404` (carried on `status`)\n * means exactly one thing — no such registered strategy for this caller.\n * It is never a stale or expired answer.\n */\n strategy(strategyId: string): Promise<StrategyState> {\n return getStrategy(strategyId);\n }\n\n /**\n * List every strategy you have registered and not deleted, most recently\n * compiled first. Never `404`s — an empty array means you have none.\n * Each entry deliberately omits `validation`; check a specific strategy's\n * verdict with {@link QTSurfer.strategy}. See {@link StrategySummary}.\n *\n * @throws QTSError on any non-2xx response, with the HTTP status on\n * `status`.\n */\n strategies(): Promise<StrategySummary[]> {\n return listStrategies();\n }\n\n /**\n * Release a registered strategy: removes it from both {@link\n * QTSurfer.strategy} and {@link QTSurfer.strategies}.\n *\n * Backtests already run against this strategy are unaffected, and\n * re-submitting the same source afterwards registers a **new** strategy\n * with a **new** id rather than undeleting this one. Deleting your own\n * copy of a strategy never affects anyone else's copy of the same source\n * (e.g. a shared/marketplace listing).\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * means no such registered strategy for this caller.\n */\n deleteStrategy(strategyId: string): Promise<void> {\n return runDeleteStrategy(strategyId);\n }\n\n /**\n * Read back the exact source last submitted for a strategy id, whitespace\n * and comments included.\n *\n * A `404` (carried on `status`) covers two indistinguishable cases: the id\n * was never registered by you, or it resolves only through a shared/\n * marketplace reference that carries no source of its own.\n *\n * @param strategyId the id returned when the strategy was compiled\n */\n strategyCode(strategyId: string): Promise<string> {\n return getStrategyCode(strategyId);\n }\n\n // Future surface:\n // TTL cache for exchanges / instruments\n // jobs: { cancel, stream, result }\n}\n","import {\n cancelBacktest,\n executeBacktest,\n getBacktestResult,\n type ResultMap,\n} from '@qtsurfer/api-client';\nimport { QTSCanceledError, QTSExecutionError } from '../errors';\nimport {\n buildStagePolicy,\n normalizeStatus,\n runStage,\n type StagePolicy,\n} from '../internal/polling';\nimport {\n TICKER,\n compileStrategySource,\n prepareDataset,\n validatePrepareTarget,\n} from '../internal/preparation';\n\n/**\n * ```ts\n * const request: BacktestRequest = {\n * strategy: source,\n * exchangeId: 'binance',\n * instrument: 'BTC/USDT',\n * from: '2026-01-01T00:00:00Z',\n * to: '2026-02-01T00:00:00Z',\n * };\n * ```\n *\n * Backtest against a dataset you uploaded instead of an exchange instrument by\n * replacing `instrument` with `datasetId` (and `exchangeId: 'user'`):\n *\n * ```ts\n * const request: BacktestRequest = {\n * strategy: source,\n * exchangeId: 'user',\n * datasetId: 'ds_123',\n * from: '2026-01-01T00:00:00Z',\n * to: '2026-02-01T00:00:00Z',\n * };\n * ```\n */\nexport interface BacktestRequest {\n /** Strategy source code (Java) */\n strategy: string;\n /** Exchange id, e.g. `binance`, or the reserved value `user` when backtesting against `datasetId`. */\n exchangeId: string;\n /** Instrument symbol, e.g. `BTC/USDT`. Exactly one of `instrument`/`datasetId` is required. */\n instrument?: string;\n /**\n * Id of a dataset you uploaded, in place of `instrument`. Exactly one of\n * `instrument`/`datasetId` is required. Pairs with `exchangeId: 'user'`.\n */\n datasetId?: string;\n /** Optional specific version of `datasetId`; omit to use its current version. Requires `datasetId`. */\n datasetVersionId?: 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\n/**\n * Resolved value of the backtest workflow (see {@link QTSurfer.backtest}).\n * Alias for api-client's `ResultMap` — always includes core fields\n * (`hostName`, `iops`, `instrument`); yield metrics (`pnlTotal`,\n * `totalTrades`, `equityCurve`, etc.) are only present once the strategy\n * has emitted at least one trade.\n */\nexport type BacktestResult = ResultMap;\n\n/** The three sequential stages {@link QTSurfer.backtest} moves through, in order. */\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 * Fraction (0-1) of the requested prepare window that actually holds data,\n * reported by the backend once preparation completes. Present only on the\n * final `preparing` event.\n */\n coverageRatio?: number;\n}\n\nexport interface BacktestOptions {\n /** Abort the workflow. Cancels the current poll and calls `cancelBacktest` 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\n/** Initial interval between polls of a single backtest. */\nconst DEFAULT_POLL_INTERVAL_MS = 500;\n/** Backoff ceiling for a single backtest. */\nconst DEFAULT_MAX_POLL_INTERVAL_MS = 5000;\n\nexport async function backtest(\n req: BacktestRequest,\n opts: BacktestOptions = {},\n): Promise<BacktestResult> {\n validatePrepareTarget('backtest', req);\n const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS, DEFAULT_MAX_POLL_INTERVAL_MS);\n\n // 1. Compile strategy (single synchronous request)\n opts.onProgress?.({ stage: 'compiling' });\n const strategyId = await compileStrategySource(req.strategy, opts.signal);\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 prepareData(\n req: BacktestRequest,\n policy: StagePolicy,\n opts: BacktestOptions,\n): Promise<string> {\n return prepareDataset(\n {\n exchangeId: req.exchangeId,\n ...(req.instrument !== undefined ? { instrument: req.instrument } : {}),\n ...(req.datasetId !== undefined ? { datasetId: req.datasetId } : {}),\n ...(req.datasetVersionId !== undefined ? { datasetVersionId: req.datasetVersionId } : {}),\n from: req.from,\n to: req.to,\n },\n policy,\n {\n ...(opts.signal ? { signal: opts.signal } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n onPercent: (percent) => opts.onProgress?.({ stage: 'preparing', percent }),\n // Surface the backend's coverage ratio for the prepared window (spec 0.98.0) on the\n // final preparing event, so callers can react to a partially-covered range.\n onPrepared: (state) =>\n opts.onProgress?.({\n stage: 'preparing',\n percent: 100,\n coverageRatio: state.coverageRatio,\n }),\n },\n );\n}\n\nasync function executeStrategy(\n req: BacktestRequest,\n prepareJobId: string,\n strategyId: string,\n policy: StagePolicy,\n opts: BacktestOptions,\n): Promise<BacktestResult> {\n const { data, error } = await executeBacktest({\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 async ({ signal }) => {\n const res = await getBacktestResult({\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 // A 202 carries an empty body: no `state`, so the spread yields an undefined status and\n // the retry predicate keeps polling. That is the intended handling, not a coincidence —\n // see normalizeStatus. Do not \"fix\" this into a throw or an early return of the result.\n return { ...res.data.state, __result: res.data.results };\n },\n {\n ...(opts.signal ? { signal: opts.signal } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n onEachAttempt: (r) => {\n if (r.size > 0) {\n opts.onProgress?.({ stage: 'executing', percent: (r.completed / r.size) * 100 });\n }\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 cancelBacktest({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n }).catch(() => undefined);\n }\n throw err;\n }\n}\n","/**\n * Base class for every error the SDK throws. Catch this to handle all SDK\n * failures generically, or catch a specific subclass below to tell which\n * stage failed. `status` is only set when the throw site had an HTTP status\n * to attach: {@link QTSDownloadError} always carries one, and so does the\n * plain `QTSError` thrown by the single-request calls (`exchanges`,\n * `instruments`, `validateStrategy`, `strategy`). The workflow-stage errors\n * carry `cause` instead and encode retryability in their message.\n */\nexport class QTSError extends Error {\n /** HTTP status code, when the underlying transport surfaced one. */\n readonly status?: number;\n constructor(message: string, readonly cause?: unknown, status?: number) {\n super(message);\n this.name = 'QTSError';\n if (status !== undefined) this.status = status;\n }\n}\n\n/**\n * Thrown when strategy compilation fails. A `429` means the source was never\n * judged — too many compilations were already in flight — and is safe to\n * retry. Any other status (typically `400`) means the source itself does\n * not compile, so retrying with the same input fails again.\n */\nexport class QTSStrategyCompileError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSStrategyCompileError';\n }\n}\n\n/**\n * Thrown when the data-preparation stage fails: submitting the prepare\n * request, polling its status, or a backend-reported preparation failure\n * (e.g. no data available for the requested range) all surface here.\n */\nexport class QTSPreparationError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSPreparationError';\n }\n}\n\n/**\n * Thrown when the execute stage fails: submitting the execute request,\n * polling its result, or a backend-reported execution failure all surface\n * here.\n */\nexport class QTSExecutionError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSExecutionError';\n }\n}\n\n/**\n * Thrown when a stage (prepare or execute) exceeds `timeoutMs`. The stage\n * may still be running server-side — this only means the SDK stopped\n * waiting locally — so it is fine to retry, optionally with a larger\n * `timeoutMs`.\n */\nexport class QTSTimeoutError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSTimeoutError';\n }\n}\n\n/**\n * Thrown when a stage is aborted — either because the caller's\n * `AbortSignal` fired, or because the backend itself reported the\n * prepare/execute job as aborted. Either way this reflects a deliberate\n * stop, not a failure, and is not something to retry automatically.\n */\nexport class QTSCanceledError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSCanceledError';\n }\n}\n\n/**\n * Thrown by the tickers/klines download functions on any non-2xx response or\n * transport failure. Carries the HTTP `status` when one was received: a\n * `4xx` means the request itself is wrong (bad hour or instrument), while a\n * `5xx` or a missing status (transport failure) is generally safe to retry.\n */\nexport class QTSDownloadError extends QTSError {\n constructor(message: string, cause?: unknown, status?: number) {\n super(message, cause, status);\n this.name = 'QTSDownloadError';\n }\n}\n\n/**\n * Thrown by the `authenticate()` helper when the apikey is missing or the JWT\n * exchange fails (HTTP 401 from `POST /v1/auth/token`, etc.).\n */\nexport class QTSAuthError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSAuthError';\n }\n}\n","import {\n ExponentialBackoff,\n TaskCancelledError,\n TimeoutStrategy,\n handleWhenResult,\n retry,\n timeout,\n wrap,\n type ICancellationContext,\n type IPolicy,\n} from 'cockatiel';\nimport { QTSCanceledError, QTSTimeoutError } from '../errors';\n\n/**\n * The stable form of a backend job/sweep status, so the rest of the SDK can\n * reason about it regardless of OpenAPI spec drift (the live API sometimes\n * returns lowercase values like `queued` / `completed` / `failed`).\n *\n * @internal\n */\nexport type NormalizedStatus = 'in-progress' | 'completed' | 'failed' | 'aborted';\n\n/**\n * Normalize a raw status value.\n *\n * Only the terminal statuses end a poll loop. Everything else — including a\n * **missing** status — means \"keep asking\": the API answers `202` with an empty body when a\n * job is known but its result is not readable yet, and that response carries no state at all.\n * Mapping absent to in-progress is what makes a 202 continue the loop under its timeout\n * instead of being mistaken for a finished job with no data.\n *\n * @internal\n */\nexport function normalizeStatus(raw: unknown): NormalizedStatus {\n const value = typeof raw === 'string' ? raw.toLowerCase() : '';\n if (value === 'completed') return 'completed';\n // A sweep that finishes with at least one shard dead reports `partial`, and that is\n // terminal: the rows it did produce are readable and nothing more is coming, so treating it\n // as in-progress would poll a finished sweep forever. `PARTIAL` exists only on the two sweep\n // schemas (`ExecuteSweepResult.status` and `SweepSensitivity.status`) and is absent from\n // `JobState`, so mapping it here cannot reach the prepare or execute paths. It folds into\n // `completed` because this enum drives the poll loop — stop asking, hand the response back —\n // and the caller reads `partial` off the response itself, where the distinction survives.\n if (value === 'partial') 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 / absent (202) / anything else → still running\n return 'in-progress';\n}\n\n/** The retry-with-backoff policy one workflow stage polls under. @internal */\nexport type StagePolicy = IPolicy<ICancellationContext, never>;\n\n/** Poll tuning shared by every workflow's options object. @internal */\nexport interface PollTuning {\n pollIntervalMs?: number;\n maxPollIntervalMs?: number;\n timeoutMs?: number;\n}\n\n/**\n * Build the poll policy for a workflow's stages.\n *\n * The defaults are a per-workflow argument rather than a constant, because how\n * often it is worth asking depends on what is being watched: a single backtest\n * advances tick by tick, while a sweep's leaderboard changes on the timescale\n * of shards finishing.\n *\n * @param opts caller overrides\n * @param defaultPollMs initial interval when the caller sets none\n * @param defaultMaxPollMs backoff ceiling when the caller sets none\n *\n * @internal\n */\nexport function buildStagePolicy(\n opts: PollTuning,\n defaultPollMs: number,\n defaultMaxPollMs: number,\n): StagePolicy {\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 ?? defaultPollMs,\n maxDelay: opts.maxPollIntervalMs ?? defaultMaxPollMs,\n }),\n },\n );\n\n return opts.timeoutMs\n ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy)\n : retryPolicy;\n}\n\n/** How one call to {@link runStage} is cancelled and reported. @internal */\nexport interface StageRun<T> {\n /**\n * Aborts the stage, surfacing as {@link QTSCanceledError}. Omit it to run a\n * stage the caller's signal must **not** interrupt.\n */\n signal?: AbortSignal;\n /** Only used to render the timeout message. */\n timeoutMs?: number;\n /** Called with every attempt's result, terminal or not. */\n onEachAttempt?: (r: T) => void;\n}\n\n/**\n * Poll `fetchFn` under `policy` until its result stops normalizing to\n * `in-progress`, then return that result.\n *\n * @internal\n */\nexport async function runStage<T>(\n policy: StagePolicy,\n fetchFn: (ctx: ICancellationContext) => Promise<T>,\n run: StageRun<T> = {},\n): Promise<T> {\n try {\n return await policy.execute(async (ctx) => {\n if (run.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n const result = await fetchFn(ctx);\n run.onEachAttempt?.(result);\n if (run.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n return result;\n }, run.signal);\n } catch (err) {\n if (err instanceof QTSCanceledError) throw err;\n if (err instanceof TaskCancelledError) {\n if (run.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw new QTSTimeoutError(`Stage exceeded ${run.timeoutMs}ms`, err);\n }\n if (run.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw err;\n }\n}\n","import {\n getPrepareStatus,\n compileStrategy as apiCompileStrategy,\n prepareBacktest,\n type DataSourceType,\n type PrepareJobState,\n} from '@qtsurfer/api-client';\nimport { QTSCanceledError, QTSError, QTSPreparationError, QTSStrategyCompileError } from '../errors';\nimport { normalizeStatus, runStage, type StagePolicy } from './polling';\n\n/** The only data source the workflows prepare against today. @internal */\nexport const TICKER: DataSourceType = 'ticker';\n\n/**\n * Compile in a single request: the API answers synchronously with the `strategyId`,\n * so there is no job to poll. A compile error arrives here as a `400`, not on a later poll.\n *\n * @internal\n */\nexport async function compileStrategySource(\n source: string,\n signal?: AbortSignal,\n): Promise<string> {\n const { data, error, response } = await apiCompileStrategy({\n body: source,\n ...(signal ? { signal } : {}),\n });\n\n if (error) {\n // A 429 means the platform is holding too many compilations at once and the source was never\n // judged — worth separating from the 400 that says the source itself does not compile.\n // Read the status inside this branch and optionally: a transport failure carries no response,\n // and dereferencing one would raise a TypeError that buries the error actually being reported.\n if (response?.status === 429) {\n throw new QTSStrategyCompileError(\n 'Strategy was not compiled, too many compilations in flight; retry later',\n error,\n );\n }\n throw new QTSStrategyCompileError('Strategy compilation failed', error);\n }\n if (!data?.strategyId) {\n throw new QTSStrategyCompileError('Compile response missing strategyId');\n }\n return data.strategyId;\n}\n\n/**\n * The instrument (or dataset) and window one prepare covers. Exactly one of\n * `instrument`/`datasetId` is set — {@link validatePrepareTarget} enforces that\n * before any network call. `datasetId` pairs with the reserved `exchangeId: 'user'`.\n *\n * @internal\n */\nexport interface PrepareTarget {\n exchangeId: string;\n instrument?: string;\n /** Id of a previously uploaded dataset, in place of `instrument`. */\n datasetId?: string;\n /** Optional specific version of `datasetId`; requires `datasetId`. */\n datasetVersionId?: string;\n from: string;\n to: string;\n}\n\n/**\n * Reject a prepare target that names both, or neither, of `instrument`/`datasetId`,\n * or that sets `datasetVersionId` without `datasetId` — the same three shapes the\n * platform would only reject over the network. Called first thing by each workflow,\n * before compiling the strategy, so a malformed request never reaches the network.\n *\n * @param workflow name of the caller, used to prefix the error message (e.g. `'backtest'`)\n * @internal\n */\nexport function validatePrepareTarget(\n workflow: string,\n target: Pick<PrepareTarget, 'instrument' | 'datasetId' | 'datasetVersionId'>,\n): void {\n const hasInstrument = target.instrument !== undefined;\n const hasDataset = target.datasetId !== undefined;\n if (hasInstrument && hasDataset) {\n throw new QTSError(`${workflow}: exactly one of instrument/datasetId is required, got both`);\n }\n if (!hasInstrument && !hasDataset) {\n throw new QTSError(\n `${workflow}: exactly one of instrument/datasetId is required, got neither`,\n );\n }\n if (target.datasetVersionId !== undefined && !hasDataset) {\n throw new QTSError(`${workflow}: datasetVersionId requires datasetId`);\n }\n}\n\n/** Reporting and cancellation for {@link prepareDataset}. @internal */\nexport interface PrepareRun {\n signal?: AbortSignal;\n timeoutMs?: number;\n /** Called after each poll that reports size information. */\n onPercent?: (percent: number) => void;\n /** Called once with the terminal state, so the caller can read its coverage. */\n onPrepared?: (state: PrepareJobState) => void;\n}\n\n/**\n * Submit a prepare and poll it to a terminal state.\n *\n * One implementation on purpose. Preparing is idempotent — the same instrument\n * and window always resolve to the same job — so a workflow that prepares on\n * every call duplicates no work, and that argument only holds while there is a\n * single place where it is true.\n *\n * @returns the prepare jobId, which is what identifies the prepared dataset\n *\n * @internal\n */\nexport async function prepareDataset(\n target: PrepareTarget,\n policy: StagePolicy,\n run: PrepareRun = {},\n): Promise<string> {\n const { data, error } = await prepareBacktest({\n path: { exchangeId: target.exchangeId, type: TICKER },\n body: {\n ...(target.instrument !== undefined ? { instrument: target.instrument } : {}),\n ...(target.datasetId !== undefined ? { datasetId: target.datasetId } : {}),\n ...(target.datasetVersionId !== undefined\n ? { datasetVersionId: target.datasetVersionId }\n : {}),\n from: target.from,\n to: target.to,\n },\n ...(run.signal ? { signal: run.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 async ({ signal }) => {\n const res = await getPrepareStatus({\n path: { exchangeId: target.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 {\n ...(run.signal ? { signal: run.signal } : {}),\n ...(run.timeoutMs !== undefined ? { timeoutMs: run.timeoutMs } : {}),\n onEachAttempt: (r) => {\n if (r.size > 0) run.onPercent?.((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 run.onPrepared?.(state);\n return prepareJobId;\n}\n","import {\n listExchanges as apiListExchanges,\n listInstruments as apiListInstruments,\n listSegmentInstruments as apiListSegmentInstruments,\n type Exchange as ApiExchange,\n type InstrumentDetail as ApiInstrumentDetail,\n} from '@qtsurfer/api-client';\nimport { QTSError } from '../errors';\nimport { requestFailed } from '../internal/requestError';\n\n/**\n * One exchange the platform serves. Alias for api-client's `Exchange`:\n * `id` (what every other call takes as `exchangeId`), `name`, and an\n * optional `description`.\n */\nexport type Exchange = ApiExchange;\n\n/**\n * One instrument on an exchange. Alias for api-client's `InstrumentDetail`:\n * `id` / `base` / `quote`, plus optional `coverage` (the date windows for\n * which tickers and klines actually exist, per data type), `lastPrice` and\n * `volume24h`.\n *\n * `coverage` is what tells you whether a backtest range is downloadable at\n * all; it is optional, and absent means the platform did not report one, not\n * that there is no data.\n */\nexport type InstrumentDetail = ApiInstrumentDetail;\n\n/**\n * A market segment of an exchange. `'spot'` is the default segment served\n * when {@link QTSurfer.instruments} is called without one.\n */\nexport type InstrumentSegment = 'spot' | 'futures';\n\n/**\n * List the exchanges the platform serves.\n *\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\nexport async function listExchanges(): Promise<Exchange[]> {\n const { data, error, response } = await apiListExchanges();\n if (error) throw requestFailed('exchanges call', error, response?.status);\n if (!data) throw new QTSError('Empty exchanges response');\n return data;\n}\n\n/**\n * List an exchange's instruments, each with its per-data-type coverage.\n *\n * Omitting `segment` asks for the exchange's **default** segment, which is\n * `'spot'` today. The API answers both routes with a HAL envelope\n * (`data` / `meta` / `_links`) that this function unwraps to the instrument\n * array, so `meta.segment`, `meta.updatedAt` and the `_links`\n * segment-discovery links do not reach the caller: if you need certainty\n * about which segment you are looking at, pass `segment` explicitly rather\n * than relying on the default.\n *\n * @param exchangeId exchange identifier, e.g. `binance`\n * @param segment market segment to list; defaults to the exchange's default\n * segment\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\nexport async function listInstruments(\n exchangeId: string,\n segment?: InstrumentSegment,\n): Promise<InstrumentDetail[]> {\n const { data, error, response } = segment\n ? await apiListSegmentInstruments({ path: { exchangeId, segment } })\n : await apiListInstruments({ path: { exchangeId } });\n if (error) throw requestFailed('instruments call', error, response?.status);\n if (!data) throw new QTSError('Empty instruments response');\n return data.data;\n}\n","import { QTSError } from '../errors';\n\n/**\n * Build the {@link QTSError} for a failed single-request call.\n *\n * The HTTP status is attached to the error rather than only rendered into the\n * message, because callers branch on it — a `4xx` means the request itself was\n * wrong, a `5xx` is generally worth retrying, and the authenticated session\n * re-mints its JWT on a `401`.\n *\n * @param what short description of the call, e.g. `'exchanges call'`\n * @param error the api-client error payload\n * @param status HTTP status of the failing response\n *\n * @internal\n */\nexport function requestFailed(\n what: string,\n error: unknown,\n status?: number,\n): QTSError {\n const prefix = status === undefined ? '' : `HTTP ${status} — `;\n return new QTSError(`${what} failed: ${prefix}${describe(error)}`, error, status);\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' || typeof e.code === 'number' ? e.code : undefined;\n const message = typeof e.message === 'string' ? e.message : undefined;\n if (code !== undefined && message) return `${code}: ${message}`;\n if (message) return message;\n if (code !== undefined) return String(code);\n }\n return String(error);\n}\n","import {\n downloadKlines as apiDownloadKlines,\n downloadTickers as apiDownloadTickers,\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 /** Exchange id, e.g. `binance`. */\n exchangeId: string;\n /** Base asset of the instrument, e.g. `BTC`. */\n base: string;\n /** Quote asset of the instrument, e.g. `USDT`. */\n quote: string;\n /** Hour selector in `YYYY-MM-DDTHH` (UTC). */\n hour: string;\n /** Defaults to `'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 apiDownloadTickers({\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 response.status,\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 apiDownloadKlines({\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 response.status,\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","import {\n getStrategy as apiGetStrategy,\n validateStrategy as apiValidateStrategy,\n listStrategies as apiListStrategies,\n deleteStrategy as apiDeleteStrategy,\n getStrategyCode as apiGetStrategyCode,\n type StrategyState as ApiStrategyState,\n type StrategySummary as ApiStrategySummary,\n} from '@qtsurfer/api-client';\nimport { QTSError } from '../errors';\nimport { requestFailed } from '../internal/requestError';\n\n/**\n * Everything the platform records about a registered strategy. Alias for\n * api-client's `StrategyState`.\n *\n * `validation` is the verdict and is one of:\n *\n * - `'not_validated'` — registered, never checked.\n * - `'pending'` — a check was asked for and has not answered yet.\n * - `'passed'` — the class loaded and survived its first event.\n * - `'failed'` — it did not; `detail` says how.\n *\n * **`'passed'` is a floor, not a guarantee.** It means the compiled class\n * could be instantiated and got through the first event of a short synthetic\n * run — not the caller's instrument, not the caller's window, and not the\n * rest of the run. It says nothing about whether the strategy is correct,\n * profitable, or safe to run at scale. `dryRunIncomplete` marks a check that\n * ran out of its budget, which makes a `'passed'` verdict a lower floor\n * still, and makes an empty `notices` list no longer a clean bill of health.\n *\n * A verdict describes the bytecode that existed when it was recorded:\n * `compiledAt` newer than `validatedAt` means the strategy was recompiled\n * afterwards and the verdict no longer describes what would run.\n *\n * `_links.code`, when present, is a discovery link to this strategy's raw\n * source (`GET /strategy/{strategyId}/code` — the same thing\n * {@link QTSurfer.strategyCode} fetches by id, so there is no need to follow\n * the link yourself). It is present on a full `StrategyState` body — this\n * function's result, and {@link QTSurfer.validateStrategy}'s already-validated\n * `200` — and **absent** from that same operation's `queued: true` (`202`)\n * outcome, which is a deliberately partial stub. This field passes through\n * unmodified from api-client, so it needs no unwrapping on the SDK's part.\n */\nexport type StrategyState = ApiStrategyState;\n\n/**\n * Outcome of {@link QTSurfer.validateStrategy} — the SDK's rendering of the\n * two answers that operation has, which the response body alone cannot tell\n * apart.\n *\n * - `queued: false` — a verdict already existed for the current compilation\n * and comes back in `state` unchanged; nothing new was queued. This is\n * **not** the same as \"terminal\": a check queued by an earlier call can\n * still be running, so read `state.validation` rather than treating\n * `queued: false` as \"there is an answer\".\n * - `queued: true` — a check was just queued. Nothing is known yet; poll\n * {@link QTSurfer.strategy} until `validation` leaves `'pending'`.\n */\nexport type StrategyValidation =\n | { queued: false; strategyId: string; state: StrategyState }\n | { queued: true; strategyId: string; state?: undefined };\n\n/**\n * Ask the platform to check that a registered strategy can actually run: it\n * instantiates the compiled class and drives it through a bounded synthetic\n * series, so a wiring fault surfaces here instead of at the first backtest.\n *\n * **Idempotent, and two-outcome.** If a verdict already exists for the\n * current compilation it is returned unchanged and nothing is queued\n * (`queued: false`); otherwise a check is queued (`queued: true`) and this\n * call is *not* terminal — poll {@link QTSurfer.strategy} until `validation`\n * is `'passed'` or `'failed'`. Because a `queued: false` answer can itself carry\n * `validation: 'pending'` (a check an earlier call queued), the discriminant\n * tells you whether work was *started*, not whether a verdict *exists*;\n * `state.validation` is what tells you that.\n *\n * **Poll with a deadline of your own.** `'pending'` is not guaranteed to\n * resolve: a queued check can go unreported for far longer than one takes,\n * which the platform eventually flags as `validationStalled` on the strategy.\n * Nothing about the strategy is disproved when that happens — the check\n * simply did not run — but a caller that waits for a terminal verdict without\n * a timeout can wait forever. This SDK deliberately ships no polling helper\n * for that reason; the timeout is the caller's policy to set.\n *\n * Whatever the verdict, remember it is a floor rather than a guarantee — see\n * {@link StrategyState}.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * means no such registered strategy for this caller.\n */\nexport async function validateStrategy(strategyId: string): Promise<StrategyValidation> {\n const { data, error, response } = await apiValidateStrategy({ path: { strategyId } });\n if (error) throw requestFailed('strategy validation request', error, response?.status);\n // The two outcomes are distinguishable only by status: a `200` body is a\n // full StrategyState whose `validation` may itself be `'pending'`, so the\n // payload cannot be used to tell \"queued just now\" from \"already queued\".\n // The queued branch echoes back the caller's own id rather than reading one\n // out of the body, because an accepted-but-not-done response on this API is\n // not guaranteed to carry a body at all.\n if (response?.status === 202) return { queued: true, strategyId };\n if (!data) throw new QTSError('Empty strategy validation response');\n return { queued: false, strategyId, state: data as StrategyState };\n}\n\n/**\n * Read everything the platform records about a strategy: whether it is\n * registered at all, its validation verdict, the market data its compiled\n * class requires, and any engine notices the check raised.\n *\n * This is the endpoint to poll after {@link QTSurfer.validateStrategy}\n * returns `queued: true`, and the only place a verdict is read from.\n *\n * Check `compiledAt` against `validatedAt` before trusting a verdict: the\n * strategy may have been recompiled since the verdict was recorded, in which\n * case the verdict describes bytecode that is no longer what would run.\n * Re-request validation to get an answer about the current compilation.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response. A `404` (carried on `status`)\n * means exactly one thing — no such registered strategy for this caller. It is\n * never a stale or expired answer: registration and verdict are stored\n * durably, not cached.\n */\nexport async function getStrategy(strategyId: string): Promise<StrategyState> {\n const { data, error, response } = await apiGetStrategy({ path: { strategyId } });\n if (error) throw requestFailed('strategy lookup', error, response?.status);\n if (!data) throw new QTSError('Empty strategy response');\n return data;\n}\n\n/**\n * One entry in {@link QTSurfer.strategies}'s result: the same provenance\n * {@link QTSurfer.strategy} reports — `compiledAt`, `requiredSources` — but\n * never `validation`, which is what keeps listing cheap no matter how many\n * strategies you have registered. Check a specific strategy's verdict with\n * {@link QTSurfer.strategy}.\n *\n * Note: the spec types this endpoint's `requiredSources` as a plain\n * `string[]`, not the `'Ticker' | 'KLine' | 'FundingRate'` union that\n * {@link StrategyState}'s own `requiredSources` carries — narrow it yourself\n * if you need the literal type. Alias for api-client's `StrategySummary`.\n */\nexport type StrategySummary = ApiStrategySummary;\n\n/**\n * List every strategy you have registered and not deleted, most recently\n * compiled first.\n *\n * **Never `404`.** An empty array means you have none registered — not an\n * error. Each entry omits `validation` on purpose (see {@link\n * StrategySummary}); check a specific strategy's verdict with {@link\n * QTSurfer.strategy}.\n *\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\nexport async function listStrategies(): Promise<StrategySummary[]> {\n const { data, error, response } = await apiListStrategies();\n if (error) throw requestFailed('strategies list', error, response?.status);\n if (!data) throw new QTSError('Empty strategies response');\n return data.strategies;\n}\n\n/**\n * Release a registered strategy: removes it from both {@link\n * QTSurfer.strategy} and {@link QTSurfer.strategies}.\n *\n * **Does not undo anything already run.** Backtests you ran against this\n * strategy before deleting it are completely unaffected — deleting only\n * stops you from validating or re-running it under this id going forward.\n * Re-submitting the exact same source afterwards registers a **new**\n * strategy with a **new** id; it does not \"undelete\" this one, because\n * nothing about the id itself is restored.\n *\n * **Scoped to your own registration.** If you copied someone else's strategy\n * (a shared/marketplace listing), deleting your copy never affects theirs,\n * or anyone else's, regardless of how many callers registered the same\n * source independently.\n *\n * Resolves with nothing: the response body is `{ strategyId, deleted: true }`,\n * and both fields are things the caller already knows before calling this —\n * `strategyId` is the argument just passed in, and `deleted` is always `true`\n * on a `200`. There is nothing in it a `void` return would lose.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * means no such registered strategy for this caller.\n */\nexport async function deleteStrategy(strategyId: string): Promise<void> {\n const { error, response } = await apiDeleteStrategy({ path: { strategyId } });\n if (error) throw requestFailed('strategy delete', error, response?.status);\n}\n\n/**\n * Read back the exact source last submitted for a strategy id — the same\n * text `strategyId` was derived from, whitespace and comments included.\n *\n * **A `404` here covers two cases the response cannot tell apart:** the id\n * was never registered by you, or it resolves only through a shared/\n * marketplace reference that carries no source of its own (a strategy you\n * copied by reference rather than by resubmitting its code). Both read as\n * \"nothing to return\" from this endpoint's point of view, and the SDK does\n * not attempt to distinguish them — there is nothing in the response to tell\n * them apart with.\n *\n * @param strategyId the id returned when the strategy was compiled\n * @throws QTSError on any non-2xx response; a `404` (carried on `status`)\n * is the two-case ambiguity described above.\n */\nexport async function getStrategyCode(strategyId: string): Promise<string> {\n const { data, error, response } = await apiGetStrategyCode({ path: { strategyId } });\n if (error) throw requestFailed('strategy code lookup', error, response?.status);\n if (!data) throw new QTSError('Empty strategy code response');\n return data.code;\n}\n","import {\n cancelSweep,\n executeSweep,\n getSweepResult,\n getSweepSensitivity,\n type ExecuteSweepAccepted,\n type ExecuteSweepRequest,\n type ExecuteSweepResult,\n type GetSweepResultData,\n type SweepHeatmap as ApiSweepHeatmap,\n type SweepHeatmapCell as ApiSweepHeatmapCell,\n type SweepMarginal as ApiSweepMarginal,\n type SweepMarginalPoint as ApiSweepMarginalPoint,\n type SweepProgress as ApiSweepProgress,\n type SweepRunRow as ApiSweepRunRow,\n type SweepSensitivity as ApiSweepSensitivity,\n type SweepSpecRequest,\n type WalkForwardFold as ApiWalkForwardFold,\n type WalkForwardResult as ApiWalkForwardResult,\n} from '@qtsurfer/api-client';\nimport { QTSCanceledError, QTSError, QTSExecutionError } from '../errors';\nimport {\n buildStagePolicy,\n normalizeStatus,\n runStage,\n type StagePolicy,\n} from '../internal/polling';\nimport {\n TICKER,\n compileStrategySource,\n prepareDataset,\n validatePrepareTarget,\n} from '../internal/preparation';\nimport { requestFailed } from '../internal/requestError';\nimport type { BacktestStage } from './backtest';\n\n// ---------------------------------------------------------------------------\n// Vocabulary\n// ---------------------------------------------------------------------------\n\n/**\n * The metric a sweep optimizes, and the one its leaderboard and its\n * sensitivity surfaces are read against.\n *\n * One vocabulary throughout: the objective a {@link SweepRequest} is submitted\n * with is the objective the leaderboard is ranked by, and the one\n * {@link Sweep.sensitivity} aggregates unless a different one is asked for.\n *\n * - `'sharpe'` — risk-adjusted return; the platform default when a request names none.\n * - `'sortino'` — downside-risk-adjusted return.\n * - `'pnl'` — absolute net profit and loss.\n * - `'maxdd'` — maximum drawdown.\n */\nexport type SweepObjective = 'sharpe' | 'sortino' | 'pnl' | 'maxdd';\n\n/**\n * How the parameter grid is turned into the list of vectors that actually run.\n *\n * - `'grid'` — every combination of every axis value. The platform default;\n * cost is the product of the axis sizes.\n * - `'random'` — uniformly random draws from the grid, capped at\n * {@link SweepRequest.samples}.\n * - `'lhs'` — Latin hypercube draws, which spread the sample more evenly than\n * uniform random.\n *\n * `samples` is required by `'random'` and `'lhs'` and ignored by `'grid'`.\n */\nexport type SweepSampler = 'grid' | 'random' | 'lhs';\n\n/**\n * How the ranked leaderboard is ordered.\n *\n * The **platform default is `'plateau'`**, so the order a sweep answers with is\n * *not* raw objective order unless you ask for it. A plateau score is the\n * objective of the worst run in a point's immediate neighbourhood, so a point\n * only ranks well when the region around it does too — which exists because\n * the highest raw score is very often a spike that does not survive the\n * parameters moving slightly.\n *\n * **What you request is not always what you get.** Read `ranking` on the\n * result to find out which ordering was actually applied: a sweep with no\n * stored parameter grid has no neighbourhood to score against and falls back\n * to `'raw'`, and a walk-forward sweep is always `'raw'` because its\n * leaderboard is one out-of-sample row per fold rather than a grid.\n *\n * This applies to the ranked view only. Alongside `order: 'natural'` it is\n * **ignored** — that view is always ordered by `runIx`.\n */\nexport type SweepRanking = 'plateau' | 'raw';\n\n/**\n * Which view of a sweep's rows to read: the display leaderboard, or every row\n * in a stable order.\n *\n * - `'ranked'` — the platform default. Sorted, and capped at a display limit;\n * `truncated` on the result is `true` when the cap actually bit, in which\n * case rows exist that this view does not carry. This is the view\n * {@link SweepRanking} applies to.\n * - `'natural'` — every available row, untruncated, in deterministic `runIx`\n * order. The view to read when materialising durable trial rows rather than\n * showing a top-N, and the only way to reach rows the ranked view dropped —\n * {@link Sweep.results} reads it off an existing sweep without re-running it.\n * {@link SweepRanking} is **ignored** here and the response reports `'raw'`;\n * rank, plateau score and neighbour count belong to the ranked view and are\n * not part of this one.\n */\nexport type SweepOrder = 'ranked' | 'natural';\n\n/**\n * Sweep lifecycle as observed by the SDK, readable off {@link Sweep.state}.\n *\n * - `'executing'` — submitted and being polled.\n * - `'completed'` — finished. The platform's own status may still be `'PARTIAL'`.\n * - `'failed'` — the poll itself failed: transport, HTTP, or a stage timeout.\n * - `'canceled'` — the sweep was aborted and the platform reported it cancelled.\n */\nexport type SweepState = 'executing' | 'completed' | 'failed' | 'canceled';\n\n/**\n * One strategy property and the values a sweep should try for it: either a\n * numeric range walked in fixed steps, or an explicit list.\n *\n * ```ts\n * const params: Record<string, ParamAxis> = {\n * rsiPeriod: { from: 7, to: 28, step: 1 },\n * useTrendFilter: { values: [true, false] },\n * };\n * ```\n *\n * The two shapes are mutually exclusive on the wire, and mixing them is a\n * request the platform rejects rather than reconciles. A list entry is a number\n * or a boolean — the axis of a boolean flag is `{ values: [true, false] }`, not\n * a range.\n */\nexport type ParamAxis =\n | {\n /** First value. */\n from: number;\n /** Last value the walk may reach. */\n to: number;\n /** Increment; must be greater than zero. */\n step: number;\n }\n | {\n /** The values to try; at least one. */\n values: Array<number | boolean>;\n };\n\n/**\n * Opt a sweep into walk-forward validation.\n *\n * Attaching this changes what the sweep does, not just how much of it runs.\n * Instead of scoring every parameter vector once over the whole range, the data\n * is cut into sequential folds; each fold optimizes the whole grid on its own\n * window and then scores only its winner on the window immediately after — data\n * that winner was never chosen on. The question it answers is not \"which\n * parameters won\" but \"does re-optimizing this periodically actually work\".\n *\n * Omit it and nothing about the sweep changes, including the shape of the\n * response.\n *\n * **It costs folds × grid.** Four folds over a 500-point grid is roughly 2000\n * backtests where the plain sweep is 500, which is why it is opt-in. The\n * platform rejects the request outright when that product exceeds its sweep\n * budget.\n *\n * **It is a different sweep, not a variant of one.** Two requests that differ\n * only in this block do not deduplicate against each other.\n *\n * The answer arrives as `walkForward` on the {@link SweepResult} — see\n * {@link Sweep.result} for how to read it.\n */\nexport interface SweepWalkForward {\n /**\n * How many sequential optimize-then-score windows to run. Two is the floor\n * and the reason is structural rather than a tuning preference: parameter\n * drift is measured between consecutive fold winners, and a single fold has\n * no consecutive pair, so it would report the strongest possible stability\n * having measured nothing. The ceiling is a platform setting; exceeding it is\n * rejected.\n */\n folds: number;\n /**\n * Share of each fold's window spent optimizing, the rest being where its\n * winner is scored. Omit to take the platform default. Lower values leave\n * more data to score on and, on short sessions, are what let the requested\n * fold count tile the data at all. Must be within 10..90.\n */\n inSamplePct?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Platform models, re-exported so callers never have to reach for api-client\n// ---------------------------------------------------------------------------\n\n/**\n * What the platform answered when it accepted the sweep, exactly as it sent it\n * — see {@link Sweep.accepted} for the three fields that make it worth reading\n * before any result exists.\n */\nexport type SweepAccepted = ExecuteSweepAccepted;\n\n/**\n * A sweep snapshot: status, progress, and the rows available for the selected\n * view. Resolved by {@link Sweep.result}, which is also where the semantics of\n * every field are documented.\n */\nexport type SweepResult = ExecuteSweepResult;\n\n/**\n * The platform's own progress record for a running sweep, carried on\n * {@link SweepProgressEvent.snapshot}. Distinct from the event that wraps it:\n * this is what the server reported, the event is what the SDK emitted.\n *\n * Two of its fields are easy to add together by mistake. `aborted` counts\n * individual runs that executed and aborted — a row-level count. `failedShards`\n * counts whole units of work (shards, or folds on a walk-forward sweep) that\n * failed and will not be retried, having never reported anything. A shard that\n * dies before producing a single row leaves `aborted` at zero, which is exactly\n * why the second count exists; **summing them double-counts nothing and\n * describes nothing**.\n *\n * `retrying` is not a failure count either — those units failed on something\n * transient and are queued to be attempted again, so a sweep with a non-zero\n * value there is still expected to finish.\n *\n * `etaSeconds` is **omitted, never zero** when it cannot be computed: a sweep\n * with nothing finished has no observed rate to extrapolate from, and a zero\n * would read as \"about to finish\". When present it runs conservative — it\n * excludes queue wait entirely, and a sweep that spent part of its life being\n * retried will have diluted the rate it is derived from.\n */\nexport type SweepProgress = ApiSweepProgress;\n\n/** One trial on a sweep's leaderboard. See {@link Sweep.result} for how to read it. */\nexport type SweepRunRow = ApiSweepRunRow;\n\n/**\n * Sensitivity aggregates over a sweep's stored rows, returned by\n * {@link Sweep.sensitivity}. Marginals are always complete; the pair surfaces\n * may be capped, in which case `heatmapsTruncated` is `true`.\n */\nexport type SweepSensitivity = ApiSweepSensitivity;\n\n/** One axis, with every other axis collapsed away. */\nexport type SweepMarginal = ApiSweepMarginal;\n\n/** How the objective behaved at one value of one axis. */\nexport type SweepMarginalPoint = ApiSweepMarginalPoint;\n\n/** The surface for one pair of axes, with all others collapsed away. */\nexport type SweepHeatmap = ApiSweepHeatmap;\n\n/** One cell of a {@link SweepHeatmap}. */\nexport type SweepHeatmapCell = ApiSweepHeatmapCell;\n\n/**\n * The walk-forward section of a {@link SweepResult}, present exactly when the\n * sweep was submitted with {@link SweepWalkForward}.\n *\n * **`paramDrift` absent is not zero.** The field is omitted whenever the figure\n * could not be computed — fewer than two folds finished, or no stored grid to\n * place the winners on — and zero is itself a meaningful reading there (winners\n * that never moved), so a placeholder would be indistinguishable from perfect\n * stability.\n */\nexport type WalkForwardResult = ApiWalkForwardResult;\n\n/**\n * What one fold concluded. The out-of-sample row is the answer; the in-sample\n * figure is only there to be compared against it, since any grid produces a\n * flattering in-sample winner — that is what optimizing does. The gap between\n * them is the whole reading.\n */\nexport type WalkForwardFold = ApiWalkForwardFold;\n\n// ---------------------------------------------------------------------------\n// Request, options, progress\n// ---------------------------------------------------------------------------\n\n/**\n * A parameter sweep over one instrument and one window: the same strategy run\n * once per parameter vector, scored and ranked against a single objective.\n *\n * ```ts\n * const request: SweepRequest = {\n * strategy: source,\n * exchangeId: 'binance',\n * instrument: 'BTC/USDT',\n * from: '2026-01-01T00:00:00Z',\n * to: '2026-02-01T00:00:00Z',\n * params: {\n * rsiPeriod: { from: 7, to: 28, step: 1 },\n * useTrendFilter: { values: [true, false] },\n * },\n * objective: 'sharpe',\n * };\n * ```\n *\n * Sweep against a dataset you uploaded instead of an exchange instrument by\n * replacing `instrument` with `datasetId` (and `exchangeId: 'user'`):\n *\n * ```ts\n * const request: SweepRequest = {\n * strategy: source,\n * exchangeId: 'user',\n * datasetId: 'ds_123',\n * from: '2026-01-01T00:00:00Z',\n * to: '2026-02-01T00:00:00Z',\n * params: { rsiPeriod: { from: 7, to: 28, step: 1 } },\n * };\n * ```\n */\nexport interface SweepRequest {\n /** Strategy source code (Java), compiled once and reused by every trial. */\n strategy: string;\n /** Exchange id, e.g. `binance`, or the reserved value `user` when sweeping against `datasetId`. */\n exchangeId: string;\n /** Instrument symbol, e.g. `BTC/USDT`. Exactly one of `instrument`/`datasetId` is required. */\n instrument?: string;\n /**\n * Id of a dataset you uploaded, in place of `instrument`. Exactly one of\n * `instrument`/`datasetId` is required. Pairs with `exchangeId: 'user'`.\n */\n datasetId?: string;\n /** Optional specific version of `datasetId`; omit to use its current version. Requires `datasetId`. */\n datasetVersionId?: string;\n /** Range start (ISO-8601, ISO DATE, or BASIC ISO DATE). */\n from: string;\n /** Range end (same formats as `from`; must be later than `from`). */\n to: string;\n /** The grid: one {@link ParamAxis} per strategy property to vary. At least one. */\n params: Record<string, ParamAxis>;\n /**\n * How the grid becomes the list of vectors actually run. Omit to keep the\n * platform default, the full cross product.\n */\n sampler?: SweepSampler;\n /**\n * How many vectors to draw for `'random'` and `'lhs'`; ignored by `'grid'`.\n */\n samples?: number;\n /**\n * Reproducibility seed. Omit to let the platform generate one and report it\n * back on {@link Sweep.accepted}, so a randomly sampled sweep can be replayed\n * exactly by submitting the same seed again.\n */\n seed?: number;\n /**\n * The metric to optimize and rank by; omit to keep the platform default\n * (`'sharpe'`). It is also what {@link Sweep.sensitivity} aggregates unless\n * told otherwise.\n */\n objective?: SweepObjective;\n /**\n * Opt into walk-forward validation, which changes both what runs and the\n * shape of the answer. Omit to run an ordinary sweep.\n */\n walkForward?: SweepWalkForward;\n}\n\n/**\n * Emitted at stage transitions and after each poll of a running sweep.\n *\n * The `snapshot` is where the detail lives — see {@link SweepProgress}, whose\n * counts measure different things and must not be added together.\n */\nexport interface SweepProgressEvent {\n /** Current workflow stage. */\n stage: BacktestStage;\n /**\n * 0-100, computed from the runs finished out of the runs expected (`done` /\n * `total` on the snapshot, both run-level counts — not the shard counts,\n * which partition units of work rather than runs). Absent on stage\n * transitions before the first poll.\n */\n percent?: number;\n /**\n * Fraction (0-1) of the requested window that actually holds data, as\n * reported once preparation completes. Present only on the final `preparing`\n * event. Worth reading on a sweep in particular: a thinly covered window is\n * about to be scored once per parameter vector.\n */\n coverageRatio?: number;\n /**\n * The platform's progress record for the sweep. Present only on `executing`\n * events.\n */\n snapshot?: SweepProgress;\n}\n\n/** Tuning knobs for a {@link QTSurfer.sweep} invocation. */\nexport interface SweepOptions {\n /**\n * Ask the platform to stop the sweep between parameter vectors.\n *\n * **Aborting does not reject {@link Sweep.result}** — it resolves with\n * whatever was scored before the stop. That is a deliberate divergence from\n * `backtest()`, which rejects with `QTSCanceledError` when its run is\n * aborted; see {@link Sweep.result} for why. Aborting *before* the platform\n * has accepted the sweep — during compile, prepare or submission — rejects\n * the {@link QTSurfer.sweep} call itself with `QTSCanceledError`, since there\n * is no sweep yet and so no rows to keep.\n */\n signal?: AbortSignal;\n /** Called on stage transitions and after each poll with updated progress. */\n onProgress?: (p: SweepProgressEvent) => void;\n /**\n * Initial interval between polls. Default 2000ms, backed off up to\n * `maxPollIntervalMs`. Longer than a single backtest's, because a sweep is\n * many backtests and its leaderboard changes on the timescale of shards\n * finishing rather than ticks.\n */\n pollIntervalMs?: number;\n /** Upper bound for exponential backoff. Default 15000ms. */\n maxPollIntervalMs?: number;\n /**\n * Per-stage timeout. Default none. A sweep is many backtests, so the execute\n * stage legitimately outlasts anything a single run would take.\n */\n timeoutMs?: number;\n /**\n * How to order the leaderboard the poll reads. Omit to send no preference and\n * take the platform default, which is `'plateau'` — so leaving this unset\n * does **not** give you raw objective order. What was actually applied is\n * reported on the result. **Ignored entirely when `order` is `'natural'`**,\n * which is always ordered by `runIx`; the platform accepts both and answers\n * with the ordering it applied.\n */\n ranking?: SweepRanking;\n /**\n * Which view of the rows the background poll reads. Omit to take the platform\n * default, `'ranked'` — the sorted, display-capped leaderboard. `'natural'`\n * returns every available row untruncated.\n *\n * This only decides what {@link Sweep.result} resolves with. Reading the same\n * sweep another way afterwards — to reach the rows a `truncated` ranked view\n * dropped, say — is {@link Sweep.results}, which re-reads rather than\n * re-running.\n */\n order?: SweepOrder;\n}\n\n// ---------------------------------------------------------------------------\n// Handle\n// ---------------------------------------------------------------------------\n\n/**\n * Handle for a running parameter sweep, returned by {@link QTSurfer.sweep} once\n * the platform has accepted it. The leaderboard keeps being polled in the\n * background.\n */\nexport interface Sweep {\n /** Server-side sweep identifier. */\n readonly sweepId: string;\n /**\n * The prepared dataset every trial ran against — the prepare jobId the\n * workflow resolved before submitting, which is also what addresses this\n * sweep on the wire.\n *\n * This is the value the workflow prepared with, not the acceptance echo of\n * it. {@link Sweep.accepted} carries the echo, unmodified, for anyone who\n * wants to compare the two.\n */\n readonly requestId: string;\n /** The compiled strategy every trial shares. */\n readonly strategyId: string;\n /**\n * What acceptance already answered, before a single trial has run, exactly as\n * the platform sent it.\n *\n * Three of its fields are the reason this is exposed rather than folded away:\n *\n * - `seed` — the effective seed, generated platform-side when the request\n * omitted one. Submitting it again is what makes a randomly sampled sweep\n * replayable.\n * - `queued` — `false` means an identical sweep already existed and nothing\n * new was enqueued. The handle is still valid and still resolves; it is\n * just reading a sweep this call did not start.\n * - `walkForward` — present exactly when this is a walk-forward sweep. It is\n * the discriminator, and it is available here immediately, so code watching\n * progress can branch on the answer's shape without waiting for\n * {@link Sweep.result}.\n */\n readonly accepted: SweepAccepted;\n /**\n * Local snapshot of the sweep lifecycle; reading it does not contact the\n * server.\n */\n readonly state: SweepState;\n /**\n * Resolves with the final leaderboard once the sweep stops advancing.\n *\n * **This resolves on every terminal status, cancellation included** —\n * `'COMPLETED'`, `'PARTIAL'` and `'CANCELLED'` all hand back the result\n * rather than raising. That is a deliberate divergence from `backtest()`,\n * which rejects with `QTSCanceledError` when its run is aborted: cancelling a\n * sweep is documented as leaving completed rows readable, and throwing them\n * away would lose the only reason to cancel a sweep late rather than early.\n * Read `status` to find out which of the three you got. The promise rejects\n * only for transport failures, HTTP errors, and stage timeouts.\n *\n * `'PARTIAL'` means at least one unit of work died and its runs are simply\n * missing. There is no failed status for a sweep as a whole, so a sweep whose\n * every shard died is `'PARTIAL'` with an empty leaderboard — check\n * `leaderboardSize` before reading anything into a top row.\n *\n * ### Reading the leaderboard\n *\n * **The default order is not the raw objective order.** It is plateau order,\n * and `ranking` on the result says which was actually applied — not always\n * the one requested, because a sweep with no stored parameter grid cannot be\n * plateau-ranked and falls back to raw. See {@link SweepRanking}.\n *\n * **The default view is capped.** When `truncated` is `true`, rows exist that\n * the leaderboard does not carry — `leaderboardSize` counts what is\n * available. {@link Sweep.results} with `order: 'natural'` is what returns\n * all of them, in `runIx` order and with no ranking applied. That is a\n * re-read of this same sweep, not a second one.\n *\n * **`plateauScore` and `neighbourCount` are read together.** A neighbour\n * count of `0` means the point had no neighbours in the grid to compare\n * against, so its plateau score is unevidenced rather than confirmed — on its\n * own it is indistinguishable from a genuinely robust one.\n *\n * **`deflatedSharpe`** is the probability that a row's Sharpe reflects real\n * edge rather than the best draw from however many vectors were tried. Around\n * 0.95 and up it survives the multiple-testing correction; near 0.5 or below\n * it is not distinguishable from the best of a pile of coin flips. It is\n * absent on aborted runs, and on sweeps with too few trials to establish any\n * dispersion to deflate against.\n *\n * **`pbo`** is the probability of backtest overfitting for the sweep as a\n * whole: how often the configuration that won in-sample lands below median\n * out-of-sample. Above roughly 0.5 the sweep is selecting noise, and that\n * verdict is about the search, not about any one row — a high value\n * discredits the top row however good it looks. It is computed once the last\n * unit of work finishes, so it is absent while the sweep is still running and\n * on sweeps too small for the statistic to mean anything.\n *\n * ### A walk-forward sweep answers in a different shape\n *\n * `walkForward` is the discriminator, and it appears as soon as the sweep is\n * accepted — before any fold has finished — so it is safe to branch on while\n * polling (it is also on {@link Sweep.accepted}). When it is present, the\n * leaderboard is one row per *completed fold*: that fold's winner as it\n * scored out-of-sample, with **`runIx` carrying the fold index rather than a\n * position in the grid**. No plateau score, deflated Sharpe or PBO figure is\n * reported for one — the out-of-sample numbers are already the honest\n * measurement. See {@link WalkForwardResult} for why an absent `paramDrift`\n * is not a zero.\n *\n * ### An empty leaderboard is not always an empty answer\n *\n * A sweep can finish having scored nothing, because every shard failed before\n * producing a row. When that happens `failReason` carries the cause reported\n * by the *first* shard to fail — typically something the whole grid would have\n * hit, such as a strategy that could not be loaded. Read it before concluding\n * that a sweep with no rows simply found nothing: those are different\n * outcomes and the leaderboard alone cannot tell them apart. Only the first\n * failure is recorded, so where several shards failed for different reasons\n * this names one of them rather than summarising all — pair it with\n * `progress.failedShards` for the count.\n */\n readonly result: Promise<SweepResult>;\n /**\n * Re-read this sweep's rows under a different view.\n *\n * **This is a read, not a re-run.** It compiles nothing, prepares nothing and\n * submits nothing: the same sweep is asked for its rows again with different\n * query parameters, so no second sweep is created and nothing is enqueued.\n * The view a {@link SweepOptions} chose applies to the background poll behind\n * {@link Sweep.result}; this is how to look at the same sweep another way\n * afterwards.\n *\n * **It is the route to rows the ranked view dropped.** When `truncated` is\n * `true` on a result, rows exist that the leaderboard does not carry;\n * `order: 'natural'` returns every available row untruncated, in\n * deterministic `runIx` order.\n *\n * **`ranking` is ignored when `order` is `'natural'`** — that view is always\n * ordered by `runIx`, and the response reports `'raw'`. The platform accepts\n * both rather than rejecting the pair, and answers with the ordering it\n * actually applied.\n *\n * Readable while the sweep is still running, in which case it returns the\n * rows finished so far — exactly like {@link Sweep.sensitivity}. Like every\n * handle-scoped call, it does not take part in an\n * {@link AuthenticatedClient}'s refresh-on-401 policy.\n *\n * @param view which view to read; an absent property takes the platform\n * default (`order: 'ranked'`, `ranking: 'plateau'`)\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\n results(view?: { order?: SweepOrder; ranking?: SweepRanking }): Promise<SweepResult>;\n /**\n * How the objective moves as each parameter moves — the question a\n * leaderboard cannot answer. A leaderboard says which point won; a sweep can\n * spend its whole budget on an axis that never moved the objective at all,\n * and the top rows hide that completely.\n *\n * A *marginal* takes one axis and collapses every other one: for each value\n * of that axis it aggregates every run that used it, whatever the rest of the\n * parameters were. A flat marginal means the axis did not matter over the\n * range swept. `best`, `mean` and `worst` are all reported because them\n * disagreeing is the signal — a value with a high best and a poor mean only\n * works in specific company, which is an interaction, and a single number\n * would hide it. A *heatmap* does the same over a pair of axes, where that\n * interaction is visible directly.\n *\n * **Check `heatmapsTruncated`.** Marginals are always complete; the pair\n * surfaces are quadratic in the axis count and may be capped to stay inside\n * the response budget. When the flag is `true`, at least one pair was left\n * out, so the list you have is not the full set of interactions. This method\n * hands back the whole {@link SweepSensitivity} rather than just its surfaces\n * precisely so that flag cannot be lost on the way out.\n *\n * Readable while the sweep is still running, in which case the aggregates\n * describe the runs finished so far and `rowsAnalysed` says how many that\n * was. Aborted runs are excluded throughout: a run that threw measured\n * nothing, and counting it as a bad outcome would invent evidence against a\n * parameter value that was never really tested.\n *\n * Like every handle-scoped call, this does not take part in an\n * {@link AuthenticatedClient}'s refresh-on-401 policy.\n *\n * @param objective which metric to aggregate; omit to use the objective the\n * sweep was submitted with\n * @throws QTSError on any non-2xx response, with the HTTP status on `status`.\n */\n sensitivity(objective?: SweepObjective): Promise<SweepSensitivity>;\n}\n\n// ---------------------------------------------------------------------------\n// Workflow\n// ---------------------------------------------------------------------------\n\n/** Initial interval between polls of a sweep's leaderboard. */\nconst DEFAULT_POLL_INTERVAL_MS = 2000;\n/** Backoff ceiling for a sweep's leaderboard poll. */\nconst DEFAULT_MAX_POLL_INTERVAL_MS = 15000;\n\n/**\n * Orchestrate compile → prepare → executeSweep, then poll the leaderboard until\n * the sweep stops advancing.\n *\n * One call, not composable stages, and deliberately so: the execute-sweep\n * endpoint is addressed by the id of an already-prepared dataset, so a\n * stage-level API would hand dataset lifecycle to the caller for no gain.\n * Preparing is idempotent — the same instrument and window always resolve to\n * the same job — so preparing on every sweep duplicates no work.\n */\nexport async function sweep(req: SweepRequest, opts: SweepOptions = {}): Promise<Sweep> {\n validateRequest(req);\n const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS, DEFAULT_MAX_POLL_INTERVAL_MS);\n\n opts.onProgress?.({ stage: 'compiling' });\n const strategyId = await compileStrategySource(req.strategy, opts.signal);\n\n opts.onProgress?.({ stage: 'preparing' });\n const requestId = await prepareData(req, policy, opts);\n\n opts.onProgress?.({ stage: 'executing' });\n const { data, error } = await executeSweep({\n path: { exchangeId: req.exchangeId, type: TICKER, requestId },\n body: buildSweepBody(req, strategyId),\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSExecutionError('Sweep submission failed', error);\n if (!data?.sweepId) throw new QTSExecutionError('Missing sweepId in executeSweep response');\n\n // The handle addresses the sweep with the requestId of the dataset just prepared — the value\n // this workflow already knows — rather than the acceptance echo of it. `data` is stored as the\n // server sent it; nothing is written back onto a generated response model.\n return createHandle(req, opts, policy, data, requestId, strategyId);\n}\n\n/**\n * Reject the request shapes the platform would only reject over the network.\n * Same set as the sibling Java SDK validates, so both answer the same question\n * the same way.\n */\nfunction validateRequest(req: SweepRequest): void {\n validatePrepareTarget('sweep', req);\n\n const names = Object.keys(req.params ?? {});\n if (names.length === 0) {\n throw new QTSError('sweep: params must hold at least one axis');\n }\n for (const name of names) {\n const axis = req.params[name];\n if ('values' in axis) {\n if (axis.values.length === 0) {\n throw new QTSError(`sweep: axis \"${name}\" must hold at least one value`);\n }\n } else if (!(axis.step > 0)) {\n throw new QTSError(`sweep: axis \"${name}\" needs step > 0, got ${axis.step}`);\n }\n }\n\n const wf = req.walkForward;\n if (!wf) return;\n if (wf.folds < 2) {\n throw new QTSError(`sweep: walkForward.folds must be >= 2, got ${wf.folds}`);\n }\n if (wf.inSamplePct !== undefined && (wf.inSamplePct < 10 || wf.inSamplePct > 90)) {\n throw new QTSError(\n `sweep: walkForward.inSamplePct must be within 10..90, got ${wf.inSamplePct}`,\n );\n }\n}\n\nfunction prepareData(\n req: SweepRequest,\n policy: StagePolicy,\n opts: SweepOptions,\n): Promise<string> {\n return prepareDataset(\n {\n exchangeId: req.exchangeId,\n ...(req.instrument !== undefined ? { instrument: req.instrument } : {}),\n ...(req.datasetId !== undefined ? { datasetId: req.datasetId } : {}),\n ...(req.datasetVersionId !== undefined ? { datasetVersionId: req.datasetVersionId } : {}),\n from: req.from,\n to: req.to,\n },\n policy,\n {\n ...(opts.signal ? { signal: opts.signal } : {}),\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n onPercent: (percent) => opts.onProgress?.({ stage: 'preparing', percent }),\n // A thinly covered window is about to be scored once per parameter vector, so the\n // coverage ratio is worth at least as much here as on a single backtest.\n onPrepared: (state) =>\n opts.onProgress?.({\n stage: 'preparing',\n percent: 100,\n coverageRatio: state.coverageRatio,\n }),\n },\n );\n}\n\nfunction buildSweepBody(req: SweepRequest, strategyId: string): ExecuteSweepRequest {\n const spec: SweepSpecRequest = { params: req.params };\n if (req.sampler !== undefined) spec.sampler = req.sampler;\n if (req.samples !== undefined) spec.samples = req.samples;\n if (req.seed !== undefined) spec.seed = req.seed;\n if (req.objective !== undefined) spec.objective = req.objective;\n\n const body: ExecuteSweepRequest = { strategyId, sweep: spec };\n if (req.walkForward) {\n body.walkForward = {\n folds: req.walkForward.folds,\n ...(req.walkForward.inSamplePct !== undefined\n ? { inSamplePct: req.walkForward.inSamplePct }\n : {}),\n };\n }\n return body;\n}\n\nfunction createHandle(\n req: SweepRequest,\n opts: SweepOptions,\n policy: StagePolicy,\n accepted: SweepAccepted,\n requestId: string,\n strategyId: string,\n): Sweep {\n const sweepId = accepted.sweepId;\n const path = { exchangeId: req.exchangeId, type: TICKER, requestId, sweepId };\n let state: SweepState = 'executing';\n\n const withQuery = viewQuery(opts);\n\n const result = (async () => {\n try {\n // Deliberately runs without `opts.signal`: aborting a sweep must not stop the poll, because\n // the rows already scored are only reachable by polling on until the platform reports the\n // sweep CANCELLED. The abort listener below asks the platform to stop instead.\n const finalResult = await runStage(\n policy,\n async ({ signal }) => {\n const res = await getSweepResult({ path, ...withQuery, signal });\n if (res.error) throw new QTSExecutionError('Sweep result request failed', res.error);\n if (!res.data) throw new QTSExecutionError('Empty sweep result response');\n return res.data;\n },\n {\n ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),\n onEachAttempt: (r) => {\n const percent = percentOf(r);\n opts.onProgress?.({\n stage: 'executing',\n ...(percent !== undefined ? { percent } : {}),\n snapshot: r.progress,\n });\n },\n },\n );\n state = normalizeStatus(finalResult.status) === 'aborted' ? 'canceled' : 'completed';\n return finalResult;\n } catch (err) {\n state = err instanceof QTSCanceledError ? 'canceled' : 'failed';\n throw err;\n }\n })();\n // A handle whose result is never awaited must not take the process down with an unhandled\n // rejection; the caller still sees the failure on their own `await`.\n void result.catch(() => undefined);\n\n const { signal } = opts;\n if (signal) {\n const requestCancel = (): void => {\n // Only ever 'executing' → 'canceled'; a cancel that lands after the last unit of work has\n // finished changes nothing, and the poll's own completion settles the state either way.\n if (state === 'executing') state = 'canceled';\n void cancelSweep({ path }).catch(() => undefined);\n };\n if (signal.aborted) {\n requestCancel();\n } else {\n signal.addEventListener('abort', requestCancel, { once: true });\n const detach = (): void => signal.removeEventListener('abort', requestCancel);\n void result.then(detach, detach);\n }\n }\n\n return {\n sweepId,\n requestId,\n strategyId,\n accepted,\n get state() {\n return state;\n },\n result,\n results: async (view = {}): Promise<SweepResult> => {\n // A read of the sweep that already exists: same path, different query. Nothing here\n // compiles, prepares or submits, so asking for the natural view costs one request rather\n // than a second pipeline.\n const res = await getSweepResult({ path, ...viewQuery(view) });\n if (res.error) {\n throw requestFailed('sweep results call', res.error, res.response?.status);\n }\n if (!res.data) throw new QTSError('Empty sweep result response');\n return res.data;\n },\n sensitivity: async (objective?: SweepObjective): Promise<SweepSensitivity> => {\n const res = await getSweepSensitivity({\n path,\n ...(objective !== undefined ? { query: { objective } } : {}),\n });\n if (res.error) {\n throw requestFailed('sweep sensitivity call', res.error, res.response?.status);\n }\n if (!res.data) throw new QTSError('Empty sweep sensitivity response');\n return res.data;\n },\n };\n}\n\n/**\n * The `order` / `ranking` query for one view of a sweep's rows.\n *\n * `ranking` is sent as asked even alongside `order: 'natural'`, which ignores\n * it: the platform accepts both and answers with the ordering it actually\n * applied, which is more informative than the SDK silently dropping the\n * preference. Left off entirely when neither is set, so a default read carries\n * no query string and takes the platform's own defaults.\n */\nfunction viewQuery(view: { order?: SweepOrder; ranking?: SweepRanking }): {\n query?: NonNullable<GetSweepResultData['query']>;\n} {\n const query: NonNullable<GetSweepResultData['query']> = {};\n if (view.order !== undefined) query.order = view.order;\n if (view.ranking !== undefined) query.ranking = view.ranking;\n return Object.keys(query).length > 0 ? { query } : {};\n}\n\n/**\n * Percentage of the sweep's runs that have finished. Deliberately computed from\n * the run-level counts: the shard counts alongside them partition units of\n * work, not runs, and mixing the two reports a percentage of neither.\n */\nfunction percentOf(result: SweepResult): number | undefined {\n const p = result.progress;\n if (!p || !(p.total > 0)) return undefined;\n return (p.done / p.total) * 100;\n}\n","import {\n authenticate as apiAuth,\n client as apiClient,\n type AuthTokenResponse,\n} from '@qtsurfer/api-client';\nimport { QTSAuthError } from '../errors';\nimport {\n backtest as runBacktest,\n type BacktestOptions,\n type BacktestRequest,\n type BacktestResult,\n} from '../workflows/backtest';\nimport {\n listExchanges,\n listInstruments,\n type Exchange,\n type InstrumentDetail,\n type InstrumentSegment,\n} from '../workflows/catalog';\nimport {\n downloadKlines,\n downloadTickers,\n} from '../workflows/downloads';\nimport {\n getStrategy,\n validateStrategy as runValidateStrategy,\n listStrategies,\n deleteStrategy as runDeleteStrategy,\n getStrategyCode,\n type StrategyState,\n type StrategyValidation,\n type StrategySummary,\n} from '../workflows/strategies';\nimport {\n sweep as runSweep,\n type Sweep,\n type SweepOptions,\n type SweepRequest,\n} from '../workflows/sweep';\nimport type { DownloadHourArgs } from '../client';\nimport { InMemoryTokenStore, type TokenStore } from './tokenStore';\n\nconst APIKEY_ENV_VAR = 'QTSURFER_APIKEY';\nconst DEFAULT_BASE_URL = 'https://api.qtsurfer.com/v1';\n\nexport interface AuthOptions {\n /** Base URL of the QTSurfer API. Defaults to the public production endpoint. */\n baseUrl?: string;\n /** Custom token store. Defaults to {@link InMemoryTokenStore}. */\n store?: TokenStore;\n /** Inject a custom `fetch` (Node 20+, browser, or test mock). */\n fetch?: typeof fetch;\n}\n\n/**\n * Authenticated SDK session.\n *\n * Returned by {@link authenticate}. Wraps the underlying api-client, owns a JWT\n * (in memory by default, or in the provided {@link TokenStore}), and\n * transparently re-exchanges the apikey for a fresh JWT on 401.\n *\n * Multi-session note: the session mutates the api-client singleton config\n * on every call. Concurrent sessions in the same process will race; today\n * the SDK targets the one-session-per-process pattern.\n */\nexport class AuthenticatedClient {\n readonly baseUrl: string;\n private readonly apikey: string;\n private readonly store: TokenStore;\n private readonly fetchImpl: typeof fetch | undefined;\n private cached: AuthTokenResponse | null = null;\n private refreshing: Promise<AuthTokenResponse> | null = null;\n\n constructor(apikey: string, opts: AuthOptions = {}) {\n this.apikey = apikey;\n this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;\n this.store = opts.store ?? new InMemoryTokenStore();\n this.fetchImpl = opts.fetch;\n }\n\n /** Currently cached token, if any. */\n get token(): AuthTokenResponse | null {\n return this.cached;\n }\n\n /** Force a fresh JWT exchange. Bypasses the cache. */\n async refresh(): Promise<AuthTokenResponse> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = (async () => {\n const { data, error, response } = await apiAuth({\n baseUrl: this.baseUrl,\n headers: { 'X-API-Key': this.apikey },\n ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),\n });\n if (error || !data) {\n throw new QTSAuthError(\n `authenticate() failed: HTTP ${response.status}`,\n error,\n );\n }\n this.cached = data;\n await this.store.save(data);\n return data;\n })();\n try {\n return await this.refreshing;\n } finally {\n this.refreshing = null;\n }\n }\n\n /**\n * Load a previously-persisted token from the store. If none, mint one.\n * Called automatically by every workflow method.\n */\n async ensureToken(): Promise<AuthTokenResponse> {\n if (this.cached) return this.cached;\n const stored = await this.store.load();\n if (stored) {\n this.cached = stored;\n return stored;\n }\n return this.refresh();\n }\n\n /** Drop the cached token (in memory and in the store). */\n async clear(): Promise<void> {\n this.cached = null;\n await this.store.clear();\n }\n\n /**\n * Run a call with the Bearer header pre-set; if it returns 401, refresh\n * once and retry. A second 401 surfaces to the caller.\n */\n private async withRefreshOn401<T>(call: () => Promise<T>): Promise<T> {\n await this.applyConfig();\n try {\n return await call();\n } catch (err) {\n if (!isUnauthorized(err)) throw err;\n this.cached = null;\n await this.refresh();\n await this.applyConfig();\n return call();\n }\n }\n\n private async applyConfig(): Promise<void> {\n const token = await this.ensureToken();\n apiClient.setConfig({\n baseUrl: this.baseUrl,\n headers: { Authorization: `Bearer ${token.access_token}` },\n ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),\n });\n }\n\n // ---- Workflow surface (mirrors QTSurfer) ----\n\n /**\n * Run a backtest end-to-end (compile → prepare → execute), sending the\n * currently cached token (minting one first if none is cached). Unlike\n * `tickers()`/`klines()`, a `401` here is not auto-retried: the underlying\n * stage errors carry no HTTP status, so a token that expires mid-backtest\n * surfaces as `QTSPreparationError`/`QTSExecutionError` rather than\n * triggering a refresh.\n */\n backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return this.withRefreshOn401(() => runBacktest(req, opts));\n }\n\n /**\n * Run the full compile → prepare → executeSweep pipeline and resolve once the\n * platform has accepted the sweep, handing back a {@link Sweep} that keeps\n * polling the leaderboard in the background. See {@link QTSurfer.sweep} for\n * why the sweep is one call rather than composable stages, and\n * {@link Sweep.result} for how to read what it found.\n *\n * The currently cached token is sent (minting one first if none is cached),\n * but as with `backtest()` a `401` here is **not** auto-retried: the\n * underlying stage errors carry no HTTP status, so a token that expires\n * mid-pipeline surfaces as `QTSPreparationError`/`QTSExecutionError` rather\n * than triggering a refresh.\n *\n * The background leaderboard poll and {@link Sweep.sensitivity} sit outside\n * the policy for a second, independent reason: both run after this promise\n * has already resolved, so a token that expires while a sweep is in flight\n * surfaces on {@link Sweep.result} whatever the stage errors carry.\n */\n sweep(req: SweepRequest, opts?: SweepOptions): Promise<Sweep> {\n return this.withRefreshOn401(() => runSweep(req, opts));\n }\n\n /** Download one hour of raw tickers. Refreshes the token once on `401` before retrying. */\n tickers(args: DownloadHourArgs): Promise<Blob> {\n return this.withRefreshOn401(() => downloadTickers(args));\n }\n\n /** Download one hour of klines. Refreshes the token once on `401` before retrying. */\n klines(args: DownloadHourArgs): Promise<Blob> {\n return this.withRefreshOn401(() => downloadKlines(args));\n }\n\n /**\n * List the exchanges the platform serves. Refreshes the token once on `401`\n * before retrying.\n */\n exchanges(): Promise<Exchange[]> {\n return this.withRefreshOn401(() => listExchanges());\n }\n\n /**\n * List an exchange's instruments, optionally for a specific segment.\n * Refreshes the token once on `401` before retrying. See\n * {@link QTSurfer.instruments} for what the unwrapped HAL envelope leaves\n * out.\n */\n instruments(\n exchangeId: string,\n segment?: InstrumentSegment,\n ): Promise<InstrumentDetail[]> {\n return this.withRefreshOn401(() => listInstruments(exchangeId, segment));\n }\n\n /**\n * Ask the platform to check that a registered strategy can actually run.\n * Refreshes the token once on `401` before retrying. Two-outcome — see\n * {@link QTSurfer.validateStrategy}; `queued: true` is not terminal and\n * must be followed by polling {@link AuthenticatedClient.strategy} under a\n * deadline of your own.\n */\n validateStrategy(strategyId: string): Promise<StrategyValidation> {\n return this.withRefreshOn401(() => runValidateStrategy(strategyId));\n }\n\n /**\n * Read a strategy's recorded state, including its validation verdict.\n * Refreshes the token once on `401` before retrying. See\n * {@link StrategyState} for why a `'passed'` verdict is a floor rather than\n * a guarantee.\n */\n strategy(strategyId: string): Promise<StrategyState> {\n return this.withRefreshOn401(() => getStrategy(strategyId));\n }\n\n /**\n * List every strategy you have registered and not deleted, most recently\n * compiled first. Refreshes the token once on `401` before retrying. See\n * {@link QTSurfer.strategies}.\n */\n strategies(): Promise<StrategySummary[]> {\n return this.withRefreshOn401(() => listStrategies());\n }\n\n /**\n * Release a registered strategy. Refreshes the token once on `401` before\n * retrying. See {@link QTSurfer.deleteStrategy} for what this does and\n * does not undo.\n */\n deleteStrategy(strategyId: string): Promise<void> {\n return this.withRefreshOn401(() => runDeleteStrategy(strategyId));\n }\n\n /**\n * Read back a strategy's exact registered source. Refreshes the token\n * once on `401` before retrying. See {@link QTSurfer.strategyCode} for\n * what its `404` covers.\n */\n strategyCode(strategyId: string): Promise<string> {\n return this.withRefreshOn401(() => getStrategyCode(strategyId));\n }\n}\n\n/**\n * Exchange a long-lived API key for an authenticated session.\n *\n * If `apikey` is omitted, the SDK reads `QTSURFER_APIKEY` from the\n * environment. The returned {@link AuthenticatedClient} caches the JWT,\n * refreshes it on 401, and exposes the same surface as `QTSurfer`\n * (`backtest`, `sweep`, `tickers`, `klines`, `exchanges`, `instruments`,\n * `validateStrategy`, `strategy`, `strategies`, `deleteStrategy`,\n * `strategyCode`).\n *\n * @throws {QTSAuthError} if no apikey is supplied or available in env.\n */\nexport async function authenticate(\n apikey?: string,\n opts: AuthOptions = {},\n): Promise<AuthenticatedClient> {\n const resolved = apikey ?? readEnvApikey();\n if (!resolved) {\n throw new QTSAuthError(\n `authenticate() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`,\n );\n }\n const session = new AuthenticatedClient(resolved, opts);\n await session.ensureToken();\n return session;\n}\n\nfunction readEnvApikey(): string | undefined {\n // `process` is undefined in browser bundlers; guard explicitly.\n if (typeof process === 'undefined' || !process.env) return undefined;\n const value = process.env[APIKEY_ENV_VAR];\n return value && value.length > 0 ? value : undefined;\n}\n\nfunction isUnauthorized(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n // SDK-thrown errors (QTSDownloadError, etc.) carry the HTTP status on\n // a top-level `status` field. Workflow errors that don't yet expose\n // status default to non-401.\n const maybeStatus = (err as { status?: unknown }).status;\n if (typeof maybeStatus === 'number' && maybeStatus === 401) return true;\n return false;\n}\n","import type { AuthTokenResponse } from '@qtsurfer/api-client';\n\n/**\n * Pluggable token persistence interface.\n *\n * The SDK ships an {@link InMemoryTokenStore} by default. Adopters can\n * implement this contract to back tokens by browser `localStorage`, an\n * on-disk file, a secret manager, etc.\n *\n * The SDK calls {@link load} once per session-startup to seed a cached\n * token (if any), {@link save} after every successful `authenticate()` / refresh,\n * and {@link clear} when the session is explicitly invalidated.\n */\nexport interface TokenStore {\n /** Return the previously persisted token, or `null` if none. */\n load(): AuthTokenResponse | null | Promise<AuthTokenResponse | null>;\n /** Persist the token returned by `POST /v1/auth/token`. */\n save(token: AuthTokenResponse): void | Promise<void>;\n /** Drop any persisted token. */\n clear(): void | Promise<void>;\n}\n\n/**\n * Default {@link TokenStore} — holds the token in a single in-memory slot.\n * Lost on process exit. Sufficient for short-lived scripts and tests.\n */\nexport class InMemoryTokenStore implements TokenStore {\n private token: AuthTokenResponse | null = null;\n\n load(): AuthTokenResponse | null {\n return this.token;\n }\n\n save(token: AuthTokenResponse): void {\n this.token = token;\n }\n\n clear(): void {\n this.token = null;\n }\n}\n"],"mappings":"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","names":["describe","DEFAULT_POLL_INTERVAL_MS","DEFAULT_MAX_POLL_INTERVAL_MS","prepareData","signal","apiClient","apiClient"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qtsurfer/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@qtsurfer/api-client": "^0.
|
|
54
|
+
"@qtsurfer/api-client": "^0.11.0",
|
|
55
55
|
"cockatiel": "^3.2.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
package/src/client.ts
CHANGED
|
@@ -111,7 +111,8 @@ export class QTSurfer {
|
|
|
111
111
|
* if a stage exceeds `timeoutMs`, or {@link QTSCanceledError} if the caller's
|
|
112
112
|
* signal fires before the sweep is accepted. A plain {@link QTSError} means
|
|
113
113
|
* the request itself is malformed (an empty grid, a non-positive `step`, a
|
|
114
|
-
* walk-forward block with fewer than two folds
|
|
114
|
+
* walk-forward block with fewer than two folds, or naming both/neither of
|
|
115
|
+
* `instrument`/`datasetId`) and never reached the network.
|
|
115
116
|
*
|
|
116
117
|
* What the sweep *found* arrives through {@link Sweep.result}, which is also
|
|
117
118
|
* where the semantics of the leaderboard are documented. Acceptance already
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type DataSourceType,
|
|
6
6
|
type PrepareJobState,
|
|
7
7
|
} from '@qtsurfer/api-client';
|
|
8
|
-
import { QTSCanceledError, QTSPreparationError, QTSStrategyCompileError } from '../errors';
|
|
8
|
+
import { QTSCanceledError, QTSError, QTSPreparationError, QTSStrategyCompileError } from '../errors';
|
|
9
9
|
import { normalizeStatus, runStage, type StagePolicy } from './polling';
|
|
10
10
|
|
|
11
11
|
/** The only data source the workflows prepare against today. @internal */
|
|
@@ -45,14 +45,52 @@ export async function compileStrategySource(
|
|
|
45
45
|
return data.strategyId;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The instrument (or dataset) and window one prepare covers. Exactly one of
|
|
50
|
+
* `instrument`/`datasetId` is set — {@link validatePrepareTarget} enforces that
|
|
51
|
+
* before any network call. `datasetId` pairs with the reserved `exchangeId: 'user'`.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
49
55
|
export interface PrepareTarget {
|
|
50
56
|
exchangeId: string;
|
|
51
|
-
instrument
|
|
57
|
+
instrument?: string;
|
|
58
|
+
/** Id of a previously uploaded dataset, in place of `instrument`. */
|
|
59
|
+
datasetId?: string;
|
|
60
|
+
/** Optional specific version of `datasetId`; requires `datasetId`. */
|
|
61
|
+
datasetVersionId?: string;
|
|
52
62
|
from: string;
|
|
53
63
|
to: string;
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Reject a prepare target that names both, or neither, of `instrument`/`datasetId`,
|
|
68
|
+
* or that sets `datasetVersionId` without `datasetId` — the same three shapes the
|
|
69
|
+
* platform would only reject over the network. Called first thing by each workflow,
|
|
70
|
+
* before compiling the strategy, so a malformed request never reaches the network.
|
|
71
|
+
*
|
|
72
|
+
* @param workflow name of the caller, used to prefix the error message (e.g. `'backtest'`)
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export function validatePrepareTarget(
|
|
76
|
+
workflow: string,
|
|
77
|
+
target: Pick<PrepareTarget, 'instrument' | 'datasetId' | 'datasetVersionId'>,
|
|
78
|
+
): void {
|
|
79
|
+
const hasInstrument = target.instrument !== undefined;
|
|
80
|
+
const hasDataset = target.datasetId !== undefined;
|
|
81
|
+
if (hasInstrument && hasDataset) {
|
|
82
|
+
throw new QTSError(`${workflow}: exactly one of instrument/datasetId is required, got both`);
|
|
83
|
+
}
|
|
84
|
+
if (!hasInstrument && !hasDataset) {
|
|
85
|
+
throw new QTSError(
|
|
86
|
+
`${workflow}: exactly one of instrument/datasetId is required, got neither`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (target.datasetVersionId !== undefined && !hasDataset) {
|
|
90
|
+
throw new QTSError(`${workflow}: datasetVersionId requires datasetId`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
56
94
|
/** Reporting and cancellation for {@link prepareDataset}. @internal */
|
|
57
95
|
export interface PrepareRun {
|
|
58
96
|
signal?: AbortSignal;
|
|
@@ -82,7 +120,15 @@ export async function prepareDataset(
|
|
|
82
120
|
): Promise<string> {
|
|
83
121
|
const { data, error } = await prepareBacktest({
|
|
84
122
|
path: { exchangeId: target.exchangeId, type: TICKER },
|
|
85
|
-
body: {
|
|
123
|
+
body: {
|
|
124
|
+
...(target.instrument !== undefined ? { instrument: target.instrument } : {}),
|
|
125
|
+
...(target.datasetId !== undefined ? { datasetId: target.datasetId } : {}),
|
|
126
|
+
...(target.datasetVersionId !== undefined
|
|
127
|
+
? { datasetVersionId: target.datasetVersionId }
|
|
128
|
+
: {}),
|
|
129
|
+
from: target.from,
|
|
130
|
+
to: target.to,
|
|
131
|
+
},
|
|
86
132
|
...(run.signal ? { signal: run.signal } : {}),
|
|
87
133
|
});
|
|
88
134
|
if (error) throw new QTSPreparationError('Prepare submission failed', error);
|
|
@@ -15,15 +15,47 @@ import {
|
|
|
15
15
|
TICKER,
|
|
16
16
|
compileStrategySource,
|
|
17
17
|
prepareDataset,
|
|
18
|
+
validatePrepareTarget,
|
|
18
19
|
} from '../internal/preparation';
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* ```ts
|
|
23
|
+
* const request: BacktestRequest = {
|
|
24
|
+
* strategy: source,
|
|
25
|
+
* exchangeId: 'binance',
|
|
26
|
+
* instrument: 'BTC/USDT',
|
|
27
|
+
* from: '2026-01-01T00:00:00Z',
|
|
28
|
+
* to: '2026-02-01T00:00:00Z',
|
|
29
|
+
* };
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Backtest against a dataset you uploaded instead of an exchange instrument by
|
|
33
|
+
* replacing `instrument` with `datasetId` (and `exchangeId: 'user'`):
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* const request: BacktestRequest = {
|
|
37
|
+
* strategy: source,
|
|
38
|
+
* exchangeId: 'user',
|
|
39
|
+
* datasetId: 'ds_123',
|
|
40
|
+
* from: '2026-01-01T00:00:00Z',
|
|
41
|
+
* to: '2026-02-01T00:00:00Z',
|
|
42
|
+
* };
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
20
45
|
export interface BacktestRequest {
|
|
21
46
|
/** Strategy source code (Java) */
|
|
22
47
|
strategy: string;
|
|
23
|
-
/** Exchange id, e.g. `binance` */
|
|
48
|
+
/** Exchange id, e.g. `binance`, or the reserved value `user` when backtesting against `datasetId`. */
|
|
24
49
|
exchangeId: string;
|
|
25
|
-
/** Instrument symbol, e.g. `BTC/USDT` */
|
|
26
|
-
instrument
|
|
50
|
+
/** Instrument symbol, e.g. `BTC/USDT`. Exactly one of `instrument`/`datasetId` is required. */
|
|
51
|
+
instrument?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Id of a dataset you uploaded, in place of `instrument`. Exactly one of
|
|
54
|
+
* `instrument`/`datasetId` is required. Pairs with `exchangeId: 'user'`.
|
|
55
|
+
*/
|
|
56
|
+
datasetId?: string;
|
|
57
|
+
/** Optional specific version of `datasetId`; omit to use its current version. Requires `datasetId`. */
|
|
58
|
+
datasetVersionId?: string;
|
|
27
59
|
/** Date range start (ISO-8601, ISO DATE or BASIC ISO DATE) */
|
|
28
60
|
from: string;
|
|
29
61
|
/** Date range end (same formats as `from`) */
|
|
@@ -78,6 +110,7 @@ export async function backtest(
|
|
|
78
110
|
req: BacktestRequest,
|
|
79
111
|
opts: BacktestOptions = {},
|
|
80
112
|
): Promise<BacktestResult> {
|
|
113
|
+
validatePrepareTarget('backtest', req);
|
|
81
114
|
const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS, DEFAULT_MAX_POLL_INTERVAL_MS);
|
|
82
115
|
|
|
83
116
|
// 1. Compile strategy (single synchronous request)
|
|
@@ -101,7 +134,9 @@ function prepareData(
|
|
|
101
134
|
return prepareDataset(
|
|
102
135
|
{
|
|
103
136
|
exchangeId: req.exchangeId,
|
|
104
|
-
instrument: req.instrument,
|
|
137
|
+
...(req.instrument !== undefined ? { instrument: req.instrument } : {}),
|
|
138
|
+
...(req.datasetId !== undefined ? { datasetId: req.datasetId } : {}),
|
|
139
|
+
...(req.datasetVersionId !== undefined ? { datasetVersionId: req.datasetVersionId } : {}),
|
|
105
140
|
from: req.from,
|
|
106
141
|
to: req.to,
|
|
107
142
|
},
|
package/src/workflows/sweep.ts
CHANGED
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
runStage,
|
|
26
26
|
type StagePolicy,
|
|
27
27
|
} from '../internal/polling';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
TICKER,
|
|
30
|
+
compileStrategySource,
|
|
31
|
+
prepareDataset,
|
|
32
|
+
validatePrepareTarget,
|
|
33
|
+
} from '../internal/preparation';
|
|
29
34
|
import { requestFailed } from '../internal/requestError';
|
|
30
35
|
import type { BacktestStage } from './backtest';
|
|
31
36
|
|
|
@@ -291,14 +296,35 @@ export type WalkForwardFold = ApiWalkForwardFold;
|
|
|
291
296
|
* objective: 'sharpe',
|
|
292
297
|
* };
|
|
293
298
|
* ```
|
|
299
|
+
*
|
|
300
|
+
* Sweep against a dataset you uploaded instead of an exchange instrument by
|
|
301
|
+
* replacing `instrument` with `datasetId` (and `exchangeId: 'user'`):
|
|
302
|
+
*
|
|
303
|
+
* ```ts
|
|
304
|
+
* const request: SweepRequest = {
|
|
305
|
+
* strategy: source,
|
|
306
|
+
* exchangeId: 'user',
|
|
307
|
+
* datasetId: 'ds_123',
|
|
308
|
+
* from: '2026-01-01T00:00:00Z',
|
|
309
|
+
* to: '2026-02-01T00:00:00Z',
|
|
310
|
+
* params: { rsiPeriod: { from: 7, to: 28, step: 1 } },
|
|
311
|
+
* };
|
|
312
|
+
* ```
|
|
294
313
|
*/
|
|
295
314
|
export interface SweepRequest {
|
|
296
315
|
/** Strategy source code (Java), compiled once and reused by every trial. */
|
|
297
316
|
strategy: string;
|
|
298
|
-
/** Exchange id, e.g. `binance`. */
|
|
317
|
+
/** Exchange id, e.g. `binance`, or the reserved value `user` when sweeping against `datasetId`. */
|
|
299
318
|
exchangeId: string;
|
|
300
|
-
/** Instrument symbol, e.g. `BTC/USDT`. */
|
|
301
|
-
instrument
|
|
319
|
+
/** Instrument symbol, e.g. `BTC/USDT`. Exactly one of `instrument`/`datasetId` is required. */
|
|
320
|
+
instrument?: string;
|
|
321
|
+
/**
|
|
322
|
+
* Id of a dataset you uploaded, in place of `instrument`. Exactly one of
|
|
323
|
+
* `instrument`/`datasetId` is required. Pairs with `exchangeId: 'user'`.
|
|
324
|
+
*/
|
|
325
|
+
datasetId?: string;
|
|
326
|
+
/** Optional specific version of `datasetId`; omit to use its current version. Requires `datasetId`. */
|
|
327
|
+
datasetVersionId?: string;
|
|
302
328
|
/** Range start (ISO-8601, ISO DATE, or BASIC ISO DATE). */
|
|
303
329
|
from: string;
|
|
304
330
|
/** Range end (same formats as `from`; must be later than `from`). */
|
|
@@ -655,6 +681,8 @@ export async function sweep(req: SweepRequest, opts: SweepOptions = {}): Promise
|
|
|
655
681
|
* the same way.
|
|
656
682
|
*/
|
|
657
683
|
function validateRequest(req: SweepRequest): void {
|
|
684
|
+
validatePrepareTarget('sweep', req);
|
|
685
|
+
|
|
658
686
|
const names = Object.keys(req.params ?? {});
|
|
659
687
|
if (names.length === 0) {
|
|
660
688
|
throw new QTSError('sweep: params must hold at least one axis');
|
|
@@ -690,7 +718,9 @@ function prepareData(
|
|
|
690
718
|
return prepareDataset(
|
|
691
719
|
{
|
|
692
720
|
exchangeId: req.exchangeId,
|
|
693
|
-
instrument: req.instrument,
|
|
721
|
+
...(req.instrument !== undefined ? { instrument: req.instrument } : {}),
|
|
722
|
+
...(req.datasetId !== undefined ? { datasetId: req.datasetId } : {}),
|
|
723
|
+
...(req.datasetVersionId !== undefined ? { datasetVersionId: req.datasetVersionId } : {}),
|
|
694
724
|
from: req.from,
|
|
695
725
|
to: req.to,
|
|
696
726
|
},
|