@qtsurfer/sdk 0.6.1 → 0.7.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/README.md +1 -1
- package/dist/index.d.ts +85 -0
- package/dist/index.js +29 -32
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/auth/session.ts +10 -0
- package/src/client.ts +26 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +6 -0
- package/src/workflows/backtest.ts +31 -41
- package/src/workflows/downloads.ts +4 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ const qts = new QTSurfer({
|
|
|
88
88
|
|
|
89
89
|
Orchestrates the full four-step workflow that the raw API exposes:
|
|
90
90
|
|
|
91
|
-
1. **Compile** the strategy (`POST /strategy`
|
|
91
|
+
1. **Compile** the strategy (`POST /strategy`), which answers synchronously with the `strategyId`.
|
|
92
92
|
2. **Prepare** the data range (`POST /backtest/{exchange}/ticker/prepare`) and poll until `Completed`.
|
|
93
93
|
3. **Execute** the backtest (`POST /backtest/{exchange}/ticker/execute`) and poll `GET /backtest/.../execute/{jobId}` until `Completed`.
|
|
94
94
|
4. Return the `ResultMap` (`pnlTotal`, `totalTrades`, `sharpeRatio`, `signalsUrl`, …).
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,15 @@ interface BacktestRequest {
|
|
|
14
14
|
/** When true, the worker uploads emitted signals to object storage. */
|
|
15
15
|
storeSignals?: boolean;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolved value of the backtest workflow (see {@link QTSurfer.backtest}).
|
|
19
|
+
* Alias for api-client's `ResultMap` — always includes core fields
|
|
20
|
+
* (`hostName`, `iops`, `instrument`); yield metrics (`pnlTotal`,
|
|
21
|
+
* `totalTrades`, `equityCurve`, etc.) are only present once the strategy
|
|
22
|
+
* has emitted at least one trade.
|
|
23
|
+
*/
|
|
17
24
|
type BacktestResult = ResultMap;
|
|
25
|
+
/** The three sequential stages {@link QTSurfer.backtest} moves through, in order. */
|
|
18
26
|
type BacktestStage = 'compiling' | 'preparing' | 'executing';
|
|
19
27
|
interface BacktestProgress {
|
|
20
28
|
stage: BacktestStage;
|
|
@@ -43,22 +51,48 @@ interface BacktestOptions {
|
|
|
43
51
|
/** Wire format for hourly tickers/klines downloads. */
|
|
44
52
|
type DownloadFormat = 'lastra' | 'parquet';
|
|
45
53
|
|
|
54
|
+
/** Configuration for {@link QTSurfer}. */
|
|
46
55
|
interface QTSurferOptions {
|
|
56
|
+
/** Base URL of the QTSurfer API, e.g. `https://api.qtsurfer.com/v1`. */
|
|
47
57
|
baseUrl: string;
|
|
58
|
+
/**
|
|
59
|
+
* Pre-obtained bearer token. When omitted, requests go out unauthenticated.
|
|
60
|
+
* Use the `authenticate()` helper instead of this constructor if you want
|
|
61
|
+
* the SDK to exchange an apikey for a JWT and refresh it on `401` for you.
|
|
62
|
+
*/
|
|
48
63
|
token?: string;
|
|
64
|
+
/** Inject a custom `fetch` (Node 20+, browser, or test mock). */
|
|
49
65
|
fetch?: typeof fetch;
|
|
50
66
|
}
|
|
67
|
+
/** Selects one hour of tickers or klines for a single instrument. */
|
|
51
68
|
interface DownloadHourArgs {
|
|
69
|
+
/** Exchange id, e.g. `binance`. */
|
|
52
70
|
exchangeId: string;
|
|
71
|
+
/** Base asset of the instrument, e.g. `BTC`. */
|
|
53
72
|
base: string;
|
|
73
|
+
/** Quote asset of the instrument, e.g. `USDT`. */
|
|
54
74
|
quote: string;
|
|
55
75
|
/** Hour selector in `YYYY-MM-DDTHH` (UTC). */
|
|
56
76
|
hour: string;
|
|
57
77
|
/** Wire format. Defaults to `'lastra'`. */
|
|
58
78
|
format?: DownloadFormat;
|
|
59
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Thin, stateless wrapper over `@qtsurfer/api-client` that exposes the SDK's
|
|
82
|
+
* workflow methods (`backtest`, `tickers`, `klines`). Constructing an
|
|
83
|
+
* instance reconfigures the underlying api-client singleton, so avoid
|
|
84
|
+
* holding two `QTSurfer`s with different `baseUrl`s or tokens alive in the
|
|
85
|
+
* same process — they will race. Prefer the `authenticate()` helper over
|
|
86
|
+
* this constructor unless you already manage the JWT lifecycle yourself.
|
|
87
|
+
*/
|
|
60
88
|
declare class QTSurfer {
|
|
61
89
|
constructor(options: QTSurferOptions);
|
|
90
|
+
/**
|
|
91
|
+
* Run a backtest end-to-end: compile the strategy, prepare the requested
|
|
92
|
+
* data range, execute it, and resolve with the result once execution
|
|
93
|
+
* completes. See the underlying `backtest` workflow for the
|
|
94
|
+
* stage-by-stage error and retry semantics.
|
|
95
|
+
*/
|
|
62
96
|
backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult>;
|
|
63
97
|
/**
|
|
64
98
|
* Download one hour of raw tickers for an instrument as a {@link Blob}.
|
|
@@ -69,27 +103,68 @@ declare class QTSurfer {
|
|
|
69
103
|
klines(args: DownloadHourArgs): Promise<Blob>;
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Base class for every error the SDK throws. Catch this to handle all SDK
|
|
108
|
+
* failures generically, or catch a specific subclass below to tell which
|
|
109
|
+
* 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.
|
|
112
|
+
*/
|
|
72
113
|
declare class QTSError extends Error {
|
|
73
114
|
readonly cause?: unknown | undefined;
|
|
74
115
|
/** HTTP status code, when the underlying transport surfaced one. */
|
|
75
116
|
readonly status?: number;
|
|
76
117
|
constructor(message: string, cause?: unknown | undefined, status?: number);
|
|
77
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Thrown when strategy compilation fails. A `429` means the source was never
|
|
121
|
+
* judged — too many compilations were already in flight — and is safe to
|
|
122
|
+
* retry. Any other status (typically `400`) means the source itself does
|
|
123
|
+
* not compile, so retrying with the same input fails again.
|
|
124
|
+
*/
|
|
78
125
|
declare class QTSStrategyCompileError extends QTSError {
|
|
79
126
|
constructor(message: string, cause?: unknown);
|
|
80
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Thrown when the data-preparation stage fails: submitting the prepare
|
|
130
|
+
* request, polling its status, or a backend-reported preparation failure
|
|
131
|
+
* (e.g. no data available for the requested range) all surface here.
|
|
132
|
+
*/
|
|
81
133
|
declare class QTSPreparationError extends QTSError {
|
|
82
134
|
constructor(message: string, cause?: unknown);
|
|
83
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Thrown when the execute stage fails: submitting the execute request,
|
|
138
|
+
* polling its result, or a backend-reported execution failure all surface
|
|
139
|
+
* here.
|
|
140
|
+
*/
|
|
84
141
|
declare class QTSExecutionError extends QTSError {
|
|
85
142
|
constructor(message: string, cause?: unknown);
|
|
86
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Thrown when a stage (prepare or execute) exceeds `timeoutMs`. The stage
|
|
146
|
+
* may still be running server-side — this only means the SDK stopped
|
|
147
|
+
* waiting locally — so it is fine to retry, optionally with a larger
|
|
148
|
+
* `timeoutMs`.
|
|
149
|
+
*/
|
|
87
150
|
declare class QTSTimeoutError extends QTSError {
|
|
88
151
|
constructor(message: string, cause?: unknown);
|
|
89
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Thrown when a stage is aborted — either because the caller's
|
|
155
|
+
* `AbortSignal` fired, or because the backend itself reported the
|
|
156
|
+
* prepare/execute job as aborted. Either way this reflects a deliberate
|
|
157
|
+
* stop, not a failure, and is not something to retry automatically.
|
|
158
|
+
*/
|
|
90
159
|
declare class QTSCanceledError extends QTSError {
|
|
91
160
|
constructor(message: string, cause?: unknown);
|
|
92
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Thrown by the tickers/klines download functions on any non-2xx response or
|
|
164
|
+
* transport failure. Carries the HTTP `status` when one was received: a
|
|
165
|
+
* `4xx` means the request itself is wrong (bad hour or instrument), while a
|
|
166
|
+
* `5xx` or a missing status (transport failure) is generally safe to retry.
|
|
167
|
+
*/
|
|
93
168
|
declare class QTSDownloadError extends QTSError {
|
|
94
169
|
constructor(message: string, cause?: unknown, status?: number);
|
|
95
170
|
}
|
|
@@ -175,8 +250,18 @@ declare class AuthenticatedClient {
|
|
|
175
250
|
*/
|
|
176
251
|
private withRefreshOn401;
|
|
177
252
|
private applyConfig;
|
|
253
|
+
/**
|
|
254
|
+
* Run a backtest end-to-end (compile → prepare → execute), sending the
|
|
255
|
+
* currently cached token (minting one first if none is cached). Unlike
|
|
256
|
+
* `tickers()`/`klines()`, a `401` here is not auto-retried: the underlying
|
|
257
|
+
* stage errors carry no HTTP status, so a token that expires mid-backtest
|
|
258
|
+
* surfaces as `QTSPreparationError`/`QTSExecutionError` rather than
|
|
259
|
+
* triggering a refresh.
|
|
260
|
+
*/
|
|
178
261
|
backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult>;
|
|
262
|
+
/** Download one hour of raw tickers. Refreshes the token once on `401` before retrying. */
|
|
179
263
|
tickers(args: DownloadHourArgs): Promise<Blob>;
|
|
264
|
+
/** Download one hour of klines. Refreshes the token once on `401` before retrying. */
|
|
180
265
|
klines(args: DownloadHourArgs): Promise<Blob>;
|
|
181
266
|
}
|
|
182
267
|
/**
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
executeBacktest,
|
|
8
8
|
getBacktestResult,
|
|
9
9
|
getPrepareStatus,
|
|
10
|
-
getStrategy,
|
|
11
10
|
compileStrategy as apiCompileStrategy,
|
|
12
11
|
prepareBacktest
|
|
13
12
|
} from "@qtsurfer/api-client";
|
|
@@ -90,7 +89,7 @@ function normalizeStatus(raw) {
|
|
|
90
89
|
async function backtest(req, opts = {}) {
|
|
91
90
|
const policy = buildStagePolicy(opts);
|
|
92
91
|
opts.onProgress?.({ stage: "compiling" });
|
|
93
|
-
const strategyId = await compileStrategy(req.strategy,
|
|
92
|
+
const strategyId = await compileStrategy(req.strategy, opts);
|
|
94
93
|
opts.onProgress?.({ stage: "preparing" });
|
|
95
94
|
const prepareJobId = await prepareData(req, policy, opts);
|
|
96
95
|
opts.onProgress?.({ stage: "executing" });
|
|
@@ -112,42 +111,24 @@ function buildStagePolicy(opts) {
|
|
|
112
111
|
);
|
|
113
112
|
return opts.timeoutMs ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy) : retryPolicy;
|
|
114
113
|
}
|
|
115
|
-
async function compileStrategy(source,
|
|
116
|
-
const { data, error } = await apiCompileStrategy({
|
|
114
|
+
async function compileStrategy(source, opts) {
|
|
115
|
+
const { data, error, response } = await apiCompileStrategy({
|
|
117
116
|
body: source,
|
|
118
|
-
headers: { "X-Compile-Async": true },
|
|
119
117
|
...opts.signal ? { signal: opts.signal } : {}
|
|
120
118
|
});
|
|
121
|
-
if (error)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throw new QTSStrategyCompileError("Missing jobId/strategyId in compile response");
|
|
128
|
-
}
|
|
129
|
-
const compileJobId = data.jobId;
|
|
130
|
-
const status = await runStage(
|
|
131
|
-
policy,
|
|
132
|
-
opts,
|
|
133
|
-
async ({ signal }) => {
|
|
134
|
-
const res = await getStrategy({ path: { strategyId: compileJobId }, signal });
|
|
135
|
-
if (res.error) throw new QTSStrategyCompileError("Compile status request failed", res.error);
|
|
136
|
-
if (!res.data) throw new QTSStrategyCompileError("Empty compile status response");
|
|
137
|
-
return res.data;
|
|
119
|
+
if (error) {
|
|
120
|
+
if (response?.status === 429) {
|
|
121
|
+
throw new QTSStrategyCompileError(
|
|
122
|
+
"Strategy was not compiled, too many compilations in flight; retry later",
|
|
123
|
+
error
|
|
124
|
+
);
|
|
138
125
|
}
|
|
139
|
-
|
|
140
|
-
const norm = normalizeStatus(status.status);
|
|
141
|
-
if (norm === "failed") {
|
|
142
|
-
throw new QTSStrategyCompileError(status.statusDetail ?? "Strategy compilation failed");
|
|
143
|
-
}
|
|
144
|
-
if (norm === "aborted") {
|
|
145
|
-
throw new QTSCanceledError("Strategy compilation aborted");
|
|
126
|
+
throw new QTSStrategyCompileError("Strategy compilation failed", error);
|
|
146
127
|
}
|
|
147
|
-
if (!
|
|
148
|
-
throw new QTSStrategyCompileError("Compile
|
|
128
|
+
if (!data?.strategyId) {
|
|
129
|
+
throw new QTSStrategyCompileError("Compile response missing strategyId");
|
|
149
130
|
}
|
|
150
|
-
return
|
|
131
|
+
return data.strategyId;
|
|
151
132
|
}
|
|
152
133
|
async function prepareData(req, policy, opts) {
|
|
153
134
|
const { data, error } = await prepareBacktest({
|
|
@@ -311,6 +292,12 @@ var QTSurfer = class {
|
|
|
311
292
|
...options.fetch ? { fetch: options.fetch } : {}
|
|
312
293
|
});
|
|
313
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Run a backtest end-to-end: compile the strategy, prepare the requested
|
|
297
|
+
* data range, execute it, and resolve with the result once execution
|
|
298
|
+
* completes. See the underlying `backtest` workflow for the
|
|
299
|
+
* stage-by-stage error and retry semantics.
|
|
300
|
+
*/
|
|
314
301
|
backtest(req, opts) {
|
|
315
302
|
return backtest(req, opts);
|
|
316
303
|
}
|
|
@@ -439,12 +426,22 @@ var AuthenticatedClient = class {
|
|
|
439
426
|
});
|
|
440
427
|
}
|
|
441
428
|
// ---- Workflow surface (mirrors QTSurfer) ----
|
|
429
|
+
/**
|
|
430
|
+
* Run a backtest end-to-end (compile → prepare → execute), sending the
|
|
431
|
+
* currently cached token (minting one first if none is cached). Unlike
|
|
432
|
+
* `tickers()`/`klines()`, a `401` here is not auto-retried: the underlying
|
|
433
|
+
* stage errors carry no HTTP status, so a token that expires mid-backtest
|
|
434
|
+
* surfaces as `QTSPreparationError`/`QTSExecutionError` rather than
|
|
435
|
+
* triggering a refresh.
|
|
436
|
+
*/
|
|
442
437
|
backtest(req, opts) {
|
|
443
438
|
return this.withRefreshOn401(() => backtest(req, opts));
|
|
444
439
|
}
|
|
440
|
+
/** Download one hour of raw tickers. Refreshes the token once on `401` before retrying. */
|
|
445
441
|
tickers(args) {
|
|
446
442
|
return this.withRefreshOn401(() => downloadTickers(args));
|
|
447
443
|
}
|
|
444
|
+
/** Download one hour of klines. Refreshes the token once on `401` before retrying. */
|
|
448
445
|
klines(args) {
|
|
449
446
|
return this.withRefreshOn401(() => downloadKlines(args));
|
|
450
447
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 cancelBacktest,\n executeBacktest,\n getBacktestResult,\n getPrepareStatus,\n getStrategy,\n compileStrategy as apiCompileStrategy,\n prepareBacktest,\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 * Fraction (0-1) of the requested prepare window that actually holds data,\n * reported by the backend once preparation completes. Present only on the\n * final `preparing` event.\n */\n coverageRatio?: number;\n}\n\nexport interface BacktestOptions {\n /** Abort the workflow. Cancels the current poll and calls `cancelBacktest` server-side if execution has started. */\n signal?: AbortSignal;\n /** Called on stage transitions and after each poll with updated progress. */\n onProgress?: (p: BacktestProgress) => void;\n /** Initial interval between polls. Default 500ms, backed off up to `maxPollIntervalMs`. */\n pollIntervalMs?: number;\n /** Upper bound for exponential backoff. Default 5000ms. */\n maxPollIntervalMs?: number;\n /** Per-stage timeout. Default none. */\n timeoutMs?: number;\n}\n\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 *\n * Only the three terminal statuses end a poll loop. Everything else — including a\n * **missing** status — means \"keep asking\": the API answers `202` with an empty body when a\n * job is known but its result is not readable yet, and that response carries no state at all.\n * Mapping absent to in-progress is what makes a 202 continue the loop under its timeout\n * instead of being mistaken for a finished job with no data.\n */\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 / absent (202) / 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 apiCompileStrategy({\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 getStrategy({ 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 prepareBacktest({\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 getPrepareStatus({\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 // Surface the backend's coverage ratio for the prepared window (spec 0.98.0) on the\n // final preparing event, so callers can react to a partially-covered range.\n opts.onProgress?.({ stage: 'preparing', percent: 100, coverageRatio: state.coverageRatio });\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 executeBacktest({\n path: { exchangeId: req.exchangeId, type: TICKER },\n body: {\n prepareJobId,\n strategyId,\n ...(req.storeSignals !== undefined ? { storeSignals: req.storeSignals } : {}),\n },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSExecutionError('Execute submission failed', error);\n if (!data?.jobId) throw new QTSExecutionError('Missing jobId in execute response');\n\n const executeJobId = data.jobId;\n\n try {\n const finalResult = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getBacktestResult({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n signal,\n });\n if (res.error) throw new QTSExecutionError('Execution result request failed', res.error);\n if (!res.data) throw new QTSExecutionError('Empty execution result response');\n // A 202 carries an empty body: no `state`, so the spread yields an undefined status and\n // the retry predicate keeps polling. That is the intended handling, not a coincidence —\n // see normalizeStatus. Do not \"fix\" this into a throw or an early return of the result.\n return { ...res.data.state, __result: res.data.results };\n },\n (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 cancelBacktest({\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 `authenticate()` helper when the apikey is missing or the JWT\n * exchange fails (HTTP 401 from `POST /v1/auth/token`, etc.).\n */\nexport class QTSAuthError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSAuthError';\n }\n}\n","import {\n downloadKlines as apiDownloadKlines,\n downloadTickers as apiDownloadTickers,\n} from '@qtsurfer/api-client';\nimport { QTSDownloadError } from '../errors';\n\n/** Wire format for hourly tickers/klines downloads. */\nexport type DownloadFormat = 'lastra' | 'parquet';\n\nexport interface DownloadParams {\n 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 apiDownloadTickers({\n path: { exchangeId, base, quote },\n query: { hour, ...(format ? { format } : {}) },\n });\n if (error) {\n throw new QTSDownloadError(\n `tickers download failed: HTTP ${response.status} — ${describe(error)}`,\n error,\n response.status,\n );\n }\n return data as Blob;\n}\n\n/**\n * Download one hour of klines as a {@link Blob}. See {@link downloadTickers}\n * for semantics.\n *\n * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.\n */\nexport async function downloadKlines(params: DownloadParams): Promise<Blob> {\n const { exchangeId, base, quote, hour, format } = params;\n const { data, error, response } = await apiDownloadKlines({\n path: { exchangeId, base, quote },\n query: { hour, ...(format ? { format } : {}) },\n });\n if (error) {\n throw new QTSDownloadError(\n `klines download failed: HTTP ${response.status} — ${describe(error)}`,\n error,\n response.status,\n );\n }\n return data as Blob;\n}\n\nfunction describe(error: unknown): string {\n if (error && typeof error === 'object') {\n const e = error as { code?: unknown; message?: unknown };\n const code = typeof e.code === 'string' ? e.code : undefined;\n const message = typeof e.message === 'string' ? e.message : undefined;\n if (code && message) return `${code}: ${message}`;\n if (message) return message;\n if (code) return code;\n }\n return String(error);\n}\n","import {\n authenticate as apiAuth,\n client as apiClient,\n type AuthTokenResponse,\n} from '@qtsurfer/api-client';\nimport { QTSAuthError } from '../errors';\nimport {\n backtest as runBacktest,\n type BacktestOptions,\n type BacktestRequest,\n type BacktestResult,\n} from '../workflows/backtest';\nimport {\n 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 authenticate}. Wraps the underlying api-client, owns a JWT\n * (in memory by default, or in the provided {@link TokenStore}), and\n * transparently re-exchanges the apikey for a fresh JWT on 401.\n *\n * Multi-session note: the session mutates the api-client singleton config\n * on every call. Concurrent sessions in the same process will race; today\n * the SDK targets the one-session-per-process pattern.\n */\nexport class AuthenticatedClient {\n readonly baseUrl: string;\n private readonly apikey: string;\n private readonly store: TokenStore;\n private readonly fetchImpl: typeof fetch | undefined;\n private cached: AuthTokenResponse | null = null;\n private refreshing: Promise<AuthTokenResponse> | null = null;\n\n constructor(apikey: string, opts: AuthOptions = {}) {\n this.apikey = apikey;\n this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;\n this.store = opts.store ?? new InMemoryTokenStore();\n this.fetchImpl = opts.fetch;\n }\n\n /** Currently cached token, if any. */\n get token(): AuthTokenResponse | null {\n return this.cached;\n }\n\n /** Force a fresh JWT exchange. Bypasses the cache. */\n async refresh(): Promise<AuthTokenResponse> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = (async () => {\n const { data, error, response } = await apiAuth({\n baseUrl: this.baseUrl,\n headers: { 'X-API-Key': this.apikey },\n ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),\n });\n if (error || !data) {\n throw new QTSAuthError(\n `authenticate() failed: HTTP ${response.status}`,\n error,\n );\n }\n this.cached = data;\n await this.store.save(data);\n return data;\n })();\n try {\n return await this.refreshing;\n } finally {\n this.refreshing = null;\n }\n }\n\n /**\n * Load a previously-persisted token from the store. If none, mint one.\n * Called automatically by every workflow method.\n */\n async ensureToken(): Promise<AuthTokenResponse> {\n if (this.cached) return this.cached;\n const stored = await this.store.load();\n if (stored) {\n this.cached = stored;\n return stored;\n }\n return this.refresh();\n }\n\n /** Drop the cached token (in memory and in the store). */\n async clear(): Promise<void> {\n this.cached = null;\n await this.store.clear();\n }\n\n /**\n * Run a call with the Bearer header pre-set; if it returns 401, refresh\n * once and retry. A second 401 surfaces to the caller.\n */\n private async withRefreshOn401<T>(call: () => Promise<T>): Promise<T> {\n await this.applyConfig();\n try {\n return await call();\n } catch (err) {\n if (!isUnauthorized(err)) throw err;\n this.cached = null;\n await this.refresh();\n await this.applyConfig();\n return call();\n }\n }\n\n private async applyConfig(): Promise<void> {\n const token = await this.ensureToken();\n apiClient.setConfig({\n baseUrl: this.baseUrl,\n headers: { Authorization: `Bearer ${token.access_token}` },\n ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),\n });\n }\n\n // ---- Workflow surface (mirrors QTSurfer) ----\n\n 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 authenticate(\n apikey?: string,\n opts: AuthOptions = {},\n): Promise<AuthenticatedClient> {\n const resolved = apikey ?? readEnvApikey();\n if (!resolved) {\n throw new QTSAuthError(\n `authenticate() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`,\n );\n }\n const session = new AuthenticatedClient(resolved, opts);\n await session.ensureToken();\n return session;\n}\n\nfunction readEnvApikey(): string | undefined {\n // `process` is undefined in browser bundlers; guard explicitly.\n if (typeof process === 'undefined' || !process.env) return undefined;\n const value = process.env[APIKEY_ENV_VAR];\n return value && value.length > 0 ? value : undefined;\n}\n\nfunction isUnauthorized(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n // SDK-thrown errors (QTSDownloadError, etc.) carry the HTTP status on\n // a top-level `status` field. Workflow errors that don't yet expose\n // status default to non-401.\n const maybeStatus = (err as { status?: unknown }).status;\n if (typeof maybeStatus === 'number' && maybeStatus === 401) return true;\n return false;\n}\n","import type { AuthTokenResponse } from '@qtsurfer/api-client';\n\n/**\n * Pluggable token persistence interface.\n *\n * The SDK ships an {@link InMemoryTokenStore} by default. Adopters can\n * implement this contract to back tokens by browser `localStorage`, an\n * on-disk file, a secret manager, etc.\n *\n * The SDK calls {@link load} once per session-startup to seed a cached\n * token (if any), {@link save} after every successful `authenticate()` / refresh,\n * and {@link clear} when the session is explicitly invalidated.\n */\nexport interface TokenStore {\n /** Return the previously persisted token, or `null` if none. */\n load(): AuthTokenResponse | null | Promise<AuthTokenResponse | null>;\n /** Persist the token returned by `POST /v1/auth/token`. */\n save(token: AuthTokenResponse): void | Promise<void>;\n /** Drop any persisted token. */\n clear(): void | Promise<void>;\n}\n\n/**\n * Default {@link TokenStore} — holds the token in a single in-memory slot.\n * Lost on process exit. Sufficient for short-lived scripts and tests.\n */\nexport class InMemoryTokenStore implements TokenStore {\n private token: AuthTokenResponse | null = null;\n\n load(): AuthTokenResponse | null {\n return this.token;\n }\n\n save(token: AuthTokenResponse): void {\n this.token = token;\n }\n\n clear(): void {\n this.token = null;\n }\n}\n"],"mappings":"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","names":["apiClient","apiClient"]}
|
|
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\n/** Configuration for {@link QTSurfer}. */\nexport interface QTSurferOptions {\n /** Base URL of the QTSurfer API, e.g. `https://api.qtsurfer.com/v1`. */\n baseUrl: string;\n /**\n * Pre-obtained bearer token. When omitted, requests go out unauthenticated.\n * Use the `authenticate()` helper instead of this constructor if you want\n * the SDK to exchange an apikey for a JWT and refresh it on `401` for you.\n */\n token?: string;\n /** Inject a custom `fetch` (Node 20+, browser, or test mock). */\n fetch?: typeof fetch;\n}\n\n/** Selects one hour of tickers or klines for a single instrument. */\nexport interface DownloadHourArgs {\n /** Exchange id, e.g. `binance`. */\n exchangeId: string;\n /** Base asset of the instrument, e.g. `BTC`. */\n base: string;\n /** Quote asset of the instrument, e.g. `USDT`. */\n quote: string;\n /** Hour selector in `YYYY-MM-DDTHH` (UTC). */\n hour: string;\n /** Wire format. Defaults to `'lastra'`. */\n format?: DownloadFormat;\n}\n\n/**\n * Thin, stateless wrapper over `@qtsurfer/api-client` that exposes the SDK's\n * workflow methods (`backtest`, `tickers`, `klines`). Constructing an\n * instance reconfigures the underlying api-client singleton, so avoid\n * holding two `QTSurfer`s with different `baseUrl`s or tokens alive in the\n * same process — they will race. Prefer the `authenticate()` helper over\n * this constructor unless you already manage the JWT lifecycle yourself.\n */\nexport class QTSurfer {\n constructor(options: QTSurferOptions) {\n apiClient.setConfig({\n baseUrl: options.baseUrl,\n ...(options.token\n ? { headers: { Authorization: `Bearer ${options.token}` } }\n : {}),\n ...(options.fetch ? { fetch: options.fetch } : {}),\n });\n }\n\n /**\n * Run a backtest end-to-end: compile the strategy, prepare the requested\n * data range, execute it, and resolve with the result once execution\n * completes. See the underlying `backtest` workflow for the\n * stage-by-stage error and retry semantics.\n */\n backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return backtest(req, opts);\n }\n\n /**\n * 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 cancelBacktest,\n executeBacktest,\n getBacktestResult,\n getPrepareStatus,\n compileStrategy as apiCompileStrategy,\n prepareBacktest,\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\n/**\n * Resolved value of the backtest workflow (see {@link QTSurfer.backtest}).\n * Alias for api-client's `ResultMap` — always includes core fields\n * (`hostName`, `iops`, `instrument`); yield metrics (`pnlTotal`,\n * `totalTrades`, `equityCurve`, etc.) are only present once the strategy\n * has emitted at least one trade.\n */\nexport type BacktestResult = ResultMap;\n\n/** The three sequential stages {@link QTSurfer.backtest} moves through, in order. */\nexport type BacktestStage = 'compiling' | 'preparing' | 'executing';\n\nexport interface BacktestProgress {\n stage: BacktestStage;\n /** 0-100 when size is known. Undefined during stage start. */\n percent?: number;\n /**\n * Fraction (0-1) of the requested prepare window that actually holds data,\n * reported by the backend once preparation completes. Present only on the\n * final `preparing` event.\n */\n coverageRatio?: number;\n}\n\nexport interface BacktestOptions {\n /** Abort the workflow. Cancels the current poll and calls `cancelBacktest` server-side if execution has started. */\n signal?: AbortSignal;\n /** Called on stage transitions and after each poll with updated progress. */\n onProgress?: (p: BacktestProgress) => void;\n /** Initial interval between polls. Default 500ms, backed off up to `maxPollIntervalMs`. */\n pollIntervalMs?: number;\n /** Upper bound for exponential backoff. Default 5000ms. */\n maxPollIntervalMs?: number;\n /** Per-stage timeout. Default none. */\n timeoutMs?: number;\n}\n\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 *\n * Only the three terminal statuses end a poll loop. Everything else — including a\n * **missing** status — means \"keep asking\": the API answers `202` with an empty body when a\n * job is known but its result is not readable yet, and that response carries no state at all.\n * Mapping absent to in-progress is what makes a 202 continue the loop under its timeout\n * instead of being mistaken for a finished job with no data.\n */\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 / absent (202) / 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 (single synchronous request)\n opts.onProgress?.({ stage: 'compiling' });\n const strategyId = await compileStrategy(req.strategy, 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\n/**\n * Compile in a single request: the API answers synchronously with the `strategyId`,\n * so there is no job to poll. A compile error arrives here as a `400`, not on a later poll.\n */\nasync function compileStrategy(source: string, opts: BacktestOptions): Promise<string> {\n const { data, error, response } = await apiCompileStrategy({\n body: source,\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n\n if (error) {\n // A 429 means the platform is holding too many compilations at once and the source was never\n // judged — worth separating from the 400 that says the source itself does not compile.\n // Read the status inside this branch and optionally: a transport failure carries no response,\n // and dereferencing one would raise a TypeError that buries the error actually being reported.\n if (response?.status === 429) {\n throw new QTSStrategyCompileError(\n 'Strategy was not compiled, too many compilations in flight; retry later',\n error,\n );\n }\n throw new QTSStrategyCompileError('Strategy compilation failed', error);\n }\n if (!data?.strategyId) {\n throw new QTSStrategyCompileError('Compile response missing strategyId');\n }\n return data.strategyId;\n}\n\nasync function prepareData(\n req: BacktestRequest,\n policy: IPolicy<ICancellationContext, never>,\n opts: BacktestOptions,\n): Promise<string> {\n const { data, error } = await prepareBacktest({\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 getPrepareStatus({\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 // Surface the backend's coverage ratio for the prepared window (spec 0.98.0) on the\n // final preparing event, so callers can react to a partially-covered range.\n opts.onProgress?.({ stage: 'preparing', percent: 100, coverageRatio: state.coverageRatio });\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 executeBacktest({\n path: { exchangeId: req.exchangeId, type: TICKER },\n body: {\n prepareJobId,\n strategyId,\n ...(req.storeSignals !== undefined ? { storeSignals: req.storeSignals } : {}),\n },\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n if (error) throw new QTSExecutionError('Execute submission failed', error);\n if (!data?.jobId) throw new QTSExecutionError('Missing jobId in execute response');\n\n const executeJobId = data.jobId;\n\n try {\n const finalResult = await runStage(\n policy,\n opts,\n async ({ signal }) => {\n const res = await getBacktestResult({\n path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },\n signal,\n });\n if (res.error) throw new QTSExecutionError('Execution result request failed', res.error);\n if (!res.data) throw new QTSExecutionError('Empty execution result response');\n // A 202 carries an empty body: no `state`, so the spread yields an undefined status and\n // the retry predicate keeps polling. That is the intended handling, not a coincidence —\n // see normalizeStatus. Do not \"fix\" this into a throw or an early return of the result.\n return { ...res.data.state, __result: res.data.results };\n },\n (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 cancelBacktest({\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","/**\n * Base class for every error the SDK throws. Catch this to handle all SDK\n * failures generically, or catch a specific subclass below to tell which\n * stage failed. `status` is only set when the throw site had an HTTP status\n * to attach — today that is just {@link QTSDownloadError}; the workflow-stage\n * errors carry `cause` instead and encode retryability in their message.\n */\nexport class QTSError extends Error {\n /** HTTP status code, when the underlying transport surfaced one. */\n readonly status?: number;\n constructor(message: string, readonly cause?: unknown, status?: number) {\n super(message);\n this.name = 'QTSError';\n if (status !== undefined) this.status = status;\n }\n}\n\n/**\n * Thrown when strategy compilation fails. A `429` means the source was never\n * judged — too many compilations were already in flight — and is safe to\n * retry. Any other status (typically `400`) means the source itself does\n * not compile, so retrying with the same input fails again.\n */\nexport class QTSStrategyCompileError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSStrategyCompileError';\n }\n}\n\n/**\n * Thrown when the data-preparation stage fails: submitting the prepare\n * request, polling its status, or a backend-reported preparation failure\n * (e.g. no data available for the requested range) all surface here.\n */\nexport class QTSPreparationError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSPreparationError';\n }\n}\n\n/**\n * Thrown when the execute stage fails: submitting the execute request,\n * polling its result, or a backend-reported execution failure all surface\n * here.\n */\nexport class QTSExecutionError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSExecutionError';\n }\n}\n\n/**\n * Thrown when a stage (prepare or execute) exceeds `timeoutMs`. The stage\n * may still be running server-side — this only means the SDK stopped\n * waiting locally — so it is fine to retry, optionally with a larger\n * `timeoutMs`.\n */\nexport class QTSTimeoutError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSTimeoutError';\n }\n}\n\n/**\n * Thrown when a stage is aborted — either because the caller's\n * `AbortSignal` fired, or because the backend itself reported the\n * prepare/execute job as aborted. Either way this reflects a deliberate\n * stop, not a failure, and is not something to retry automatically.\n */\nexport class QTSCanceledError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSCanceledError';\n }\n}\n\n/**\n * Thrown by the tickers/klines download functions on any non-2xx response or\n * transport failure. Carries the HTTP `status` when one was received: a\n * `4xx` means the request itself is wrong (bad hour or instrument), while a\n * `5xx` or a missing status (transport failure) is generally safe to retry.\n */\nexport class QTSDownloadError extends QTSError {\n constructor(message: string, cause?: unknown, status?: number) {\n super(message, cause, status);\n this.name = 'QTSDownloadError';\n }\n}\n\n/**\n * Thrown by the `authenticate()` helper when the apikey is missing or the JWT\n * exchange fails (HTTP 401 from `POST /v1/auth/token`, etc.).\n */\nexport class QTSAuthError extends QTSError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'QTSAuthError';\n }\n}\n","import {\n downloadKlines as apiDownloadKlines,\n downloadTickers as apiDownloadTickers,\n} from '@qtsurfer/api-client';\nimport { QTSDownloadError } from '../errors';\n\n/** Wire format for hourly tickers/klines downloads. */\nexport type DownloadFormat = 'lastra' | 'parquet';\n\nexport interface DownloadParams {\n /** Exchange id, e.g. `binance`. */\n exchangeId: string;\n /** Base asset of the instrument, e.g. `BTC`. */\n base: string;\n /** Quote asset of the instrument, e.g. `USDT`. */\n quote: string;\n /** Hour selector in `YYYY-MM-DDTHH` (UTC). */\n hour: string;\n /** Defaults to `'lastra'`. */\n format?: DownloadFormat;\n}\n\n/**\n * Download one hour of raw tickers as a {@link Blob}.\n *\n * The default wire format is Lastra (`application/vnd.lastra`); pass\n * `format: 'parquet'` for on-the-fly Parquet conversion.\n *\n * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.\n */\nexport async function downloadTickers(params: DownloadParams): Promise<Blob> {\n const { exchangeId, base, quote, hour, format } = params;\n const { data, error, response } = await apiDownloadTickers({\n path: { exchangeId, base, quote },\n query: { hour, ...(format ? { format } : {}) },\n });\n if (error) {\n throw new QTSDownloadError(\n `tickers download failed: HTTP ${response.status} — ${describe(error)}`,\n error,\n response.status,\n );\n }\n return data as Blob;\n}\n\n/**\n * Download one hour of klines as a {@link Blob}. See {@link downloadTickers}\n * for semantics.\n *\n * @throws QTSDownloadError on HTTP 4xx/5xx or transport failure.\n */\nexport async function downloadKlines(params: DownloadParams): Promise<Blob> {\n const { exchangeId, base, quote, hour, format } = params;\n const { data, error, response } = await apiDownloadKlines({\n path: { exchangeId, base, quote },\n query: { hour, ...(format ? { format } : {}) },\n });\n if (error) {\n throw new QTSDownloadError(\n `klines download failed: HTTP ${response.status} — ${describe(error)}`,\n error,\n response.status,\n );\n }\n return data as Blob;\n}\n\nfunction describe(error: unknown): string {\n if (error && typeof error === 'object') {\n const e = error as { code?: unknown; message?: unknown };\n const code = typeof e.code === 'string' ? e.code : undefined;\n const message = typeof e.message === 'string' ? e.message : undefined;\n if (code && message) return `${code}: ${message}`;\n if (message) return message;\n if (code) return code;\n }\n return String(error);\n}\n","import {\n authenticate as apiAuth,\n client as apiClient,\n type AuthTokenResponse,\n} from '@qtsurfer/api-client';\nimport { QTSAuthError } from '../errors';\nimport {\n backtest as runBacktest,\n type BacktestOptions,\n type BacktestRequest,\n type BacktestResult,\n} from '../workflows/backtest';\nimport {\n 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 authenticate}. Wraps the underlying api-client, owns a JWT\n * (in memory by default, or in the provided {@link TokenStore}), and\n * transparently re-exchanges the apikey for a fresh JWT on 401.\n *\n * Multi-session note: the session mutates the api-client singleton config\n * on every call. Concurrent sessions in the same process will race; today\n * the SDK targets the one-session-per-process pattern.\n */\nexport class AuthenticatedClient {\n readonly baseUrl: string;\n private readonly apikey: string;\n private readonly store: TokenStore;\n private readonly fetchImpl: typeof fetch | undefined;\n private cached: AuthTokenResponse | null = null;\n private refreshing: Promise<AuthTokenResponse> | null = null;\n\n constructor(apikey: string, opts: AuthOptions = {}) {\n this.apikey = apikey;\n this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;\n this.store = opts.store ?? new InMemoryTokenStore();\n this.fetchImpl = opts.fetch;\n }\n\n /** Currently cached token, if any. */\n get token(): AuthTokenResponse | null {\n return this.cached;\n }\n\n /** Force a fresh JWT exchange. Bypasses the cache. */\n async refresh(): Promise<AuthTokenResponse> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = (async () => {\n const { data, error, response } = await apiAuth({\n baseUrl: this.baseUrl,\n headers: { 'X-API-Key': this.apikey },\n ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),\n });\n if (error || !data) {\n throw new QTSAuthError(\n `authenticate() failed: HTTP ${response.status}`,\n error,\n );\n }\n this.cached = data;\n await this.store.save(data);\n return data;\n })();\n try {\n return await this.refreshing;\n } finally {\n this.refreshing = null;\n }\n }\n\n /**\n * Load a previously-persisted token from the store. If none, mint one.\n * Called automatically by every workflow method.\n */\n async ensureToken(): Promise<AuthTokenResponse> {\n if (this.cached) return this.cached;\n const stored = await this.store.load();\n if (stored) {\n this.cached = stored;\n return stored;\n }\n return this.refresh();\n }\n\n /** Drop the cached token (in memory and in the store). */\n async clear(): Promise<void> {\n this.cached = null;\n await this.store.clear();\n }\n\n /**\n * Run a call with the Bearer header pre-set; if it returns 401, refresh\n * once and retry. A second 401 surfaces to the caller.\n */\n private async withRefreshOn401<T>(call: () => Promise<T>): Promise<T> {\n await this.applyConfig();\n try {\n return await call();\n } catch (err) {\n if (!isUnauthorized(err)) throw err;\n this.cached = null;\n await this.refresh();\n await this.applyConfig();\n return call();\n }\n }\n\n private async applyConfig(): Promise<void> {\n const token = await this.ensureToken();\n apiClient.setConfig({\n baseUrl: this.baseUrl,\n headers: { Authorization: `Bearer ${token.access_token}` },\n ...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),\n });\n }\n\n // ---- Workflow surface (mirrors QTSurfer) ----\n\n /**\n * Run a backtest end-to-end (compile → prepare → execute), sending the\n * currently cached token (minting one first if none is cached). Unlike\n * `tickers()`/`klines()`, a `401` here is not auto-retried: the underlying\n * stage errors carry no HTTP status, so a token that expires mid-backtest\n * surfaces as `QTSPreparationError`/`QTSExecutionError` rather than\n * triggering a refresh.\n */\n backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {\n return this.withRefreshOn401(() => runBacktest(req, opts));\n }\n\n /** Download one hour of raw tickers. Refreshes the token once on `401` before retrying. */\n tickers(args: DownloadHourArgs): Promise<Blob> {\n return this.withRefreshOn401(() => downloadTickers(args));\n }\n\n /** Download one hour of klines. Refreshes the token once on `401` before retrying. */\n klines(args: DownloadHourArgs): Promise<Blob> {\n return this.withRefreshOn401(() => downloadKlines(args));\n }\n}\n\n/**\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 authenticate(\n apikey?: string,\n opts: AuthOptions = {},\n): Promise<AuthenticatedClient> {\n const resolved = apikey ?? readEnvApikey();\n if (!resolved) {\n throw new QTSAuthError(\n `authenticate() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`,\n );\n }\n const session = new AuthenticatedClient(resolved, opts);\n await session.ensureToken();\n return session;\n}\n\nfunction readEnvApikey(): string | undefined {\n // `process` is undefined in browser bundlers; guard explicitly.\n if (typeof process === 'undefined' || !process.env) return undefined;\n const value = process.env[APIKEY_ENV_VAR];\n return value && value.length > 0 ? value : undefined;\n}\n\nfunction isUnauthorized(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n // SDK-thrown errors (QTSDownloadError, etc.) carry the HTTP status on\n // a top-level `status` field. Workflow errors that don't yet expose\n // status default to non-401.\n const maybeStatus = (err as { status?: unknown }).status;\n if (typeof maybeStatus === 'number' && maybeStatus === 401) return true;\n return false;\n}\n","import type { AuthTokenResponse } from '@qtsurfer/api-client';\n\n/**\n * Pluggable token persistence interface.\n *\n * The SDK ships an {@link InMemoryTokenStore} by default. Adopters can\n * implement this contract to back tokens by browser `localStorage`, an\n * on-disk file, a secret manager, etc.\n *\n * The SDK calls {@link load} once per session-startup to seed a cached\n * token (if any), {@link save} after every successful `authenticate()` / refresh,\n * and {@link clear} when the session is explicitly invalidated.\n */\nexport interface TokenStore {\n /** Return the previously persisted token, or `null` if none. */\n load(): AuthTokenResponse | null | Promise<AuthTokenResponse | null>;\n /** Persist the token returned by `POST /v1/auth/token`. */\n save(token: AuthTokenResponse): void | Promise<void>;\n /** Drop any persisted token. */\n clear(): void | Promise<void>;\n}\n\n/**\n * Default {@link TokenStore} — holds the token in a single in-memory slot.\n * Lost on process exit. Sufficient for short-lived scripts and tests.\n */\nexport class InMemoryTokenStore implements TokenStore {\n private token: AuthTokenResponse | null = null;\n\n load(): AuthTokenResponse | null {\n return this.token;\n }\n\n save(token: AuthTokenResponse): void {\n this.token = token;\n }\n\n clear(): void {\n this.token = null;\n }\n}\n"],"mappings":"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","names":["apiClient","apiClient"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qtsurfer/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsup",
|
|
22
22
|
"lint": "tsc --noEmit -p tsconfig.test.json",
|
|
23
|
+
"docs": "typedoc",
|
|
23
24
|
"test": "vitest run",
|
|
24
25
|
"test:watch": "vitest",
|
|
25
26
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"access": "public"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
|
-
"@qtsurfer/api-client": "^0.
|
|
54
|
+
"@qtsurfer/api-client": "^0.7.0",
|
|
54
55
|
"cockatiel": "^3.2.1"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"@types/node": "^25.6.0",
|
|
60
61
|
"@vitest/coverage-v8": "^4.1.4",
|
|
61
62
|
"tsup": "^8.5.1",
|
|
63
|
+
"typedoc": "^0.28.20",
|
|
62
64
|
"typescript": "^5.8.0",
|
|
63
65
|
"vitest": "^4.1.4"
|
|
64
66
|
},
|
package/src/auth/session.ts
CHANGED
|
@@ -134,14 +134,24 @@ export class AuthenticatedClient {
|
|
|
134
134
|
|
|
135
135
|
// ---- Workflow surface (mirrors QTSurfer) ----
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Run a backtest end-to-end (compile → prepare → execute), sending the
|
|
139
|
+
* currently cached token (minting one first if none is cached). Unlike
|
|
140
|
+
* `tickers()`/`klines()`, a `401` here is not auto-retried: the underlying
|
|
141
|
+
* stage errors carry no HTTP status, so a token that expires mid-backtest
|
|
142
|
+
* surfaces as `QTSPreparationError`/`QTSExecutionError` rather than
|
|
143
|
+
* triggering a refresh.
|
|
144
|
+
*/
|
|
137
145
|
backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {
|
|
138
146
|
return this.withRefreshOn401(() => runBacktest(req, opts));
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
/** Download one hour of raw tickers. Refreshes the token once on `401` before retrying. */
|
|
141
150
|
tickers(args: DownloadHourArgs): Promise<Blob> {
|
|
142
151
|
return this.withRefreshOn401(() => downloadTickers(args));
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
/** Download one hour of klines. Refreshes the token once on `401` before retrying. */
|
|
145
155
|
klines(args: DownloadHourArgs): Promise<Blob> {
|
|
146
156
|
return this.withRefreshOn401(() => downloadKlines(args));
|
|
147
157
|
}
|
package/src/client.ts
CHANGED
|
@@ -11,15 +11,27 @@ import {
|
|
|
11
11
|
type DownloadFormat,
|
|
12
12
|
} from './workflows/downloads';
|
|
13
13
|
|
|
14
|
+
/** Configuration for {@link QTSurfer}. */
|
|
14
15
|
export interface QTSurferOptions {
|
|
16
|
+
/** Base URL of the QTSurfer API, e.g. `https://api.qtsurfer.com/v1`. */
|
|
15
17
|
baseUrl: string;
|
|
18
|
+
/**
|
|
19
|
+
* Pre-obtained bearer token. When omitted, requests go out unauthenticated.
|
|
20
|
+
* Use the `authenticate()` helper instead of this constructor if you want
|
|
21
|
+
* the SDK to exchange an apikey for a JWT and refresh it on `401` for you.
|
|
22
|
+
*/
|
|
16
23
|
token?: string;
|
|
24
|
+
/** Inject a custom `fetch` (Node 20+, browser, or test mock). */
|
|
17
25
|
fetch?: typeof fetch;
|
|
18
26
|
}
|
|
19
27
|
|
|
28
|
+
/** Selects one hour of tickers or klines for a single instrument. */
|
|
20
29
|
export interface DownloadHourArgs {
|
|
30
|
+
/** Exchange id, e.g. `binance`. */
|
|
21
31
|
exchangeId: string;
|
|
32
|
+
/** Base asset of the instrument, e.g. `BTC`. */
|
|
22
33
|
base: string;
|
|
34
|
+
/** Quote asset of the instrument, e.g. `USDT`. */
|
|
23
35
|
quote: string;
|
|
24
36
|
/** Hour selector in `YYYY-MM-DDTHH` (UTC). */
|
|
25
37
|
hour: string;
|
|
@@ -27,6 +39,14 @@ export interface DownloadHourArgs {
|
|
|
27
39
|
format?: DownloadFormat;
|
|
28
40
|
}
|
|
29
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Thin, stateless wrapper over `@qtsurfer/api-client` that exposes the SDK's
|
|
44
|
+
* workflow methods (`backtest`, `tickers`, `klines`). Constructing an
|
|
45
|
+
* instance reconfigures the underlying api-client singleton, so avoid
|
|
46
|
+
* holding two `QTSurfer`s with different `baseUrl`s or tokens alive in the
|
|
47
|
+
* same process — they will race. Prefer the `authenticate()` helper over
|
|
48
|
+
* this constructor unless you already manage the JWT lifecycle yourself.
|
|
49
|
+
*/
|
|
30
50
|
export class QTSurfer {
|
|
31
51
|
constructor(options: QTSurferOptions) {
|
|
32
52
|
apiClient.setConfig({
|
|
@@ -38,6 +58,12 @@ export class QTSurfer {
|
|
|
38
58
|
});
|
|
39
59
|
}
|
|
40
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Run a backtest end-to-end: compile the strategy, prepare the requested
|
|
63
|
+
* data range, execute it, and resolve with the result once execution
|
|
64
|
+
* completes. See the underlying `backtest` workflow for the
|
|
65
|
+
* stage-by-stage error and retry semantics.
|
|
66
|
+
*/
|
|
41
67
|
backtest(req: BacktestRequest, opts?: BacktestOptions): Promise<BacktestResult> {
|
|
42
68
|
return backtest(req, opts);
|
|
43
69
|
}
|
package/src/errors.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for every error the SDK throws. Catch this to handle all SDK
|
|
3
|
+
* failures generically, or catch a specific subclass below to tell which
|
|
4
|
+
* stage failed. `status` is only set when the throw site had an HTTP status
|
|
5
|
+
* to attach — today that is just {@link QTSDownloadError}; the workflow-stage
|
|
6
|
+
* errors carry `cause` instead and encode retryability in their message.
|
|
7
|
+
*/
|
|
1
8
|
export class QTSError extends Error {
|
|
2
9
|
/** HTTP status code, when the underlying transport surfaced one. */
|
|
3
10
|
readonly status?: number;
|
|
@@ -8,6 +15,12 @@ export class QTSError extends Error {
|
|
|
8
15
|
}
|
|
9
16
|
}
|
|
10
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when strategy compilation fails. A `429` means the source was never
|
|
20
|
+
* judged — too many compilations were already in flight — and is safe to
|
|
21
|
+
* retry. Any other status (typically `400`) means the source itself does
|
|
22
|
+
* not compile, so retrying with the same input fails again.
|
|
23
|
+
*/
|
|
11
24
|
export class QTSStrategyCompileError extends QTSError {
|
|
12
25
|
constructor(message: string, cause?: unknown) {
|
|
13
26
|
super(message, cause);
|
|
@@ -15,6 +28,11 @@ export class QTSStrategyCompileError extends QTSError {
|
|
|
15
28
|
}
|
|
16
29
|
}
|
|
17
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Thrown when the data-preparation stage fails: submitting the prepare
|
|
33
|
+
* request, polling its status, or a backend-reported preparation failure
|
|
34
|
+
* (e.g. no data available for the requested range) all surface here.
|
|
35
|
+
*/
|
|
18
36
|
export class QTSPreparationError extends QTSError {
|
|
19
37
|
constructor(message: string, cause?: unknown) {
|
|
20
38
|
super(message, cause);
|
|
@@ -22,6 +40,11 @@ export class QTSPreparationError extends QTSError {
|
|
|
22
40
|
}
|
|
23
41
|
}
|
|
24
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Thrown when the execute stage fails: submitting the execute request,
|
|
45
|
+
* polling its result, or a backend-reported execution failure all surface
|
|
46
|
+
* here.
|
|
47
|
+
*/
|
|
25
48
|
export class QTSExecutionError extends QTSError {
|
|
26
49
|
constructor(message: string, cause?: unknown) {
|
|
27
50
|
super(message, cause);
|
|
@@ -29,6 +52,12 @@ export class QTSExecutionError extends QTSError {
|
|
|
29
52
|
}
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Thrown when a stage (prepare or execute) exceeds `timeoutMs`. The stage
|
|
57
|
+
* may still be running server-side — this only means the SDK stopped
|
|
58
|
+
* waiting locally — so it is fine to retry, optionally with a larger
|
|
59
|
+
* `timeoutMs`.
|
|
60
|
+
*/
|
|
32
61
|
export class QTSTimeoutError extends QTSError {
|
|
33
62
|
constructor(message: string, cause?: unknown) {
|
|
34
63
|
super(message, cause);
|
|
@@ -36,6 +65,12 @@ export class QTSTimeoutError extends QTSError {
|
|
|
36
65
|
}
|
|
37
66
|
}
|
|
38
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Thrown when a stage is aborted — either because the caller's
|
|
70
|
+
* `AbortSignal` fired, or because the backend itself reported the
|
|
71
|
+
* prepare/execute job as aborted. Either way this reflects a deliberate
|
|
72
|
+
* stop, not a failure, and is not something to retry automatically.
|
|
73
|
+
*/
|
|
39
74
|
export class QTSCanceledError extends QTSError {
|
|
40
75
|
constructor(message: string, cause?: unknown) {
|
|
41
76
|
super(message, cause);
|
|
@@ -43,6 +78,12 @@ export class QTSCanceledError extends QTSError {
|
|
|
43
78
|
}
|
|
44
79
|
}
|
|
45
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Thrown by the tickers/klines download functions on any non-2xx response or
|
|
83
|
+
* transport failure. Carries the HTTP `status` when one was received: a
|
|
84
|
+
* `4xx` means the request itself is wrong (bad hour or instrument), while a
|
|
85
|
+
* `5xx` or a missing status (transport failure) is generally safe to retry.
|
|
86
|
+
*/
|
|
46
87
|
export class QTSDownloadError extends QTSError {
|
|
47
88
|
constructor(message: string, cause?: unknown, status?: number) {
|
|
48
89
|
super(message, cause, status);
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of `@qtsurfer/sdk`. Everything a consumer needs — the
|
|
3
|
+
* `QTSurfer` client, the `authenticate()` session helper, workflow types,
|
|
4
|
+
* and the `QTSError` hierarchy — is re-exported from here; see each
|
|
5
|
+
* symbol's own doc comment for behavior and retry semantics.
|
|
6
|
+
*/
|
|
1
7
|
export { QTSurfer, type QTSurferOptions, type DownloadHourArgs } from './client';
|
|
2
8
|
export {
|
|
3
9
|
QTSError,
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
executeBacktest,
|
|
4
4
|
getBacktestResult,
|
|
5
5
|
getPrepareStatus,
|
|
6
|
-
getStrategy,
|
|
7
6
|
compileStrategy as apiCompileStrategy,
|
|
8
7
|
prepareBacktest,
|
|
9
8
|
type DataSourceType,
|
|
@@ -43,8 +42,16 @@ export interface BacktestRequest {
|
|
|
43
42
|
storeSignals?: boolean;
|
|
44
43
|
}
|
|
45
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Resolved value of the backtest workflow (see {@link QTSurfer.backtest}).
|
|
47
|
+
* Alias for api-client's `ResultMap` — always includes core fields
|
|
48
|
+
* (`hostName`, `iops`, `instrument`); yield metrics (`pnlTotal`,
|
|
49
|
+
* `totalTrades`, `equityCurve`, etc.) are only present once the strategy
|
|
50
|
+
* has emitted at least one trade.
|
|
51
|
+
*/
|
|
46
52
|
export type BacktestResult = ResultMap;
|
|
47
53
|
|
|
54
|
+
/** The three sequential stages {@link QTSurfer.backtest} moves through, in order. */
|
|
48
55
|
export type BacktestStage = 'compiling' | 'preparing' | 'executing';
|
|
49
56
|
|
|
50
57
|
export interface BacktestProgress {
|
|
@@ -106,9 +113,9 @@ export async function backtest(
|
|
|
106
113
|
): Promise<BacktestResult> {
|
|
107
114
|
const policy = buildStagePolicy(opts);
|
|
108
115
|
|
|
109
|
-
// 1. Compile strategy (
|
|
116
|
+
// 1. Compile strategy (single synchronous request)
|
|
110
117
|
opts.onProgress?.({ stage: 'compiling' });
|
|
111
|
-
const strategyId = await compileStrategy(req.strategy,
|
|
118
|
+
const strategyId = await compileStrategy(req.strategy, opts);
|
|
112
119
|
|
|
113
120
|
// 2. Prepare data
|
|
114
121
|
opts.onProgress?.({ stage: 'preparing' });
|
|
@@ -139,50 +146,33 @@ function buildStagePolicy(opts: BacktestOptions): IPolicy<ICancellationContext,
|
|
|
139
146
|
: retryPolicy;
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
): Promise<string> {
|
|
147
|
-
const { data, error } = await apiCompileStrategy({
|
|
149
|
+
/**
|
|
150
|
+
* Compile in a single request: the API answers synchronously with the `strategyId`,
|
|
151
|
+
* so there is no job to poll. A compile error arrives here as a `400`, not on a later poll.
|
|
152
|
+
*/
|
|
153
|
+
async function compileStrategy(source: string, opts: BacktestOptions): Promise<string> {
|
|
154
|
+
const { data, error, response } = await apiCompileStrategy({
|
|
148
155
|
body: source,
|
|
149
|
-
headers: { 'X-Compile-Async': true },
|
|
150
156
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
151
157
|
});
|
|
152
|
-
if (error) throw new QTSStrategyCompileError('Strategy submission failed', error);
|
|
153
|
-
if (!data) throw new QTSStrategyCompileError('Empty response from strategy endpoint');
|
|
154
|
-
|
|
155
|
-
// Sync mode returns { strategyId }; async mode returns { jobId }.
|
|
156
|
-
if ('strategyId' in data && data.strategyId) {
|
|
157
|
-
return data.strategyId;
|
|
158
|
-
}
|
|
159
|
-
if (!('jobId' in data) || !data.jobId) {
|
|
160
|
-
throw new QTSStrategyCompileError('Missing jobId/strategyId in compile response');
|
|
161
|
-
}
|
|
162
158
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const norm = normalizeStatus(status.status);
|
|
176
|
-
if (norm === 'failed') {
|
|
177
|
-
throw new QTSStrategyCompileError(status.statusDetail ?? 'Strategy compilation failed');
|
|
178
|
-
}
|
|
179
|
-
if (norm === 'aborted') {
|
|
180
|
-
throw new QTSCanceledError('Strategy compilation aborted');
|
|
159
|
+
if (error) {
|
|
160
|
+
// A 429 means the platform is holding too many compilations at once and the source was never
|
|
161
|
+
// judged — worth separating from the 400 that says the source itself does not compile.
|
|
162
|
+
// Read the status inside this branch and optionally: a transport failure carries no response,
|
|
163
|
+
// and dereferencing one would raise a TypeError that buries the error actually being reported.
|
|
164
|
+
if (response?.status === 429) {
|
|
165
|
+
throw new QTSStrategyCompileError(
|
|
166
|
+
'Strategy was not compiled, too many compilations in flight; retry later',
|
|
167
|
+
error,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
throw new QTSStrategyCompileError('Strategy compilation failed', error);
|
|
181
171
|
}
|
|
182
|
-
if (!
|
|
183
|
-
throw new QTSStrategyCompileError('Compile
|
|
172
|
+
if (!data?.strategyId) {
|
|
173
|
+
throw new QTSStrategyCompileError('Compile response missing strategyId');
|
|
184
174
|
}
|
|
185
|
-
return
|
|
175
|
+
return data.strategyId;
|
|
186
176
|
}
|
|
187
177
|
|
|
188
178
|
async function prepareData(
|
|
@@ -8,12 +8,15 @@ import { QTSDownloadError } from '../errors';
|
|
|
8
8
|
export type DownloadFormat = 'lastra' | 'parquet';
|
|
9
9
|
|
|
10
10
|
export interface DownloadParams {
|
|
11
|
+
/** Exchange id, e.g. `binance`. */
|
|
11
12
|
exchangeId: string;
|
|
13
|
+
/** Base asset of the instrument, e.g. `BTC`. */
|
|
12
14
|
base: string;
|
|
15
|
+
/** Quote asset of the instrument, e.g. `USDT`. */
|
|
13
16
|
quote: string;
|
|
14
17
|
/** Hour selector in `YYYY-MM-DDTHH` (UTC). */
|
|
15
18
|
hour: string;
|
|
16
|
-
/** Defaults to
|
|
19
|
+
/** Defaults to `'lastra'`. */
|
|
17
20
|
format?: DownloadFormat;
|
|
18
21
|
}
|
|
19
22
|
|