@qtsurfer/sdk 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2026 QTSurfer
189
+ Copyright 2026 Wualabs LTD
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -20,38 +20,68 @@ pnpm add @qtsurfer/sdk
20
20
 
21
21
  ## Quick start
22
22
 
23
+ One call: API key in, ready-to-use session out. JWT refresh on 401 is handled
24
+ for you.
25
+
23
26
  ```ts
24
- import { QTSurfer } from '@qtsurfer/sdk';
27
+ import { auth } from '@qtsurfer/sdk';
25
28
  import { readFileSync } from 'node:fs';
26
29
 
27
- const qts = new QTSurfer({
28
- baseUrl: 'https://api.qtsurfer.com/v1',
29
- token: process.env.QTSURFER_TOKEN,
30
+ // Reads QTSURFER_APIKEY from env when no argument is passed.
31
+ const qts = await auth();
32
+ // Or: const qts = await auth('ak_...');
33
+
34
+ const result = await qts.backtest({
35
+ strategy: readFileSync('./MyStrategy.java', 'utf8'),
36
+ exchangeId: 'binance',
37
+ instrument: 'BTC/USDT',
38
+ from: '2024-01-01',
39
+ to: '2024-12-31',
40
+ storeSignals: true,
30
41
  });
31
42
 
32
- const controller = new AbortController();
33
-
34
- const result = await qts.backtest(
35
- {
36
- strategy: readFileSync('./MyStrategy.java', 'utf8'),
37
- exchangeId: 'binance',
38
- instrument: 'BTC/USDT',
39
- from: '2024-01-01',
40
- to: '2024-12-31',
41
- storeSignals: true,
42
- },
43
- {
44
- signal: controller.signal,
45
- onProgress: (p) => console.log(`[${p.stage}] ${p.percent?.toFixed(1) ?? '-'}%`),
46
- pollIntervalMs: 500,
47
- maxPollIntervalMs: 5000,
48
- timeoutMs: 10 * 60 * 1000,
49
- },
50
- );
51
-
52
43
  console.log('PnL:', result.pnlTotal);
53
44
  console.log('Trades:', result.totalTrades);
54
- console.log('Signals:', result.signalsUrl);
45
+ ```
46
+
47
+ ### Environment
48
+
49
+ | Variable | Purpose |
50
+ | ------------------ | -------------------------------------------------- |
51
+ | `QTSURFER_APIKEY` | API key consumed by `auth()` when no arg is passed |
52
+
53
+ ### Pluggable token storage
54
+
55
+ Tokens are kept in memory by default. Implement `TokenStore` to swap in
56
+ browser storage, a file, or a secret manager:
57
+
58
+ ```ts
59
+ import { auth, type TokenStore, type AuthTokenResponse } from '@qtsurfer/sdk';
60
+
61
+ const browserStore: TokenStore = {
62
+ load: () => JSON.parse(localStorage.getItem('qts.jwt') ?? 'null'),
63
+ save: (t) => localStorage.setItem('qts.jwt', JSON.stringify(t)),
64
+ clear: () => localStorage.removeItem('qts.jwt'),
65
+ };
66
+
67
+ const qts = await auth(undefined, { store: browserStore });
68
+ ```
69
+
70
+ `auth()` also accepts `{ baseUrl, fetch }` for staging, custom HTTP
71
+ transports, or a Node-`fetch` polyfill in legacy runtimes.
72
+
73
+ ### Lower-level: hand-managed JWT
74
+
75
+ If you already hold a JWT and want to manage refresh yourself, the
76
+ `QTSurfer` constructor still accepts a `token`:
77
+
78
+ ```ts
79
+ import { QTSurfer } from '@qtsurfer/sdk';
80
+
81
+ const qts = new QTSurfer({
82
+ baseUrl: 'https://api.qtsurfer.com/v1',
83
+ token: process.env.QTSURFER_TOKEN,
84
+ });
55
85
  ```
56
86
 
57
87
  ## What `backtest()` does
@@ -67,6 +97,32 @@ Polling uses exponential backoff (`intervalMs * 1.5`, capped at `maxIntervalMs`)
67
97
 
68
98
  Progress is emitted on every stage transition and after each poll whose `size > 0`.
69
99
 
