@qtsurfer/sdk 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ResultMap, AuthTokenResponse } from '@qtsurfer/api-client';
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
3
  interface BacktestRequest {
4
4
  /** Strategy source code (Java) */
@@ -48,9 +48,641 @@ interface BacktestOptions {
48
48
  timeoutMs?: number;
49
49
  }
50
50
 
51
+ /**
52
+ * One exchange the platform serves. Alias for api-client's `Exchange`:
53
+ * `id` (what every other call takes as `exchangeId`), `name`, and an
54
+ * optional `description`.
55
+ */
56
+ type Exchange = Exchange$1;
57
+ /**
58
+ * One instrument on an exchange. Alias for api-client's `InstrumentDetail`:
59
+ * `id` / `base` / `quote`, plus optional `coverage` (the date windows for
60
+ * which tickers and klines actually exist, per data type), `lastPrice` and
61
+ * `volume24h`.
62
+ *
63
+ * `coverage` is what tells you whether a backtest range is downloadable at
64
+ * all; it is optional, and absent means the platform did not report one, not
65
+ * that there is no data.
66
+ */
67
+ type InstrumentDetail = InstrumentDetail$1;
68
+ /**
69
+ * A market segment of an exchange. `'spot'` is the default segment served
70
+ * when {@link QTSurfer.instruments} is called without one.
71
+ */
72
+ type InstrumentSegment = 'spot' | 'futures';
73
+
51
74
  /** Wire format for hourly tickers/klines downloads. */
52
75
  type DownloadFormat = 'lastra' | 'parquet';
53
76
 
77
+ /**
78
+ * Everything the platform records about a registered strategy. Alias for
79
+ * api-client's `StrategyState`.
80
+ *
81
+ * `validation` is the verdict and is one of:
82
+ *
83
+ * - `'not_validated'` — registered, never checked.
84
+ * - `'pending'` — a check was asked for and has not answered yet.
85
+ * - `'passed'` — the class loaded and survived its first event.
86
+ * - `'failed'` — it did not; `detail` says how.
87
+ *
88
+ * **`'passed'` is a floor, not a guarantee.** It means the compiled class
89
+ * could be instantiated and got through the first event of a short synthetic
90
+ * run — not the caller's instrument, not the caller's window, and not the
91
+ * rest of the run. It says nothing about whether the strategy is correct,
92
+ * profitable, or safe to run at scale. `dryRunIncomplete` marks a check that
93
+ * ran out of its budget, which makes a `'passed'` verdict a lower floor
94
+ * still, and makes an empty `notices` list no longer a clean bill of health.
95
+ *
96
+ * A verdict describes the bytecode that existed when it was recorded:
97
+ * `compiledAt` newer than `validatedAt` means the strategy was recompiled
98
+ * afterwards and the verdict no longer describes what would run.
99
+ *
100
+ * `_links.code`, when present, is a discovery link to this strategy's raw
101
+ * source (`GET /strategy/{strategyId}/code` — the same thing
102
+ * {@link QTSurfer.strategyCode} fetches by id, so there is no need to follow
103
+ * the link yourself). It is present on a full `StrategyState` body — this
104
+ * function's result, and {@link QTSurfer.validateStrategy}'s already-validated
105
+ * `200` — and **absent** from that same operation's `queued: true` (`202`)
106
+ * outcome, which is a deliberately partial stub. This field passes through
107
+ * unmodified from api-client, so it needs no unwrapping on the SDK's part.
108
+ */
109
+ type StrategyState = StrategyState$1;
110
+ /**
111
+ * Outcome of {@link QTSurfer.validateStrategy} — the SDK's rendering of the
112
+ * two answers that operation has, which the response body alone cannot tell
113
+ * apart.
114
+ *
115
+ * - `queued: false` — a verdict already existed for the current compilation
116
+ * and comes back in `state` unchanged; nothing new was queued. This is
117
+ * **not** the same as "terminal": a check queued by an earlier call can
118
+ * still be running, so read `state.validation` rather than treating
119
+ * `queued: false` as "there is an answer".
120
+ * - `queued: true` — a check was just queued. Nothing is known yet; poll
121
+ * {@link QTSurfer.strategy} until `validation` leaves `'pending'`.
122
+ */
123
+ type StrategyValidation = {
124
+ queued: false;
125
+ strategyId: string;
126
+ state: StrategyState;
127
+ } | {
128
+ queued: true;
129
+ strategyId: string;
130
+ state?: undefined;
131
+ };
132
+ /**
133
+ * One entry in {@link QTSurfer.strategies}'s result: the same provenance
134
+ * {@link QTSurfer.strategy} reports — `compiledAt`, `requiredSources` — but
135
+ * never `validation`, which is what keeps listing cheap no matter how many
136
+ * strategies you have registered. Check a specific strategy's verdict with
137
+ * {@link QTSurfer.strategy}.
138
+ *
139
+ * Note: the spec types this endpoint's `requiredSources` as a plain
140
+ * `string[]`, not the `'Ticker' | 'KLine' | 'FundingRate'` union that
141
+ * {@link StrategyState}'s own `requiredSources` carries — narrow it yourself
142
+ * if you need the literal type. Alias for api-client's `StrategySummary`.
143
+ */
144
+ type StrategySummary = StrategySummary$1;
145
+
146
+ /**
147
+ * The metric a sweep optimizes, and the one its leaderboard and its
148
+ * sensitivity surfaces are read against.
149
+ *
150
+ * One vocabulary throughout: the objective a {@link SweepRequest} is submitted
151
+ * with is the objective the leaderboard is ranked by, and the one
152
+ * {@link Sweep.sensitivity} aggregates unless a different one is asked for.
153
+ *
154
+ * - `'sharpe'` — risk-adjusted return; the platform default when a request names none.
155
+ * - `'sortino'` — downside-risk-adjusted return.
156
+ * - `'pnl'` — absolute net profit and loss.
157
+ * - `'maxdd'` — maximum drawdown.
158
+ */
159
+ type SweepObjective = 'sharpe' | 'sortino' | 'pnl' | 'maxdd';
160
+ /**
161
+ * How the parameter grid is turned into the list of vectors that actually run.
162
+ *
163
+ * - `'grid'` — every combination of every axis value. The platform default;
164
+ * cost is the product of the axis sizes.
165
+ * - `'random'` — uniformly random draws from the grid, capped at
166
+ * {@link SweepRequest.samples}.
167
+ * - `'lhs'` — Latin hypercube draws, which spread the sample more evenly than
168
+ * uniform random.
169
+ *
170
+ * `samples` is required by `'random'` and `'lhs'` and ignored by `'grid'`.
171
+ */
172
+ type SweepSampler = 'grid' | 'random' | 'lhs';
173
+ /**
174
+ * How the ranked leaderboard is ordered.
175
+ *
176
+ * The **platform default is `'plateau'`**, so the order a sweep answers with is
177
+ * *not* raw objective order unless you ask for it. A plateau score is the
178
+ * objective of the worst run in a point's immediate neighbourhood, so a point
179
+ * only ranks well when the region around it does too — which exists because
180
+ * the highest raw score is very often a spike that does not survive the
181
+ * parameters moving slightly.
182
+ *
183
+ * **What you request is not always what you get.** Read `ranking` on the
184
+ * result to find out which ordering was actually applied: a sweep with no
185
+ * stored parameter grid has no neighbourhood to score against and falls back
186
+ * to `'raw'`, and a walk-forward sweep is always `'raw'` because its
187
+ * leaderboard is one out-of-sample row per fold rather than a grid.
188
+ *
189
+ * This applies to the ranked view only. Alongside `order: 'natural'` it is
190
+ * **ignored** — that view is always ordered by `runIx`.
191
+ */
192
+ type SweepRanking = 'plateau' | 'raw';
193
+ /**
194
+ * Which view of a sweep's rows to read: the display leaderboard, or every row
195
+ * in a stable order.
196
+ *
197
+ * - `'ranked'` — the platform default. Sorted, and capped at a display limit;
198
+ * `truncated` on the result is `true` when the cap actually bit, in which
199
+ * case rows exist that this view does not carry. This is the view
200
+ * {@link SweepRanking} applies to.
201
+ * - `'natural'` — every available row, untruncated, in deterministic `runIx`
202
+ * order. The view to read when materialising durable trial rows rather than
203
+ * showing a top-N, and the only way to reach rows the ranked view dropped —
204
+ * {@link Sweep.results} reads it off an existing sweep without re-running it.
205
+ * {@link SweepRanking} is **ignored** here and the response reports `'raw'`;
206
+ * rank, plateau score and neighbour count belong to the ranked view and are
207
+ * not part of this one.
208
+ */
209
+ type SweepOrder = 'ranked' | 'natural';
210
+ /**
211
+ * Sweep lifecycle as observed by the SDK, readable off {@link Sweep.state}.
212
+ *
213
+ * - `'executing'` — submitted and being polled.
214
+ * - `'completed'` — finished. The platform's own status may still be `'PARTIAL'`.
215
+ * - `'failed'` — the poll itself failed: transport, HTTP, or a stage timeout.
216
+ * - `'canceled'` — the sweep was aborted and the platform reported it cancelled.
217
+ */
218
+ type SweepState = 'executing' | 'completed' | 'failed' | 'canceled';
219
+ /**
220
+ * One strategy property and the values a sweep should try for it: either a
221
+ * numeric range walked in fixed steps, or an explicit list.
222
+ *
223
+ * ```ts
224
+ * const params: Record<string, ParamAxis> = {
225
+ * rsiPeriod: { from: 7, to: 28, step: 1 },
226
+ * useTrendFilter: { values: [true, false] },
227
+ * };
228
+ * ```
229
+ *
230
+ * The two shapes are mutually exclusive on the wire, and mixing them is a
231
+ * request the platform rejects rather than reconciles. A list entry is a number
232
+ * or a boolean — the axis of a boolean flag is `{ values: [true, false] }`, not
233
+ * a range.
234
+ */
235
+ type ParamAxis = {
236
+ /** First value. */
237
+ from: number;
238
+ /** Last value the walk may reach. */
239
+ to: number;
240
+ /** Increment; must be greater than zero. */
241
+ step: number;
242
+ } | {
243
+ /** The values to try; at least one. */
244
+ values: Array<number | boolean>;
245
+ };
246
+ /**
247
+ * Opt a sweep into walk-forward validation.
248
+ *
249
+ * Attaching this changes what the sweep does, not just how much of it runs.
250
+ * Instead of scoring every parameter vector once over the whole range, the data
251
+ * is cut into sequential folds; each fold optimizes the whole grid on its own
252
+ * window and then scores only its winner on the window immediately after — data
253
+ * that winner was never chosen on. The question it answers is not "which
254
+ * parameters won" but "does re-optimizing this periodically actually work".
255
+ *
256
+ * Omit it and nothing about the sweep changes, including the shape of the
257
+ * response.
258
+ *
259
+ * **It costs folds × grid.** Four folds over a 500-point grid is roughly 2000
260
+ * backtests where the plain sweep is 500, which is why it is opt-in. The
261
+ * platform rejects the request outright when that product exceeds its sweep
262
+ * budget.
263
+ *
264
+ * **It is a different sweep, not a variant of one.** Two requests that differ
265
+ * only in this block do not deduplicate against each other.
266
+ *
267
+ * The answer arrives as `walkForward` on the {@link SweepResult} — see
268
+ * {@link Sweep.result} for how to read it.
269
+ */
270
+ interface SweepWalkForward {
271
+ /**
272
+ * How many sequential optimize-then-score windows to run. Two is the floor
273
+ * and the reason is structural rather than a tuning preference: parameter
274
+ * drift is measured between consecutive fold winners, and a single fold has
275
+ * no consecutive pair, so it would report the strongest possible stability
276
+ * having measured nothing. The ceiling is a platform setting; exceeding it is
277
+ * rejected.
278
+ */
279
+ folds: number;
280
+ /**
281
+ * Share of each fold's window spent optimizing, the rest being where its
282
+ * winner is scored. Omit to take the platform default. Lower values leave
283
+ * more data to score on and, on short sessions, are what let the requested
284
+ * fold count tile the data at all. Must be within 10..90.
285
+ */
286
+ inSamplePct?: number;
287
+ }
288
+ /**
289
+ * What the platform answered when it accepted the sweep, exactly as it sent it
290
+ * — see {@link Sweep.accepted} for the three fields that make it worth reading
291
+ * before any result exists.
292
+ */
293
+ type SweepAccepted = ExecuteSweepAccepted;
294
+ /**
295
+ * A sweep snapshot: status, progress, and the rows available for the selected
296
+ * view. Resolved by {@link Sweep.result}, which is also where the semantics of
297
+ * every field are documented.
298
+ */
299
+ type SweepResult = ExecuteSweepResult;
300
+ /**
301
+ * The platform's own progress record for a running sweep, carried on
302
+ * {@link SweepProgressEvent.snapshot}. Distinct from the event that wraps it:
303
+ * this is what the server reported, the event is what the SDK emitted.
304
+ *
305
+ * Two of its fields are easy to add together by mistake. `aborted` counts
306
+ * individual runs that executed and aborted — a row-level count. `failedShards`
307
+ * counts whole units of work (shards, or folds on a walk-forward sweep) that
308
+ * failed and will not be retried, having never reported anything. A shard that
309
+ * dies before producing a single row leaves `aborted` at zero, which is exactly
310
+ * why the second count exists; **summing them double-counts nothing and
311
+ * describes nothing**.
312
+ *
313
+ * `retrying` is not a failure count either — those units failed on something
314
+ * transient and are queued to be attempted again, so a sweep with a non-zero
315
+ * value there is still expected to finish.
316
+ *
317
+ * `etaSeconds` is **omitted, never zero** when it cannot be computed: a sweep
318
+ * with nothing finished has no observed rate to extrapolate from, and a zero
319
+ * would read as "about to finish". When present it runs conservative — it
320
+ * excludes queue wait entirely, and a sweep that spent part of its life being
321
+ * retried will have diluted the rate it is derived from.
322
+ */
323
+ type SweepProgress = SweepProgress$1;
324
+ /** One trial on a sweep's leaderboard. See {@link Sweep.result} for how to read it. */
325
+ type SweepRunRow = SweepRunRow$1;
326
+ /**
327
+ * Sensitivity aggregates over a sweep's stored rows, returned by
328
+ * {@link Sweep.sensitivity}. Marginals are always complete; the pair surfaces
329
+ * may be capped, in which case `heatmapsTruncated` is `true`.
330
+ */
331
+ type SweepSensitivity = SweepSensitivity$1;
332
+ /** One axis, with every other axis collapsed away. */
333
+ type SweepMarginal = SweepMarginal$1;
334
+ /** How the objective behaved at one value of one axis. */
335
+ type SweepMarginalPoint = SweepMarginalPoint$1;
336
+ /** The surface for one pair of axes, with all others collapsed away. */
337
+ type SweepHeatmap = SweepHeatmap$1;
338
+ /** One cell of a {@link SweepHeatmap}. */
339
+ type SweepHeatmapCell = SweepHeatmapCell$1;
340
+ /**
341
+ * The walk-forward section of a {@link SweepResult}, present exactly when the
342
+ * sweep was submitted with {@link SweepWalkForward}.
343
+ *
344
+ * **`paramDrift` absent is not zero.** The field is omitted whenever the figure
345
+ * could not be computed — fewer than two folds finished, or no stored grid to
346
+ * place the winners on — and zero is itself a meaningful reading there (winners
347
+ * that never moved), so a placeholder would be indistinguishable from perfect
348
+ * stability.
349
+ */
350
+ type WalkForwardResult = WalkForwardResult$1;
351
+ /**
352
+ * What one fold concluded. The out-of-sample row is the answer; the in-sample
353
+ * figure is only there to be compared against it, since any grid produces a
354
+ * flattering in-sample winner — that is what optimizing does. The gap between
355
+ * them is the whole reading.
356
+ */
357
+ type WalkForwardFold = WalkForwardFold$1;
358
+ /**
359
+ * A parameter sweep over one instrument and one window: the same strategy run
360
+ * once per parameter vector, scored and ranked against a single objective.
361
+ *
362
+ * ```ts
363
+ * const request: SweepRequest = {
364
+ * strategy: source,
365
+ * exchangeId: 'binance',
366
+ * instrument: 'BTC/USDT',
367
+ * from: '2026-01-01T00:00:00Z',
368
+ * to: '2026-02-01T00:00:00Z',
369
+ * params: {
370
+ * rsiPeriod: { from: 7, to: 28, step: 1 },
371
+ * useTrendFilter: { values: [true, false] },
372
+ * },
373
+ * objective: 'sharpe',
374
+ * };
375
+ * ```
376
+ */
377
+ interface SweepRequest {
378
+ /** Strategy source code (Java), compiled once and reused by every trial. */
379
+ strategy: string;
380
+ /** Exchange id, e.g. `binance`. */
381
+ exchangeId: string;
382
+ /** Instrument symbol, e.g. `BTC/USDT`. */
383
+ instrument: string;
384
+ /** Range start (ISO-8601, ISO DATE, or BASIC ISO DATE). */
385
+ from: string;
386
+ /** Range end (same formats as `from`; must be later than `from`). */
387
+ to: string;
388
+ /** The grid: one {@link ParamAxis} per strategy property to vary. At least one. */
389
+ params: Record<string, ParamAxis>;
390
+ /**
391
+ * How the grid becomes the list of vectors actually run. Omit to keep the
392
+ * platform default, the full cross product.
393
+ */
394
+ sampler?: SweepSampler;
395
+ /**
396
+ * How many vectors to draw for `'random'` and `'lhs'`; ignored by `'grid'`.
397
+ */
398
+ samples?: number;
399
+ /**
400
+ * Reproducibility seed. Omit to let the platform generate one and report it
401
+ * back on {@link Sweep.accepted}, so a randomly sampled sweep can be replayed
402
+ * exactly by submitting the same seed again.
403
+ */
404
+ seed?: number;
405
+ /**
406
+ * The metric to optimize and rank by; omit to keep the platform default
407
+ * (`'sharpe'`). It is also what {@link Sweep.sensitivity} aggregates unless
408
+ * told otherwise.
409
+ */
410
+ objective?: SweepObjective;
411
+ /**
412
+ * Opt into walk-forward validation, which changes both what runs and the
413
+ * shape of the answer. Omit to run an ordinary sweep.
414
+ */
415
+ walkForward?: SweepWalkForward;
416
+ }
417
+ /**
418
+ * Emitted at stage transitions and after each poll of a running sweep.
419
+ *
420
+ * The `snapshot` is where the detail lives — see {@link SweepProgress}, whose
421
+ * counts measure different things and must not be added together.
422
+ */
423
+ interface SweepProgressEvent {
424
+ /** Current workflow stage. */
425
+ stage: BacktestStage;
426
+ /**
427
+ * 0-100, computed from the runs finished out of the runs expected (`done` /
428
+ * `total` on the snapshot, both run-level counts — not the shard counts,
429
+ * which partition units of work rather than runs). Absent on stage
430
+ * transitions before the first poll.
431
+ */
432
+ percent?: number;
433
+ /**
434
+ * Fraction (0-1) of the requested window that actually holds data, as
435
+ * reported once preparation completes. Present only on the final `preparing`
436
+ * event. Worth reading on a sweep in particular: a thinly covered window is
437
+ * about to be scored once per parameter vector.
438
+ */
439
+ coverageRatio?: number;
440
+ /**
441
+ * The platform's progress record for the sweep. Present only on `executing`
442
+ * events.
443
+ */
444
+ snapshot?: SweepProgress;
445
+ }
446
+ /** Tuning knobs for a {@link QTSurfer.sweep} invocation. */
447
+ interface SweepOptions {
448
+ /**
449
+ * Ask the platform to stop the sweep between parameter vectors.
450
+ *
451
+ * **Aborting does not reject {@link Sweep.result}** — it resolves with
452
+ * whatever was scored before the stop. That is a deliberate divergence from
453
+ * `backtest()`, which rejects with `QTSCanceledError` when its run is
454
+ * aborted; see {@link Sweep.result} for why. Aborting *before* the platform
455
+ * has accepted the sweep — during compile, prepare or submission — rejects
456
+ * the {@link QTSurfer.sweep} call itself with `QTSCanceledError`, since there
457
+ * is no sweep yet and so no rows to keep.
458
+ */
459
+ signal?: AbortSignal;
460
+ /** Called on stage transitions and after each poll with updated progress. */
461
+ onProgress?: (p: SweepProgressEvent) => void;
462
+ /**
463
+ * Initial interval between polls. Default 2000ms, backed off up to
464
+ * `maxPollIntervalMs`. Longer than a single backtest's, because a sweep is
465
+ * many backtests and its leaderboard changes on the timescale of shards
466
+ * finishing rather than ticks.
467
+ */
468
+ pollIntervalMs?: number;
469
+ /** Upper bound for exponential backoff. Default 15000ms. */
470
+ maxPollIntervalMs?: number;
471
+ /**
472
+ * Per-stage timeout. Default none. A sweep is many backtests, so the execute
473
+ * stage legitimately outlasts anything a single run would take.
474
+ */
475
+ timeoutMs?: number;
476
+ /**
477
+ * How to order the leaderboard the poll reads. Omit to send no preference and
478
+ * take the platform default, which is `'plateau'` — so leaving this unset
479
+ * does **not** give you raw objective order. What was actually applied is
480
+ * reported on the result. **Ignored entirely when `order` is `'natural'`**,
481
+ * which is always ordered by `runIx`; the platform accepts both and answers
482
+ * with the ordering it applied.
483
+ */
484
+ ranking?: SweepRanking;
485
+ /**
486
+ * Which view of the rows the background poll reads. Omit to take the platform
487
+ * default, `'ranked'` — the sorted, display-capped leaderboard. `'natural'`
488
+ * returns every available row untruncated.
489
+ *
490
+ * This only decides what {@link Sweep.result} resolves with. Reading the same
491
+ * sweep another way afterwards — to reach the rows a `truncated` ranked view
492
+ * dropped, say — is {@link Sweep.results}, which re-reads rather than
493
+ * re-running.
494
+ */
495
+ order?: SweepOrder;
496
+ }
497
+ /**
498
+ * Handle for a running parameter sweep, returned by {@link QTSurfer.sweep} once
499
+ * the platform has accepted it. The leaderboard keeps being polled in the
500
+ * background.
501
+ */
502
+ interface Sweep {
503
+ /** Server-side sweep identifier. */
504
+ readonly sweepId: string;
505
+ /**
506
+ * The prepared dataset every trial ran against — the prepare jobId the
507
+ * workflow resolved before submitting, which is also what addresses this
508
+ * sweep on the wire.
509
+ *
510
+ * This is the value the workflow prepared with, not the acceptance echo of
511
+ * it. {@link Sweep.accepted} carries the echo, unmodified, for anyone who
512
+ * wants to compare the two.
513
+ */
514
+ readonly requestId: string;
515
+ /** The compiled strategy every trial shares. */
516
+ readonly strategyId: string;
517
+ /**
518
+ * What acceptance already answered, before a single trial has run, exactly as
519
+ * the platform sent it.
520
+ *
521
+ * Three of its fields are the reason this is exposed rather than folded away:
522
+ *
523
+ * - `seed` — the effective seed, generated platform-side when the request
524
+ * omitted one. Submitting it again is what makes a randomly sampled sweep
525
+ * replayable.
526
+ * - `queued` — `false` means an identical sweep already existed and nothing
527
+ * new was enqueued. The handle is still valid and still resolves; it is
528
+ * just reading a sweep this call did not start.
529
+ * - `walkForward` — present exactly when this is a walk-forward sweep. It is
530
+ * the discriminator, and it is available here immediately, so code watching
531
+ * progress can branch on the answer's shape without waiting for
532
+ * {@link Sweep.result}.
533
+ */
534
+ readonly accepted: SweepAccepted;
535
+ /**
536
+ * Local snapshot of the sweep lifecycle; reading it does not contact the
537
+ * server.
538
+ */
539
+ readonly state: SweepState;
540
+ /**
541
+ * Resolves with the final leaderboard once the sweep stops advancing.
542
+ *
543
+ * **This resolves on every terminal status, cancellation included** —
544
+ * `'COMPLETED'`, `'PARTIAL'` and `'CANCELLED'` all hand back the result
545
+ * rather than raising. That is a deliberate divergence from `backtest()`,
546
+ * which rejects with `QTSCanceledError` when its run is aborted: cancelling a
547
+ * sweep is documented as leaving completed rows readable, and throwing them
548
+ * away would lose the only reason to cancel a sweep late rather than early.
549
+ * Read `status` to find out which of the three you got. The promise rejects
550
+ * only for transport failures, HTTP errors, and stage timeouts.
551
+ *
552
+ * `'PARTIAL'` means at least one unit of work died and its runs are simply
553
+ * missing. There is no failed status for a sweep as a whole, so a sweep whose
554
+ * every shard died is `'PARTIAL'` with an empty leaderboard — check
555
+ * `leaderboardSize` before reading anything into a top row.
556
+ *
557
+ * ### Reading the leaderboard
558
+ *
559
+ * **The default order is not the raw objective order.** It is plateau order,
560
+ * and `ranking` on the result says which was actually applied — not always
561
+ * the one requested, because a sweep with no stored parameter grid cannot be
562
+ * plateau-ranked and falls back to raw. See {@link SweepRanking}.
563
+ *
564
+ * **The default view is capped.** When `truncated` is `true`, rows exist that
565
+ * the leaderboard does not carry — `leaderboardSize` counts what is
566
+ * available. {@link Sweep.results} with `order: 'natural'` is what returns
567
+ * all of them, in `runIx` order and with no ranking applied. That is a
568
+ * re-read of this same sweep, not a second one.
569
+ *
570
+ * **`plateauScore` and `neighbourCount` are read together.** A neighbour
571
+ * count of `0` means the point had no neighbours in the grid to compare
572
+ * against, so its plateau score is unevidenced rather than confirmed — on its
573
+ * own it is indistinguishable from a genuinely robust one.
574
+ *
575
+ * **`deflatedSharpe`** is the probability that a row's Sharpe reflects real
576
+ * edge rather than the best draw from however many vectors were tried. Around
577
+ * 0.95 and up it survives the multiple-testing correction; near 0.5 or below
578
+ * it is not distinguishable from the best of a pile of coin flips. It is
579
+ * absent on aborted runs, and on sweeps with too few trials to establish any
580
+ * dispersion to deflate against.
581
+ *
582
+ * **`pbo`** is the probability of backtest overfitting for the sweep as a
583
+ * whole: how often the configuration that won in-sample lands below median
584
+ * out-of-sample. Above roughly 0.5 the sweep is selecting noise, and that
585
+ * verdict is about the search, not about any one row — a high value
586
+ * discredits the top row however good it looks. It is computed once the last
587
+ * unit of work finishes, so it is absent while the sweep is still running and
588
+ * on sweeps too small for the statistic to mean anything.
589
+ *
590
+ * ### A walk-forward sweep answers in a different shape
591
+ *
592
+ * `walkForward` is the discriminator, and it appears as soon as the sweep is
593
+ * accepted — before any fold has finished — so it is safe to branch on while
594
+ * polling (it is also on {@link Sweep.accepted}). When it is present, the
595
+ * leaderboard is one row per *completed fold*: that fold's winner as it
596
+ * scored out-of-sample, with **`runIx` carrying the fold index rather than a
597
+ * position in the grid**. No plateau score, deflated Sharpe or PBO figure is
598
+ * reported for one — the out-of-sample numbers are already the honest
599
+ * measurement. See {@link WalkForwardResult} for why an absent `paramDrift`
600
+ * is not a zero.
601
+ *
602
+ * ### An empty leaderboard is not always an empty answer
603
+ *
604
+ * A sweep can finish having scored nothing, because every shard failed before
605
+ * producing a row. When that happens `failReason` carries the cause reported
606
+ * by the *first* shard to fail — typically something the whole grid would have
607
+ * hit, such as a strategy that could not be loaded. Read it before concluding
608
+ * that a sweep with no rows simply found nothing: those are different
609
+ * outcomes and the leaderboard alone cannot tell them apart. Only the first
610
+ * failure is recorded, so where several shards failed for different reasons
611
+ * this names one of them rather than summarising all — pair it with
612
+ * `progress.failedShards` for the count.
613
+ */
614
+ readonly result: Promise<SweepResult>;
615
+ /**
616
+ * Re-read this sweep's rows under a different view.
617
+ *
618
+ * **This is a read, not a re-run.** It compiles nothing, prepares nothing and
619
+ * submits nothing: the same sweep is asked for its rows again with different
620
+ * query parameters, so no second sweep is created and nothing is enqueued.
621
+ * The view a {@link SweepOptions} chose applies to the background poll behind
622
+ * {@link Sweep.result}; this is how to look at the same sweep another way
623
+ * afterwards.
624
+ *
625
+ * **It is the route to rows the ranked view dropped.** When `truncated` is
626
+ * `true` on a result, rows exist that the leaderboard does not carry;
627
+ * `order: 'natural'` returns every available row untruncated, in
628
+ * deterministic `runIx` order.
629
+ *
630
+ * **`ranking` is ignored when `order` is `'natural'`** — that view is always
631
+ * ordered by `runIx`, and the response reports `'raw'`. The platform accepts
632
+ * both rather than rejecting the pair, and answers with the ordering it
633
+ * actually applied.
634
+ *
635
+ * Readable while the sweep is still running, in which case it returns the
636
+ * rows finished so far — exactly like {@link Sweep.sensitivity}. Like every
637
+ * handle-scoped call, it does not take part in an
638
+ * {@link AuthenticatedClient}'s refresh-on-401 policy.
639
+ *
640
+ * @param view which view to read; an absent property takes the platform
641
+ * default (`order: 'ranked'`, `ranking: 'plateau'`)
642
+ * @throws QTSError on any non-2xx response, with the HTTP status on `status`.
643
+ */
644
+ results(view?: {
645
+ order?: SweepOrder;
646
+ ranking?: SweepRanking;
647
+ }): Promise<SweepResult>;
648
+ /**
649
+ * How the objective moves as each parameter moves — the question a
650
+ * leaderboard cannot answer. A leaderboard says which point won; a sweep can
651
+ * spend its whole budget on an axis that never moved the objective at all,
652
+ * and the top rows hide that completely.
653
+ *
654
+ * A *marginal* takes one axis and collapses every other one: for each value
655
+ * of that axis it aggregates every run that used it, whatever the rest of the
656
+ * parameters were. A flat marginal means the axis did not matter over the
657
+ * range swept. `best`, `mean` and `worst` are all reported because them
658
+ * disagreeing is the signal — a value with a high best and a poor mean only
659
+ * works in specific company, which is an interaction, and a single number
660
+ * would hide it. A *heatmap* does the same over a pair of axes, where that
661
+ * interaction is visible directly.
662
+ *
663
+ * **Check `heatmapsTruncated`.** Marginals are always complete; the pair
664
+ * surfaces are quadratic in the axis count and may be capped to stay inside
665
+ * the response budget. When the flag is `true`, at least one pair was left
666
+ * out, so the list you have is not the full set of interactions. This method
667
+ * hands back the whole {@link SweepSensitivity} rather than just its surfaces
668
+ * precisely so that flag cannot be lost on the way out.
669
+ *
670
+ * Readable while the sweep is still running, in which case the aggregates
671
+ * describe the runs finished so far and `rowsAnalysed` says how many that
672
+ * was. Aborted runs are excluded throughout: a run that threw measured
673
+ * nothing, and counting it as a bad outcome would invent evidence against a
674
+ * parameter value that was never really tested.
675
+ *
676
+ * Like every handle-scoped call, this does not take part in an
677
+ * {@link AuthenticatedClient}'s refresh-on-401 policy.
678
+ *
679
+ * @param objective which metric to aggregate; omit to use the objective the
680
+ * sweep was submitted with
681
+ * @throws QTSError on any non-2xx response, with the HTTP status on `status`.
682
+ */
683
+ sensitivity(objective?: SweepObjective): Promise<SweepSensitivity>;
684
+ }
685
+
54
686
  /** Configuration for {@link QTSurfer}. */
55
687
  interface QTSurferOptions {
56
688
  /** Base URL of the QTSurfer API, e.g. `https://api.qtsurfer.com/v1`. */
@@ -79,7 +711,9 @@ interface DownloadHourArgs {
79
711
  }
80
712
  /**
81
713
  * Thin, stateless wrapper over `@qtsurfer/api-client` that exposes the SDK's
82
- * workflow methods (`backtest`, `tickers`, `klines`). Constructing an
714
+ * workflow methods (`backtest`, `sweep`, `tickers`, `klines`), the platform catalog
715
+ * (`exchanges`, `instruments`) and the strategy surface (`validateStrategy`,
716
+ * `strategy`, `strategies`, `deleteStrategy`, `strategyCode`). Constructing an
83
717
  * instance reconfigures the underlying api-client singleton, so avoid
84
718
  * holding two `QTSurfer`s with different `baseUrl`s or tokens alive in the
85
719
  * same process — they will race. Prefer the `authenticate()` helper over
@@ -94,6 +728,45 @@ declare class QTSurfer {
94
728
  * stage-by-stage error and retry semantics.
95
729
  */
96
730
  backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult>;
731
+ /**
732
+ * Run the full compile → prepare → executeSweep pipeline and resolve once the
733
+ * platform has accepted the sweep, handing back a {@link Sweep} that keeps
734
+ * polling the leaderboard in the background.
735
+ *
736
+ * The whole sweep is one call because the execute-sweep endpoint is addressed
737
+ * by the id of an already-prepared dataset: exposing the stages separately
738
+ * would hand dataset lifecycle to the caller and buy nothing. Preparing is
739
+ * idempotent, so sweeping the same window twice prepares it once.
740
+ *
741
+ * The returned promise rejects with {@link QTSStrategyCompileError} if
742
+ * compilation fails, {@link QTSPreparationError} if data preparation fails,
743
+ * {@link QTSExecutionError} if the platform rejects the sweep — an expanded
744
+ * grid over the server limit, or a walk-forward request whose fold count
745
+ * multiplies past the sweep budget, both answer `400` — {@link QTSTimeoutError}
746
+ * if a stage exceeds `timeoutMs`, or {@link QTSCanceledError} if the caller's
747
+ * signal fires before the sweep is accepted. A plain {@link QTSError} means
748
+ * the request itself is malformed (an empty grid, a non-positive `step`, a
749
+ * walk-forward block with fewer than two folds) and never reached the network.
750
+ *
751
+ * What the sweep *found* arrives through {@link Sweep.result}, which is also
752
+ * where the semantics of the leaderboard are documented. Acceptance already
753
+ * answers three things worth reading before any result exists — the effective
754
+ * seed, whether this submission enqueued anything, and whether this is a
755
+ * walk-forward sweep — see {@link Sweep.accepted}.
756
+ *
757
+ * ```ts
758
+ * const handle = await qts.sweep({
759
+ * strategy: source,
760
+ * exchangeId: 'binance',
761
+ * instrument: 'BTC/USDT',
762
+ * from: '2026-01-01T00:00:00Z',
763
+ * to: '2026-02-01T00:00:00Z',
764
+ * params: { rsiPeriod: { from: 7, to: 28, step: 1 } },
765
+ * });
766
+ * const leaderboard = await handle.result;
767
+ * ```
768
+ */
769
+ sweep(req: SweepRequest, opts?: SweepOptions): Promise<Sweep>;
97
770
  /**
98
771
  * Download one hour of raw tickers for an instrument as a {@link Blob}.
99
772
  * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.
@@ -101,14 +774,125 @@ declare class QTSurfer {
101
774
  tickers(args: DownloadHourArgs): Promise<Blob>;
102
775
  /** Download one hour of klines for an instrument as a {@link Blob}. */
103
776
  klines(args: DownloadHourArgs): Promise<Blob>;
777
+ /**
778
+ * List the exchanges the platform serves. Each `id` is what every other
779
+ * method takes as `exchangeId`.
780
+ *
781
+ * @throws QTSError on any non-2xx response, with the HTTP status on
782
+ * `status`.
783
+ */
784
+ exchanges(): Promise<Exchange[]>;
785
+ /**
786
+ * List an exchange's instruments, each with the per-data-type `coverage`
787
+ * that says which date windows are actually downloadable.
788
+ *
789
+ * Omitting `segment` asks for the exchange's **default** segment, which is
790
+ * `'spot'` today. The API answers with a HAL envelope that this method
791
+ * unwraps to the instrument array, so the envelope's `meta.segment`,
792
+ * `meta.updatedAt` and segment-discovery `_links` do not reach you: if you
793
+ * need certainty about which segment you are looking at, pass `segment`
794
+ * explicitly rather than relying on the default.
795
+ *
796
+ * @param exchangeId exchange identifier, e.g. `binance`
797
+ * @throws QTSError on any non-2xx response, with the HTTP status on
798
+ * `status`.
799
+ */
800
+ instruments(exchangeId: string, segment?: InstrumentSegment): Promise<InstrumentDetail[]>;
801
+ /**
802
+ * Ask the platform to check that a registered strategy can actually run:
803
+ * it instantiates the compiled class and drives it through a bounded
804
+ * synthetic series, so a wiring fault surfaces here instead of at the first
805
+ * backtest.
806
+ *
807
+ * **Idempotent, and two-outcome.** `queued: false` means a verdict already
808
+ * existed for the current compilation and came back unchanged in `state` —
809
+ * nothing was queued. `queued: true` means a check was just started, and is
810
+ * **not** terminal: poll {@link QTSurfer.strategy} until `validation`
811
+ * leaves `'pending'`. The discriminant reports whether work was *started*,
812
+ * not whether a verdict *exists*, because a `queued: false` answer can
813
+ * itself carry `validation: 'pending'` from a check an earlier call queued;
814
+ * `state.validation` is what tells you that.
815
+ *
816
+ * **Poll with a deadline of your own.** `'pending'` is not guaranteed to
817
+ * resolve — a queued check can go unreported for far longer than one takes,
818
+ * which the platform eventually flags as `validationStalled`. Nothing about
819
+ * the strategy is disproved when that happens, but a caller that waits for
820
+ * a terminal verdict without a timeout can wait forever. This SDK ships no
821
+ * polling helper for that reason: the timeout is the caller's policy.
822
+ *
823
+ * Whatever the verdict, it is a floor rather than a guarantee — see
824
+ * {@link StrategyState}.
825
+ *
826
+ * @param strategyId the id returned when the strategy was compiled
827
+ * @throws QTSError on any non-2xx response; a `404` (carried on `status`)
828
+ * means no such registered strategy for this caller.
829
+ */
830
+ validateStrategy(strategyId: string): Promise<StrategyValidation>;
831
+ /**
832
+ * Read everything the platform records about a strategy: whether it is
833
+ * registered at all, its validation verdict, the market data its compiled
834
+ * class requires, and any engine notices the check raised. This is what to
835
+ * poll after {@link QTSurfer.validateStrategy} returns `queued: true`, and
836
+ * the only place a verdict is read from.
837
+ *
838
+ * Check `compiledAt` against `validatedAt` before trusting a verdict: the
839
+ * strategy may have been recompiled since it was recorded, in which case
840
+ * the verdict describes bytecode that is no longer what would run.
841
+ * See {@link StrategyState} for why even a fresh `'passed'` is a floor
842
+ * rather than a guarantee.
843
+ *
844
+ * @param strategyId the id returned when the strategy was compiled
845
+ * @throws QTSError on any non-2xx response. A `404` (carried on `status`)
846
+ * means exactly one thing — no such registered strategy for this caller.
847
+ * It is never a stale or expired answer.
848
+ */
849
+ strategy(strategyId: string): Promise<StrategyState>;
850
+ /**
851
+ * List every strategy you have registered and not deleted, most recently
852
+ * compiled first. Never `404`s — an empty array means you have none.
853
+ * Each entry deliberately omits `validation`; check a specific strategy's
854
+ * verdict with {@link QTSurfer.strategy}. See {@link StrategySummary}.
855
+ *
856
+ * @throws QTSError on any non-2xx response, with the HTTP status on
857
+ * `status`.
858
+ */
859
+ strategies(): Promise<StrategySummary[]>;
860
+ /**
861
+ * Release a registered strategy: removes it from both {@link
862
+ * QTSurfer.strategy} and {@link QTSurfer.strategies}.
863
+ *
864
+ * Backtests already run against this strategy are unaffected, and
865
+ * re-submitting the same source afterwards registers a **new** strategy
866
+ * with a **new** id rather than undeleting this one. Deleting your own
867
+ * copy of a strategy never affects anyone else's copy of the same source
868
+ * (e.g. a shared/marketplace listing).
869
+ *
870
+ * @param strategyId the id returned when the strategy was compiled
871
+ * @throws QTSError on any non-2xx response; a `404` (carried on `status`)
872
+ * means no such registered strategy for this caller.
873
+ */
874
+ deleteStrategy(strategyId: string): Promise<void>;
875
+ /**
876
+ * Read back the exact source last submitted for a strategy id, whitespace
877
+ * and comments included.
878
+ *
879
+ * A `404` (carried on `status`) covers two indistinguishable cases: the id
880
+ * was never registered by you, or it resolves only through a shared/
881
+ * marketplace reference that carries no source of its own.
882
+ *
883
+ * @param strategyId the id returned when the strategy was compiled
884
+ */
885
+ strategyCode(strategyId: string): Promise<string>;
104
886
  }
105
887
 
106
888
  /**
107
889
  * Base class for every error the SDK throws. Catch this to handle all SDK
108
890
  * failures generically, or catch a specific subclass below to tell which
109
891
  * stage failed. `status` is only set when the throw site had an HTTP status
110
- * to attach — today that is just {@link QTSDownloadError}; the workflow-stage
111
- * errors carry `cause` instead and encode retryability in their message.
892
+ * to attach: {@link QTSDownloadError} always carries one, and so does the
893
+ * plain `QTSError` thrown by the single-request calls (`exchanges`,
894
+ * `instruments`, `validateStrategy`, `strategy`). The workflow-stage errors
895
+ * carry `cause` instead and encode retryability in their message.
112
896
  */
113
897
  declare class QTSError extends Error {
114
898
  readonly cause?: unknown | undefined;
@@ -259,21 +1043,87 @@ declare class AuthenticatedClient {
259
1043
  * triggering a refresh.
260
1044
  */
261
1045
  backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult>;
1046
+ /**
1047
+ * Run the full compile → prepare → executeSweep pipeline and resolve once the
1048
+ * platform has accepted the sweep, handing back a {@link Sweep} that keeps
1049
+ * polling the leaderboard in the background. See {@link QTSurfer.sweep} for
1050
+ * why the sweep is one call rather than composable stages, and
1051
+ * {@link Sweep.result} for how to read what it found.
1052
+ *
1053
+ * The currently cached token is sent (minting one first if none is cached),
1054
+ * but as with `backtest()` a `401` here is **not** auto-retried: the
1055
+ * underlying stage errors carry no HTTP status, so a token that expires
1056
+ * mid-pipeline surfaces as `QTSPreparationError`/`QTSExecutionError` rather
1057
+ * than triggering a refresh.
1058
+ *
1059
+ * The background leaderboard poll and {@link Sweep.sensitivity} sit outside
1060
+ * the policy for a second, independent reason: both run after this promise
1061
+ * has already resolved, so a token that expires while a sweep is in flight
1062
+ * surfaces on {@link Sweep.result} whatever the stage errors carry.
1063
+ */
1064
+ sweep(req: SweepRequest, opts?: SweepOptions): Promise<Sweep>;
262
1065
  /** Download one hour of raw tickers. Refreshes the token once on `401` before retrying. */
263
1066
  tickers(args: DownloadHourArgs): Promise<Blob>;
264
1067
  /** Download one hour of klines. Refreshes the token once on `401` before retrying. */
265
1068
  klines(args: DownloadHourArgs): Promise<Blob>;
1069
+ /**
1070
+ * List the exchanges the platform serves. Refreshes the token once on `401`
1071
+ * before retrying.
1072
+ */
1073
+ exchanges(): Promise<Exchange[]>;
1074
+ /**
1075
+ * List an exchange's instruments, optionally for a specific segment.
1076
+ * Refreshes the token once on `401` before retrying. See
1077
+ * {@link QTSurfer.instruments} for what the unwrapped HAL envelope leaves
1078
+ * out.
1079
+ */
1080
+ instruments(exchangeId: string, segment?: InstrumentSegment): Promise<InstrumentDetail[]>;
1081
+ /**
1082
+ * Ask the platform to check that a registered strategy can actually run.
1083
+ * Refreshes the token once on `401` before retrying. Two-outcome — see
1084
+ * {@link QTSurfer.validateStrategy}; `queued: true` is not terminal and
1085
+ * must be followed by polling {@link AuthenticatedClient.strategy} under a
1086
+ * deadline of your own.
1087
+ */
1088
+ validateStrategy(strategyId: string): Promise<StrategyValidation>;
1089
+ /**
1090
+ * Read a strategy's recorded state, including its validation verdict.
1091
+ * Refreshes the token once on `401` before retrying. See
1092
+ * {@link StrategyState} for why a `'passed'` verdict is a floor rather than
1093
+ * a guarantee.
1094
+ */
1095
+ strategy(strategyId: string): Promise<StrategyState>;
1096
+ /**
1097
+ * List every strategy you have registered and not deleted, most recently
1098
+ * compiled first. Refreshes the token once on `401` before retrying. See
1099
+ * {@link QTSurfer.strategies}.
1100
+ */
1101
+ strategies(): Promise<StrategySummary[]>;
1102
+ /**
1103
+ * Release a registered strategy. Refreshes the token once on `401` before
1104
+ * retrying. See {@link QTSurfer.deleteStrategy} for what this does and
1105
+ * does not undo.
1106
+ */
1107
+ deleteStrategy(strategyId: string): Promise<void>;
1108
+ /**
1109
+ * Read back a strategy's exact registered source. Refreshes the token
1110
+ * once on `401` before retrying. See {@link QTSurfer.strategyCode} for
1111
+ * what its `404` covers.
1112
+ */
1113
+ strategyCode(strategyId: string): Promise<string>;
266
1114
  }
267
1115
  /**
268
1116
  * Exchange a long-lived API key for an authenticated session.
269
1117
  *
270
1118
  * If `apikey` is omitted, the SDK reads `QTSURFER_APIKEY` from the
271
1119
  * environment. The returned {@link AuthenticatedClient} caches the JWT,
272
- * refreshes it on 401, and exposes the same workflow surface as
273
- * `QTSurfer` (`backtest`, `tickers`, `klines`).
1120
+ * refreshes it on 401, and exposes the same surface as `QTSurfer`
1121
+ * (`backtest`, `sweep`, `tickers`, `klines`, `exchanges`, `instruments`,
1122
+ * `validateStrategy`, `strategy`, `strategies`, `deleteStrategy`,
1123
+ * `strategyCode`).
274
1124
  *
275
1125
  * @throws {QTSAuthError} if no apikey is supplied or available in env.
276
1126
  */
277
1127
  declare function authenticate(apikey?: string, opts?: AuthOptions): Promise<AuthenticatedClient>;
278
1128
 
279
- export { type AuthOptions, AuthenticatedClient, type BacktestOptions, type BacktestProgress, type BacktestRequest, type BacktestResult, type BacktestStage, type DownloadFormat, type DownloadHourArgs, InMemoryTokenStore, QTSAuthError, QTSCanceledError, QTSDownloadError, QTSError, QTSExecutionError, QTSPreparationError, QTSStrategyCompileError, QTSTimeoutError, QTSurfer, type QTSurferOptions, type TokenStore, authenticate };
1129
+ export { type AuthOptions, AuthenticatedClient, type BacktestOptions, type BacktestProgress, type BacktestRequest, type BacktestResult, type BacktestStage, type DownloadFormat, type DownloadHourArgs, type Exchange, InMemoryTokenStore, type InstrumentDetail, type InstrumentSegment, type ParamAxis, QTSAuthError, QTSCanceledError, QTSDownloadError, QTSError, QTSExecutionError, QTSPreparationError, QTSStrategyCompileError, QTSTimeoutError, QTSurfer, type QTSurferOptions, type StrategyState, type StrategySummary, type StrategyValidation, type Sweep, type SweepAccepted, type SweepHeatmap, type SweepHeatmapCell, type SweepMarginal, type SweepMarginalPoint, type SweepObjective, type SweepOptions, type SweepOrder, type SweepProgress, type SweepProgressEvent, type SweepRanking, type SweepRequest, type SweepResult, type SweepRunRow, type SweepSampler, type SweepSensitivity, type SweepState, type SweepWalkForward, type TokenStore, type WalkForwardFold, type WalkForwardResult, authenticate };