@qtsurfer/sdk 0.4.0 → 0.6.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 CHANGED
@@ -24,12 +24,12 @@ One call: API key in, ready-to-use session out. JWT refresh on 401 is handled
24
24
  for you.
25
25
 
26
26
  ```ts
27
- import { auth } from '@qtsurfer/sdk';
27
+ import { authenticate } from '@qtsurfer/sdk';
28
28
  import { readFileSync } from 'node:fs';
29
29
 
30
30
  // Reads QTSURFER_APIKEY from env when no argument is passed.
31
- const qts = await auth();
32
- // Or: const qts = await auth('ak_...');
31
+ const qts = await authenticate();
32
+ // Or: const qts = await authenticate('ak_...');
33
33
 
34
34
  const result = await qts.backtest({
35
35
  strategy: readFileSync('./MyStrategy.java', 'utf8'),
@@ -46,9 +46,9 @@ console.log('Trades:', result.totalTrades);
46
46
 
47
47
  ### Environment
48
48
 
49
- | Variable | Purpose |
50
- | ------------------ | -------------------------------------------------- |
51
- | `QTSURFER_APIKEY` | API key consumed by `auth()` when no arg is passed |
49
+ | Variable | Purpose |
50
+ | ------------------ | ------------------------------------------------------------ |
51
+ | `QTSURFER_APIKEY` | API key consumed by `authenticate()` when no arg is passed |
52
52
 
53
53
  ### Pluggable token storage
54
54
 
@@ -56,7 +56,7 @@ Tokens are kept in memory by default. Implement `TokenStore` to swap in
56
56
  browser storage, a file, or a secret manager:
57
57
 
58
58
  ```ts
59
- import { auth, type TokenStore, type AuthTokenResponse } from '@qtsurfer/sdk';
59
+ import { authenticate, type TokenStore, type AuthTokenResponse } from '@qtsurfer/sdk';
60
60
 
