@alphafox/cli 0.2.0 → 0.3.3
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 +42 -4
- package/dist/auth/loopback-callback.js +6 -6
- package/dist/catalog/command-tree.d.ts +1 -0
- package/dist/catalog/command-tree.js +5 -3
- package/dist/catalog/validate-body.d.ts +22 -0
- package/dist/catalog/validate-body.js +98 -0
- package/dist/commands/request-body.d.ts +21 -0
- package/dist/commands/request-body.js +92 -0
- package/dist/commands/run.js +113 -10
- package/dist/engine-backtest/errors.d.ts +18 -0
- package/dist/engine-backtest/errors.js +26 -0
- package/dist/engine-backtest/fetch-runtime.d.ts +38 -0
- package/dist/engine-backtest/fetch-runtime.js +170 -0
- package/dist/engine-backtest/native-import.d.ts +1 -0
- package/dist/engine-backtest/native-import.js +10 -0
- package/dist/engine-backtest/parse-args.d.ts +6 -0
- package/dist/engine-backtest/parse-args.js +281 -0
- package/dist/engine-backtest/persist.d.ts +32 -0
- package/dist/engine-backtest/persist.js +94 -0
- package/dist/engine-backtest/replay-timeframe.d.ts +16 -0
- package/dist/engine-backtest/replay-timeframe.js +48 -0
- package/dist/engine-backtest/resolve-packages.d.ts +27 -0
- package/dist/engine-backtest/resolve-packages.js +288 -0
- package/dist/engine-backtest/run-command.d.ts +38 -0
- package/dist/engine-backtest/run-command.js +540 -0
- package/dist/engine-backtest/types.d.ts +249 -0
- package/dist/engine-backtest/types.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +16 -1
- package/dist/install/exec.d.ts +13 -0
- package/dist/install/exec.js +77 -0
- package/dist/install/package-root.d.ts +4 -0
- package/dist/install/package-root.js +59 -0
- package/dist/install/types.d.ts +69 -0
- package/dist/install/types.js +26 -0
- package/dist/install/wizard.d.ts +21 -0
- package/dist/install/wizard.js +330 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/.nojekyll +0 -0
- package/docs/alphafox-cli-installation-guide.md +105 -0
- package/docs/favicon.svg +4 -0
- package/docs/index.html +748 -0
- package/docs/logo/alphafox-mark.svg +4 -0
- package/docs/logo/alphafox-wordmark-black-en.svg +17 -0
- package/docs/logo/alphafox-wordmark-white-en.svg +16 -0
- package/docs/release-supply-chain.md +10 -2
- package/package.json +7 -2
- package/skills/account/SKILL.md +2 -2
- package/skills/admin/SKILL.md +2 -2
- package/skills/alphafox-shared/SKILL.md +22 -1
- package/skills/auth/SKILL.md +1 -1
- package/skills/engine-backtest/SKILL.md +71 -0
- package/skills/exchange/SKILL.md +2 -2
- package/skills/market/SKILL.md +1 -1
- package/skills/notification/SKILL.md +2 -2
- package/skills/strategy/SKILL.md +10 -2
- package/skills/trading/SKILL.md +6 -3
- package/vendor/backtest-runner/NOTICE.md +1 -0
- package/vendor/backtest-runner/README.md +97 -0
- package/vendor/backtest-runner/index.d.ts +423 -0
- package/vendor/backtest-runner/index.mjs +59 -0
- package/vendor/backtest-runner/lib/abortable.mjs +23 -0
- package/vendor/backtest-runner/lib/cache.mjs +159 -0
- package/vendor/backtest-runner/lib/coverage.mjs +238 -0
- package/vendor/backtest-runner/lib/encode.mjs +28 -0
- package/vendor/backtest-runner/lib/exchanges.mjs +143 -0
- package/vendor/backtest-runner/lib/proxy.mjs +78 -0
- package/vendor/backtest-runner/lib/scenario.mjs +66 -0
- package/vendor/backtest-runner/lib/series.mjs +370 -0
- package/vendor/backtest-runner/lib/tape-loader.mjs +614 -0
- package/vendor/backtest-runner/lib/timeframes.mjs +55 -0
- package/vendor/backtest-runner/package.json +13 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadConfigValue = loadConfigValue;
|
|
4
|
+
exports.executeEngineBacktestRun = executeEngineBacktestRun;
|
|
5
|
+
exports.engineBacktestHelpData = engineBacktestHelpData;
|
|
6
|
+
exports.cmdEngineBacktest = cmdEngineBacktest;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const profiles_1 = require("../config/profiles");
|
|
11
|
+
const envelope_1 = require("../envelope");
|
|
12
|
+
const client_1 = require("../http/client");
|
|
13
|
+
const store_1 = require("../keychain/store");
|
|
14
|
+
const errors_1 = require("./errors");
|
|
15
|
+
const parse_args_1 = require("./parse-args");
|
|
16
|
+
const persist_1 = require("./persist");
|
|
17
|
+
const replay_timeframe_1 = require("./replay-timeframe");
|
|
18
|
+
const resolve_packages_1 = require("./resolve-packages");
|
|
19
|
+
const CREATE_EXPERIMENT_PATH = "/api/v1/engine-backtest/experiments";
|
|
20
|
+
const SUBSCRIPTION_PATH = "/api/v1/subscriptions/me";
|
|
21
|
+
const SUBSCRIPTION_TIERS = new Set([
|
|
22
|
+
"free",
|
|
23
|
+
"pro",
|
|
24
|
+
"pro_max",
|
|
25
|
+
]);
|
|
26
|
+
function runsPath(experimentId) {
|
|
27
|
+
return `/api/v1/engine-backtest/experiments/${encodeURIComponent(experimentId)}/runs`;
|
|
28
|
+
}
|
|
29
|
+
function loadConfigValue(raw, options = {}) {
|
|
30
|
+
const cwd = options.cwd ?? process.cwd();
|
|
31
|
+
const read = options.readFile ?? ((p) => (0, node_fs_1.readFileSync)(p, "utf8"));
|
|
32
|
+
if (raw.startsWith("@")) {
|
|
33
|
+
const rel = raw.slice(1);
|
|
34
|
+
if (!rel) {
|
|
35
|
+
throw new errors_1.EngineBacktestError({
|
|
36
|
+
type: "usage",
|
|
37
|
+
subtype: "missing_config",
|
|
38
|
+
message: "--config @path is empty",
|
|
39
|
+
status: 400,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const abs = (0, node_path_1.isAbsolute)(rel) ? rel : (0, node_path_1.resolve)(cwd, rel);
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(read(abs));
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
throw new errors_1.EngineBacktestError({
|
|
48
|
+
type: "usage",
|
|
49
|
+
subtype: "invalid_config",
|
|
50
|
+
message: `Cannot read --config file ${abs}: ${err instanceof Error ? err.message : String(err)}`,
|
|
51
|
+
status: 400,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new errors_1.EngineBacktestError({
|
|
60
|
+
type: "usage",
|
|
61
|
+
subtype: "invalid_config",
|
|
62
|
+
message: "--config must be JSON or @path-to.json",
|
|
63
|
+
status: 400,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function extractErrorMessage(json, fallback) {
|
|
68
|
+
if (json && typeof json === "object") {
|
|
69
|
+
const o = json;
|
|
70
|
+
if (typeof o.message === "string")
|
|
71
|
+
return o.message;
|
|
72
|
+
if (typeof o.detail === "string")
|
|
73
|
+
return o.detail;
|
|
74
|
+
if (typeof o.error === "string")
|
|
75
|
+
return o.error;
|
|
76
|
+
if (o.error && typeof o.error === "object") {
|
|
77
|
+
const e = o.error;
|
|
78
|
+
if (typeof e.message === "string")
|
|
79
|
+
return e.message;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return fallback || "Request failed";
|
|
83
|
+
}
|
|
84
|
+
function extractErrorCode(json) {
|
|
85
|
+
if (json && typeof json === "object") {
|
|
86
|
+
const o = json;
|
|
87
|
+
if (typeof o.code === "string" || typeof o.code === "number")
|
|
88
|
+
return o.code;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
function extractEntityId(json) {
|
|
93
|
+
if (!json || typeof json !== "object")
|
|
94
|
+
return undefined;
|
|
95
|
+
const o = json;
|
|
96
|
+
if (typeof o.id === "string" && o.id.trim())
|
|
97
|
+
return o.id.trim();
|
|
98
|
+
if (o.data && typeof o.data === "object") {
|
|
99
|
+
const d = o.data;
|
|
100
|
+
if (typeof d.id === "string" && d.id.trim())
|
|
101
|
+
return d.id.trim();
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
function isSupportedPlan(plan) {
|
|
106
|
+
return (!!plan &&
|
|
107
|
+
typeof plan === "object" &&
|
|
108
|
+
!("status" in plan && plan.status === "failed") &&
|
|
109
|
+
"support" in plan &&
|
|
110
|
+
plan.support?.status === "supported");
|
|
111
|
+
}
|
|
112
|
+
function definitionDisplay(args) {
|
|
113
|
+
const fallback = args.definitionId ?? "strategy";
|
|
114
|
+
return {
|
|
115
|
+
zh: args.definitionLabelZh?.trim() || fallback,
|
|
116
|
+
en: args.definitionLabelEn?.trim() || fallback,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function requireAuth(profile, env, loadTokensFn) {
|
|
120
|
+
const tokens = loadTokensFn(profile.name, env);
|
|
121
|
+
if (tokens?.accessToken?.trim())
|
|
122
|
+
return;
|
|
123
|
+
throw new errors_1.EngineBacktestError({
|
|
124
|
+
type: "http",
|
|
125
|
+
status: 401,
|
|
126
|
+
message: "Not authenticated. Run alphafox auth login.",
|
|
127
|
+
hint: "Tokens live in the OS keychain. Do not pass --token.",
|
|
128
|
+
subtype: "unauthenticated",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async function postJson(api, profile, env, path, body, mintId) {
|
|
132
|
+
const res = await api({
|
|
133
|
+
method: "POST",
|
|
134
|
+
path,
|
|
135
|
+
body,
|
|
136
|
+
profile,
|
|
137
|
+
idempotencyKey: mintId(),
|
|
138
|
+
}, env);
|
|
139
|
+
if (res.status >= 400) {
|
|
140
|
+
throw new errors_1.EngineBacktestError({
|
|
141
|
+
type: "http",
|
|
142
|
+
status: res.status,
|
|
143
|
+
message: extractErrorMessage(res.json, res.bodyText),
|
|
144
|
+
code: extractErrorCode(res.json),
|
|
145
|
+
details: res.json,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return res;
|
|
149
|
+
}
|
|
150
|
+
function extractSubscriptionTier(json) {
|
|
151
|
+
if (!json || typeof json !== "object")
|
|
152
|
+
return undefined;
|
|
153
|
+
const root = json;
|
|
154
|
+
const nested = root.data && typeof root.data === "object"
|
|
155
|
+
? root.data
|
|
156
|
+
: undefined;
|
|
157
|
+
const value = root.subscriptionTier ?? nested?.subscriptionTier;
|
|
158
|
+
return typeof value === "string" &&
|
|
159
|
+
SUBSCRIPTION_TIERS.has(value)
|
|
160
|
+
? value
|
|
161
|
+
: undefined;
|
|
162
|
+
}
|
|
163
|
+
async function loadAccountSubscriptionTier(api, profile, env) {
|
|
164
|
+
const res = await api({
|
|
165
|
+
method: "GET",
|
|
166
|
+
path: SUBSCRIPTION_PATH,
|
|
167
|
+
profile,
|
|
168
|
+
}, env);
|
|
169
|
+
if (res.status >= 400) {
|
|
170
|
+
throw new errors_1.EngineBacktestError({
|
|
171
|
+
type: "http",
|
|
172
|
+
status: res.status,
|
|
173
|
+
message: extractErrorMessage(res.json, res.bodyText),
|
|
174
|
+
code: extractErrorCode(res.json),
|
|
175
|
+
details: res.json,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const tier = extractSubscriptionTier(res.json);
|
|
179
|
+
if (!tier) {
|
|
180
|
+
throw new errors_1.EngineBacktestError({
|
|
181
|
+
type: "runtime",
|
|
182
|
+
subtype: "subscription_tier_unresolved",
|
|
183
|
+
message: "subscriptions.me response did not include a valid subscriptionTier",
|
|
184
|
+
details: res.json,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return tier;
|
|
188
|
+
}
|
|
189
|
+
function emitProgress(flags, writeLine, stage, fraction, detail) {
|
|
190
|
+
if (flags.format !== "jsonl")
|
|
191
|
+
return;
|
|
192
|
+
writeLine({
|
|
193
|
+
event: "progress",
|
|
194
|
+
stage,
|
|
195
|
+
fraction,
|
|
196
|
+
...(detail ? { detail } : {}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async function executeEngineBacktestRun(args, flags, env = process.env, deps = {}) {
|
|
200
|
+
if (args.help) {
|
|
201
|
+
throw new errors_1.EngineBacktestError({
|
|
202
|
+
type: "usage",
|
|
203
|
+
message: "internal: help should be handled by the command wrapper",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (!args.definitionId || !args.configRaw || !args.exchange || !args.range) {
|
|
207
|
+
throw new errors_1.EngineBacktestError({
|
|
208
|
+
type: "usage",
|
|
209
|
+
subtype: "invalid_args",
|
|
210
|
+
message: "Missing required run arguments",
|
|
211
|
+
status: 400,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const profile = (0, profiles_1.resolveProfile)(flags.profile, env, {
|
|
215
|
+
unsafeCustomEndpoint: flags.unsafeCustomEndpoint,
|
|
216
|
+
});
|
|
217
|
+
const api = deps.apiRequest ?? client_1.apiRequest;
|
|
218
|
+
const tokensFn = deps.loadTokens ?? store_1.loadTokens;
|
|
219
|
+
const mintId = deps.randomUUID ?? node_crypto_1.randomUUID;
|
|
220
|
+
const writeLine = deps.writeLine ??
|
|
221
|
+
((value) => {
|
|
222
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
223
|
+
});
|
|
224
|
+
const persist = args.persist && !flags.dryRun;
|
|
225
|
+
const createExperiment = args.createExperiment && !flags.dryRun;
|
|
226
|
+
const needsApi = persist || createExperiment;
|
|
227
|
+
if (needsApi) {
|
|
228
|
+
requireAuth(profile, env, tokensFn);
|
|
229
|
+
}
|
|
230
|
+
const config = loadConfigValue(args.configRaw, {
|
|
231
|
+
cwd: deps.cwd,
|
|
232
|
+
readFile: deps.readFile,
|
|
233
|
+
});
|
|
234
|
+
let experimentId = args.experimentId;
|
|
235
|
+
if (!experimentId && !createExperiment) {
|
|
236
|
+
throw new errors_1.EngineBacktestError({
|
|
237
|
+
type: "usage",
|
|
238
|
+
subtype: "missing_experiment",
|
|
239
|
+
message: "Provide --experiment <uuid> or --create-experiment --name <name>",
|
|
240
|
+
status: 400,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
let wasm;
|
|
244
|
+
let runner;
|
|
245
|
+
if (deps.createNodeBacktestClient &&
|
|
246
|
+
deps.loadTape &&
|
|
247
|
+
deps.assembleScenario &&
|
|
248
|
+
deps.resolveTapeExchange) {
|
|
249
|
+
wasm = { createNodeBacktestClient: deps.createNodeBacktestClient };
|
|
250
|
+
runner = {
|
|
251
|
+
loadTape: deps.loadTape,
|
|
252
|
+
assembleScenario: deps.assembleScenario,
|
|
253
|
+
resolveTapeExchange: deps.resolveTapeExchange,
|
|
254
|
+
DEFAULT_EXECUTION_MODEL: deps.defaultExecutionModel ?? persist_1.DEFAULT_EXECUTION_MODEL,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const [wasmLoaded, runnerLoaded] = await Promise.all([
|
|
259
|
+
(0, resolve_packages_1.loadBacktestWasm)(env, deps.resolveHooks),
|
|
260
|
+
(0, resolve_packages_1.loadBacktestRunner)(env, deps.resolveHooks),
|
|
261
|
+
]);
|
|
262
|
+
wasm = wasmLoaded.module;
|
|
263
|
+
runner = runnerLoaded.module;
|
|
264
|
+
}
|
|
265
|
+
const client = wasm.createNodeBacktestClient();
|
|
266
|
+
try {
|
|
267
|
+
if (typeof client.init === "function") {
|
|
268
|
+
await client.init();
|
|
269
|
+
}
|
|
270
|
+
let configSchemaVersion = args.configSchemaVersion;
|
|
271
|
+
if (configSchemaVersion === undefined) {
|
|
272
|
+
const listed = await client.strategyDefinitions();
|
|
273
|
+
const found = listed.definitions.find((d) => d.id === args.definitionId);
|
|
274
|
+
const version = found?.configSchemaVersion;
|
|
275
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version <= 0) {
|
|
276
|
+
throw new errors_1.EngineBacktestError({
|
|
277
|
+
type: "runtime",
|
|
278
|
+
subtype: "config_schema_version_unresolved",
|
|
279
|
+
message: `strategyDefinitions() has no configSchemaVersion for "${args.definitionId}"`,
|
|
280
|
+
hint: "Pass --config-schema-version explicitly.",
|
|
281
|
+
details: { definitionId: args.definitionId },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
configSchemaVersion = version;
|
|
285
|
+
}
|
|
286
|
+
const plan = await client.planBacktest({
|
|
287
|
+
definitionId: args.definitionId,
|
|
288
|
+
configSchemaVersion,
|
|
289
|
+
config,
|
|
290
|
+
});
|
|
291
|
+
if (!isSupportedPlan(plan)) {
|
|
292
|
+
const reason = plan && typeof plan === "object" && "support" in plan
|
|
293
|
+
? plan
|
|
294
|
+
.support?.reason
|
|
295
|
+
: undefined;
|
|
296
|
+
const errors = plan && typeof plan === "object" && "errors" in plan
|
|
297
|
+
? plan.errors
|
|
298
|
+
: undefined;
|
|
299
|
+
throw new errors_1.EngineBacktestError({
|
|
300
|
+
type: "runtime",
|
|
301
|
+
subtype: "plan_unsupported",
|
|
302
|
+
message: reason?.message ??
|
|
303
|
+
`planBacktest does not support definition "${args.definitionId}"`,
|
|
304
|
+
code: reason?.code,
|
|
305
|
+
details: { reason, errors, plan },
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
let subscriptionTier = args.tier ?? "pro";
|
|
309
|
+
if (persist) {
|
|
310
|
+
const accountTier = await loadAccountSubscriptionTier(api, profile, env);
|
|
311
|
+
if (args.tier !== undefined && args.tier !== accountTier) {
|
|
312
|
+
throw new errors_1.EngineBacktestError({
|
|
313
|
+
type: "usage",
|
|
314
|
+
subtype: "subscription_tier_mismatch",
|
|
315
|
+
message: `--tier ${args.tier} does not match account subscription tier ${accountTier}`,
|
|
316
|
+
hint: "Remove --tier for persisted runs, or use --no-persist to simulate another tier.",
|
|
317
|
+
status: 400,
|
|
318
|
+
details: { requestedTier: args.tier, accountTier },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
subscriptionTier = accountTier;
|
|
322
|
+
}
|
|
323
|
+
let exchange;
|
|
324
|
+
try {
|
|
325
|
+
exchange = runner.resolveTapeExchange(args.exchange);
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
throw new errors_1.EngineBacktestError({
|
|
329
|
+
type: "usage",
|
|
330
|
+
subtype: "invalid_exchange",
|
|
331
|
+
message: err instanceof Error ? err.message : String(err),
|
|
332
|
+
status: 400,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const replayPlan = (0, replay_timeframe_1.mergeReplayTimeframeWithPlan)({
|
|
336
|
+
replayTimeframe: args.replayTimeframe,
|
|
337
|
+
planTimeframes: plan.timeframes,
|
|
338
|
+
seriesRequirements: plan.seriesRequirements,
|
|
339
|
+
symbols: plan.symbols,
|
|
340
|
+
});
|
|
341
|
+
let tapeResult;
|
|
342
|
+
try {
|
|
343
|
+
tapeResult = await runner.loadTape({
|
|
344
|
+
exchangeId: exchange.id,
|
|
345
|
+
exchange,
|
|
346
|
+
symbols: plan.symbols,
|
|
347
|
+
baseTimeframe: replayPlan.baseTimeframe,
|
|
348
|
+
timeframes: replayPlan.timeframes,
|
|
349
|
+
seriesRequirements: replayPlan.seriesRequirements,
|
|
350
|
+
needsFunding: plan.needsFunding,
|
|
351
|
+
auxiliaryDataRequirements: plan.auxiliaryDataRequirements,
|
|
352
|
+
fromMs: args.range.fromMs,
|
|
353
|
+
toMs: args.range.toMs,
|
|
354
|
+
dataQualityMode: args.dataQualityMode,
|
|
355
|
+
onProgress: (progress) => {
|
|
356
|
+
emitProgress(flags, writeLine, progress.stage || "tape", progress.fraction, progress.detail);
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
if ((0, errors_1.isEngineBacktestError)(err))
|
|
362
|
+
throw err;
|
|
363
|
+
const issues = err && typeof err === "object" && "issues" in err
|
|
364
|
+
? err.issues
|
|
365
|
+
: undefined;
|
|
366
|
+
throw new errors_1.EngineBacktestError({
|
|
367
|
+
type: "runtime",
|
|
368
|
+
subtype: issues ? "tape_unavailable" : "tape_load_failed",
|
|
369
|
+
message: err instanceof Error ? err.message : String(err),
|
|
370
|
+
details: issues ? { issues } : undefined,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const runId = mintId();
|
|
374
|
+
const executionModel = {
|
|
375
|
+
...(runner.DEFAULT_EXECUTION_MODEL ?? persist_1.DEFAULT_EXECUTION_MODEL),
|
|
376
|
+
...args.executionModelOverride,
|
|
377
|
+
};
|
|
378
|
+
const scenario = runner.assembleScenario({
|
|
379
|
+
runId,
|
|
380
|
+
definitionId: args.definitionId,
|
|
381
|
+
configSchemaVersion,
|
|
382
|
+
config: plan.effectiveConfig ?? config,
|
|
383
|
+
subscriptionTier,
|
|
384
|
+
initialEquity: args.initialEquity,
|
|
385
|
+
tape: tapeResult.tape,
|
|
386
|
+
executionModel,
|
|
387
|
+
});
|
|
388
|
+
const result = await client.runBacktest(scenario, tapeResult.buffers, (fraction) => {
|
|
389
|
+
emitProgress(flags, writeLine, "wasm", fraction);
|
|
390
|
+
});
|
|
391
|
+
if (result.status === "failed") {
|
|
392
|
+
throw new errors_1.EngineBacktestError({
|
|
393
|
+
type: "runtime",
|
|
394
|
+
subtype: "backtest_failed",
|
|
395
|
+
message: "runBacktest returned status=failed",
|
|
396
|
+
details: { errors: result.errors, runId: result.runId },
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const engineVersion = result.engineVersion?.trim() ||
|
|
400
|
+
(typeof client.version === "function" ? (await client.version()).trim() : "") ||
|
|
401
|
+
"node-wasm";
|
|
402
|
+
if (createExperiment) {
|
|
403
|
+
const body = {
|
|
404
|
+
name: args.name,
|
|
405
|
+
strategyDefinitionId: args.definitionId,
|
|
406
|
+
strategyDefinitionDisplay: definitionDisplay(args),
|
|
407
|
+
};
|
|
408
|
+
const res = await postJson(api, profile, env, CREATE_EXPERIMENT_PATH, body, mintId);
|
|
409
|
+
experimentId = extractEntityId(res.json);
|
|
410
|
+
if (!experimentId) {
|
|
411
|
+
throw new errors_1.EngineBacktestError({
|
|
412
|
+
type: "runtime",
|
|
413
|
+
subtype: "create_experiment_no_id",
|
|
414
|
+
message: "experiments.create response did not include an id",
|
|
415
|
+
details: res.json,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!experimentId) {
|
|
420
|
+
throw new errors_1.EngineBacktestError({
|
|
421
|
+
type: "usage",
|
|
422
|
+
subtype: "missing_experiment",
|
|
423
|
+
message: "Provide --experiment <uuid> or --create-experiment --name <name>",
|
|
424
|
+
status: 400,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
let persistedRunId;
|
|
428
|
+
if (persist) {
|
|
429
|
+
const body = (0, persist_1.buildCreateRunRequest)({
|
|
430
|
+
clientRunId: mintId(),
|
|
431
|
+
scenario,
|
|
432
|
+
metrics: result.metrics,
|
|
433
|
+
exchangeId: exchange.id,
|
|
434
|
+
dataQualityMode: args.dataQualityMode,
|
|
435
|
+
engineVersion,
|
|
436
|
+
});
|
|
437
|
+
const res = await postJson(api, profile, env, runsPath(experimentId), body, mintId);
|
|
438
|
+
persistedRunId = extractEntityId(res.json);
|
|
439
|
+
if (!persistedRunId) {
|
|
440
|
+
throw new errors_1.EngineBacktestError({
|
|
441
|
+
type: "runtime",
|
|
442
|
+
subtype: "create_run_no_id",
|
|
443
|
+
message: "runs.create response did not include an id",
|
|
444
|
+
details: res.json,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
metrics: result.metrics,
|
|
450
|
+
engineVersion,
|
|
451
|
+
experimentId,
|
|
452
|
+
runId: persistedRunId,
|
|
453
|
+
experimentUrl: (0, persist_1.experimentPageUrl)(profile.name, experimentId),
|
|
454
|
+
persisted: Boolean(persistedRunId),
|
|
455
|
+
coverageWarnings: tapeResult.coverageWarnings ?? [],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
client.terminate();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function engineBacktestHelpData() {
|
|
463
|
+
return {
|
|
464
|
+
name: "engine-backtest",
|
|
465
|
+
usage: [
|
|
466
|
+
...parse_args_1.ENGINE_BACKTEST_RUN_USAGE,
|
|
467
|
+
"Catalog CRUD (underscore domain): alphafox engine_backtest experiments list|create|...",
|
|
468
|
+
],
|
|
469
|
+
notes: [
|
|
470
|
+
"Local WASM run is hyphenated engine-backtest so it does not steal typed catalog engine_backtest.*",
|
|
471
|
+
"runs.create is write (not high-risk-write); --yes is not required",
|
|
472
|
+
"Do not pass --token; use alphafox auth login",
|
|
473
|
+
"--replay-timeframe defaults to 1m (min 1m). Indicator series still download their native plan timeframes.",
|
|
474
|
+
],
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
async function cmdEngineBacktest(args, flags, env = process.env, deps = {}) {
|
|
478
|
+
const [sub, ...rest] = args;
|
|
479
|
+
if (!sub ||
|
|
480
|
+
sub === "help" ||
|
|
481
|
+
sub === "--help" ||
|
|
482
|
+
sub === "-h") {
|
|
483
|
+
(0, envelope_1.writeSuccess)(engineBacktestHelpData(), {
|
|
484
|
+
format: flags.format,
|
|
485
|
+
jq: flags.jq,
|
|
486
|
+
});
|
|
487
|
+
return 0;
|
|
488
|
+
}
|
|
489
|
+
if (sub !== "run") {
|
|
490
|
+
(0, envelope_1.writeError)({
|
|
491
|
+
type: "usage",
|
|
492
|
+
subtype: "unknown_subcommand",
|
|
493
|
+
message: `Unknown engine-backtest subcommand "${sub}"`,
|
|
494
|
+
hint: "Local WASM: alphafox engine-backtest run. Catalog CRUD: alphafox engine_backtest experiments list",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
let parsed;
|
|
498
|
+
try {
|
|
499
|
+
parsed = (0, parse_args_1.parseEngineBacktestRunArgs)(rest);
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
if ((0, errors_1.isEngineBacktestError)(err)) {
|
|
503
|
+
(0, envelope_1.writeError)({
|
|
504
|
+
type: err.type,
|
|
505
|
+
subtype: err.subtype,
|
|
506
|
+
message: err.message,
|
|
507
|
+
hint: err.hint,
|
|
508
|
+
status: err.status,
|
|
509
|
+
details: err.details,
|
|
510
|
+
}, { exitCode: err.status === 401 || err.status === 403 ? 77 : undefined });
|
|
511
|
+
}
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
if (parsed.help) {
|
|
515
|
+
(0, envelope_1.writeSuccess)(engineBacktestHelpData(), {
|
|
516
|
+
format: flags.format,
|
|
517
|
+
jq: flags.jq,
|
|
518
|
+
});
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const result = await executeEngineBacktestRun(parsed, flags, env, deps);
|
|
523
|
+
(0, envelope_1.writeSuccess)(result, { format: flags.format, jq: flags.jq });
|
|
524
|
+
return 0;
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
if ((0, errors_1.isEngineBacktestError)(err)) {
|
|
528
|
+
(0, envelope_1.writeError)({
|
|
529
|
+
type: err.type,
|
|
530
|
+
subtype: err.subtype,
|
|
531
|
+
message: err.message,
|
|
532
|
+
hint: err.hint,
|
|
533
|
+
status: err.status,
|
|
534
|
+
code: err.code,
|
|
535
|
+
details: err.details,
|
|
536
|
+
}, { exitCode: err.status === 401 || err.status === 403 ? 77 : undefined });
|
|
537
|
+
}
|
|
538
|
+
throw err;
|
|
539
|
+
}
|
|
540
|
+
}
|