@qtsurfer/sdk 0.7.0 → 0.8.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 +245 -10
- package/dist/index.d.ts +780 -7
- package/dist/index.js +516 -65
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/auth/session.ts +86 -2
- package/src/client.ts +153 -3
- package/src/errors.ts +4 -2
- package/src/index.ts +33 -0
- package/src/internal/polling.ts +142 -0
- package/src/internal/preparation.ts +121 -0
- package/src/internal/requestError.ts +36 -0
- package/src/workflows/backtest.ts +46 -167
- package/src/workflows/catalog.ts +74 -0
- package/src/workflows/strategies.ts +118 -0
- package/src/workflows/sweep.ts +861 -0
|
@@ -2,30 +2,20 @@ import {
|
|
|
2
2
|
cancelBacktest,
|
|
3
3
|
executeBacktest,
|
|
4
4
|
getBacktestResult,
|
|
5
|
-
getPrepareStatus,
|
|
6
|
-
compileStrategy as apiCompileStrategy,
|
|
7
|
-
prepareBacktest,
|
|
8
|
-
type DataSourceType,
|
|
9
5
|
type ResultMap,
|
|
10
6
|
} from '@qtsurfer/api-client';
|
|
7
|
+
import { QTSCanceledError, QTSExecutionError } from '../errors';
|
|
11
8
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
timeout,
|
|
18
|
-
wrap,
|
|
19
|
-
type IPolicy,
|
|
20
|
-
type ICancellationContext,
|
|
21
|
-
} from 'cockatiel';
|
|
9
|
+
buildStagePolicy,
|
|
10
|
+
normalizeStatus,
|
|
11
|
+
runStage,
|
|
12
|
+
type StagePolicy,
|
|
13
|
+
} from '../internal/polling';
|
|
22
14
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
QTSTimeoutError,
|
|
28
|
-
} from '../errors';
|
|
15
|
+
TICKER,
|
|
16
|
+
compileStrategySource,
|
|
17
|
+
prepareDataset,
|
|
18
|
+
} from '../internal/preparation';
|
|
29
19
|
|
|
30
20
|
export interface BacktestRequest {
|
|
31
21
|
/** Strategy source code (Java) */
|
|
@@ -79,43 +69,20 @@ export interface BacktestOptions {
|
|
|
79
69
|
timeoutMs?: number;
|
|
80
70
|
}
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Normalize the backend job status to a stable lowercase form so we can
|
|
88
|
-
* reason about it regardless of OpenAPI spec drift (the live API sometimes
|
|
89
|
-
* returns lowercase values like `queued` / `completed` / `failed`).
|
|
90
|
-
*
|
|
91
|
-
* Only the three terminal statuses end a poll loop. Everything else — including a
|
|
92
|
-
* **missing** status — means "keep asking": the API answers `202` with an empty body when a
|
|
93
|
-
* job is known but its result is not readable yet, and that response carries no state at all.
|
|
94
|
-
* Mapping absent to in-progress is what makes a 202 continue the loop under its timeout
|
|
95
|
-
* instead of being mistaken for a finished job with no data.
|
|
96
|
-
*/
|
|
97
|
-
type NormalizedStatus = 'in-progress' | 'completed' | 'failed' | 'aborted';
|
|
98
|
-
|
|
99
|
-
function normalizeStatus(raw: unknown): NormalizedStatus {
|
|
100
|
-
const value = typeof raw === 'string' ? raw.toLowerCase() : '';
|
|
101
|
-
if (value === 'completed') return 'completed';
|
|
102
|
-
if (value === 'failed') return 'failed';
|
|
103
|
-
if (value === 'aborted' || value === 'cancelled' || value === 'canceled') {
|
|
104
|
-
return 'aborted';
|
|
105
|
-
}
|
|
106
|
-
// new / started / queued / running / absent (202) / anything else → still running
|
|
107
|
-
return 'in-progress';
|
|
108
|
-
}
|
|
72
|
+
/** Initial interval between polls of a single backtest. */
|
|
73
|
+
const DEFAULT_POLL_INTERVAL_MS = 500;
|
|
74
|
+
/** Backoff ceiling for a single backtest. */
|
|
75
|
+
const DEFAULT_MAX_POLL_INTERVAL_MS = 5000;
|
|
109
76
|
|
|
110
77
|
export async function backtest(
|
|
111
78
|
req: BacktestRequest,
|
|
112
79
|
opts: BacktestOptions = {},
|
|
113
80
|
): Promise<BacktestResult> {
|
|
114
|
-
const policy = buildStagePolicy(opts);
|
|
81
|
+
const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS, DEFAULT_MAX_POLL_INTERVAL_MS);
|
|
115
82
|
|
|
116
83
|
// 1. Compile strategy (single synchronous request)
|
|
117
84
|
opts.onProgress?.({ stage: 'compiling' });
|
|
118
|
-
const strategyId = await
|
|
85
|
+
const strategyId = await compileStrategySource(req.strategy, opts.signal);
|
|
119
86
|
|
|
120
87
|
// 2. Prepare data
|
|
121
88
|
opts.onProgress?.({ stage: 'preparing' });
|
|
@@ -126,106 +93,40 @@ export async function backtest(
|
|
|
126
93
|
return executeStrategy(req, prepareJobId, strategyId, policy, opts);
|
|
127
94
|
}
|
|
128
95
|
|
|
129
|
-
function
|
|
130
|
-
const retryPolicy = retry(
|
|
131
|
-
handleWhenResult((r) => {
|
|
132
|
-
const status = (r as { status?: unknown } | undefined)?.status;
|
|
133
|
-
return normalizeStatus(status) === 'in-progress';
|
|
134
|
-
}),
|
|
135
|
-
{
|
|
136
|
-
maxAttempts: Number.MAX_SAFE_INTEGER,
|
|
137
|
-
backoff: new ExponentialBackoff({
|
|
138
|
-
initialDelay: opts.pollIntervalMs ?? 500,
|
|
139
|
-
maxDelay: opts.maxPollIntervalMs ?? 5000,
|
|
140
|
-
}),
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
return opts.timeoutMs
|
|
145
|
-
? wrap(timeout(opts.timeoutMs, TimeoutStrategy.Cooperative), retryPolicy)
|
|
146
|
-
: retryPolicy;
|
|
147
|
-
}
|
|
148
|
-
|
|
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({
|
|
155
|
-
body: source,
|
|
156
|
-
...(opts.signal ? { signal: opts.signal } : {}),
|
|
157
|
-
});
|
|
158
|
-
|
|
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);
|
|
171
|
-
}
|
|
172
|
-
if (!data?.strategyId) {
|
|
173
|
-
throw new QTSStrategyCompileError('Compile response missing strategyId');
|
|
174
|
-
}
|
|
175
|
-
return data.strategyId;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function prepareData(
|
|
96
|
+
function prepareData(
|
|
179
97
|
req: BacktestRequest,
|
|
180
|
-
policy:
|
|
98
|
+
policy: StagePolicy,
|
|
181
99
|
opts: BacktestOptions,
|
|
182
100
|
): Promise<string> {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (!data?.jobId) throw new QTSPreparationError('Missing jobId in prepare response');
|
|
190
|
-
|
|
191
|
-
const prepareJobId = data.jobId;
|
|
192
|
-
const state = await runStage(
|
|
193
|
-
policy,
|
|
194
|
-
opts,
|
|
195
|
-
async ({ signal }) => {
|
|
196
|
-
const res = await getPrepareStatus({
|
|
197
|
-
path: { exchangeId: req.exchangeId, type: TICKER, jobId: prepareJobId },
|
|
198
|
-
signal,
|
|
199
|
-
});
|
|
200
|
-
if (res.error) throw new QTSPreparationError('Preparation status request failed', res.error);
|
|
201
|
-
if (!res.data) throw new QTSPreparationError('Empty preparation status response');
|
|
202
|
-
return res.data;
|
|
101
|
+
return prepareDataset(
|
|
102
|
+
{
|
|
103
|
+
exchangeId: req.exchangeId,
|
|
104
|
+
instrument: req.instrument,
|
|
105
|
+
from: req.from,
|
|
106
|
+
to: req.to,
|
|
203
107
|
},
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
108
|
+
policy,
|
|
109
|
+
{
|
|
110
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
111
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
112
|
+
onPercent: (percent) => opts.onProgress?.({ stage: 'preparing', percent }),
|
|
113
|
+
// Surface the backend's coverage ratio for the prepared window (spec 0.98.0) on the
|
|
114
|
+
// final preparing event, so callers can react to a partially-covered range.
|
|
115
|
+
onPrepared: (state) =>
|
|
116
|
+
opts.onProgress?.({
|
|
117
|
+
stage: 'preparing',
|
|
118
|
+
percent: 100,
|
|
119
|
+
coverageRatio: state.coverageRatio,
|
|
120
|
+
}),
|
|
208
121
|
},
|
|
209
122
|
);
|
|
210
|
-
|
|
211
|
-
const prepNorm = normalizeStatus(state.status);
|
|
212
|
-
if (prepNorm === 'failed') {
|
|
213
|
-
throw new QTSPreparationError(state.statusDetail ?? 'Data preparation failed');
|
|
214
|
-
}
|
|
215
|
-
if (prepNorm === 'aborted') {
|
|
216
|
-
throw new QTSCanceledError('Data preparation aborted');
|
|
217
|
-
}
|
|
218
|
-
// Surface the backend's coverage ratio for the prepared window (spec 0.98.0) on the
|
|
219
|
-
// final preparing event, so callers can react to a partially-covered range.
|
|
220
|
-
opts.onProgress?.({ stage: 'preparing', percent: 100, coverageRatio: state.coverageRatio });
|
|
221
|
-
return prepareJobId;
|
|
222
123
|
}
|
|
223
124
|
|
|
224
125
|
async function executeStrategy(
|
|
225
126
|
req: BacktestRequest,
|
|
226
127
|
prepareJobId: string,
|
|
227
128
|
strategyId: string,
|
|
228
|
-
policy:
|
|
129
|
+
policy: StagePolicy,
|
|
229
130
|
opts: BacktestOptions,
|
|
230
131
|
): Promise<BacktestResult> {
|
|
231
132
|
const { data, error } = await executeBacktest({
|
|
@@ -245,7 +146,6 @@ async function executeStrategy(
|
|
|
245
146
|
try {
|
|
246
147
|
const finalResult = await runStage(
|
|
247
148
|
policy,
|
|
248
|
-
opts,
|
|
249
149
|
async ({ signal }) => {
|
|
250
150
|
const res = await getBacktestResult({
|
|
251
151
|
path: { exchangeId: req.exchangeId, type: TICKER, jobId: executeJobId },
|
|
@@ -258,10 +158,14 @@ async function executeStrategy(
|
|
|
258
158
|
// see normalizeStatus. Do not "fix" this into a throw or an early return of the result.
|
|
259
159
|
return { ...res.data.state, __result: res.data.results };
|
|
260
160
|
},
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
161
|
+
{
|
|
162
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
163
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
164
|
+
onEachAttempt: (r) => {
|
|
165
|
+
if (r.size > 0) {
|
|
166
|
+
opts.onProgress?.({ stage: 'executing', percent: (r.completed / r.size) * 100 });
|
|
167
|
+
}
|
|
168
|
+
},
|
|
265
169
|
},
|
|
266
170
|
);
|
|
267
171
|
|
|
@@ -282,28 +186,3 @@ async function executeStrategy(
|
|
|
282
186
|
throw err;
|
|
283
187
|
}
|
|
284
188
|
}
|
|
285
|
-
|
|
286
|
-
async function runStage<T extends { status: JobStatus }>(
|
|
287
|
-
policy: IPolicy<ICancellationContext, never>,
|
|
288
|
-
opts: BacktestOptions,
|
|
289
|
-
fetchFn: (ctx: ICancellationContext) => Promise<T>,
|
|
290
|
-
onEachAttempt?: (r: T) => void,
|
|
291
|
-
): Promise<T> {
|
|
292
|
-
try {
|
|
293
|
-
return await policy.execute(async (ctx) => {
|
|
294
|
-
if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');
|
|
295
|
-
const result = await fetchFn(ctx);
|
|
296
|
-
onEachAttempt?.(result);
|
|
297
|
-
if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted');
|
|
298
|
-
return result;
|
|
299
|
-
}, opts.signal);
|
|
300
|
-
} catch (err) {
|
|
301
|
-
if (err instanceof QTSCanceledError) throw err;
|
|
302
|
-
if (err instanceof TaskCancelledError) {
|
|
303
|
-
if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);
|
|
304
|
-
throw new QTSTimeoutError(`Stage exceeded ${opts.timeoutMs}ms`, err);
|
|
305
|
-
}
|
|
306
|
-
if (opts.signal?.aborted) throw new QTSCanceledError('Workflow aborted', err);
|
|
307
|
-
throw err;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listExchanges as apiListExchanges,
|
|
3
|
+
listInstruments as apiListInstruments,
|
|
4
|
+
listSegmentInstruments as apiListSegmentInstruments,
|
|
5
|
+
type Exchange as ApiExchange,
|
|
6
|
+
type InstrumentDetail as ApiInstrumentDetail,
|
|
7
|
+
} from '@qtsurfer/api-client';
|
|
8
|
+
import { QTSError } from '../errors';
|
|
9
|
+
import { requestFailed } from '../internal/requestError';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One exchange the platform serves. Alias for api-client's `Exchange`:
|
|
13
|
+
* `id` (what every other call takes as `exchangeId`), `name`, and an
|
|
14
|
+
* optional `description`.
|
|
15
|
+
*/
|
|
16
|
+
export type Exchange = ApiExchange;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One instrument on an exchange. Alias for api-client's `InstrumentDetail`:
|
|
20
|
+
* `id` / `base` / `quote`, plus optional `coverage` (the date windows for
|
|
21
|
+
* which tickers and klines actually exist, per data type), `lastPrice` and
|
|
22
|
+
* `volume24h`.
|
|
23
|
+
*
|
|
24
|
+
* `coverage` is what tells you whether a backtest range is downloadable at
|
|
25
|
+
* all; it is optional, and absent means the platform did not report one, not
|
|
26
|
+
* that there is no data.
|
|
27
|
+
*/
|
|
28
|
+
export type InstrumentDetail = ApiInstrumentDetail;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A market segment of an exchange. `'spot'` is the default segment served
|
|
32
|
+
* when {@link QTSurfer.instruments} is called without one.
|
|
33
|
+
*/
|
|
34
|
+
export type InstrumentSegment = 'spot' | 'futures';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* List the exchanges the platform serves.
|
|
38
|
+
*
|
|
39
|
+
* @throws QTSError on any non-2xx response, with the HTTP status on `status`.
|
|
40
|
+
*/
|
|
41
|
+
export async function listExchanges(): Promise<Exchange[]> {
|
|
42
|
+
const { data, error, response } = await apiListExchanges();
|
|
43
|
+
if (error) throw requestFailed('exchanges call', error, response?.status);
|
|
44
|
+
if (!data) throw new QTSError('Empty exchanges response');
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List an exchange's instruments, each with its per-data-type coverage.
|
|
50
|
+
*
|
|
51
|
+
* Omitting `segment` asks for the exchange's **default** segment, which is
|
|
52
|
+
* `'spot'` today. The API answers both routes with a HAL envelope
|
|
53
|
+
* (`data` / `meta` / `_links`) that this function unwraps to the instrument
|
|
54
|
+
* array, so `meta.segment`, `meta.updatedAt` and the `_links`
|
|
55
|
+
* segment-discovery links do not reach the caller: if you need certainty
|
|
56
|
+
* about which segment you are looking at, pass `segment` explicitly rather
|
|
57
|
+
* than relying on the default.
|
|
58
|
+
*
|
|
59
|
+
* @param exchangeId exchange identifier, e.g. `binance`
|
|
60
|
+
* @param segment market segment to list; defaults to the exchange's default
|
|
61
|
+
* segment
|
|
62
|
+
* @throws QTSError on any non-2xx response, with the HTTP status on `status`.
|
|
63
|
+
*/
|
|
64
|
+
export async function listInstruments(
|
|
65
|
+
exchangeId: string,
|
|
66
|
+
segment?: InstrumentSegment,
|
|
67
|
+
): Promise<InstrumentDetail[]> {
|
|
68
|
+
const { data, error, response } = segment
|
|
69
|
+
? await apiListSegmentInstruments({ path: { exchangeId, segment } })
|
|
70
|
+
: await apiListInstruments({ path: { exchangeId } });
|
|
71
|
+
if (error) throw requestFailed('instruments call', error, response?.status);
|
|
72
|
+
if (!data) throw new QTSError('Empty instruments response');
|
|
73
|
+
return data.data;
|
|
74
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStrategy as apiGetStrategy,
|
|
3
|
+
validateStrategy as apiValidateStrategy,
|
|
4
|
+
type StrategyState as ApiStrategyState,
|
|
5
|
+
} from '@qtsurfer/api-client';
|
|
6
|
+
import { QTSError } from '../errors';
|
|
7
|
+
import { requestFailed } from '../internal/requestError';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Everything the platform records about a registered strategy. Alias for
|
|
11
|
+
* api-client's `StrategyState`.
|
|
12
|
+
*
|
|
13
|
+
* `validation` is the verdict and is one of:
|
|
14
|
+
*
|
|
15
|
+
* - `'not_validated'` — registered, never checked.
|
|
16
|
+
* - `'pending'` — a check was asked for and has not answered yet.
|
|
17
|
+
* - `'passed'` — the class loaded and survived its first event.
|
|
18
|
+
* - `'failed'` — it did not; `detail` says how.
|
|
19
|
+
*
|
|
20
|
+
* **`'passed'` is a floor, not a guarantee.** It means the compiled class
|
|
21
|
+
* could be instantiated and got through the first event of a short synthetic
|
|
22
|
+
* run — not the caller's instrument, not the caller's window, and not the
|
|
23
|
+
* rest of the run. It says nothing about whether the strategy is correct,
|
|
24
|
+
* profitable, or safe to run at scale. `dryRunIncomplete` marks a check that
|
|
25
|
+
* ran out of its budget, which makes a `'passed'` verdict a lower floor
|
|
26
|
+
* still, and makes an empty `notices` list no longer a clean bill of health.
|
|
27
|
+
*
|
|
28
|
+
* A verdict describes the bytecode that existed when it was recorded:
|
|
29
|
+
* `compiledAt` newer than `validatedAt` means the strategy was recompiled
|
|
30
|
+
* afterwards and the verdict no longer describes what would run.
|
|
31
|
+
*/
|
|
32
|
+
export type StrategyState = ApiStrategyState;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Outcome of {@link QTSurfer.validateStrategy} — the SDK's rendering of the
|
|
36
|
+
* two answers that operation has, which the response body alone cannot tell
|
|
37
|
+
* apart.
|
|
38
|
+
*
|
|
39
|
+
* - `queued: false` — a verdict already existed for the current compilation
|
|
40
|
+
* and comes back in `state` unchanged; nothing new was queued. This is
|
|
41
|
+
* **not** the same as "terminal": a check queued by an earlier call can
|
|
42
|
+
* still be running, so read `state.validation` rather than treating
|
|
43
|
+
* `queued: false` as "there is an answer".
|
|
44
|
+
* - `queued: true` — a check was just queued. Nothing is known yet; poll
|
|
45
|
+
* {@link QTSurfer.strategy} until `validation` leaves `'pending'`.
|
|
46
|
+
*/
|
|
47
|
+
export type StrategyValidation =
|
|
48
|
+
| { queued: false; strategyId: string; state: StrategyState }
|
|
49
|
+
| { queued: true; strategyId: string; state?: undefined };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ask the platform to check that a registered strategy can actually run: it
|
|
53
|
+
* instantiates the compiled class and drives it through a bounded synthetic
|
|
54
|
+
* series, so a wiring fault surfaces here instead of at the first backtest.
|
|
55
|
+
*
|
|
56
|
+
* **Idempotent, and two-outcome.** If a verdict already exists for the
|
|
57
|
+
* current compilation it is returned unchanged and nothing is queued
|
|
58
|
+
* (`queued: false`); otherwise a check is queued (`queued: true`) and this
|
|
59
|
+
* call is *not* terminal — poll {@link QTSurfer.strategy} until `validation`
|
|
60
|
+
* is `'passed'` or `'failed'`. Because a `queued: false` answer can itself carry
|
|
61
|
+
* `validation: 'pending'` (a check an earlier call queued), the discriminant
|
|
62
|
+
* tells you whether work was *started*, not whether a verdict *exists*;
|
|
63
|
+
* `state.validation` is what tells you that.
|
|
64
|
+
*
|
|
65
|
+
* **Poll with a deadline of your own.** `'pending'` is not guaranteed to
|
|
66
|
+
* resolve: a queued check can go unreported for far longer than one takes,
|
|
67
|
+
* which the platform eventually flags as `validationStalled` on the strategy.
|
|
68
|
+
* Nothing about the strategy is disproved when that happens — the check
|
|
69
|
+
* simply did not run — but a caller that waits for a terminal verdict without
|
|
70
|
+
* a timeout can wait forever. This SDK deliberately ships no polling helper
|
|
71
|
+
* for that reason; the timeout is the caller's policy to set.
|
|
72
|
+
*
|
|
73
|
+
* Whatever the verdict, remember it is a floor rather than a guarantee — see
|
|
74
|
+
* {@link StrategyState}.
|
|
75
|
+
*
|
|
76
|
+
* @param strategyId the id returned when the strategy was compiled
|
|
77
|
+
* @throws QTSError on any non-2xx response; a `404` (carried on `status`)
|
|
78
|
+
* means no such registered strategy for this caller.
|
|
79
|
+
*/
|
|
80
|
+
export async function validateStrategy(strategyId: string): Promise<StrategyValidation> {
|
|
81
|
+
const { data, error, response } = await apiValidateStrategy({ path: { strategyId } });
|
|
82
|
+
if (error) throw requestFailed('strategy validation request', error, response?.status);
|
|
83
|
+
// The two outcomes are distinguishable only by status: a `200` body is a
|
|
84
|
+
// full StrategyState whose `validation` may itself be `'pending'`, so the
|
|
85
|
+
// payload cannot be used to tell "queued just now" from "already queued".
|
|
86
|
+
// The queued branch echoes back the caller's own id rather than reading one
|
|
87
|
+
// out of the body, because an accepted-but-not-done response on this API is
|
|
88
|
+
// not guaranteed to carry a body at all.
|
|
89
|
+
if (response?.status === 202) return { queued: true, strategyId };
|
|
90
|
+
if (!data) throw new QTSError('Empty strategy validation response');
|
|
91
|
+
return { queued: false, strategyId, state: data as StrategyState };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read everything the platform records about a strategy: whether it is
|
|
96
|
+
* registered at all, its validation verdict, the market data its compiled
|
|
97
|
+
* class requires, and any engine notices the check raised.
|
|
98
|
+
*
|
|
99
|
+
* This is the endpoint to poll after {@link QTSurfer.validateStrategy}
|
|
100
|
+
* returns `queued: true`, and the only place a verdict is read from.
|
|
101
|
+
*
|
|
102
|
+
* Check `compiledAt` against `validatedAt` before trusting a verdict: the
|
|
103
|
+
* strategy may have been recompiled since the verdict was recorded, in which
|
|
104
|
+
* case the verdict describes bytecode that is no longer what would run.
|
|
105
|
+
* Re-request validation to get an answer about the current compilation.
|
|
106
|
+
*
|
|
107
|
+
* @param strategyId the id returned when the strategy was compiled
|
|
108
|
+
* @throws QTSError on any non-2xx response. A `404` (carried on `status`)
|
|
109
|
+
* means exactly one thing — no such registered strategy for this caller. It is
|
|
110
|
+
* never a stale or expired answer: registration and verdict are stored
|
|
111
|
+
* durably, not cached.
|
|
112
|
+
*/
|
|
113
|
+
export async function getStrategy(strategyId: string): Promise<StrategyState> {
|
|
114
|
+
const { data, error, response } = await apiGetStrategy({ path: { strategyId } });
|
|
115
|
+
if (error) throw requestFailed('strategy lookup', error, response?.status);
|
|
116
|
+
if (!data) throw new QTSError('Empty strategy response');
|
|
117
|
+
return data;
|
|
118
|
+
}
|