@qtsurfer/sdk 0.8.0 → 0.10.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 +41 -3
- package/dist/index.d.ts +141 -11
- package/dist/index.js +118 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/auth/session.ts +33 -1
- package/src/client.ts +52 -2
- package/src/index.ts +1 -0
- package/src/internal/preparation.ts +50 -4
- package/src/workflows/backtest.ts +39 -4
- package/src/workflows/strategies.ts +98 -0
- package/src/workflows/sweep.ts +35 -5
package/src/auth/session.ts
CHANGED
|
@@ -24,8 +24,12 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
getStrategy,
|
|
26
26
|
validateStrategy as runValidateStrategy,
|
|
27
|
+
listStrategies,
|
|
28
|
+
deleteStrategy as runDeleteStrategy,
|
|
29
|
+
getStrategyCode,
|
|
27
30
|
type StrategyState,
|
|
28
31
|
type StrategyValidation,
|
|
32
|
+
type StrategySummary,
|
|
29
33
|
} from '../workflows/strategies';
|
|
30
34
|
import {
|
|
31
35
|
sweep as runSweep,
|
|
@@ -238,6 +242,33 @@ export class AuthenticatedClient {
|
|
|
238
242
|
strategy(strategyId: string): Promise<StrategyState> {
|
|
239
243
|
return this.withRefreshOn401(() => getStrategy(strategyId));
|
|
240
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* List every strategy you have registered and not deleted, most recently
|
|
248
|
+
* compiled first. Refreshes the token once on `401` before retrying. See
|
|
249
|
+
* {@link QTSurfer.strategies}.
|
|
250
|
+
*/
|
|
251
|
+
strategies(): Promise<StrategySummary[]> {
|
|
252
|
+
return this.withRefreshOn401(() => listStrategies());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Release a registered strategy. Refreshes the token once on `401` before
|
|
257
|
+
* retrying. See {@link QTSurfer.deleteStrategy} for what this does and
|
|
258
|
+
* does not undo.
|
|
259
|
+
*/
|
|
260
|
+
deleteStrategy(strategyId: string): Promise<void> {
|
|
261
|
+
return this.withRefreshOn401(() => runDeleteStrategy(strategyId));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Read back a strategy's exact registered source. Refreshes the token
|
|
266
|
+
* once on `401` before retrying. See {@link QTSurfer.strategyCode} for
|
|
267
|
+
* what its `404` covers.
|
|
268
|
+
*/
|
|
269
|
+
strategyCode(strategyId: string): Promise<string> {
|
|
270
|
+
return this.withRefreshOn401(() => getStrategyCode(strategyId));
|
|
271
|
+
}
|
|
241
272
|
}
|
|
242
273
|
|
|
243
274
|
/**
|
|
@@ -247,7 +278,8 @@ export class AuthenticatedClient {
|
|
|
247
278
|
* environment. The returned {@link AuthenticatedClient} caches the JWT,
|
|
248
279
|
* refreshes it on 401, and exposes the same surface as `QTSurfer`
|
|
249
280
|
* (`backtest`, `sweep`, `tickers`, `klines`, `exchanges`, `instruments`,
|
|
250
|
-
* `validateStrategy`, `strategy`
|
|
281
|
+
* `validateStrategy`, `strategy`, `strategies`, `deleteStrategy`,
|
|
282
|
+
* `strategyCode`).
|
|
251
283
|
*
|
|
252
284
|
* @throws {QTSAuthError} if no apikey is supplied or available in env.
|
|
253
285
|
*/
|
package/src/client.ts
CHANGED
|
@@ -20,8 +20,12 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
getStrategy,
|
|
22
22
|
validateStrategy as runValidateStrategy,
|
|
23
|
+
listStrategies,
|
|
24
|
+
deleteStrategy as runDeleteStrategy,
|
|
25
|
+
getStrategyCode,
|
|
23
26
|
type StrategyState,
|
|
24
27
|
type StrategyValidation,
|
|
28
|
+
type StrategySummary,
|
|
25
29
|
} from './workflows/strategies';
|
|
26
30
|
import {
|
|
27
31
|
sweep as runSweep,
|
|
@@ -62,7 +66,7 @@ export interface DownloadHourArgs {
|
|
|
62
66
|
* Thin, stateless wrapper over `@qtsurfer/api-client` that exposes the SDK's
|
|
63
67
|
* workflow methods (`backtest`, `sweep`, `tickers`, `klines`), the platform catalog
|
|
64
68
|
* (`exchanges`, `instruments`) and the strategy surface (`validateStrategy`,
|
|
65
|
-
* `strategy`). Constructing an
|
|
69
|
+
* `strategy`, `strategies`, `deleteStrategy`, `strategyCode`). Constructing an
|
|
66
70
|
* instance reconfigures the underlying api-client singleton, so avoid
|
|
67
71
|
* holding two `QTSurfer`s with different `baseUrl`s or tokens alive in the
|
|
68
72
|
* same process — they will race. Prefer the `authenticate()` helper over
|
|
@@ -107,7 +111,8 @@ export class QTSurfer {
|
|
|
107
111
|
* if a stage exceeds `timeoutMs`, or {@link QTSCanceledError} if the caller's
|
|
108
112
|
* signal fires before the sweep is accepted. A plain {@link QTSError} means
|
|
109
113
|
* the request itself is malformed (an empty grid, a non-positive `step`, a
|
|
110
|
-
* walk-forward block with fewer than two folds
|
|
114
|
+
* walk-forward block with fewer than two folds, or naming both/neither of
|
|
115
|
+
* `instrument`/`datasetId`) and never reached the network.
|
|
111
116
|
*
|
|
112
117
|
* What the sweep *found* arrives through {@link Sweep.result}, which is also
|
|
113
118
|
* where the semantics of the leaderboard are documented. Acceptance already
|
|
@@ -232,6 +237,51 @@ export class QTSurfer {
|
|
|
232
237
|
return getStrategy(strategyId);
|
|
233
238
|
}
|
|
234
239
|
|
|
240
|
+
/**
|
|
241
|
+
* List every strategy you have registered and not deleted, most recently
|
|
242
|
+
* compiled first. Never `404`s — an empty array means you have none.
|
|
243
|
+
* Each entry deliberately omits `validation`; check a specific strategy's
|
|
244
|
+
* verdict with {@link QTSurfer.strategy}. See {@link StrategySummary}.
|
|
245
|
+
*
|
|
246
|
+
* @throws QTSError on any non-2xx response, with the HTTP status on
|
|
247
|
+
* `status`.
|
|
248
|
+
*/
|
|
249
|
+
strategies(): Promise<StrategySummary[]> {
|
|
250
|
+
return listStrategies();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Release a registered strategy: removes it from both {@link
|
|
255
|
+
* QTSurfer.strategy} and {@link QTSurfer.strategies}.
|
|
256
|
+
*
|
|
257
|
+
* Backtests already run against this strategy are unaffected, and
|
|
258
|
+
* re-submitting the same source afterwards registers a **new** strategy
|
|
259
|
+
* with a **new** id rather than undeleting this one. Deleting your own
|
|
260
|
+
* copy of a strategy never affects anyone else's copy of the same source
|
|
261
|
+
* (e.g. a shared/marketplace listing).
|
|
262
|
+
*
|
|
263
|
+
* @param strategyId the id returned when the strategy was compiled
|
|
264
|
+
* @throws QTSError on any non-2xx response; a `404` (carried on `status`)
|
|
265
|
+
* means no such registered strategy for this caller.
|
|
266
|
+
*/
|
|
267
|
+
deleteStrategy(strategyId: string): Promise<void> {
|
|
268
|
+
return runDeleteStrategy(strategyId);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Read back the exact source last submitted for a strategy id, whitespace
|
|
273
|
+
* and comments included.
|
|
274
|
+
*
|
|
275
|
+
* A `404` (carried on `status`) covers two indistinguishable cases: the id
|
|
276
|
+
* was never registered by you, or it resolves only through a shared/
|
|
277
|
+
* marketplace reference that carries no source of its own.
|
|
278
|
+
*
|
|
279
|
+
* @param strategyId the id returned when the strategy was compiled
|
|
280
|
+
*/
|
|
281
|
+
strategyCode(strategyId: string): Promise<string> {
|
|
282
|
+
return getStrategyCode(strategyId);
|
|
283
|
+
}
|
|
284
|
+
|
|
235
285
|
// Future surface:
|
|
236
286
|
// TTL cache for exchanges / instruments
|
|
237
287
|
// jobs: { cancel, stream, result }
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type DataSourceType,
|
|
6
6
|
type PrepareJobState,
|
|
7
7
|
} from '@qtsurfer/api-client';
|
|
8
|
-
import { QTSCanceledError, QTSPreparationError, QTSStrategyCompileError } from '../errors';
|
|
8
|
+
import { QTSCanceledError, QTSError, QTSPreparationError, QTSStrategyCompileError } from '../errors';
|
|
9
9
|
import { normalizeStatus, runStage, type StagePolicy } from './polling';
|
|
10
10
|
|
|
11
11
|
/** The only data source the workflows prepare against today. @internal */
|
|
@@ -45,14 +45,52 @@ export async function compileStrategySource(
|
|
|
45
45
|
return data.strategyId;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The instrument (or dataset) and window one prepare covers. Exactly one of
|
|
50
|
+
* `instrument`/`datasetId` is set — {@link validatePrepareTarget} enforces that
|
|
51
|
+
* before any network call. `datasetId` pairs with the reserved `exchangeId: 'user'`.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
49
55
|
export interface PrepareTarget {
|
|
50
56
|
exchangeId: string;
|
|
51
|
-
instrument
|
|
57
|
+
instrument?: string;
|
|
58
|
+
/** Id of a previously uploaded dataset, in place of `instrument`. */
|
|
59
|
+
datasetId?: string;
|
|
60
|
+
/** Optional specific version of `datasetId`; requires `datasetId`. */
|
|
61
|
+
datasetVersionId?: string;
|
|
52
62
|
from: string;
|
|
53
63
|
to: string;
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Reject a prepare target that names both, or neither, of `instrument`/`datasetId`,
|
|
68
|
+
* or that sets `datasetVersionId` without `datasetId` — the same three shapes the
|
|
69
|
+
* platform would only reject over the network. Called first thing by each workflow,
|
|
70
|
+
* before compiling the strategy, so a malformed request never reaches the network.
|
|
71
|
+
*
|
|
72
|
+
* @param workflow name of the caller, used to prefix the error message (e.g. `'backtest'`)
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export function validatePrepareTarget(
|
|
76
|
+
workflow: string,
|
|
77
|
+
target: Pick<PrepareTarget, 'instrument' | 'datasetId' | 'datasetVersionId'>,
|
|
78
|
+
): void {
|
|
79
|
+
const hasInstrument = target.instrument !== undefined;
|
|
80
|
+
const hasDataset = target.datasetId !== undefined;
|
|
81
|
+
if (hasInstrument && hasDataset) {
|
|
82
|
+
throw new QTSError(`${workflow}: exactly one of instrument/datasetId is required, got both`);
|
|
83
|
+
}
|
|
84
|
+
if (!hasInstrument && !hasDataset) {
|
|
85
|
+
throw new QTSError(
|
|
86
|
+
`${workflow}: exactly one of instrument/datasetId is required, got neither`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (target.datasetVersionId !== undefined && !hasDataset) {
|
|
90
|
+
throw new QTSError(`${workflow}: datasetVersionId requires datasetId`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
56
94
|
/** Reporting and cancellation for {@link prepareDataset}. @internal */
|
|
57
95
|
export interface PrepareRun {
|
|
58
96
|
signal?: AbortSignal;
|
|
@@ -82,7 +120,15 @@ export async function prepareDataset(
|
|
|
82
120
|
): Promise<string> {
|
|
83
121
|
const { data, error } = await prepareBacktest({
|
|
84
122
|
path: { exchangeId: target.exchangeId, type: TICKER },
|
|
85
|
-
body: {
|
|
123
|
+
body: {
|
|
124
|
+
...(target.instrument !== undefined ? { instrument: target.instrument } : {}),
|
|
125
|
+
...(target.datasetId !== undefined ? { datasetId: target.datasetId } : {}),
|
|
126
|
+
...(target.datasetVersionId !== undefined
|
|
127
|
+
? { datasetVersionId: target.datasetVersionId }
|
|
128
|
+
: {}),
|
|
129
|
+
from: target.from,
|
|
130
|
+
to: target.to,
|
|
131
|
+
},
|
|
86
132
|
...(run.signal ? { signal: run.signal } : {}),
|
|
87
133
|
});
|
|
88
134
|
if (error) throw new QTSPreparationError('Prepare submission failed', error);
|
|
@@ -15,15 +15,47 @@ import {
|
|
|
15
15
|
TICKER,
|
|
16
16
|
compileStrategySource,
|
|
17
17
|
prepareDataset,
|
|
18
|
+
validatePrepareTarget,
|
|
18
19
|
} from '../internal/preparation';
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* ```ts
|
|
23
|
+
* const request: BacktestRequest = {
|
|
24
|
+
* strategy: source,
|
|
25
|
+
* exchangeId: 'binance',
|
|
26
|
+
* instrument: 'BTC/USDT',
|
|
27
|
+
* from: '2026-01-01T00:00:00Z',
|
|
28
|
+
* to: '2026-02-01T00:00:00Z',
|
|
29
|
+
* };
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Backtest against a dataset you uploaded instead of an exchange instrument by
|
|
33
|
+
* replacing `instrument` with `datasetId` (and `exchangeId: 'user'`):
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* const request: BacktestRequest = {
|
|
37
|
+
* strategy: source,
|
|
38
|
+
* exchangeId: 'user',
|
|
39
|
+
* datasetId: 'ds_123',
|
|
40
|
+
* from: '2026-01-01T00:00:00Z',
|
|
41
|
+
* to: '2026-02-01T00:00:00Z',
|
|
42
|
+
* };
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
20
45
|
export interface BacktestRequest {
|
|
21
46
|
/** Strategy source code (Java) */
|
|
22
47
|
strategy: string;
|
|
23
|
-
/** Exchange id, e.g. `binance` */
|
|
48
|
+
/** Exchange id, e.g. `binance`, or the reserved value `user` when backtesting against `datasetId`. */
|
|
24
49
|
exchangeId: string;
|
|
25
|
-
/** Instrument symbol, e.g. `BTC/USDT` */
|
|
26
|
-
instrument
|
|
50
|
+
/** Instrument symbol, e.g. `BTC/USDT`. Exactly one of `instrument`/`datasetId` is required. */
|
|
51
|
+
instrument?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Id of a dataset you uploaded, in place of `instrument`. Exactly one of
|
|
54
|
+
* `instrument`/`datasetId` is required. Pairs with `exchangeId: 'user'`.
|
|
55
|
+
*/
|
|
56
|
+
datasetId?: string;
|
|
57
|
+
/** Optional specific version of `datasetId`; omit to use its current version. Requires `datasetId`. */
|
|
58
|
+
datasetVersionId?: string;
|
|
27
59
|
/** Date range start (ISO-8601, ISO DATE or BASIC ISO DATE) */
|
|
28
60
|
from: string;
|
|
29
61
|
/** Date range end (same formats as `from`) */
|
|
@@ -78,6 +110,7 @@ export async function backtest(
|
|
|
78
110
|
req: BacktestRequest,
|
|
79
111
|
opts: BacktestOptions = {},
|
|
80
112
|
): Promise<BacktestResult> {
|
|
113
|
+
validatePrepareTarget('backtest', req);
|
|
81
114
|
const policy = buildStagePolicy(opts, DEFAULT_POLL_INTERVAL_MS, DEFAULT_MAX_POLL_INTERVAL_MS);
|
|
82
115
|
|
|
83
116
|
// 1. Compile strategy (single synchronous request)
|
|
@@ -101,7 +134,9 @@ function prepareData(
|
|
|
101
134
|
return prepareDataset(
|
|
102
135
|
{
|
|
103
136
|
exchangeId: req.exchangeId,
|
|
104
|
-
instrument: req.instrument,
|
|
137
|
+
...(req.instrument !== undefined ? { instrument: req.instrument } : {}),
|
|
138
|
+
...(req.datasetId !== undefined ? { datasetId: req.datasetId } : {}),
|
|
139
|
+
...(req.datasetVersionId !== undefined ? { datasetVersionId: req.datasetVersionId } : {}),
|
|
105
140
|
from: req.from,
|
|
106
141
|
to: req.to,
|
|
107
142
|
},
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getStrategy as apiGetStrategy,
|
|
3
3
|
validateStrategy as apiValidateStrategy,
|
|
4
|
+
listStrategies as apiListStrategies,
|
|
5
|
+
deleteStrategy as apiDeleteStrategy,
|
|
6
|
+
getStrategyCode as apiGetStrategyCode,
|
|
4
7
|
type StrategyState as ApiStrategyState,
|
|
8
|
+
type StrategySummary as ApiStrategySummary,
|
|
5
9
|
} from '@qtsurfer/api-client';
|
|
6
10
|
import { QTSError } from '../errors';
|
|
7
11
|
import { requestFailed } from '../internal/requestError';
|
|
@@ -28,6 +32,15 @@ import { requestFailed } from '../internal/requestError';
|
|
|
28
32
|
* A verdict describes the bytecode that existed when it was recorded:
|
|
29
33
|
* `compiledAt` newer than `validatedAt` means the strategy was recompiled
|
|
30
34
|
* afterwards and the verdict no longer describes what would run.
|
|
35
|
+
*
|
|
36
|
+
* `_links.code`, when present, is a discovery link to this strategy's raw
|
|
37
|
+
* source (`GET /strategy/{strategyId}/code` — the same thing
|
|
38
|
+
* {@link QTSurfer.strategyCode} fetches by id, so there is no need to follow
|
|
39
|
+
* the link yourself). It is present on a full `StrategyState` body — this
|
|
40
|
+
* function's result, and {@link QTSurfer.validateStrategy}'s already-validated
|
|
41
|
+
* `200` — and **absent** from that same operation's `queued: true` (`202`)
|
|
42
|
+
* outcome, which is a deliberately partial stub. This field passes through
|
|
43
|
+
* unmodified from api-client, so it needs no unwrapping on the SDK's part.
|
|
31
44
|
*/
|
|
32
45
|
export type StrategyState = ApiStrategyState;
|
|
33
46
|
|
|
@@ -116,3 +129,88 @@ export async function getStrategy(strategyId: string): Promise<StrategyState> {
|
|
|
116
129
|
if (!data) throw new QTSError('Empty strategy response');
|
|
117
130
|
return data;
|
|
118
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* One entry in {@link QTSurfer.strategies}'s result: the same provenance
|
|
135
|
+
* {@link QTSurfer.strategy} reports — `compiledAt`, `requiredSources` — but
|
|
136
|
+
* never `validation`, which is what keeps listing cheap no matter how many
|
|
137
|
+
* strategies you have registered. Check a specific strategy's verdict with
|
|
138
|
+
* {@link QTSurfer.strategy}.
|
|
139
|
+
*
|
|
140
|
+
* Note: the spec types this endpoint's `requiredSources` as a plain
|
|
141
|
+
* `string[]`, not the `'Ticker' | 'KLine' | 'FundingRate'` union that
|
|
142
|
+
* {@link StrategyState}'s own `requiredSources` carries — narrow it yourself
|
|
143
|
+
* if you need the literal type. Alias for api-client's `StrategySummary`.
|
|
144
|
+
*/
|
|
145
|
+
export type StrategySummary = ApiStrategySummary;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* List every strategy you have registered and not deleted, most recently
|
|
149
|
+
* compiled first.
|
|
150
|
+
*
|
|
151
|
+
* **Never `404`.** An empty array means you have none registered — not an
|
|
152
|
+
* error. Each entry omits `validation` on purpose (see {@link
|
|
153
|
+
* StrategySummary}); check a specific strategy's verdict with {@link
|
|
154
|
+
* QTSurfer.strategy}.
|
|
155
|
+
*
|
|
156
|
+
* @throws QTSError on any non-2xx response, with the HTTP status on `status`.
|
|
157
|
+
*/
|
|
158
|
+
export async function listStrategies(): Promise<StrategySummary[]> {
|
|
159
|
+
const { data, error, response } = await apiListStrategies();
|
|
160
|
+
if (error) throw requestFailed('strategies list', error, response?.status);
|
|
161
|
+
if (!data) throw new QTSError('Empty strategies response');
|
|
162
|
+
return data.strategies;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Release a registered strategy: removes it from both {@link
|
|
167
|
+
* QTSurfer.strategy} and {@link QTSurfer.strategies}.
|
|
168
|
+
*
|
|
169
|
+
* **Does not undo anything already run.** Backtests you ran against this
|
|
170
|
+
* strategy before deleting it are completely unaffected — deleting only
|
|
171
|
+
* stops you from validating or re-running it under this id going forward.
|
|
172
|
+
* Re-submitting the exact same source afterwards registers a **new**
|
|
173
|
+
* strategy with a **new** id; it does not "undelete" this one, because
|
|
174
|
+
* nothing about the id itself is restored.
|
|
175
|
+
*
|
|
176
|
+
* **Scoped to your own registration.** If you copied someone else's strategy
|
|
177
|
+
* (a shared/marketplace listing), deleting your copy never affects theirs,
|
|
178
|
+
* or anyone else's, regardless of how many callers registered the same
|
|
179
|
+
* source independently.
|
|
180
|
+
*
|
|
181
|
+
* Resolves with nothing: the response body is `{ strategyId, deleted: true }`,
|
|
182
|
+
* and both fields are things the caller already knows before calling this —
|
|
183
|
+
* `strategyId` is the argument just passed in, and `deleted` is always `true`
|
|
184
|
+
* on a `200`. There is nothing in it a `void` return would lose.
|
|
185
|
+
*
|
|
186
|
+
* @param strategyId the id returned when the strategy was compiled
|
|
187
|
+
* @throws QTSError on any non-2xx response; a `404` (carried on `status`)
|
|
188
|
+
* means no such registered strategy for this caller.
|
|
189
|
+
*/
|
|
190
|
+
export async function deleteStrategy(strategyId: string): Promise<void> {
|
|
191
|
+
const { error, response } = await apiDeleteStrategy({ path: { strategyId } });
|
|
192
|
+
if (error) throw requestFailed('strategy delete', error, response?.status);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Read back the exact source last submitted for a strategy id — the same
|
|
197
|
+
* text `strategyId` was derived from, whitespace and comments included.
|
|
198
|
+
*
|
|
199
|
+
* **A `404` here covers two cases the response cannot tell apart:** the id
|
|
200
|
+
* was never registered by you, or it resolves only through a shared/
|
|
201
|
+
* marketplace reference that carries no source of its own (a strategy you
|
|
202
|
+
* copied by reference rather than by resubmitting its code). Both read as
|
|
203
|
+
* "nothing to return" from this endpoint's point of view, and the SDK does
|
|
204
|
+
* not attempt to distinguish them — there is nothing in the response to tell
|
|
205
|
+
* them apart with.
|
|
206
|
+
*
|
|
207
|
+
* @param strategyId the id returned when the strategy was compiled
|
|
208
|
+
* @throws QTSError on any non-2xx response; a `404` (carried on `status`)
|
|
209
|
+
* is the two-case ambiguity described above.
|
|
210
|
+
*/
|
|
211
|
+
export async function getStrategyCode(strategyId: string): Promise<string> {
|
|
212
|
+
const { data, error, response } = await apiGetStrategyCode({ path: { strategyId } });
|
|
213
|
+
if (error) throw requestFailed('strategy code lookup', error, response?.status);
|
|
214
|
+
if (!data) throw new QTSError('Empty strategy code response');
|
|
215
|
+
return data.code;
|
|
216
|
+
}
|
package/src/workflows/sweep.ts
CHANGED
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
runStage,
|
|
26
26
|
type StagePolicy,
|
|
27
27
|
} from '../internal/polling';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
TICKER,
|
|
30
|
+
compileStrategySource,
|
|
31
|
+
prepareDataset,
|
|
32
|
+
validatePrepareTarget,
|
|
33
|
+
} from '../internal/preparation';
|
|
29
34
|
import { requestFailed } from '../internal/requestError';
|
|
30
35
|
import type { BacktestStage } from './backtest';
|
|
31
36
|
|
|
@@ -291,14 +296,35 @@ export type WalkForwardFold = ApiWalkForwardFold;
|
|
|
291
296
|
* objective: 'sharpe',
|
|
292
297
|
* };
|
|
293
298
|
* ```
|
|
299
|
+
*
|
|
300
|
+
* Sweep against a dataset you uploaded instead of an exchange instrument by
|
|
301
|
+
* replacing `instrument` with `datasetId` (and `exchangeId: 'user'`):
|
|
302
|
+
*
|
|
303
|
+
* ```ts
|
|
304
|
+
* const request: SweepRequest = {
|
|
305
|
+
* strategy: source,
|
|
306
|
+
* exchangeId: 'user',
|
|
307
|
+
* datasetId: 'ds_123',
|
|
308
|
+
* from: '2026-01-01T00:00:00Z',
|
|
309
|
+
* to: '2026-02-01T00:00:00Z',
|
|
310
|
+
* params: { rsiPeriod: { from: 7, to: 28, step: 1 } },
|
|
311
|
+
* };
|
|
312
|
+
* ```
|
|
294
313
|
*/
|
|
295
314
|
export interface SweepRequest {
|
|
296
315
|
/** Strategy source code (Java), compiled once and reused by every trial. */
|
|
297
316
|
strategy: string;
|
|
298
|
-
/** Exchange id, e.g. `binance`. */
|
|
317
|
+
/** Exchange id, e.g. `binance`, or the reserved value `user` when sweeping against `datasetId`. */
|
|
299
318
|
exchangeId: string;
|
|
300
|
-
/** Instrument symbol, e.g. `BTC/USDT`. */
|
|
301
|
-
instrument
|
|
319
|
+
/** Instrument symbol, e.g. `BTC/USDT`. Exactly one of `instrument`/`datasetId` is required. */
|
|
320
|
+
instrument?: string;
|
|
321
|
+
/**
|
|
322
|
+
* Id of a dataset you uploaded, in place of `instrument`. Exactly one of
|
|
323
|
+
* `instrument`/`datasetId` is required. Pairs with `exchangeId: 'user'`.
|
|
324
|
+
*/
|
|
325
|
+
datasetId?: string;
|
|
326
|
+
/** Optional specific version of `datasetId`; omit to use its current version. Requires `datasetId`. */
|
|
327
|
+
datasetVersionId?: string;
|
|
302
328
|
/** Range start (ISO-8601, ISO DATE, or BASIC ISO DATE). */
|
|
303
329
|
from: string;
|
|
304
330
|
/** Range end (same formats as `from`; must be later than `from`). */
|
|
@@ -655,6 +681,8 @@ export async function sweep(req: SweepRequest, opts: SweepOptions = {}): Promise
|
|
|
655
681
|
* the same way.
|
|
656
682
|
*/
|
|
657
683
|
function validateRequest(req: SweepRequest): void {
|
|
684
|
+
validatePrepareTarget('sweep', req);
|
|
685
|
+
|
|
658
686
|
const names = Object.keys(req.params ?? {});
|
|
659
687
|
if (names.length === 0) {
|
|
660
688
|
throw new QTSError('sweep: params must hold at least one axis');
|
|
@@ -690,7 +718,9 @@ function prepareData(
|
|
|
690
718
|
return prepareDataset(
|
|
691
719
|
{
|
|
692
720
|
exchangeId: req.exchangeId,
|
|
693
|
-
instrument: req.instrument,
|
|
721
|
+
...(req.instrument !== undefined ? { instrument: req.instrument } : {}),
|
|
722
|
+
...(req.datasetId !== undefined ? { datasetId: req.datasetId } : {}),
|
|
723
|
+
...(req.datasetVersionId !== undefined ? { datasetVersionId: req.datasetVersionId } : {}),
|
|
694
724
|
from: req.from,
|
|
695
725
|
to: req.to,
|
|
696
726
|
},
|