@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.
Files changed (73) hide show
  1. package/README.md +42 -4
  2. package/dist/auth/loopback-callback.js +6 -6
  3. package/dist/catalog/command-tree.d.ts +1 -0
  4. package/dist/catalog/command-tree.js +5 -3
  5. package/dist/catalog/validate-body.d.ts +22 -0
  6. package/dist/catalog/validate-body.js +98 -0
  7. package/dist/commands/request-body.d.ts +21 -0
  8. package/dist/commands/request-body.js +92 -0
  9. package/dist/commands/run.js +113 -10
  10. package/dist/engine-backtest/errors.d.ts +18 -0
  11. package/dist/engine-backtest/errors.js +26 -0
  12. package/dist/engine-backtest/fetch-runtime.d.ts +38 -0
  13. package/dist/engine-backtest/fetch-runtime.js +170 -0
  14. package/dist/engine-backtest/native-import.d.ts +1 -0
  15. package/dist/engine-backtest/native-import.js +10 -0
  16. package/dist/engine-backtest/parse-args.d.ts +6 -0
  17. package/dist/engine-backtest/parse-args.js +281 -0
  18. package/dist/engine-backtest/persist.d.ts +32 -0
  19. package/dist/engine-backtest/persist.js +94 -0
  20. package/dist/engine-backtest/replay-timeframe.d.ts +16 -0
  21. package/dist/engine-backtest/replay-timeframe.js +48 -0
  22. package/dist/engine-backtest/resolve-packages.d.ts +27 -0
  23. package/dist/engine-backtest/resolve-packages.js +288 -0
  24. package/dist/engine-backtest/run-command.d.ts +38 -0
  25. package/dist/engine-backtest/run-command.js +540 -0
  26. package/dist/engine-backtest/types.d.ts +249 -0
  27. package/dist/engine-backtest/types.js +2 -0
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.js +16 -1
  30. package/dist/install/exec.d.ts +13 -0
  31. package/dist/install/exec.js +77 -0
  32. package/dist/install/package-root.d.ts +4 -0
  33. package/dist/install/package-root.js +59 -0
  34. package/dist/install/types.d.ts +69 -0
  35. package/dist/install/types.js +26 -0
  36. package/dist/install/wizard.d.ts +21 -0
  37. package/dist/install/wizard.js +330 -0
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/docs/.nojekyll +0 -0
  41. package/docs/alphafox-cli-installation-guide.md +105 -0
  42. package/docs/favicon.svg +4 -0
  43. package/docs/index.html +748 -0
  44. package/docs/logo/alphafox-mark.svg +4 -0
  45. package/docs/logo/alphafox-wordmark-black-en.svg +17 -0
  46. package/docs/logo/alphafox-wordmark-white-en.svg +16 -0
  47. package/docs/release-supply-chain.md +10 -2
  48. package/package.json +7 -2
  49. package/skills/account/SKILL.md +2 -2
  50. package/skills/admin/SKILL.md +2 -2
  51. package/skills/alphafox-shared/SKILL.md +22 -1
  52. package/skills/auth/SKILL.md +1 -1
  53. package/skills/engine-backtest/SKILL.md +71 -0
  54. package/skills/exchange/SKILL.md +2 -2
  55. package/skills/market/SKILL.md +1 -1
  56. package/skills/notification/SKILL.md +2 -2
  57. package/skills/strategy/SKILL.md +10 -2
  58. package/skills/trading/SKILL.md +6 -3
  59. package/vendor/backtest-runner/NOTICE.md +1 -0
  60. package/vendor/backtest-runner/README.md +97 -0
  61. package/vendor/backtest-runner/index.d.ts +423 -0
  62. package/vendor/backtest-runner/index.mjs +59 -0
  63. package/vendor/backtest-runner/lib/abortable.mjs +23 -0
  64. package/vendor/backtest-runner/lib/cache.mjs +159 -0
  65. package/vendor/backtest-runner/lib/coverage.mjs +238 -0
  66. package/vendor/backtest-runner/lib/encode.mjs +28 -0
  67. package/vendor/backtest-runner/lib/exchanges.mjs +143 -0
  68. package/vendor/backtest-runner/lib/proxy.mjs +78 -0
  69. package/vendor/backtest-runner/lib/scenario.mjs +66 -0
  70. package/vendor/backtest-runner/lib/series.mjs +370 -0
  71. package/vendor/backtest-runner/lib/tape-loader.mjs +614 -0
  72. package/vendor/backtest-runner/lib/timeframes.mjs +55 -0
  73. package/vendor/backtest-runner/package.json +13 -0
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BLOB_RUNTIME_FILES = exports.DEFAULT_BACKTEST_WASM_MANIFEST_URL = exports.BACKTEST_RUNTIME_PROTOCOL = void 0;
4
+ exports.resolveBacktestWasmManifestUrl = resolveBacktestWasmManifestUrl;
5
+ exports.resolveRuntimeCacheDir = resolveRuntimeCacheDir;
6
+ exports.parseEngineBacktestBlobManifest = parseEngineBacktestBlobManifest;
7
+ exports.ensureBlobRuntime = ensureBlobRuntime;
8
+ const node_fs_1 = require("node:fs");
9
+ const promises_1 = require("node:fs/promises");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = require("node:path");
12
+ const promises_2 = require("node:stream/promises");
13
+ const node_stream_1 = require("node:stream");
14
+ const errors_1 = require("./errors");
15
+ exports.BACKTEST_RUNTIME_PROTOCOL = 1;
16
+ exports.DEFAULT_BACKTEST_WASM_MANIFEST_URL = "https://zwggllrrna54e2d6.public.blob.vercel-storage.com/engine-backtest/latest.json";
17
+ exports.BLOB_RUNTIME_FILES = {
18
+ wasm: "tradingfox-backtest.wasm",
19
+ wasmExec: "wasm_exec.js",
20
+ worker: "worker.mjs",
21
+ client: "index.mjs",
22
+ node: "node.mjs",
23
+ nodeWorker: "worker-node.mjs",
24
+ nodeWorkerPath: "worker-node-path.mjs",
25
+ };
26
+ function resolveBacktestWasmManifestUrl(env = process.env) {
27
+ return (env.ALPHAFOX_BACKTEST_WASM_MANIFEST_URL?.trim() ||
28
+ exports.DEFAULT_BACKTEST_WASM_MANIFEST_URL);
29
+ }
30
+ function resolveRuntimeCacheDir(hash, env = process.env) {
31
+ const override = env.ALPHAFOX_BACKTEST_RUNTIME_CACHE_DIR?.trim();
32
+ if (override) {
33
+ return (0, node_path_1.join)(override, hash);
34
+ }
35
+ const xdg = env.XDG_CACHE_HOME?.trim();
36
+ return (0, node_path_1.join)(xdg || (0, node_path_1.join)((0, node_os_1.homedir)(), ".cache"), "alphafox", "engine-backtest", hash);
37
+ }
38
+ function parseEngineBacktestBlobManifest(value) {
39
+ if (!value || typeof value !== "object") {
40
+ throw new errors_1.EngineBacktestError({
41
+ type: "runtime",
42
+ subtype: "runtime_manifest_invalid",
43
+ message: "Backtest runtime manifest is not an object.",
44
+ });
45
+ }
46
+ const record = value;
47
+ if (record.protocol !== exports.BACKTEST_RUNTIME_PROTOCOL) {
48
+ throw new errors_1.EngineBacktestError({
49
+ type: "runtime",
50
+ subtype: "runtime_protocol_mismatch",
51
+ message: `Backtest runtime protocol is incompatible (got ${String(record.protocol)}, expected ${exports.BACKTEST_RUNTIME_PROTOCOL}).`,
52
+ });
53
+ }
54
+ return {
55
+ version: readRequiredString(record.version, "version"),
56
+ hash: readRequiredString(record.hash, "hash"),
57
+ protocol: exports.BACKTEST_RUNTIME_PROTOCOL,
58
+ engineSha: readOptionalString(record.engineSha),
59
+ packageVersion: readOptionalString(record.packageVersion),
60
+ wasm: readRequiredHttps(record.wasm, "wasm"),
61
+ wasmExec: readRequiredHttps(record.wasmExec, "wasmExec"),
62
+ worker: readRequiredHttps(record.worker, "worker"),
63
+ client: readRequiredHttps(record.client, "client"),
64
+ node: readRequiredHttps(record.node, "node"),
65
+ nodeWorker: readRequiredHttps(record.nodeWorker, "nodeWorker"),
66
+ nodeWorkerPath: readRequiredHttps(record.nodeWorkerPath, "nodeWorkerPath"),
67
+ };
68
+ }
69
+ async function ensureBlobRuntime(env = process.env, hooks) {
70
+ const fetchImpl = hooks?.fetch ?? fetch;
71
+ const manifestUrl = resolveBacktestWasmManifestUrl(env);
72
+ let response;
73
+ try {
74
+ response = await fetchImpl(manifestUrl, { cache: "no-cache" });
75
+ }
76
+ catch (error) {
77
+ throw new errors_1.EngineBacktestError({
78
+ type: "runtime",
79
+ subtype: "runtime_manifest_unavailable",
80
+ message: `Cannot load backtest runtime manifest: ${error instanceof Error ? error.message : String(error)}`,
81
+ hint: "Check network access to Vercel Blob, or set ALPHAFOX_USE_LOCAL_BACKTEST=1 with a local Engine build.",
82
+ details: { manifestUrl },
83
+ });
84
+ }
85
+ if (!response.ok) {
86
+ throw new errors_1.EngineBacktestError({
87
+ type: "runtime",
88
+ subtype: "runtime_manifest_unavailable",
89
+ message: `Backtest runtime manifest unavailable (HTTP ${response.status}).`,
90
+ hint: "Check network access to Vercel Blob, or set ALPHAFOX_USE_LOCAL_BACKTEST=1 with a local Engine build.",
91
+ details: { manifestUrl, status: response.status },
92
+ });
93
+ }
94
+ const manifest = parseEngineBacktestBlobManifest(await response.json());
95
+ const directory = hooks?.cacheDir ?? resolveRuntimeCacheDir(manifest.hash, env);
96
+ await (0, promises_1.mkdir)(directory, { recursive: true });
97
+ for (const key of Object.keys(exports.BLOB_RUNTIME_FILES)) {
98
+ const fileName = exports.BLOB_RUNTIME_FILES[key];
99
+ const target = (0, node_path_1.join)(directory, fileName);
100
+ if (await fileExists(target)) {
101
+ continue;
102
+ }
103
+ await downloadFile(fetchImpl, manifest[key], target);
104
+ }
105
+ return {
106
+ manifest,
107
+ directory,
108
+ nodeEntry: (0, node_path_1.join)(directory, exports.BLOB_RUNTIME_FILES.node),
109
+ };
110
+ }
111
+ async function downloadFile(fetchImpl, url, target) {
112
+ let response;
113
+ try {
114
+ response = await fetchImpl(url);
115
+ }
116
+ catch (error) {
117
+ throw new errors_1.EngineBacktestError({
118
+ type: "runtime",
119
+ subtype: "runtime_download_failed",
120
+ message: `Cannot download backtest runtime ${url}: ${error instanceof Error ? error.message : String(error)}`,
121
+ });
122
+ }
123
+ if (!response.ok || !response.body) {
124
+ throw new errors_1.EngineBacktestError({
125
+ type: "runtime",
126
+ subtype: "runtime_download_failed",
127
+ message: `Backtest runtime file unavailable (${url} HTTP ${response.status}).`,
128
+ });
129
+ }
130
+ const tmp = `${target}.tmp`;
131
+ await (0, promises_2.pipeline)(node_stream_1.Readable.fromWeb(response.body), (0, node_fs_1.createWriteStream)(tmp));
132
+ await (0, promises_1.rename)(tmp, target);
133
+ }
134
+ async function fileExists(path) {
135
+ try {
136
+ const info = await (0, promises_1.stat)(path);
137
+ return info.isFile() && info.size > 0;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
143
+ function readRequiredHttps(value, field) {
144
+ const text = readRequiredString(value, field);
145
+ if (!text.startsWith("https://")) {
146
+ throw new errors_1.EngineBacktestError({
147
+ type: "runtime",
148
+ subtype: "runtime_manifest_invalid",
149
+ message: `Backtest runtime manifest ${field} must be an https URL.`,
150
+ });
151
+ }
152
+ return text;
153
+ }
154
+ function readRequiredString(value, field) {
155
+ if (typeof value !== "string" || value.trim() === "") {
156
+ throw new errors_1.EngineBacktestError({
157
+ type: "runtime",
158
+ subtype: "runtime_manifest_invalid",
159
+ message: `Backtest runtime manifest is missing ${field}.`,
160
+ });
161
+ }
162
+ return value.trim();
163
+ }
164
+ function readOptionalString(value) {
165
+ if (typeof value !== "string") {
166
+ return undefined;
167
+ }
168
+ const trimmed = value.trim();
169
+ return trimmed === "" ? undefined : trimmed;
170
+ }
@@ -0,0 +1 @@
1
+ export declare function importNativeModule(specifier: string): Promise<unknown>;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.importNativeModule = importNativeModule;
4
+ // Keep import() behind a runtime compiler boundary. TypeScript rewrites a
5
+ // directly-authored import() to require() when emitting this project as CJS,
6
+ // and require() cannot load the runner/wasm .mjs entry points.
7
+ const nativeImport = new Function("specifier", "return import(specifier);");
8
+ function importNativeModule(specifier) {
9
+ return nativeImport(specifier);
10
+ }
@@ -0,0 +1,6 @@
1
+ import type { EngineBacktestRunArgs, ExecutionModel, InclusiveUtcDateRange } from "./types";
2
+ export declare const ENGINE_BACKTEST_RUN_USAGE: string[];
3
+ export declare function parseInclusiveUtcDateRange(rangeStart: string, rangeEnd: string): InclusiveUtcDateRange;
4
+ export declare function parseRangeFlag(raw: string): InclusiveUtcDateRange;
5
+ export declare function parseExecutionModelJson(raw: string): Partial<ExecutionModel>;
6
+ export declare function parseEngineBacktestRunArgs(args: readonly string[]): EngineBacktestRunArgs;
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ENGINE_BACKTEST_RUN_USAGE = void 0;
4
+ exports.parseInclusiveUtcDateRange = parseInclusiveUtcDateRange;
5
+ exports.parseRangeFlag = parseRangeFlag;
6
+ exports.parseExecutionModelJson = parseExecutionModelJson;
7
+ exports.parseEngineBacktestRunArgs = parseEngineBacktestRunArgs;
8
+ const errors_1 = require("./errors");
9
+ const replay_timeframe_1 = require("./replay-timeframe");
10
+ const DAY_MS = 86_400_000;
11
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
12
+ const TIERS = new Set(["free", "pro", "pro_max"]);
13
+ const DATA_QUALITY = new Set(["strict", "basic"]);
14
+ const PRICE_PATHS = new Set(["ohlc_path_4", "close_only"]);
15
+ exports.ENGINE_BACKTEST_RUN_USAGE = [
16
+ "alphafox engine-backtest run --experiment <uuid> --definition <id> --config @file.json --exchange <id> --range YYYY-MM-DD..YYYY-MM-DD --initial-equity N [--replay-timeframe 1m]",
17
+ "alphafox engine-backtest run --create-experiment --name <name> --definition <id> --config @file.json --exchange <id> --from YYYY-MM-DD --to YYYY-MM-DD --initial-equity N",
18
+ ];
19
+ function usage(message, subtype = "invalid_args") {
20
+ throw new errors_1.EngineBacktestError({
21
+ type: "usage",
22
+ subtype,
23
+ message,
24
+ hint: exports.ENGINE_BACKTEST_RUN_USAGE[0],
25
+ status: 400,
26
+ });
27
+ }
28
+ function takeValue(args, i, flag) {
29
+ const next = args[i + 1];
30
+ if (next === undefined || next.startsWith("--")) {
31
+ usage(`Missing value for ${flag}`, "missing_flag_value");
32
+ }
33
+ return { value: next, next: i + 1 };
34
+ }
35
+ function parseInclusiveUtcDateRange(rangeStart, rangeEnd) {
36
+ if (!ISO_DATE.test(rangeStart) || !ISO_DATE.test(rangeEnd)) {
37
+ usage(`Dates must be YYYY-MM-DD (got ${rangeStart}..${rangeEnd})`, "invalid_range");
38
+ }
39
+ const fromMs = Date.parse(`${rangeStart}T00:00:00Z`);
40
+ const endStartMs = Date.parse(`${rangeEnd}T00:00:00Z`);
41
+ const toMs = endStartMs + DAY_MS;
42
+ if (!Number.isFinite(fromMs) ||
43
+ !Number.isFinite(endStartMs) ||
44
+ !Number.isFinite(toMs) ||
45
+ new Date(fromMs).toISOString().slice(0, 10) !== rangeStart ||
46
+ new Date(endStartMs).toISOString().slice(0, 10) !== rangeEnd ||
47
+ endStartMs < fromMs) {
48
+ usage(`Invalid inclusive UTC range ${rangeStart}..${rangeEnd}`, "invalid_range");
49
+ }
50
+ return { rangeStart, rangeEnd, fromMs, toMs };
51
+ }
52
+ function parseRangeFlag(raw) {
53
+ const parts = raw.split("..");
54
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
55
+ usage(`--range must be YYYY-MM-DD..YYYY-MM-DD (got ${raw})`, "invalid_range");
56
+ }
57
+ return parseInclusiveUtcDateRange(parts[0], parts[1]);
58
+ }
59
+ function parseExecutionModelJson(raw) {
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(raw);
63
+ }
64
+ catch {
65
+ usage("--execution-model must be JSON", "invalid_execution_model");
66
+ }
67
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
68
+ usage("--execution-model must be a JSON object", "invalid_execution_model");
69
+ }
70
+ const o = parsed;
71
+ const out = {};
72
+ if (o.pricePath !== undefined) {
73
+ if (typeof o.pricePath !== "string" || !PRICE_PATHS.has(o.pricePath)) {
74
+ usage(`--execution-model.pricePath must be ohlc_path_4|close_only`, "invalid_execution_model");
75
+ }
76
+ out.pricePath = o.pricePath;
77
+ }
78
+ for (const key of ["makerFeeRate", "takerFeeRate", "slippageRate"]) {
79
+ if (o[key] === undefined)
80
+ continue;
81
+ const n = Number(o[key]);
82
+ if (!Number.isFinite(n)) {
83
+ usage(`--execution-model.${key} must be a finite number`, "invalid_execution_model");
84
+ }
85
+ out[key] = n;
86
+ }
87
+ return out;
88
+ }
89
+ function parseEngineBacktestRunArgs(args) {
90
+ let experimentId;
91
+ let createExperiment = false;
92
+ let name;
93
+ let definitionId;
94
+ let definitionLabelZh;
95
+ let definitionLabelEn;
96
+ let configRaw;
97
+ let exchange;
98
+ let rangeRaw;
99
+ let from;
100
+ let to;
101
+ let initialEquity;
102
+ let tier;
103
+ let dataQualityMode = "strict";
104
+ let configSchemaVersion;
105
+ let executionModelOverride;
106
+ let persist = true;
107
+ let help = false;
108
+ let replayTimeframe = replay_timeframe_1.ENGINE_BACKTEST_DEFAULT_REPLAY_TIMEFRAME;
109
+ for (let i = 0; i < args.length; i += 1) {
110
+ const a = args[i];
111
+ if (a === "--help" || a === "-h") {
112
+ help = true;
113
+ continue;
114
+ }
115
+ if (a === "--create-experiment") {
116
+ createExperiment = true;
117
+ continue;
118
+ }
119
+ if (a === "--no-persist") {
120
+ persist = false;
121
+ continue;
122
+ }
123
+ if (!a.startsWith("--")) {
124
+ usage(`Unexpected argument: ${a}`, "unexpected_arg");
125
+ }
126
+ const eq = a.indexOf("=");
127
+ const flag = eq >= 0 ? a.slice(0, eq) : a;
128
+ const inline = eq >= 0 ? a.slice(eq + 1) : undefined;
129
+ const read = () => {
130
+ if (inline !== undefined)
131
+ return inline;
132
+ const taken = takeValue(args, i, flag);
133
+ i = taken.next;
134
+ return taken.value;
135
+ };
136
+ switch (flag) {
137
+ case "--experiment":
138
+ experimentId = read().trim();
139
+ break;
140
+ case "--name":
141
+ name = read();
142
+ break;
143
+ case "--definition":
144
+ definitionId = read().trim();
145
+ break;
146
+ case "--definition-label-zh":
147
+ definitionLabelZh = read();
148
+ break;
149
+ case "--definition-label-en":
150
+ definitionLabelEn = read();
151
+ break;
152
+ case "--config":
153
+ configRaw = read();
154
+ break;
155
+ case "--exchange":
156
+ exchange = read().trim();
157
+ break;
158
+ case "--range":
159
+ rangeRaw = read();
160
+ break;
161
+ case "--from":
162
+ from = read().trim();
163
+ break;
164
+ case "--to":
165
+ to = read().trim();
166
+ break;
167
+ case "--initial-equity": {
168
+ const raw = read();
169
+ const n = Number(raw);
170
+ if (!Number.isFinite(n) || n <= 0) {
171
+ usage("--initial-equity must be a positive number", "invalid_equity");
172
+ }
173
+ initialEquity = n;
174
+ break;
175
+ }
176
+ case "--tier": {
177
+ const raw = read().trim();
178
+ if (!TIERS.has(raw)) {
179
+ usage("--tier must be free|pro|pro_max", "invalid_tier");
180
+ }
181
+ tier = raw;
182
+ break;
183
+ }
184
+ case "--data-quality": {
185
+ const raw = read().trim();
186
+ if (!DATA_QUALITY.has(raw)) {
187
+ usage("--data-quality must be strict|basic", "invalid_data_quality");
188
+ }
189
+ dataQualityMode = raw;
190
+ break;
191
+ }
192
+ case "--config-schema-version": {
193
+ const n = Number(read());
194
+ if (!Number.isInteger(n) || n <= 0) {
195
+ usage("--config-schema-version must be a positive integer", "invalid_config_schema_version");
196
+ }
197
+ configSchemaVersion = n;
198
+ break;
199
+ }
200
+ case "--execution-model":
201
+ executionModelOverride = parseExecutionModelJson(read());
202
+ break;
203
+ case "--replay-timeframe": {
204
+ const raw = read().trim();
205
+ if (!(0, replay_timeframe_1.isEngineBacktestReplayTimeframe)(raw)) {
206
+ usage(`--replay-timeframe must be ${replay_timeframe_1.ENGINE_BACKTEST_REPLAY_TIMEFRAMES.join("|")} (got ${raw})`, "invalid_replay_timeframe");
207
+ }
208
+ replayTimeframe = raw;
209
+ break;
210
+ }
211
+ default:
212
+ usage(`Unknown flag: ${flag}`, "unknown_flag");
213
+ }
214
+ }
215
+ if (help) {
216
+ return {
217
+ help: true,
218
+ createExperiment: false,
219
+ tier,
220
+ dataQualityMode,
221
+ persist,
222
+ replayTimeframe,
223
+ };
224
+ }
225
+ if (!definitionId) {
226
+ usage("--definition is required", "missing_definition");
227
+ }
228
+ if (!configRaw) {
229
+ usage("--config is required (@file.json or JSON)", "missing_config");
230
+ }
231
+ if (!exchange) {
232
+ usage("--exchange is required", "missing_exchange");
233
+ }
234
+ if (initialEquity === undefined) {
235
+ usage("--initial-equity is required", "missing_equity");
236
+ }
237
+ let range;
238
+ if (rangeRaw) {
239
+ if (from || to) {
240
+ usage("Use either --range or --from/--to, not both", "range_conflict");
241
+ }
242
+ range = parseRangeFlag(rangeRaw);
243
+ }
244
+ else if (from || to) {
245
+ if (!from || !to) {
246
+ usage("--from and --to must be used together", "range_incomplete");
247
+ }
248
+ range = parseInclusiveUtcDateRange(from, to);
249
+ }
250
+ else {
251
+ usage("--range or --from/--to is required", "missing_range");
252
+ }
253
+ if (!experimentId && !createExperiment) {
254
+ usage("Provide --experiment <uuid> or --create-experiment --name <name>", "missing_experiment");
255
+ }
256
+ if (createExperiment && !name?.trim()) {
257
+ usage("--create-experiment requires --name", "missing_experiment_name");
258
+ }
259
+ if (experimentId && createExperiment) {
260
+ usage("Use either --experiment or --create-experiment, not both", "experiment_conflict");
261
+ }
262
+ return {
263
+ help: false,
264
+ experimentId,
265
+ createExperiment,
266
+ name: name?.trim(),
267
+ definitionId,
268
+ definitionLabelZh,
269
+ definitionLabelEn,
270
+ configRaw,
271
+ exchange,
272
+ range,
273
+ initialEquity,
274
+ tier,
275
+ dataQualityMode,
276
+ configSchemaVersion,
277
+ executionModelOverride,
278
+ persist,
279
+ replayTimeframe,
280
+ };
281
+ }
@@ -0,0 +1,32 @@
1
+ import type { ProfileName } from "../config/profiles";
2
+ import type { CreateRunRequestBody, DataQualityMode, EngineBacktestMetrics, EngineBacktestScenario, ExecutionModel } from "./types";
3
+ /** Aligned with `@alphafoxai/backtest-runner` DEFAULT_EXECUTION_MODEL. */
4
+ export declare const DEFAULT_EXECUTION_MODEL: ExecutionModel;
5
+ export declare const SNAPSHOT_SCHEMA_VERSION: 1;
6
+ /**
7
+ * Web persist uses tape.to as an exclusive end, then subtracts 1ms and
8
+ * slices to YYYY-MM-DD so an inclusive calendar last-day is recovered.
9
+ */
10
+ export declare function exclusiveTapeEndToRangeEnd(tapeTo: string): string;
11
+ export declare function uniqueSeriesField(series: readonly {
12
+ readonly symbol: string;
13
+ readonly timeframe: string;
14
+ }[], field: "symbol" | "timeframe"): string[];
15
+ /**
16
+ * Build POST .../runs body from a completed WASM run.
17
+ * Field set matches web `buildCreateRunRequestFromCompleted`.
18
+ */
19
+ export declare function buildCreateRunRequest(input: {
20
+ readonly clientRunId: string;
21
+ readonly scenario: EngineBacktestScenario;
22
+ readonly metrics: EngineBacktestMetrics;
23
+ readonly exchangeId: string;
24
+ readonly dataQualityMode: DataQualityMode;
25
+ readonly engineVersion: string;
26
+ }): CreateRunRequestBody;
27
+ /**
28
+ * Experiment detail URL. Locale prefix (`/zh`, `/en`) is optional — web
29
+ * `[locale]/dashboard/traders/backtest/[experimentId]` also serves the
30
+ * unprefixed `/dashboard/traders/backtest/{id}` path.
31
+ */
32
+ export declare function experimentPageUrl(profile: ProfileName, experimentId: string): string;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SNAPSHOT_SCHEMA_VERSION = exports.DEFAULT_EXECUTION_MODEL = void 0;
4
+ exports.exclusiveTapeEndToRangeEnd = exclusiveTapeEndToRangeEnd;
5
+ exports.uniqueSeriesField = uniqueSeriesField;
6
+ exports.buildCreateRunRequest = buildCreateRunRequest;
7
+ exports.experimentPageUrl = experimentPageUrl;
8
+ const errors_1 = require("./errors");
9
+ /** Aligned with `@alphafoxai/backtest-runner` DEFAULT_EXECUTION_MODEL. */
10
+ exports.DEFAULT_EXECUTION_MODEL = {
11
+ pricePath: "ohlc_path_4",
12
+ makerFeeRate: 0.0002,
13
+ takerFeeRate: 0.0005,
14
+ slippageRate: 0.0001,
15
+ };
16
+ exports.SNAPSHOT_SCHEMA_VERSION = 1;
17
+ /**
18
+ * Web persist uses tape.to as an exclusive end, then subtracts 1ms and
19
+ * slices to YYYY-MM-DD so an inclusive calendar last-day is recovered.
20
+ */
21
+ function exclusiveTapeEndToRangeEnd(tapeTo) {
22
+ const exclusiveEndMs = Date.parse(tapeTo);
23
+ if (!Number.isFinite(exclusiveEndMs)) {
24
+ throw new errors_1.EngineBacktestError({
25
+ type: "usage",
26
+ subtype: "invalid_tape_end",
27
+ message: `Cannot derive rangeEnd from tape.to "${tapeTo}"`,
28
+ status: 400,
29
+ });
30
+ }
31
+ return new Date(exclusiveEndMs - 1).toISOString().slice(0, 10);
32
+ }
33
+ function uniqueSeriesField(series, field) {
34
+ return [...new Set(series.map((item) => item[field]))];
35
+ }
36
+ /**
37
+ * Build POST .../runs body from a completed WASM run.
38
+ * Field set matches web `buildCreateRunRequestFromCompleted`.
39
+ */
40
+ function buildCreateRunRequest(input) {
41
+ const { scenario } = input;
42
+ const rangeStart = scenario.tape.from.slice(0, 10);
43
+ const rangeEnd = exclusiveTapeEndToRangeEnd(scenario.tape.to);
44
+ const symbols = uniqueSeriesField(scenario.tape.series, "symbol");
45
+ if (symbols.length === 0) {
46
+ throw new errors_1.EngineBacktestError({
47
+ type: "runtime",
48
+ subtype: "persist_no_symbols",
49
+ message: "Completed run has no symbols to persist.",
50
+ });
51
+ }
52
+ const timeframes = uniqueSeriesField(scenario.tape.series, "timeframe");
53
+ const engineVersion = input.engineVersion.trim() || "node-wasm";
54
+ const snapshot = {
55
+ snapshotSchemaVersion: exports.SNAPSHOT_SCHEMA_VERSION,
56
+ strategyDefinitionId: scenario.trader.strategyDefinitionId,
57
+ configSchemaVersion: scenario.trader.configSchemaVersion,
58
+ config: scenario.trader.config,
59
+ exchangeId: input.exchangeId.trim() || "binance",
60
+ rangeStart,
61
+ rangeEnd,
62
+ initialEquity: scenario.exchange.initialEquity,
63
+ subscriptionTier: scenario.trader.subscriptionTier,
64
+ dataQualityMode: input.dataQualityMode,
65
+ symbols,
66
+ timeframes: timeframes.length > 0 ? timeframes : ["1m"],
67
+ baseTimeframe: scenario.tape.baseTimeframe || "1m",
68
+ executionModel: scenario.executionModel ?? exports.DEFAULT_EXECUTION_MODEL,
69
+ positionSideDual: scenario.exchange.positionSideDual,
70
+ engineVersion,
71
+ };
72
+ return {
73
+ clientRunId: input.clientRunId,
74
+ snapshot,
75
+ metrics: input.metrics,
76
+ engineVersion,
77
+ configSchemaVersion: snapshot.configSchemaVersion,
78
+ };
79
+ }
80
+ /**
81
+ * Experiment detail URL. Locale prefix (`/zh`, `/en`) is optional — web
82
+ * `[locale]/dashboard/traders/backtest/[experimentId]` also serves the
83
+ * unprefixed `/dashboard/traders/backtest/{id}` path.
84
+ */
85
+ function experimentPageUrl(profile, experimentId) {
86
+ const path = `/dashboard/traders/backtest/${encodeURIComponent(experimentId)}`;
87
+ if (profile === "staging") {
88
+ return `https://staging.alphafox.app${path}`;
89
+ }
90
+ if (profile === "local") {
91
+ return `http://127.0.0.1:3000${path}`;
92
+ }
93
+ return `https://alphafox.app${path}`;
94
+ }
@@ -0,0 +1,16 @@
1
+ import type { EngineBacktestSeriesRequirement } from "./types";
2
+ export declare const ENGINE_BACKTEST_REPLAY_TIMEFRAMES: readonly ["1m", "3m", "5m", "15m", "30m", "1h", "4h"];
3
+ export type EngineBacktestReplayTimeframe = (typeof ENGINE_BACKTEST_REPLAY_TIMEFRAMES)[number];
4
+ export declare const ENGINE_BACKTEST_DEFAULT_REPLAY_TIMEFRAME = "1m";
5
+ export declare function isEngineBacktestReplayTimeframe(value: string): value is EngineBacktestReplayTimeframe;
6
+ export declare function parseReplayTimeframe(value: unknown): EngineBacktestReplayTimeframe;
7
+ export declare function mergeReplayTimeframeWithPlan(input: {
8
+ readonly replayTimeframe: string;
9
+ readonly planTimeframes: readonly string[];
10
+ readonly seriesRequirements: readonly EngineBacktestSeriesRequirement[];
11
+ readonly symbols: readonly string[];
12
+ }): {
13
+ readonly baseTimeframe: EngineBacktestReplayTimeframe;
14
+ readonly timeframes: readonly string[];
15
+ readonly seriesRequirements: readonly EngineBacktestSeriesRequirement[];
16
+ };
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ENGINE_BACKTEST_DEFAULT_REPLAY_TIMEFRAME = exports.ENGINE_BACKTEST_REPLAY_TIMEFRAMES = void 0;
4
+ exports.isEngineBacktestReplayTimeframe = isEngineBacktestReplayTimeframe;
5
+ exports.parseReplayTimeframe = parseReplayTimeframe;
6
+ exports.mergeReplayTimeframeWithPlan = mergeReplayTimeframeWithPlan;
7
+ exports.ENGINE_BACKTEST_REPLAY_TIMEFRAMES = [
8
+ "1m",
9
+ "3m",
10
+ "5m",
11
+ "15m",
12
+ "30m",
13
+ "1h",
14
+ "4h",
15
+ ];
16
+ exports.ENGINE_BACKTEST_DEFAULT_REPLAY_TIMEFRAME = "1m";
17
+ function isEngineBacktestReplayTimeframe(value) {
18
+ return exports.ENGINE_BACKTEST_REPLAY_TIMEFRAMES.includes(value);
19
+ }
20
+ function parseReplayTimeframe(value) {
21
+ return typeof value === "string" && isEngineBacktestReplayTimeframe(value)
22
+ ? value
23
+ : exports.ENGINE_BACKTEST_DEFAULT_REPLAY_TIMEFRAME;
24
+ }
25
+ function mergeReplayTimeframeWithPlan(input) {
26
+ const replay = parseReplayTimeframe(input.replayTimeframe);
27
+ const timeframes = input.planTimeframes.includes(replay)
28
+ ? [...input.planTimeframes]
29
+ : [replay, ...input.planTimeframes];
30
+ const requirements = [...input.seriesRequirements];
31
+ const seen = new Set(requirements.map((requirement) => `${requirement.symbol}\u0000${requirement.timeframe}`));
32
+ for (const symbol of input.symbols) {
33
+ const key = `${symbol}\u0000${replay}`;
34
+ if (seen.has(key))
35
+ continue;
36
+ requirements.push({
37
+ symbol,
38
+ timeframe: replay,
39
+ minWarmupCandles: 0,
40
+ });
41
+ seen.add(key);
42
+ }
43
+ return {
44
+ baseTimeframe: replay,
45
+ timeframes,
46
+ seriesRequirements: requirements,
47
+ };
48
+ }