100
+ ## Hourly tickers/klines downloads
101
+
102
+ Stream one hour of raw ticker or kline data for an instrument. The default wire format is [Lastra](https://github.com/QTSurfer/lastra-ts) (`application/vnd.lastra`); pass `format: 'parquet'` for on-the-fly Parquet conversion.
103
+
104
+ ```ts
105
+ // Lastra (default)
106
+ const blob = await qts.tickers({
107
+ exchangeId: 'binance',
108
+ base: 'BTC',
109
+ quote: 'USDT',
110
+ hour: '2026-01-15T10',
111
+ });
112
+ await Bun.write('BTC_USDT_2026-01-15_h10.lastra', await blob.arrayBuffer());
113
+
114
+ // Parquet
115
+ const klines = await qts.klines({
116
+ exchangeId: 'binance',
117
+ base: 'BTC',
118
+ quote: 'USDT',
119
+ hour: '2026-01-15T10',
120
+ format: 'parquet',
121
+ });
122
+ ```
123
+
124
+ HTTP errors surface as `QTSDownloadError` (subclass of `QTSError`).
125
+
70
126
  ## Error hierarchy
71
127
 
72
128
  All SDK errors extend `QTSError` so you can catch them generically or match by subclass.
@@ -77,6 +133,7 @@ import {
77
133
  QTSStrategyCompileError,
78
134
  QTSPreparationError,
79
135
  QTSExecutionError,
136
+ QTSDownloadError,
80
137
  QTSTimeoutError,
81
138
  QTSCanceledError,
82
139
  } from '@qtsurfer/sdk';
@@ -90,6 +147,8 @@ try {
90
147
  console.error('Data prep failed:', e.message);
91
148
  } else if (e instanceof QTSExecutionError) {
92
149
  console.error('Execution failed:', e.message);
150
+ } else if (e instanceof QTSDownloadError) {
151
+ console.error('Download failed:', e.message);
93
152
  } else if (e instanceof QTSTimeoutError) {
94
153
  console.error('Stage timed out');
95
154
  } else if (e instanceof QTSCanceledError) {
@@ -145,7 +204,8 @@ src/
145
204
  ├── client.ts # QTSurfer class
146
205
  ├── errors.ts # QTSError hierarchy
147
206
  └── workflows/
148
- └── backtest.ts # compile → prepare → execute (cockatiel policies)
207
+ ├── backtest.ts # compile → prepare → execute (cockatiel policies)
208
+ └── downloads.ts # hourly tickers/klines as Lastra/Parquet blobs
149
209
  ```
150
210
 
151
211
  ## Development
@@ -153,7 +213,21 @@ src/
153
213
  | Script | Description |
154
214
  | ------ | ----------- |
155
215
  | `npm run lint` | Type-check without emitting |
156
- | `npm run build` | Compile to `dist/` |
216
+ | `npm run build` | Bundle to `dist/` via `tsup` |
217
+ | `npm test` | Run unit tests |
218
+ | `npm run test:integration` | Run the integration test (requires `JWT_API_TOKEN`). Set `QTSURFER_TEST_VERBOSE=1` to stream progress + final result |
219
+ | `npm run changeset` | Record a changeset for the next release |
220
+ | `npm run changeset:version` | Consume pending changesets: bump `package.json` and update `CHANGELOG.md` |
221
+ | `npm run changeset:publish` | Publish released packages to npm (used by CI) |
222
+
223
+ ### Releasing
224
+
225
+ Versioning and changelogs are managed with [changesets](https://github.com/changesets/changesets):
226
+
227
+ 1. Create a changeset describing your change: `npm run changeset`
228
+ 2. Commit the generated `.changeset/<slug>.md` with your PR.
229
+ 3. When ready to release, run `npm run changeset:version` locally. It bumps `package.json` and appends to `CHANGELOG.md`.
230
+ 4. Commit the version bump, tag `vX.Y.Z`, and push the tag; the `Publish to npm` workflow handles the rest.
157
231
 
158
232
  ## License
159
233
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ResultMap } from '@qtsurfer/api-client';
1
+ import { ResultMap, AuthTokenResponse } from '@qtsurfer/api-client';
2
2
 
3
3
  interface BacktestRequest {
4
4
  /** Strategy source code (Java) */
@@ -34,19 +34,40 @@ interface BacktestOptions {
34
34
  timeoutMs?: number;
35
35
  }
36
36
 
37
+ /** Wire format for hourly tickers/klines downloads. */
38
+ type DownloadFormat = 'lastra' | 'parquet';
39
+
37
40
  interface QTSurferOptions {
38
41
  baseUrl: string;
39
42
  token?: string;
40
43
  fetch?: typeof fetch;
41
44
  }
45
+ interface DownloadHourArgs {
46
+ exchangeId: string;
47
+ base: string;
48
+ quote: string;
49
+ /** Hour selector in `YYYY-MM-DDTHH` (UTC). */
50
+ hour: string;
51
+ /** Wire format. Defaults to `'lastra'`. */
52
+ format?: DownloadFormat;
53
+ }
42
54
  declare class QTSurfer {
43
55
  constructor(options: QTSurferOptions);
44
56
  backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult>;
57
+ /**
58
+ * Download one hour of raw tickers for an instrument as a {@link Blob}.
59
+ * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.
60
+ */
61
+ tickers(args: DownloadHourArgs): Promise<Blob>;
62
+ /** Download one hour of klines for an instrument as a {@link Blob}. */
63
+ klines(args: DownloadHourArgs): Promise<Blob>;
45
64
  }
46
65
 
47
66
  declare class QTSError extends Error {
48
67
  readonly cause?: unknown | undefined;
49
- constructor(message: string, cause?: unknown | undefined);
68
+ /** HTTP status code, when the underlying transport surfaced one. */
69
+ readonly status?: number;
70
+ constructor(message: string, cause?: unknown | undefined, status?: number);
50
71
  }
51
72
  declare class QTSStrategyCompileError extends QTSError {
52
73
  constructor(message: string, cause?: unknown);
@@ -63,5 +84,105 @@ declare class QTSTimeoutError extends QTSError {
63
84
  declare class QTSCanceledError extends QTSError {
64
85
  constructor(message: string, cause?: unknown);
65
86
  }
87
+ declare class QTSDownloadError extends QTSError {
88
+ constructor(message: string, cause?: unknown, status?: number);
89
+ }
90
+ /**
91
+ * Thrown by the `auth()` helper when the apikey is missing or the JWT
92
+ * exchange fails (HTTP 401 from `POST /v1/auth/token`, etc.).
93
+ */
94
+ declare class QTSAuthError extends QTSError {
95
+ constructor(message: string, cause?: unknown);
96
+ }
97
+
98
+ /**
99
+ * Pluggable token persistence interface.
100
+ *
101
+ * The SDK ships an {@link InMemoryTokenStore} by default. Adopters can
102
+ * implement this contract to back tokens by browser `localStorage`, an
103
+ * on-disk file, a secret manager, etc.
104
+ *
105
+ * The SDK calls {@link load} once per session-startup to seed a cached
106
+ * token (if any), {@link save} after every successful `auth()` / refresh,
107
+ * and {@link clear} when the session is explicitly invalidated.
108
+ */
109
+ interface TokenStore {
110
+ /** Return the previously persisted token, or `null` if none. */
111
+ load(): AuthTokenResponse | null | Promise<AuthTokenResponse | null>;
112
+ /** Persist the token returned by `POST /v1/auth/token`. */
113
+ save(token: AuthTokenResponse): void | Promise<void>;
114
+ /** Drop any persisted token. */
115
+ clear(): void | Promise<void>;
116
+ }
117
+ /**
118
+ * Default {@link TokenStore} — holds the token in a single in-memory slot.
119
+ * Lost on process exit. Sufficient for short-lived scripts and tests.
120
+ */
121
+ declare class InMemoryTokenStore implements TokenStore {
122
+ private token;
123
+ load(): AuthTokenResponse | null;
124
+ save(token: AuthTokenResponse): void;
125
+ clear(): void;
126
+ }
127
+
128
+ interface AuthOptions {
129
+ /** Base URL of the QTSurfer API. Defaults to the public production endpoint. */
130
+ baseUrl?: string;
131
+ /** Custom token store. Defaults to {@link InMemoryTokenStore}. */
132
+ store?: TokenStore;
133
+ /** Inject a custom `fetch` (Node 20+, browser, or test mock). */
134
+ fetch?: typeof fetch;
135
+ }
136
+ /**
137
+ * Authenticated SDK session.
138
+ *
139
+ * Returned by {@link auth}. Wraps the underlying api-client, owns a JWT
140
+ * (in memory by default, or in the provided {@link TokenStore}), and
141
+ * transparently re-exchanges the apikey for a fresh JWT on 401.
142
+ *
143
+ * Multi-session note: the session mutates the api-client singleton config
144
+ * on every call. Concurrent sessions in the same process will race; today
145
+ * the SDK targets the one-session-per-process pattern.
146
+ */
147
+ declare class AuthenticatedClient {
148
+ readonly baseUrl: string;
149
+ private readonly apikey;
150
+ private readonly store;
151
+ private readonly fetchImpl;
152
+ private cached;
153
+ private refreshing;
154
+ constructor(apikey: string, opts?: AuthOptions);
155
+ /** Currently cached token, if any. */
156
+ get token(): AuthTokenResponse | null;
157
+ /** Force a fresh JWT exchange. Bypasses the cache. */
158
+ refresh(): Promise<AuthTokenResponse>;
159
+ /**
160
+ * Load a previously-persisted token from the store. If none, mint one.
161
+ * Called automatically by every workflow method.
162
+ */
163
+ ensureToken(): Promise<AuthTokenResponse>;
164
+ /** Drop the cached token (in memory and in the store). */
165
+ clear(): Promise<void>;
166
+ /**
167
+ * Run a call with the Bearer header pre-set; if it returns 401, refresh
168
+ * once and retry. A second 401 surfaces to the caller.
169
+ */
170
+ private withRefreshOn401;
171
+ private applyConfig;
172
+ backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult>;
173
+ tickers(args: DownloadHourArgs): Promise<Blob>;
174
+ klines(args: DownloadHourArgs): Promise<Blob>;
175
+ }
176
+ /**
177
+ * Exchange a long-lived API key for an authenticated session.
178
+ *
179
+ * If `apikey` is omitted, the SDK reads `QTSURFER_APIKEY` from the
180
+ * environment. The returned {@link AuthenticatedClient} caches the JWT,
181
+ * refreshes it on 401, and exposes the same workflow surface as
182
+ * `QTSurfer` (`backtest`, `tickers`, `klines`).
183
+ *
184
+ * @throws {QTSAuthError} if no apikey is supplied or available in env.
185
+ */
186
+ declare function auth(apikey?: string, opts?: AuthOptions): Promise<AuthenticatedClient>;
66
187
 
67
- export { type BacktestOptions, type BacktestProgress, type BacktestRequest, type BacktestResult, type BacktestStage, QTSCanceledError, QTSError, QTSExecutionError, QTSPreparationError, QTSStrategyCompileError, QTSTimeoutError, QTSurfer, type QTSurferOptions };
188
+ 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, auth };
package/dist/index.js CHANGED
@@ -23,12 +23,15 @@ import {
23
23
 
24
24
  // src/errors.ts
25
25
  var QTSError = class extends Error {
26
- constructor(message, cause) {
26
+ constructor(message, cause, status) {
27
27
  super(message);
28
28
  this.cause = cause;
29
29
  this.name = "QTSError";
30
+ if (status !== void 0) this.status = status;
30
31
  }
31
32
  cause;
33
+ /** HTTP status code, when the underlying transport surfaced one. */
34
+ status;
32
35
  };
33
36
  var QTSStrategyCompileError = class extends QTSError {
34
37
  constructor(message, cause) {
@@ -60,6 +63,18 @@ var QTSCanceledError = class extends QTSError {
60
63
  this.name = "QTSCanceledError";
61
64
  }
62
65
  };
66
+ var QTSDownloadError = class extends QTSError {
67
+ constructor(message, cause, status) {
68
+ super(message, cause, status);
69
+ this.name = "QTSDownloadError";
70
+ }
71
+ };
72
+ var QTSAuthError = class extends QTSError {
73
+ constructor(message, cause) {
74
+ super(message, cause);
75
+ this.name = "QTSAuthError";
76
+ }
77
+ };
63
78
 
64
79
  // src/workflows/backtest.ts
65
80
  var TICKER = "ticker";
@@ -239,6 +254,53 @@ async function runStage(policy, opts, fetchFn, onEachAttempt) {
239
254
  }
240
255
  }
241
256
 
257
+ // src/workflows/downloads.ts
258
+ import {
259
+ getExchangeKlinesHour,
260
+ getExchangeTickersHour
261
+ } from "@qtsurfer/api-client";
262
+ async function downloadTickers(params) {
263
+ const { exchangeId, base, quote, hour, format } = params;
264
+ const { data, error, response } = await getExchangeTickersHour({
265
+ path: { exchangeId, base, quote },
266
+ query: { hour, ...format ? { format } : {} }
267
+ });
268
+ if (error) {
269
+ throw new QTSDownloadError(
270
+ `tickers download failed: HTTP ${response.status} \u2014 ${describe(error)}`,
271
+ error,
272
+ response.status
273
+ );
274
+ }
275
+ return data;
276
+ }
277
+ async function downloadKlines(params) {
278
+ const { exchangeId, base, quote, hour, format } = params;
279
+ const { data, error, response } = await getExchangeKlinesHour({
280
+ path: { exchangeId, base, quote },
281
+ query: { hour, ...format ? { format } : {} }
282
+ });
283
+ if (error) {
284
+ throw new QTSDownloadError(
285
+ `klines download failed: HTTP ${response.status} \u2014 ${describe(error)}`,
286
+ error,
287
+ response.status
288
+ );
289
+ }
290
+ return data;
291
+ }
292
+ function describe(error) {
293
+ if (error && typeof error === "object") {
294
+ const e = error;
295
+ const code = typeof e.code === "string" ? e.code : void 0;
296
+ const message = typeof e.message === "string" ? e.message : void 0;
297
+ if (code && message) return `${code}: ${message}`;
298
+ if (message) return message;
299
+ if (code) return code;
300
+ }
301
+ return String(error);
302
+ }
303
+
242
304
  // src/client.ts
243
305
  var QTSurfer = class {
244
306
  constructor(options) {
@@ -251,18 +313,175 @@ var QTSurfer = class {
251
313
  backtest(req, opts) {
252
314
  return backtest(req, opts);
253
315
  }
316
+ /**
317
+ * Download one hour of raw tickers for an instrument as a {@link Blob}.
318
+ * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.
319
+ */
320
+ tickers(args) {
321
+ return downloadTickers(args);
322
+ }
323
+ /** Download one hour of klines for an instrument as a {@link Blob}. */
324
+ klines(args) {
325
+ return downloadKlines(args);
326
+ }
254
327
  // Future surface:
255
328
  // strategies: { compile, status, list }
256
329
  // instruments: { list, get } with TTL cache
257
330
  // jobs: { cancel, stream, result }
258
331
  };
332
+
333
+ // src/auth/session.ts
334
+ import {
335
+ auth as apiAuth,
336
+ client as apiClient2
337
+ } from "@qtsurfer/api-client";
338
+
339
+ // src/auth/tokenStore.ts
340
+ var InMemoryTokenStore = class {
341
+ token = null;
342
+ load() {
343
+ return this.token;
344
+ }
345
+ save(token) {
346
+ this.token = token;
347
+ }
348
+ clear() {
349
+ this.token = null;
350
+ }
351
+ };
352
+
353
+ // src/auth/session.ts
354
+ var APIKEY_ENV_VAR = "QTSURFER_APIKEY";
355
+ var DEFAULT_BASE_URL = "https://api.qtsurfer.com/v1";
356
+ var AuthenticatedClient = class {
357
+ baseUrl;
358
+ apikey;
359
+ store;
360
+ fetchImpl;
361
+ cached = null;
362
+ refreshing = null;
363
+ constructor(apikey, opts = {}) {
364
+ this.apikey = apikey;
365
+ this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
366
+ this.store = opts.store ?? new InMemoryTokenStore();
367
+ this.fetchImpl = opts.fetch;
368
+ }
369
+ /** Currently cached token, if any. */
370
+ get token() {
371
+ return this.cached;
372
+ }
373
+ /** Force a fresh JWT exchange. Bypasses the cache. */
374
+ async refresh() {
375
+ if (this.refreshing) return this.refreshing;
376
+ this.refreshing = (async () => {
377
+ const { data, error, response } = await apiAuth({
378
+ baseUrl: this.baseUrl,
379
+ headers: { "X-API-Key": this.apikey },
380
+ ...this.fetchImpl ? { fetch: this.fetchImpl } : {}
381
+ });
382
+ if (error || !data) {
383
+ throw new QTSAuthError(
384
+ `auth() failed: HTTP ${response.status}`,
385
+ error
386
+ );
387
+ }
388
+ this.cached = data;
389
+ await this.store.save(data);
390
+ return data;
391
+ })();
392
+ try {
393
+ return await this.refreshing;
394
+ } finally {
395
+ this.refreshing = null;
396
+ }
397
+ }
398
+ /**
399
+ * Load a previously-persisted token from the store. If none, mint one.
400
+ * Called automatically by every workflow method.
401
+ */
402
+ async ensureToken() {
403
+ if (this.cached) return this.cached;
404
+ const stored = await this.store.load();
405
+ if (stored) {
406
+ this.cached = stored;
407
+ return stored;
408
+ }
409
+ return this.refresh();
410
+ }
411
+ /** Drop the cached token (in memory and in the store). */
412
+ async clear() {
413
+ this.cached = null;
414
+ await this.store.clear();
415
+ }
416
+ /**
417
+ * Run a call with the Bearer header pre-set; if it returns 401, refresh
418
+ * once and retry. A second 401 surfaces to the caller.
419
+ */
420
+ async withRefreshOn401(call) {
421
+ await this.applyConfig();
422
+ try {
423
+ return await call();
424
+ } catch (err) {
425
+ if (!isUnauthorized(err)) throw err;
426
+ this.cached = null;
427
+ await this.refresh();
428
+ await this.applyConfig();
429
+ return call();
430
+ }
431
+ }
432
+ async applyConfig() {
433
+ const token = await this.ensureToken();
434
+ apiClient2.setConfig({
435
+ baseUrl: this.baseUrl,
436
+ headers: { Authorization: `Bearer ${token.access_token}` },
437
+ ...this.fetchImpl ? { fetch: this.fetchImpl } : {}
438
+ });
439
+ }
440
+ // ---- Workflow surface (mirrors QTSurfer) ----
441
+ backtest(req, opts) {
442
+ return this.withRefreshOn401(() => backtest(req, opts));
443
+ }
444
+ tickers(args) {
445
+ return this.withRefreshOn401(() => downloadTickers(args));
446
+ }
447
+ klines(args) {
448
+ return this.withRefreshOn401(() => downloadKlines(args));
449
+ }
450
+ };
451
+ async function auth(apikey, opts = {}) {
452
+ const resolved = apikey ?? readEnvApikey();
453
+ if (!resolved) {
454
+ throw new QTSAuthError(
455
+ `auth() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`
456
+ );
457
+ }
458
+ const session = new AuthenticatedClient(resolved, opts);
459
+ await session.ensureToken();
460
+ return session;
461
+ }
462
+ function readEnvApikey() {
463
+ if (typeof process === "undefined" || !process.env) return void 0;
464
+ const value = process.env[APIKEY_ENV_VAR];
465
+ return value && value.length > 0 ? value : void 0;
466
+ }
467
+ function isUnauthorized(err) {
468
+ if (!err || typeof err !== "object") return false;
469
+ const maybeStatus = err.status;
470
+ if (typeof maybeStatus === "number" && maybeStatus === 401) return true;
471
+ return false;
472
+ }
259
473
  export {
474
+ AuthenticatedClient,
475
+ InMemoryTokenStore,
476
+ QTSAuthError,
260
477
  QTSCanceledError,
478
+ QTSDownloadError,
261
479
  QTSError,
262
480
  QTSExecutionError,
263
481
  QTSPreparationError,
264
482
  QTSStrategyCompileError,
265
483
  QTSTimeoutError,
266
- QTSurfer
484
+ QTSurfer,
485
+ auth
267
486
  };
268
487
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/workflows/backtest.ts","../src/errors.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';\n\nexport interface QTSurferOptions {\n baseUrl: string;\n token?: string;\n fetch?: typeof fetch;\n}\n\nexport class QTSurfer {\n constructor(options: QTSurferOptions) {\n apiClient.setConfig({\n baseUrl: options.baseUrl,\n ...(options.token\n ? { headers: { Authorization: `Bearer ${options.token}` } }\n : {}),\n ...(options.fetch ? { fetch: options.fetch } : {}),\n });\n }\n\n backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return backtest(req, opts);\n }\n\n // Future surface:\n // strategies: { compile, status, list }\n // instruments: { list, get } with TTL cache\n // jobs: { cancel, stream, result }\n}\n","import {\n cancelExecution,\n executeBacktesting,\n getExecutionResult,\n getPreparationStatus,\n getStrategyStatus,\n postStrategy,\n prepareBacktesting,\n type DataSourceType,\n type ResultMap,\n} from '@qtsurfer/api-client';\nimport {\n ExponentialBackoff,\n TaskCancelledError,\n TimeoutStrategy,\n handleWhenResult,\n retry,\n timeout,\n wrap,\n type IPolicy,\n type ICancellationContext,\n} from 'cockatiel';\nimport {\n QTSCanceledError,\n QTSExecutionError,\n QTSPreparationError,\n QTSStrategyCompileError,\n QTSTimeoutError,\n} from '../errors';\n\nexport interface BacktestRequest {\n /** Strategy source code (Java) */\n strategy: string;\n /** Exchange id, e.g. `binance` */\n exchangeId: string;\n /** Instrument symbol, e.g. `BTC/USDT` */\n instrument: string;\n /** Date range start (ISO-8601, ISO DATE or BASIC ISO DATE) */\n from: string;\n /** Date range end (same formats as `from`) */\n to: string;\n /** When true, the worker uploads emitted signals to object storage. */\n storeSignals?: boolean;\n}\n\nexport type BacktestResult = ResultMap;\n\nexport type BacktestStage = 'compiling' | 'preparing' | 'executing';\n\nexport interface BacktestProgress {\n stage: BacktestStage;\n /** 0-100 when size is known. Undefined during stage start. */\n percent?: number;\n}\n\nexport interface BacktestOptions {\n /** Abort the workflow. Cancels the current poll and calls `cancelExecution` server-side if execution has started. */\n signal?: AbortSignal;\n /** Called on stage transitions and after each poll with updated progress. */\n onProgress?: (p: BacktestProgress) => void;\n /** Initial interval between polls. Default 500ms, backed off up to `maxPollIntervalMs`. */\n pollIntervalMs?: number;\n /** Upper bound for exponential backoff. Default 5000ms. */\n maxPollIntervalMs?: number;\n /** Per-stage timeout. Default none. */\n timeoutMs?: number;\n}\n\nconst TICKER: DataSourceType = 'ticker';\n\ntype JobStatus = 'New' | 'Started' | 'Completed' | 'Aborted' | 'Failed';\n\n/**\n * Normalize the backend job status to a stable lowercase form so we can\n * reason about it regardless of OpenAPI spec drift (the live API sometimes\n * returns lowercase values like `queued` / `completed` / `failed`).\n */\ntype NormalizedStatus = 'in-progress' | 'completed' | 'failed' | 'aborted';\n\nfunction normalizeStatus(raw: unknown): NormalizedStatus {\n const value = typeof raw === 'string' ? raw.toLowerCase() : '';\n if (value === 'completed') return 'completed';\n if (value === 'failed') return 'failed';\n if (value === 'aborted' || value === 'cancelled' || value === 'canceled') {\n return 'aborted';\n }\n // new / started / queued / running / anything else → still running\n return 'in-progress';\n}\n\nexport async function backtest(\n req: BacktestRequest,\n opts: BacktestOptions = {},\n): Promise<BacktestResult> {\n const policy = buildStagePolicy(opts);\n\n // 1. Compile strategy (async mode)\n opts.onProgress?.({ stage: 'compiling' });\n const strategyId = await compileStrategy(req.strategy, policy, opts);\n\n // 2. Prepare data\n opts.onProgress?.({ stage: 'preparing' });\n const prepareJobId = await prepareData(req, policy, opts);\n\n // 3. Execute\n opts.onProgress?.({ stage: 'executing' });\n return executeStrategy(req, prepareJobId, strategyId, policy, opts);\n}\n\nfunction buildStagePolicy(opts: BacktestOptions): IPolicy<ICancellationContext, never> {\n const retryPolicy = retry(\n handleWhenResult((r) => {\n const status = (r as { status?: unknown } | undefined)?.status;\n return normalizeStatus(status) === 'in-progress';\n }),\n {\n maxAttempts: Number.MAX_SAFE_INTEGER,\n backoff: new ExponentialBackoff({\n initialDelay: opts.pollIntervalMs ?? 500,\n maxDelay: opts.maxPollIntervalMs ?? 5000,\n }),\n },\n );\n\n return opts.timeoutMs\n ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy)\n : retryPolicy;\n}\n\nasync function compileStrategy(\n source: string,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<string> {\n const { data, error } = await postStrategy({\n body: source,\n headers: { 'X-Compile-Async': true },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSStrategyCompileError('Strategy submission failed', error);\n if (!data) throw new QTSStrategyCompileError('Empty response from strategy endpoint');\n\n // Sync mode returns { strategyId }; async mode returns { jobId }.\n if ('strategyId' in data && data.strategyId) {\n return data.strategyId;\n }\n if (!('jobId' in data) || !data.jobId) {\n throw new QTSStrategyCompileError('Missing jobId/strategyId in compile response');\n }\n\n const compileJobId = data.jobId;\n const status = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getStrategyStatus({ path: { strategyId: compileJobId }, signal });\n if (res.error) throw new QTSStrategyCompileError('Compile status request failed', res.error);\n if (!res.data) throw new QTSStrategyCompileError('Empty compile status response');\n return res.data;\n },\n );\n\n const norm = normalizeStatus(status.status);\n if (norm === 'failed') {\n throw new QTSStrategyCompileError(status.statusDetail ?? 'Strategy compilation failed');\n }\n if (norm === 'aborted') {\n throw new QTSCanceledError('Strategy compilation aborted');\n }\n if (!status.strategyId) {\n throw new QTSStrategyCompileError('Compile completed without a strategyId');\n }\n return status.strategyId;\n}\n\nasync function prepareData(\n req: BacktestRequest,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<string> {\n const { data, error } = await prepareBacktesting({\n path: { exchangeId: req.exchangeId, type: TICKER },\n body: { instrument: req.instrument, from: req.from, to: req.to },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSPreparationError('Prepare submission failed', error);\n if (!data?.jobId) throw new QTSPreparationError('Missing jobId in prepare response');\n\n const prepareJobId = data.jobId;\n const state = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getPreparationStatus({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: prepareJobId },\n signal,\n });\n if (res.error) throw new QTSPreparationError('Preparation status request failed', res.error);\n if (!res.data) throw new QTSPreparationError('Empty preparation status response');\n return res.data;\n },\n (r) => {\n if (r.size > 0) {\n opts.onProgress?.({ stage: 'preparing', percent: (r.completed / r.size) * 100 });\n }\n },\n );\n\n const prepNorm = normalizeStatus(state.status);\n if (prepNorm === 'failed') {\n throw new QTSPreparationError(state.statusDetail ?? 'Data preparation failed');\n }\n if (prepNorm === 'aborted') {\n throw new QTSCanceledError('Data preparation aborted');\n }\n return prepareJobId;\n}\n\nasync function executeStrategy(\n req: BacktestRequest,\n prepareJobId: string,\n strategyId: string,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<BacktestResult> {\n const { data, error } = await executeBacktesting({\n path: { exchangeId: req.exchangeId, type: TICKER },\n body: {\n prepareJobId,\n strategyId,\n ...(req.storeSignals !== undefined ? { storeSignals: req.storeSignals } : {}),\n },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSExecutionError('Execute submission failed', error);\n if (!data?.jobId) throw new QTSExecutionError('Missing jobId in execute response');\n\n const executeJobId = data.jobId;\n\n try {\n const finalResult = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getExecutionResult({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n signal,\n });\n if (res.error) throw new QTSExecutionError('Execution result request failed', res.error);\n if (!res.data) throw new QTSExecutionError('Empty execution result response');\n return { ...res.data.state, __result: res.data.results };\n },\n (r) => {\n if (r.size > 0) {\n opts.onProgress?.({ stage: 'executing', percent: (r.completed / r.size) * 100 });\n }\n },\n );\n\n const execNorm = normalizeStatus(finalResult.status);\n if (execNorm === 'failed') {\n throw new QTSExecutionError(finalResult.statusDetail ?? 'Execution failed');\n }\n if (execNorm === 'aborted') {\n throw new QTSCanceledError('Execution aborted');\n }\n return finalResult.__result;\n } catch (err) {\n if (err instanceof QTSCanceledError) {\n await cancelExecution({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n }).catch(() => undefined);\n }\n throw err;\n }\n}\n\nasync function runStage<T extends { status: JobStatus }>(\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n fetchFn: (ctx: ICancellationContext) => Promise<T>,\n onEachAttempt?: (r: T) => void,\n): Promise<T> {\n try {\n return await policy.execute(async (ctx) => {\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n const result = await fetchFn(ctx);\n onEachAttempt?.(result);\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n return result;\n }, opts.signal);\n } catch (err) {\n if (err instanceof QTSCanceledError) throw err;\n if (err instanceof TaskCancelledError) {\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw new QTSTimeoutError(`Stage exceeded ${opts.timeoutMs}ms`, err);\n }\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw err;\n }\n}\n","export class QTSError extends Error {\n constructor(message: string, readonly cause?: unknown) {\n super(message);\n this.name = 'QTSError';\n }\n}\n\nexport class QTSStrategyCompileError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSStrategyCompileError';\n }\n}\n\nexport class QTSPreparationError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSPreparationError';\n }\n}\n\nexport class QTSExecutionError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSExecutionError';\n }\n}\n\nexport class QTSTimeoutError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSTimeoutError';\n }\n}\n\nexport class QTSCanceledError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSCanceledError';\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/client.ts","../src/workflows/backtest.ts","../src/errors.ts","../src/workflows/downloads.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 downloadKlines,\n downloadTickers,\n type DownloadFormat,\n} from './workflows/downloads';\n\nexport interface QTSurferOptions {\n baseUrl: string;\n token?: string;\n fetch?: typeof fetch;\n}\n\nexport interface DownloadHourArgs {\n exchangeId: string;\n base: string;\n quote: string;\n /** Hour selector in `YYYY-MM-DDTHH` (UTC). */\n hour: string;\n /** Wire format. Defaults to `'lastra'`. */\n format?: DownloadFormat;\n}\n\nexport class QTSurfer {\n constructor(options: QTSurferOptions) {\n apiClient.setConfig({\n baseUrl: options.baseUrl,\n ...(options.token\n ? { headers: { Authorization: `Bearer ${options.token}` } }\n : {}),\n ...(options.fetch ? { fetch: options.fetch } : {}),\n });\n }\n\n backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return backtest(req, opts);\n }\n\n /**\n * Download one hour of raw tickers for an instrument as a {@link Blob}.\n * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.\n */\n tickers(args: DownloadHourArgs): Promise<Blob> {\n return downloadTickers(args);\n }\n\n /** Download one hour of klines for an instrument as a {@link Blob}. */\n klines(args: DownloadHourArgs): Promise<Blob> {\n return downloadKlines(args);\n }\n\n // Future surface:\n // strategies: { compile, status, list }\n // instruments: { list, get } with TTL cache\n // jobs: { cancel, stream, result }\n}\n","import {\n cancelExecution,\n executeBacktesting,\n getExecutionResult,\n getPreparationStatus,\n getStrategyStatus,\n postStrategy,\n prepareBacktesting,\n type DataSourceType,\n type ResultMap,\n} from '@qtsurfer/api-client';\nimport {\n ExponentialBackoff,\n TaskCancelledError,\n TimeoutStrategy,\n handleWhenResult,\n retry,\n timeout,\n wrap,\n type IPolicy,\n type ICancellationContext,\n} from 'cockatiel';\nimport {\n QTSCanceledError,\n QTSExecutionError,\n QTSPreparationError,\n QTSStrategyCompileError,\n QTSTimeoutError,\n} from '../errors';\n\nexport interface BacktestRequest {\n /** Strategy source code (Java) */\n strategy: string;\n /** Exchange id, e.g. `binance` */\n exchangeId: string;\n /** Instrument symbol, e.g. `BTC/USDT` */\n instrument: string;\n /** Date range start (ISO-8601, ISO DATE or BASIC ISO DATE) */\n from: string;\n /** Date range end (same formats as `from`) */\n to: string;\n /** When true, the worker uploads emitted signals to object storage. */\n storeSignals?: boolean;\n}\n\nexport type BacktestResult = ResultMap;\n\nexport type BacktestStage = 'compiling' | 'preparing' | 'executing';\n\nexport interface BacktestProgress {\n stage: BacktestStage;\n /** 0-100 when size is known. Undefined during stage start. */\n percent?: number;\n}\n\nexport interface BacktestOptions {\n /** Abort the workflow. Cancels the current poll and calls `cancelExecution` server-side if execution has started. */\n signal?: AbortSignal;\n /** Called on stage transitions and after each poll with updated progress. */\n onProgress?: (p: BacktestProgress) => void;\n /** Initial interval between polls. Default 500ms, backed off up to `maxPollIntervalMs`. */\n pollIntervalMs?: number;\n /** Upper bound for exponential backoff. Default 5000ms. */\n maxPollIntervalMs?: number;\n /** Per-stage timeout. Default none. */\n timeoutMs?: number;\n}\n\nconst TICKER: DataSourceType = 'ticker';\n\ntype JobStatus = 'New' | 'Started' | 'Completed' | 'Aborted' | 'Failed' | 'Partial';\n\n/**\n * Normalize the backend job status to a stable lowercase form so we can\n * reason about it regardless of OpenAPI spec drift (the live API sometimes\n * returns lowercase values like `queued` / `completed` / `failed`).\n */\ntype NormalizedStatus = 'in-progress' | 'completed' | 'failed' | 'aborted';\n\nfunction normalizeStatus(raw: unknown): NormalizedStatus {\n const value = typeof raw === 'string' ? raw.toLowerCase() : '';\n if (value === 'completed') return 'completed';\n if (value === 'failed') return 'failed';\n if (value === 'aborted' || value === 'cancelled' || value === 'canceled') {\n return 'aborted';\n }\n // new / started / queued / running / anything else → still running\n return 'in-progress';\n}\n\nexport async function backtest(\n req: BacktestRequest,\n opts: BacktestOptions = {},\n): Promise<BacktestResult> {\n const policy = buildStagePolicy(opts);\n\n // 1. Compile strategy (async mode)\n opts.onProgress?.({ stage: 'compiling' });\n const strategyId = await compileStrategy(req.strategy, policy, opts);\n\n // 2. Prepare data\n opts.onProgress?.({ stage: 'preparing' });\n const prepareJobId = await prepareData(req, policy, opts);\n\n // 3. Execute\n opts.onProgress?.({ stage: 'executing' });\n return executeStrategy(req, prepareJobId, strategyId, policy, opts);\n}\n\nfunction buildStagePolicy(opts: BacktestOptions): IPolicy<ICancellationContext, never> {\n const retryPolicy = retry(\n handleWhenResult((r) => {\n const status = (r as { status?: unknown } | undefined)?.status;\n return normalizeStatus(status) === 'in-progress';\n }),\n {\n maxAttempts: Number.MAX_SAFE_INTEGER,\n backoff: new ExponentialBackoff({\n initialDelay: opts.pollIntervalMs ?? 500,\n maxDelay: opts.maxPollIntervalMs ?? 5000,\n }),\n },\n );\n\n return opts.timeoutMs\n ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy)\n : retryPolicy;\n}\n\nasync function compileStrategy(\n source: string,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<string> {\n const { data, error } = await postStrategy({\n body: source,\n headers: { 'X-Compile-Async': true },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSStrategyCompileError('Strategy submission failed', error);\n if (!data) throw new QTSStrategyCompileError('Empty response from strategy endpoint');\n\n // Sync mode returns { strategyId }; async mode returns { jobId }.\n if ('strategyId' in data && data.strategyId) {\n return data.strategyId;\n }\n if (!('jobId' in data) || !data.jobId) {\n throw new QTSStrategyCompileError('Missing jobId/strategyId in compile response');\n }\n\n const compileJobId = data.jobId;\n const status = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getStrategyStatus({ path: { strategyId: compileJobId }, signal });\n if (res.error) throw new QTSStrategyCompileError('Compile status request failed', res.error);\n if (!res.data) throw new QTSStrategyCompileError('Empty compile status response');\n return res.data;\n },\n );\n\n const norm = normalizeStatus(status.status);\n if (norm === 'failed') {\n throw new QTSStrategyCompileError(status.statusDetail ?? 'Strategy compilation failed');\n }\n if (norm === 'aborted') {\n throw new QTSCanceledError('Strategy compilation aborted');\n }\n if (!status.strategyId) {\n throw new QTSStrategyCompileError('Compile completed without a strategyId');\n }\n return status.strategyId;\n}\n\nasync function prepareData(\n req: BacktestRequest,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<string> {\n const { data, error } = await prepareBacktesting({\n path: { exchangeId: req.exchangeId, type: TICKER },\n body: { instrument: req.instrument, from: req.from, to: req.to },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSPreparationError('Prepare submission failed', error);\n if (!data?.jobId) throw new QTSPreparationError('Missing jobId in prepare response');\n\n const prepareJobId = data.jobId;\n const state = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getPreparationStatus({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: prepareJobId },\n signal,\n });\n if (res.error) throw new QTSPreparationError('Preparation status request failed', res.error);\n if (!res.data) throw new QTSPreparationError('Empty preparation status response');\n return res.data;\n },\n (r) => {\n if (r.size > 0) {\n opts.onProgress?.({ stage: 'preparing', percent: (r.completed / r.size) * 100 });\n }\n },\n );\n\n const prepNorm = normalizeStatus(state.status);\n if (prepNorm === 'failed') {\n throw new QTSPreparationError(state.statusDetail ?? 'Data preparation failed');\n }\n if (prepNorm === 'aborted') {\n throw new QTSCanceledError('Data preparation aborted');\n }\n return prepareJobId;\n}\n\nasync function executeStrategy(\n req: BacktestRequest,\n prepareJobId: string,\n strategyId: string,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<BacktestResult> {\n const { data, error } = await executeBacktesting({\n path: { exchangeId: req.exchangeId, type: TICKER },\n body: {\n prepareJobId,\n strategyId,\n ...(req.storeSignals !== undefined ? { storeSignals: req.storeSignals } : {}),\n },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSExecutionError('Execute submission failed', error);\n if (!data?.jobId) throw new QTSExecutionError('Missing jobId in execute response');\n\n const executeJobId = data.jobId;\n\n try {\n const finalResult = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getExecutionResult({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n signal,\n });\n if (res.error) throw new QTSExecutionError('Execution result request failed', res.error);\n if (!res.data) throw new QTSExecutionError('Empty execution result response');\n return { ...res.data.state, __result: res.data.results };\n },\n (r) => {\n if (r.size > 0) {\n opts.onProgress?.({ stage: 'executing', percent: (r.completed / r.size) * 100 });\n }\n },\n );\n\n const execNorm = normalizeStatus(finalResult.status);\n if (execNorm === 'failed') {\n throw new QTSExecutionError(finalResult.statusDetail ?? 'Execution failed');\n }\n if (execNorm === 'aborted') {\n throw new QTSCanceledError('Execution aborted');\n }\n return finalResult.__result;\n } catch (err) {\n if (err instanceof QTSCanceledError) {\n await cancelExecution({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n }).catch(() => undefined);\n }\n throw err;\n }\n}\n\nasync function runStage<T extends { status: JobStatus }>(\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n fetchFn: (ctx: ICancellationContext) => Promise<T>,\n onEachAttempt?: (r: T) => void,\n): Promise<T> {\n try {\n return await policy.execute(async (ctx) => {\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n const result = await fetchFn(ctx);\n onEachAttempt?.(result);\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');\n return result;\n }, opts.signal);\n } catch (err) {\n if (err instanceof QTSCanceledError) throw err;\n if (err instanceof TaskCancelledError) {\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw new QTSTimeoutError(`Stage exceeded ${opts.timeoutMs}ms`, err);\n }\n if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);\n throw err;\n }\n}\n","export class QTSError extends Error {\n /** 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\nexport class QTSStrategyCompileError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSStrategyCompileError';\n }\n}\n\nexport class QTSPreparationError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSPreparationError';\n }\n}\n\nexport class QTSExecutionError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSExecutionError';\n }\n}\n\nexport class QTSTimeoutError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSTimeoutError';\n }\n}\n\nexport class QTSCanceledError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSCanceledError';\n }\n}\n\nexport class QTSDownloadError extends QTSError {\n constructor(message: string, cause?: unknown, status?: number) {\n super(message, cause, status);\n this.name = 'QTSDownloadError';\n }\n}\n\n/**\n * Thrown by the `auth()` 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 getExchangeKlinesHour,\n getExchangeTickersHour,\n} from '@qtsurfer/api-client';\nimport { QTSDownloadError } from '../errors';\n\n/** Wire format for hourly tickers/klines downloads. */\nexport type DownloadFormat = 'lastra' | 'parquet';\n\nexport interface DownloadParams {\n exchangeId: string;\n base: string;\n quote: string;\n /** Hour selector in `YYYY-MM-DDTHH` (UTC). */\n hour: string;\n /** Defaults to {@code 'lastra'}. */\n format?: DownloadFormat;\n}\n\n/**\n * Download one hour of raw tickers as a {@link Blob}.\n *\n * The default wire format is Lastra (`application/vnd.lastra`); pass\n * `format: 'parquet'` for on-the-fly Parquet conversion.\n *\n * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.\n */\nexport async function downloadTickers(params: DownloadParams): Promise<Blob> {\n const { exchangeId, base, quote, hour, format } = params;\n const { data, error, response } = await getExchangeTickersHour({\n path: { exchangeId, base, quote },\n query: { hour, ...(format ? { format } : {}) },\n });\n if (error) {\n throw new QTSDownloadError(\n `tickers download failed: HTTP ${response.status} — ${describe(error)}`,\n error,\n 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 getExchangeKlinesHour({\n path: { exchangeId, base, quote },\n query: { hour, ...(format ? { format } : {}) },\n });\n if (error) {\n throw new QTSDownloadError(\n `klines download failed: HTTP ${response.status} — ${describe(error)}`,\n error,\n 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 auth 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 downloadKlines,\n downloadTickers,\n} from '../workflows/downloads';\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 auth}. 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 `auth() 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 backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return this.withRefreshOn401(() => runBacktest(req, opts));\n }\n\n tickers(args: DownloadHourArgs): Promise<Blob> {\n return this.withRefreshOn401(() => downloadTickers(args));\n }\n\n klines(args: DownloadHourArgs): Promise<Blob> {\n return this.withRefreshOn401(() => downloadKlines(args));\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 workflow surface as\n * `QTSurfer` (`backtest`, `tickers`, `klines`).\n *\n * @throws {QTSAuthError} if no apikey is supplied or available in env.\n */\nexport async function auth(\n apikey?: string,\n opts: AuthOptions = {},\n): Promise<AuthenticatedClient> {\n const resolved = apikey ?? readEnvApikey();\n if (!resolved) {\n throw new QTSAuthError(\n `auth() 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 `auth()` / 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":["apiClient","apiClient"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qtsurfer/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.3.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",
@@ -23,6 +23,9 @@
23
23
  "test": "vitest run",
24
24
  "test:watch": "vitest",
25
25
  "test:integration": "vitest run --config vitest.integration.config.ts",
26
+ "changeset": "changeset",
27
+ "changeset:version": "changeset version",
28
+ "changeset:publish": "changeset publish",
26
29
  "prepublishOnly": "npm run build"
27
30
  },
28
31
  "repository": {
@@ -47,10 +50,12 @@
47
50
  "access": "public"
48
51
  },
49
52
  "dependencies": {
50
- "@qtsurfer/api-client": "^0.1.1",
53
+ "@qtsurfer/api-client": "^0.2.1",
51
54
  "cockatiel": "^3.2.1"
52
55
  },
53
56
  "devDependencies": {
57
+ "@changesets/changelog-github": "^0.6.0",
58
+ "@changesets/cli": "^2.30.0",
54
59
  "@types/node": "^25.6.0",
55
60
  "@vitest/coverage-v8": "^4.1.4",
56
61
  "tsup": "^8.5.1",
@@ -0,0 +1,190 @@
1
+ import {
2
+ auth as apiAuth,
3
+ client as apiClient,
4
+ type AuthTokenResponse,
5
+ } from '@qtsurfer/api-client';
6
+ import { QTSAuthError } from '../errors';
7
+ import {
8
+ backtest as runBacktest,
9
+ type BacktestOptions,
10
+ type BacktestRequest,
11
+ type BacktestResult,
12
+ } from '../workflows/backtest';
13
+ import {
14
+ downloadKlines,
15
+ downloadTickers,
16
+ } from '../workflows/downloads';
17
+ import type { DownloadHourArgs } from '../client';
18
+ import { InMemoryTokenStore, type TokenStore } from './tokenStore';
19
+
20
+ const APIKEY_ENV_VAR = 'QTSURFER_APIKEY';
21
+ const DEFAULT_BASE_URL = 'https://api.qtsurfer.com/v1';
22
+
23
+ export interface AuthOptions {
24
+ /** Base URL of the QTSurfer API. Defaults to the public production endpoint. */
25
+ baseUrl?: string;
26
+ /** Custom token store. Defaults to {@link InMemoryTokenStore}. */
27
+ store?: TokenStore;
28
+ /** Inject a custom `fetch` (Node 20+, browser, or test mock). */
29
+ fetch?: typeof fetch;
30
+ }
31
+
32
+ /**
33
+ * Authenticated SDK session.
34
+ *
35
+ * Returned by {@link auth}. Wraps the underlying api-client, owns a JWT
36
+ * (in memory by default, or in the provided {@link TokenStore}), and
37
+ * transparently re-exchanges the apikey for a fresh JWT on 401.
38
+ *
39
+ * Multi-session note: the session mutates the api-client singleton config
40
+ * on every call. Concurrent sessions in the same process will race; today
41
+ * the SDK targets the one-session-per-process pattern.
42
+ */
43
+ export class AuthenticatedClient {
44
+ readonly baseUrl: string;
45
+ private readonly apikey: string;
46
+ private readonly store: TokenStore;
47
+ private readonly fetchImpl: typeof fetch | undefined;
48
+ private cached: AuthTokenResponse | null = null;
49
+ private refreshing: Promise<AuthTokenResponse> | null = null;
50
+
51
+ constructor(apikey: string, opts: AuthOptions = {}) {
52
+ this.apikey = apikey;
53
+ this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
54
+ this.store = opts.store ?? new InMemoryTokenStore();
55
+ this.fetchImpl = opts.fetch;
56
+ }
57
+
58
+ /** Currently cached token, if any. */
59
+ get token(): AuthTokenResponse | null {
60
+ return this.cached;
61
+ }
62
+
63
+ /** Force a fresh JWT exchange. Bypasses the cache. */
64
+ async refresh(): Promise<AuthTokenResponse> {
65
+ if (this.refreshing) return this.refreshing;
66
+ this.refreshing = (async () => {
67
+ const { data, error, response } = await apiAuth({
68
+ baseUrl: this.baseUrl,
69
+ headers: { 'X-API-Key': this.apikey },
70
+ ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),
71
+ });
72
+ if (error || !data) {
73
+ throw new QTSAuthError(
74
+ `auth() failed: HTTP ${response.status}`,
75
+ error,
76
+ );
77
+ }
78
+ this.cached = data;
79
+ await this.store.save(data);
80
+ return data;
81
+ })();
82
+ try {
83
+ return await this.refreshing;
84
+ } finally {
85
+ this.refreshing = null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Load a previously-persisted token from the store. If none, mint one.
91
+ * Called automatically by every workflow method.
92
+ */
93
+ async ensureToken(): Promise<AuthTokenResponse> {
94
+ if (this.cached) return this.cached;
95
+ const stored = await this.store.load();
96
+ if (stored) {
97
+ this.cached = stored;
98
+ return stored;
99
+ }
100
+ return this.refresh();
101
+ }
102
+
103
+ /** Drop the cached token (in memory and in the store). */
104
+ async clear(): Promise<void> {
105
+ this.cached = null;
106
+ await this.store.clear();
107
+ }
108
+
109
+ /**
110
+ * Run a call with the Bearer header pre-set; if it returns 401, refresh
111
+ * once and retry. A second 401 surfaces to the caller.
112
+ */
113
+ private async withRefreshOn401<T>(call: () => Promise<T>): Promise<T> {
114
+ await this.applyConfig();
115
+ try {
116
+ return await call();
117
+ } catch (err) {
118
+ if (!isUnauthorized(err)) throw err;
119
+ this.cached = null;
120
+ await this.refresh();
121
+ await this.applyConfig();
122
+ return call();
123
+ }
124
+ }
125
+
126
+ private async applyConfig(): Promise<void> {
127
+ const token = await this.ensureToken();
128
+ apiClient.setConfig({
129
+ baseUrl: this.baseUrl,
130
+ headers: { Authorization: `Bearer ${token.access_token}` },
131
+ ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),
132
+ });
133
+ }
134
+
135
+ // ---- Workflow surface (mirrors QTSurfer) ----
136
+
137
+ backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {
138
+ return this.withRefreshOn401(() => runBacktest(req, opts));
139
+ }
140
+
141
+ tickers(args: DownloadHourArgs): Promise<Blob> {
142
+ return this.withRefreshOn401(() => downloadTickers(args));
143
+ }
144
+
145
+ klines(args: DownloadHourArgs): Promise<Blob> {
146
+ return this.withRefreshOn401(() => downloadKlines(args));
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Exchange a long-lived API key for an authenticated session.
152
+ *
153
+ * If `apikey` is omitted, the SDK reads `QTSURFER_APIKEY` from the
154
+ * environment. The returned {@link AuthenticatedClient} caches the JWT,
155
+ * refreshes it on 401, and exposes the same workflow surface as
156
+ * `QTSurfer` (`backtest`, `tickers`, `klines`).
157
+ *
158
+ * @throws {QTSAuthError} if no apikey is supplied or available in env.
159
+ */
160
+ export async function auth(
161
+ apikey?: string,
162
+ opts: AuthOptions = {},
163
+ ): Promise<AuthenticatedClient> {
164
+ const resolved = apikey ?? readEnvApikey();
165
+ if (!resolved) {
166
+ throw new QTSAuthError(
167
+ `auth() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`,
168
+ );
169
+ }
170
+ const session = new AuthenticatedClient(resolved, opts);
171
+ await session.ensureToken();
172
+ return session;
173
+ }
174
+
175
+ function readEnvApikey(): string | undefined {
176
+ // `process` is undefined in browser bundlers; guard explicitly.
177
+ if (typeof process === 'undefined' || !process.env) return undefined;
178
+ const value = process.env[APIKEY_ENV_VAR];
179
+ return value && value.length > 0 ? value : undefined;
180
+ }
181
+
182
+ function isUnauthorized(err: unknown): boolean {
183
+ if (!err || typeof err !== 'object') return false;
184
+ // SDK-thrown errors (QTSDownloadError, etc.) carry the HTTP status on
185
+ // a top-level `status` field. Workflow errors that don't yet expose
186
+ // status default to non-401.
187
+ const maybeStatus = (err as { status?: unknown }).status;
188
+ if (typeof maybeStatus === 'number' && maybeStatus === 401) return true;
189
+ return false;
190
+ }
@@ -0,0 +1,41 @@
1
+ import type { AuthTokenResponse } from '@qtsurfer/api-client';
2
+
3
+ /**
4
+ * Pluggable token persistence interface.
5
+ *
6
+ * The SDK ships an {@link InMemoryTokenStore} by default. Adopters can
7
+ * implement this contract to back tokens by browser `localStorage`, an
8
+ * on-disk file, a secret manager, etc.
9
+ *
10
+ * The SDK calls {@link load} once per session-startup to seed a cached
11
+ * token (if any), {@link save} after every successful `auth()` / refresh,
12
+ * and {@link clear} when the session is explicitly invalidated.
13
+ */
14
+ export interface TokenStore {
15
+ /** Return the previously persisted token, or `null` if none. */
16
+ load(): AuthTokenResponse | null | Promise<AuthTokenResponse | null>;
17
+ /** Persist the token returned by `POST /v1/auth/token`. */
18
+ save(token: AuthTokenResponse): void | Promise<void>;
19
+ /** Drop any persisted token. */
20
+ clear(): void | Promise<void>;
21
+ }
22
+
23
+ /**
24
+ * Default {@link TokenStore} — holds the token in a single in-memory slot.
25
+ * Lost on process exit. Sufficient for short-lived scripts and tests.
26
+ */
27
+ export class InMemoryTokenStore implements TokenStore {
28
+ private token: AuthTokenResponse | null = null;
29
+
30
+ load(): AuthTokenResponse | null {
31
+ return this.token;
32
+ }
33
+
34
+ save(token: AuthTokenResponse): void {
35
+ this.token = token;
36
+ }
37
+
38
+ clear(): void {
39
+ this.token = null;
40
+ }
41
+ }
package/src/client.ts CHANGED
@@ -5,6 +5,11 @@ import {
5
5
  type BacktestRequest,
6
6
  type BacktestResult,
7
7
  } from './workflows/backtest';
8
+ import {
9
+ downloadKlines,
10
+ downloadTickers,
11
+ type DownloadFormat,
12
+ } from './workflows/downloads';
8
13
 
9
14
  export interface QTSurferOptions {
10
15
  baseUrl: string;
@@ -12,6 +17,16 @@ export interface QTSurferOptions {
12
17
  fetch?: typeof fetch;
13
18
  }
14
19
 
20
+ export interface DownloadHourArgs {
21
+ exchangeId: string;
22
+ base: string;
23
+ quote: string;
24
+ /** Hour selector in `YYYY-MM-DDTHH` (UTC). */
25
+ hour: string;
26
+ /** Wire format. Defaults to `'lastra'`. */
27
+ format?: DownloadFormat;
28
+ }
29
+
15
30
  export class QTSurfer {
16
31
  constructor(options: QTSurferOptions) {
17
32
  apiClient.setConfig({
@@ -27,6 +42,19 @@ export class QTSurfer {
27
42
  return backtest(req, opts);
28
43
  }
29
44
 
45
+ /**
46
+ * Download one hour of raw tickers for an instrument as a {@link Blob}.
47
+ * Defaults to Lastra; pass `{ format: 'parquet' }` for Parquet.
48
+ */
49
+ tickers(args: DownloadHourArgs): Promise<Blob> {
50
+ return downloadTickers(args);
51
+ }
52
+
53
+ /** Download one hour of klines for an instrument as a {@link Blob}. */
54
+ klines(args: DownloadHourArgs): Promise<Blob> {
55
+ return downloadKlines(args);
56
+ }
57
+
30
58
  // Future surface:
31
59
  // strategies: { compile, status, list }
32
60
  // instruments: { list, get } with TTL cache
package/src/errors.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export class QTSError extends Error {
2
- constructor(message: string, readonly cause?: unknown) {
2
+ /** HTTP status code, when the underlying transport surfaced one. */
3
+ readonly status?: number;
4
+ constructor(message: string, readonly cause?: unknown, status?: number) {
3
5
  super(message);
4
6
  this.name = 'QTSError';
7
+ if (status !== undefined) this.status = status;
5
8
  }
6
9
  }
7
10
 
@@ -39,3 +42,21 @@ export class QTSCanceledError extends QTSError {
39
42
  this.name = 'QTSCanceledError';
40
43
  }
41
44
  }
45
+
46
+ export class QTSDownloadError extends QTSError {
47
+ constructor(message: string, cause?: unknown, status?: number) {
48
+ super(message, cause, status);
49
+ this.name = 'QTSDownloadError';
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Thrown by the `auth()` helper when the apikey is missing or the JWT
55
+ * exchange fails (HTTP 401 from `POST /v1/auth/token`, etc.).
56
+ */
57
+ export class QTSAuthError extends QTSError {
58
+ constructor(message: string, cause?: unknown) {
59
+ super(message, cause);
60
+ this.name = 'QTSAuthError';
61
+ }
62
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { QTSurfer, type QTSurferOptions } from './client';
1
+ export { QTSurfer, type QTSurferOptions, type DownloadHourArgs } from './client';
2
2
  export {
3
3
  QTSError,
4
4
  QTSStrategyCompileError,
@@ -6,6 +6,8 @@ export {
6
6
  QTSExecutionError,
7
7
  QTSTimeoutError,
8
8
  QTSCanceledError,
9
+ QTSDownloadError,
10
+ QTSAuthError,
9
11
  } from './errors';
10
12
  export type {
11
13
  BacktestRequest,
@@ -14,3 +16,13 @@ export type {
14
16
  BacktestStage,
15
17
  BacktestOptions,
16
18
  } from './workflows/backtest';
19
+ export type { DownloadFormat } from './workflows/downloads';
20
+ export {
21
+ auth,
22
+ AuthenticatedClient,
23
+ type AuthOptions,
24
+ } from './auth/session';
25
+ export {
26
+ InMemoryTokenStore,
27
+ type TokenStore,
28
+ } from './auth/tokenStore';
@@ -68,7 +68,7 @@ export interface BacktestOptions {
68
68
 
69
69
  const TICKER: DataSourceType = 'ticker';
70
70
 
71
- type JobStatus = 'New' | 'Started' | 'Completed' | 'Aborted' | 'Failed';
71
+ type JobStatus = 'New' | 'Started' | 'Completed' | 'Aborted' | 'Failed' | 'Partial';
72
72
 
73
73
  /**
74
74
  * Normalize the backend job status to a stable lowercase form so we can
@@ -0,0 +1,76 @@
1
+ import {
2
+ getExchangeKlinesHour,
3
+ getExchangeTickersHour,
4
+ } from '@qtsurfer/api-client';
5
+ import { QTSDownloadError } from '../errors';
6
+
7
+ /** Wire format for hourly tickers/klines downloads. */
8
+ export type DownloadFormat = 'lastra' | 'parquet';
9
+
10
+ export interface DownloadParams {
11
+ exchangeId: string;
12
+ base: string;
13
+ quote: string;
14
+ /** Hour selector in `YYYY-MM-DDTHH` (UTC). */
15
+ hour: string;
16
+ /** Defaults to {@code 'lastra'}. */
17
+ format?: DownloadFormat;
18
+ }
19
+
20
+ /**
21
+ * Download one hour of raw tickers as a {@link Blob}.
22
+ *
23
+ * The default wire format is Lastra (`application/vnd.lastra`); pass
24
+ * `format: 'parquet'` for on-the-fly Parquet conversion.
25
+ *
26
+ * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.
27
+ */
28
+ export async function downloadTickers(params: DownloadParams): Promise<Blob> {
29
+ const { exchangeId, base, quote, hour, format } = params;
30
+ const { data, error, response } = await getExchangeTickersHour({
31
+ path: { exchangeId, base, quote },
32
+ query: { hour, ...(format ? { format } : {}) },
33
+ });
34
+ if (error) {
35
+ throw new QTSDownloadError(
36
+ `tickers download failed: HTTP ${response.status} — ${describe(error)}`,
37
+ error,
38
+ response.status,
39
+ );
40
+ }
41
+ return data as Blob;
42
+ }
43
+
44
+ /**
45
+ * Download one hour of klines as a {@link Blob}. See {@link downloadTickers}
46
+ * for semantics.
47
+ *
48
+ * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.
49
+ */
50
+ export async function downloadKlines(params: DownloadParams): Promise<Blob> {
51
+ const { exchangeId, base, quote, hour, format } = params;
52
+ const { data, error, response } = await getExchangeKlinesHour({
53
+ path: { exchangeId, base, quote },
54
+ query: { hour, ...(format ? { format } : {}) },
55
+ });
56
+ if (error) {
57
+ throw new QTSDownloadError(
58
+ `klines download failed: HTTP ${response.status} — ${describe(error)}`,
59
+ error,
60
+ response.status,
61
+ );
62
+ }
63
+ return data as Blob;
64
+ }
65
+
66
+ function describe(error: unknown): string {
67
+ if (error && typeof error === 'object') {
68
+ const e = error as { code?: unknown; message?: unknown };
69
+ const code = typeof e.code === 'string' ? e.code : undefined;
70
+ const message = typeof e.message === 'string' ? e.message : undefined;
71
+ if (code && message) return `${code}: ${message}`;
72
+ if (message) return message;
73
+ if (code) return code;
74
+ }
75
+ return String(error);
76
+ }