61
61
  const browserStore: TokenStore = {
62
62
  load: () => JSON.parse(localStorage.getItem('qts.jwt') ?? 'null'),
@@ -64,10 +64,10 @@ const browserStore: TokenStore = {
64
64
  clear: () => localStorage.removeItem('qts.jwt'),
65
65
  };
66
66
 
67
- const qts = await auth(undefined, { store: browserStore });
67
+ const qts = await authenticate(undefined, { store: browserStore });
68
68
  ```
69
69
 
70
- `auth()` also accepts `{ baseUrl, fetch }` for staging, custom HTTP
70
+ `authenticate()` also accepts `{ baseUrl, fetch }` for staging, custom HTTP
71
71
  transports, or a Node-`fetch` polyfill in legacy runtimes.
72
72
 
73
73
  ### Lower-level: hand-managed JWT
@@ -159,7 +159,7 @@ try {
159
159
 
160
160
  ## Cancellation
161
161
 
162
- Pass an `AbortSignal`. The SDK stops polling immediately and, if execution has already started server-side, best-effort calls `cancelExecution` on the QTSurfer API.
162
+ Pass an `AbortSignal`. The SDK stops polling immediately and, if execution has already started server-side, best-effort calls `cancelBacktest` on the QTSurfer API.
163
163
 
164
164
  ```ts
165
165
  const controller = new AbortController();
package/dist/index.d.ts CHANGED
@@ -20,9 +20,15 @@ interface BacktestProgress {
20
20
  stage: BacktestStage;
21
21
  /** 0-100 when size is known. Undefined during stage start. */
22
22
  percent?: number;
23
+ /**
24
+ * Fraction (0-1) of the requested prepare window that actually holds data,
25
+ * reported by the backend once preparation completes. Present only on the
26
+ * final `preparing` event.
27
+ */
28
+ coverageRatio?: number;
23
29
  }
24
30
  interface BacktestOptions {
25
- /** Abort the workflow. Cancels the current poll and calls `cancelExecution` server-side if execution has started. */
31
+ /** Abort the workflow. Cancels the current poll and calls `cancelBacktest` server-side if execution has started. */
26
32
  signal?: AbortSignal;
27
33
  /** Called on stage transitions and after each poll with updated progress. */
28
34
  onProgress?: (p: BacktestProgress) => void;
@@ -88,7 +94,7 @@ declare class QTSDownloadError extends QTSError {
88
94
  constructor(message: string, cause?: unknown, status?: number);
89
95
  }
90
96
  /**
91
- * Thrown by the `auth()` helper when the apikey is missing or the JWT
97
+ * Thrown by the `authenticate()` helper when the apikey is missing or the JWT
92
98
  * exchange fails (HTTP 401 from `POST /v1/auth/token`, etc.).
93
99
  */
94
100
  declare class QTSAuthError extends QTSError {
@@ -103,7 +109,7 @@ declare class QTSAuthError extends QTSError {
103
109
  * on-disk file, a secret manager, etc.
104
110
  *
105
111
  * The SDK calls {@link load} once per session-startup to seed a cached
106
- * token (if any), {@link save} after every successful `auth()` / refresh,
112
+ * token (if any), {@link save} after every successful `authenticate()` / refresh,
107
113
  * and {@link clear} when the session is explicitly invalidated.
108
114
  */
109
115
  interface TokenStore {
@@ -136,7 +142,7 @@ interface AuthOptions {
136
142
  /**
137
143
  * Authenticated SDK session.
138
144
  *
139
- * Returned by {@link auth}. Wraps the underlying api-client, owns a JWT
145
+ * Returned by {@link authenticate}. Wraps the underlying api-client, owns a JWT
140
146
  * (in memory by default, or in the provided {@link TokenStore}), and
141
147
  * transparently re-exchanges the apikey for a fresh JWT on 401.
142
148
  *
@@ -183,6 +189,6 @@ declare class AuthenticatedClient {
183
189
  *
184
190
  * @throws {QTSAuthError} if no apikey is supplied or available in env.
185
191
  */
186
- declare function auth(apikey?: string, opts?: AuthOptions): Promise<AuthenticatedClient>;
192
+ declare function authenticate(apikey?: string, opts?: AuthOptions): Promise<AuthenticatedClient>;
187
193
 
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 };
194
+ export { type AuthOptions, AuthenticatedClient, type BacktestOptions, type BacktestProgress, type BacktestRequest, type BacktestResult, type BacktestStage, type DownloadFormat, type DownloadHourArgs, InMemoryTokenStore, QTSAuthError, QTSCanceledError, QTSDownloadError, QTSError, QTSExecutionError, QTSPreparationError, QTSStrategyCompileError, QTSTimeoutError, QTSurfer, type QTSurferOptions, type TokenStore, authenticate };
package/dist/index.js CHANGED
@@ -3,13 +3,13 @@ import { client as apiClient } from "@qtsurfer/api-client";
3
3
 
4
4
  // src/workflows/backtest.ts
5
5
  import {
6
- cancelExecution,
7
- executeBacktesting,
8
- getExecutionResult,
9
- getPreparationStatus,
10
- getStrategyStatus,
11
- postStrategy,
12
- prepareBacktesting
6
+ cancelBacktest,
7
+ executeBacktest,
8
+ getBacktestResult,
9
+ getPrepareStatus,
10
+ getStrategy,
11
+ compileStrategy as apiCompileStrategy,
12
+ prepareBacktest
13
13
  } from "@qtsurfer/api-client";
14
14
  import {
15
15
  ExponentialBackoff,
@@ -113,7 +113,7 @@ function buildStagePolicy(opts) {
113
113
  return opts.timeoutMs ? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy) : retryPolicy;
114
114
  }
115
115
  async function compileStrategy(source, policy, opts) {
116
- const { data, error } = await postStrategy({
116
+ const { data, error } = await apiCompileStrategy({
117
117
  body: source,
118
118
  headers: { "X-Compile-Async": true },
119
119
  ...opts.signal ? { signal: opts.signal } : {}
@@ -131,7 +131,7 @@ async function compileStrategy(source, policy, opts) {
131
131
  policy,
132
132
  opts,
133
133
  async ({ signal }) => {
134
- const res = await getStrategyStatus({ path: { strategyId: compileJobId }, signal });
134
+ const res = await getStrategy({ path: { strategyId: compileJobId }, signal });
135
135
  if (res.error) throw new QTSStrategyCompileError("Compile status request failed", res.error);
136
136
  if (!res.data) throw new QTSStrategyCompileError("Empty compile status response");
137
137
  return res.data;
@@ -150,7 +150,7 @@ async function compileStrategy(source, policy, opts) {
150
150
  return status.strategyId;
151
151
  }
152
152
  async function prepareData(req, policy, opts) {
153
- const { data, error } = await prepareBacktesting({
153
+ const { data, error } = await prepareBacktest({
154
154
  path: { exchangeId: req.exchangeId, type: TICKER },
155
155
  body: { instrument: req.instrument, from: req.from, to: req.to },
156
156
  ...opts.signal ? { signal: opts.signal } : {}
@@ -162,7 +162,7 @@ async function prepareData(req, policy, opts) {
162
162
  policy,
163
163
  opts,
164
164
  async ({ signal }) => {
165
- const res = await getPreparationStatus({
165
+ const res = await getPrepareStatus({
166
166
  path: { exchangeId: req.exchangeId, type: TICKER, jobId: prepareJobId },
167
167
  signal
168
168
  });
@@ -183,10 +183,11 @@ async function prepareData(req, policy, opts) {
183
183
  if (prepNorm === "aborted") {
184
184
  throw new QTSCanceledError("Data preparation aborted");
185
185
  }
186
+ opts.onProgress?.({ stage: "preparing", percent: 100, coverageRatio: state.coverageRatio });
186
187
  return prepareJobId;
187
188
  }
188
189
  async function executeStrategy(req, prepareJobId, strategyId, policy, opts) {
189
- const { data, error } = await executeBacktesting({
190
+ const { data, error } = await executeBacktest({
190
191
  path: { exchangeId: req.exchangeId, type: TICKER },
191
192
  body: {
192
193
  prepareJobId,
@@ -203,7 +204,7 @@ async function executeStrategy(req, prepareJobId, strategyId, policy, opts) {
203
204
  policy,
204
205
  opts,
205
206
  async ({ signal }) => {
206
- const res = await getExecutionResult({
207
+ const res = await getBacktestResult({
207
208
  path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },
208
209
  signal
209
210
  });
@@ -227,7 +228,7 @@ async function executeStrategy(req, prepareJobId, strategyId, policy, opts) {
227
228
  return finalResult.__result;
228
229
  } catch (err) {
229
230
  if (err instanceof QTSCanceledError) {
230
- await cancelExecution({
231
+ await cancelBacktest({
231
232
  path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId }
232
233
  }).catch(() => void 0);
233
234
  }
@@ -256,12 +257,12 @@ async function runStage(policy, opts, fetchFn, onEachAttempt) {
256
257
 
257
258
  // src/workflows/downloads.ts
258
259
  import {
259
- getExchangeKlinesHour,
260
- getExchangeTickersHour
260
+ downloadKlines as apiDownloadKlines,
261
+ downloadTickers as apiDownloadTickers
261
262
  } from "@qtsurfer/api-client";
262
263
  async function downloadTickers(params) {
263
264
  const { exchangeId, base, quote, hour, format } = params;
264
- const { data, error, response } = await getExchangeTickersHour({
265
+ const { data, error, response } = await apiDownloadTickers({
265
266
  path: { exchangeId, base, quote },
266
267
  query: { hour, ...format ? { format } : {} }
267
268
  });
@@ -276,7 +277,7 @@ async function downloadTickers(params) {
276
277
  }
277
278
  async function downloadKlines(params) {
278
279
  const { exchangeId, base, quote, hour, format } = params;
279
- const { data, error, response } = await getExchangeKlinesHour({
280
+ const { data, error, response } = await apiDownloadKlines({
280
281
  path: { exchangeId, base, quote },
281
282
  query: { hour, ...format ? { format } : {} }
282
283
  });
@@ -332,7 +333,7 @@ var QTSurfer = class {
332
333
 
333
334
  // src/auth/session.ts
334
335
  import {
335
- auth as apiAuth,
336
+ authenticate as apiAuth,
336
337
  client as apiClient2
337
338
  } from "@qtsurfer/api-client";
338
339
 
@@ -381,7 +382,7 @@ var AuthenticatedClient = class {
381
382
  });
382
383
  if (error || !data) {
383
384
  throw new QTSAuthError(
384
- `auth() failed: HTTP ${response.status}`,
385
+ `authenticate() failed: HTTP ${response.status}`,
385
386
  error
386
387
  );
387
388
  }
@@ -448,11 +449,11 @@ var AuthenticatedClient = class {
448
449
  return this.withRefreshOn401(() => downloadKlines(args));
449
450
  }
450
451
  };
451
- async function auth(apikey, opts = {}) {
452
+ async function authenticate(apikey, opts = {}) {
452
453
  const resolved = apikey ?? readEnvApikey();
453
454
  if (!resolved) {
454
455
  throw new QTSAuthError(
455
- `auth() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`
456
+ `authenticate() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`
456
457
  );
457
458
  }
458
459
  const session = new AuthenticatedClient(resolved, opts);
@@ -482,6 +483,6 @@ export {
482
483
  QTSStrategyCompileError,
483
484
  QTSTimeoutError,
484
485
  QTSurfer,
485
- auth
486
+ authenticate
486
487
  };
487
488
  //# 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","../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"]}
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 */\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 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 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qtsurfer/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",
@@ -50,7 +50,7 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@qtsurfer/api-client": "^0.3.0",
53
+ "@qtsurfer/api-client": "^0.5.0",
54
54
  "cockatiel": "^3.2.1"
55
55
  },
56
56
  "devDependencies": {
@@ -1,5 +1,5 @@
1
1
  import {
2
- auth as apiAuth,
2
+ authenticate as apiAuth,
3
3
  client as apiClient,
4
4
  type AuthTokenResponse,
5
5
  } from '@qtsurfer/api-client';
@@ -32,7 +32,7 @@ export interface AuthOptions {
32
32
  /**
33
33
  * Authenticated SDK session.
34
34
  *
35
- * Returned by {@link auth}. Wraps the underlying api-client, owns a JWT
35
+ * Returned by {@link authenticate}. Wraps the underlying api-client, owns a JWT
36
36
  * (in memory by default, or in the provided {@link TokenStore}), and
37
37
  * transparently re-exchanges the apikey for a fresh JWT on 401.
38
38
  *
@@ -71,7 +71,7 @@ export class AuthenticatedClient {
71
71
  });
72
72
  if (error || !data) {
73
73
  throw new QTSAuthError(
74
- `auth() failed: HTTP ${response.status}`,
74
+ `authenticate() failed: HTTP ${response.status}`,
75
75
  error,
76
76
  );
77
77
  }
@@ -157,14 +157,14 @@ export class AuthenticatedClient {
157
157
  *
158
158
  * @throws {QTSAuthError} if no apikey is supplied or available in env.
159
159
  */
160
- export async function auth(
160
+ export async function authenticate(
161
161
  apikey?: string,
162
162
  opts: AuthOptions = {},
163
163
  ): Promise<AuthenticatedClient> {
164
164
  const resolved = apikey ?? readEnvApikey();
165
165
  if (!resolved) {
166
166
  throw new QTSAuthError(
167
- `auth() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`,
167
+ `authenticate() requires an apikey (argument or ${APIKEY_ENV_VAR} env var)`,
168
168
  );
169
169
  }
170
170
  const session = new AuthenticatedClient(resolved, opts);
@@ -8,7 +8,7 @@ import type { AuthTokenResponse } from '@qtsurfer/api-client';
8
8
  * on-disk file, a secret manager, etc.
9
9
  *
10
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,
11
+ * token (if any), {@link save} after every successful `authenticate()` / refresh,
12
12
  * and {@link clear} when the session is explicitly invalidated.
13
13
  */
14
14
  export interface TokenStore {
package/src/errors.ts CHANGED
@@ -51,7 +51,7 @@ export class QTSDownloadError extends QTSError {
51
51
  }
52
52
 
53
53
  /**
54
- * Thrown by the `auth()` helper when the apikey is missing or the JWT
54
+ * Thrown by the `authenticate()` helper when the apikey is missing or the JWT
55
55
  * exchange fails (HTTP 401 from `POST /v1/auth/token`, etc.).
56
56
  */
57
57
  export class QTSAuthError extends QTSError {
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ export type {
18
18
  } from './workflows/backtest';
19
19
  export type { DownloadFormat } from './workflows/downloads';
20
20
  export {
21
- auth,
21
+ authenticate,
22
22
  AuthenticatedClient,
23
23
  type AuthOptions,
24
24
  } from './auth/session';
@@ -1,11 +1,11 @@
1
1
  import {
2
- cancelExecution,
3
- executeBacktesting,
4
- getExecutionResult,
5
- getPreparationStatus,
6
- getStrategyStatus,
7
- postStrategy,
8
- prepareBacktesting,
2
+ cancelBacktest,
3
+ executeBacktest,
4
+ getBacktestResult,
5
+ getPrepareStatus,
6
+ getStrategy,
7
+ compileStrategy as apiCompileStrategy,
8
+ prepareBacktest,
9
9
  type DataSourceType,
10
10
  type ResultMap,
11
11
  } from '@qtsurfer/api-client';
@@ -51,10 +51,16 @@ export interface BacktestProgress {
51
51
  stage: BacktestStage;
52
52
  /** 0-100 when size is known. Undefined during stage start. */
53
53
  percent?: number;
54
+ /**
55
+ * Fraction (0-1) of the requested prepare window that actually holds data,
56
+ * reported by the backend once preparation completes. Present only on the
57
+ * final `preparing` event.
58
+ */
59
+ coverageRatio?: number;
54
60
  }
55
61
 
56
62
  export interface BacktestOptions {
57
- /** Abort the workflow. Cancels the current poll and calls `cancelExecution` server-side if execution has started. */
63
+ /** Abort the workflow. Cancels the current poll and calls `cancelBacktest` server-side if execution has started. */
58
64
  signal?: AbortSignal;
59
65
  /** Called on stage transitions and after each poll with updated progress. */
60
66
  onProgress?: (p: BacktestProgress) => void;
@@ -68,7 +74,7 @@ export interface BacktestOptions {
68
74
 
69
75
  const TICKER: DataSourceType = 'ticker';
70
76
 
71
- type JobStatus = 'New' | 'Started' | 'Completed' | 'Aborted' | 'Failed' | 'Partial';
77
+ type JobStatus = 'New' | 'Started' | 'Completed' | 'Aborted' | 'Failed';
72
78
 
73
79
  /**
74
80
  * Normalize the backend job status to a stable lowercase form so we can
@@ -132,7 +138,7 @@ async function compileStrategy(
132
138
  policy: IPolicy<ICancellationContext, never>,
133
139
  opts: BacktestOptions,
134
140
  ): Promise<string> {
135
- const { data, error } = await postStrategy({
141
+ const { data, error } = await apiCompileStrategy({
136
142
  body: source,
137
143
  headers: { 'X-Compile-Async': true },
138
144
  ...(opts.signal ? { signal: opts.signal } : {}),
@@ -153,7 +159,7 @@ async function compileStrategy(
153
159
  policy,
154
160
  opts,
155
161
  async ({ signal }) => {
156
- const res = await getStrategyStatus({ path: { strategyId: compileJobId }, signal });
162
+ const res = await getStrategy({ path: { strategyId: compileJobId }, signal });
157
163
  if (res.error) throw new QTSStrategyCompileError('Compile status request failed', res.error);
158
164
  if (!res.data) throw new QTSStrategyCompileError('Empty compile status response');
159
165
  return res.data;
@@ -178,7 +184,7 @@ async function prepareData(
178
184
  policy: IPolicy<ICancellationContext, never>,
179
185
  opts: BacktestOptions,
180
186
  ): Promise<string> {
181
- const { data, error } = await prepareBacktesting({
187
+ const { data, error } = await prepareBacktest({
182
188
  path: { exchangeId: req.exchangeId, type: TICKER },
183
189
  body: { instrument: req.instrument, from: req.from, to: req.to },
184
190
  ...(opts.signal ? { signal: opts.signal } : {}),
@@ -191,7 +197,7 @@ async function prepareData(
191
197
  policy,
192
198
  opts,
193
199
  async ({ signal }) => {
194
- const res = await getPreparationStatus({
200
+ const res = await getPrepareStatus({
195
201
  path: { exchangeId: req.exchangeId, type: TICKER, jobId: prepareJobId },
196
202
  signal,
197
203
  });
@@ -213,6 +219,9 @@ async function prepareData(
213
219
  if (prepNorm === 'aborted') {
214
220
  throw new QTSCanceledError('Data preparation aborted');
215
221
  }
222
+ // Surface the backend's coverage ratio for the prepared window (spec 0.98.0) on the
223
+ // final preparing event, so callers can react to a partially-covered range.
224
+ opts.onProgress?.({ stage: 'preparing', percent: 100, coverageRatio: state.coverageRatio });
216
225
  return prepareJobId;
217
226
  }
218
227
 
@@ -223,7 +232,7 @@ async function executeStrategy(
223
232
  policy: IPolicy<ICancellationContext, never>,
224
233
  opts: BacktestOptions,
225
234
  ): Promise<BacktestResult> {
226
- const { data, error } = await executeBacktesting({
235
+ const { data, error } = await executeBacktest({
227
236
  path: { exchangeId: req.exchangeId, type: TICKER },
228
237
  body: {
229
238
  prepareJobId,
@@ -242,7 +251,7 @@ async function executeStrategy(
242
251
  policy,
243
252
  opts,
244
253
  async ({ signal }) => {
245
- const res = await getExecutionResult({
254
+ const res = await getBacktestResult({
246
255
  path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },
247
256
  signal,
248
257
  });
@@ -267,7 +276,7 @@ async function executeStrategy(
267
276
  return finalResult.__result;
268
277
  } catch (err) {
269
278
  if (err instanceof QTSCanceledError) {
270
- await cancelExecution({
279
+ await cancelBacktest({
271
280
  path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },
272
281
  }).catch(() => undefined);
273
282
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
- getExchangeKlinesHour,
3
- getExchangeTickersHour,
2
+ downloadKlines as apiDownloadKlines,
3
+ downloadTickers as apiDownloadTickers,
4
4
  } from '@qtsurfer/api-client';
5
5
  import { QTSDownloadError } from '../errors';
6
6
 
@@ -27,7 +27,7 @@ export interface DownloadParams {
27
27
  */
28
28
  export async function downloadTickers(params: DownloadParams): Promise<Blob> {
29
29
  const { exchangeId, base, quote, hour, format } = params;
30
- const { data, error, response } = await getExchangeTickersHour({
30
+ const { data, error, response } = await apiDownloadTickers({
31
31
  path: { exchangeId, base, quote },
32
32
  query: { hour, ...(format ? { format } : {}) },
33
33
  });
@@ -49,7 +49,7 @@ export async function downloadTickers(params: DownloadParams): Promise<Blob> {
49
49
  */
50
50
  export async function downloadKlines(params: DownloadParams): Promise<Blob> {
51
51
  const { exchangeId, base, quote, hour, format } = params;
52
- const { data, error, response } = await getExchangeKlinesHour({
52
+ const { data, error, response } = await apiDownloadKlines({
53
53
  path: { exchangeId, base, quote },
54
54
  query: { hour, ...(format ? { format } : {}) },
55
55
  });