@alphafox/cli 0.1.5 → 0.3.2
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/browser-login.d.ts +30 -0
- package/dist/auth/browser-login.js +193 -0
- package/dist/auth/loopback-callback.d.ts +32 -0
- package/dist/auth/loopback-callback.js +175 -0
- package/dist/auth/open-browser.d.ts +15 -0
- package/dist/auth/open-browser.js +54 -0
- package/dist/auth/refresh.d.ts +27 -2
- package/dist/auth/refresh.js +52 -15
- package/dist/catalog/allowlist.d.ts +20 -4
- package/dist/catalog/allowlist.js +126 -25
- package/dist/catalog/command-tree.d.ts +35 -0
- package/dist/catalog/command-tree.js +119 -0
- package/dist/catalog/compatibility.d.ts +23 -0
- package/dist/catalog/compatibility.js +58 -0
- package/dist/catalog/generated/registry.json +6137 -0
- package/dist/catalog/generated/schemas.json +31036 -0
- package/dist/catalog/operations.d.ts +76 -3
- package/dist/catalog/operations.js +87 -213
- 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 +283 -162
- package/dist/config/profiles.js +3 -3
- 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/envelope.d.ts +3 -0
- package/dist/envelope.js +43 -3
- package/dist/http/client.js +18 -15
- package/dist/index.d.ts +16 -5
- package/dist/index.js +47 -1
- package/dist/install/exec.d.ts +13 -0
- package/dist/install/exec.js +73 -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 +332 -0
- package/dist/keychain/linux-secret-service.d.ts +11 -0
- package/dist/keychain/linux-secret-service.js +93 -0
- package/dist/keychain/store.d.ts +20 -1
- package/dist/keychain/store.js +94 -6
- package/dist/keychain/windows-credential.d.ts +13 -0
- package/dist/keychain/windows-credential.js +176 -0
- package/dist/safety/confirmation.d.ts +10 -3
- package/dist/safety/confirmation.js +27 -4
- package/dist/version.d.ts +2 -2
- package/dist/version.js +3 -2
- package/docs/.nojekyll +0 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +156 -0
- package/docs/agents/triage-labels.md +18 -0
- package/docs/alphafox-cli-installation-guide.md +105 -0
- package/docs/e2e-staging.md +76 -6
- 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 +107 -26
- package/package.json +10 -3
- package/skills/account/SKILL.md +8 -6
- package/skills/admin/SKILL.md +9 -4
- package/skills/alphafox-shared/SKILL.md +44 -14
- package/skills/auth/SKILL.md +21 -6
- package/skills/engine-backtest/SKILL.md +71 -0
- package/skills/exchange/SKILL.md +10 -3
- package/skills/market/SKILL.md +9 -4
- package/skills/notification/SKILL.md +8 -3
- package/skills/strategy/SKILL.md +23 -9
- package/skills/trading/SKILL.md +18 -5
- 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
package/dist/config/profiles.js
CHANGED
|
@@ -16,7 +16,7 @@ const DEFAULTS = {
|
|
|
16
16
|
issuer: "https://alphafox.app/api/auth",
|
|
17
17
|
audience: "https://alphafox.app/api/v1",
|
|
18
18
|
clientId: "alphafox-cli-prod",
|
|
19
|
-
contractVersion: "2026-08-
|
|
19
|
+
contractVersion: "2026-08-13",
|
|
20
20
|
},
|
|
21
21
|
staging: {
|
|
22
22
|
name: "staging",
|
|
@@ -24,7 +24,7 @@ const DEFAULTS = {
|
|
|
24
24
|
issuer: "https://staging.alphafox.app/api/auth",
|
|
25
25
|
audience: "https://staging.alphafox.app/api/v1",
|
|
26
26
|
clientId: "alphafox-cli-staging",
|
|
27
|
-
contractVersion: "2026-08-
|
|
27
|
+
contractVersion: "2026-08-13",
|
|
28
28
|
},
|
|
29
29
|
local: {
|
|
30
30
|
name: "local",
|
|
@@ -33,7 +33,7 @@ const DEFAULTS = {
|
|
|
33
33
|
audience: "http://127.0.0.1:3000/api/v1",
|
|
34
34
|
clientId: "alphafox-cli-local",
|
|
35
35
|
localOrigin: "http://127.0.0.1:3000",
|
|
36
|
-
contractVersion: "2026-08-
|
|
36
|
+
contractVersion: "2026-08-13",
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
function defaultConfigDir(env = process.env) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class EngineBacktestError extends Error {
|
|
2
|
+
readonly type: string;
|
|
3
|
+
readonly subtype?: string;
|
|
4
|
+
readonly status?: number;
|
|
5
|
+
readonly code?: string | number;
|
|
6
|
+
readonly hint?: string;
|
|
7
|
+
readonly details?: unknown;
|
|
8
|
+
constructor(input: {
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly type?: string;
|
|
11
|
+
readonly subtype?: string;
|
|
12
|
+
readonly status?: number;
|
|
13
|
+
readonly code?: string | number;
|
|
14
|
+
readonly hint?: string;
|
|
15
|
+
readonly details?: unknown;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export declare function isEngineBacktestError(value: unknown): value is EngineBacktestError;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EngineBacktestError = void 0;
|
|
4
|
+
exports.isEngineBacktestError = isEngineBacktestError;
|
|
5
|
+
class EngineBacktestError extends Error {
|
|
6
|
+
type;
|
|
7
|
+
subtype;
|
|
8
|
+
status;
|
|
9
|
+
code;
|
|
10
|
+
hint;
|
|
11
|
+
details;
|
|
12
|
+
constructor(input) {
|
|
13
|
+
super(input.message);
|
|
14
|
+
this.name = "EngineBacktestError";
|
|
15
|
+
this.type = input.type ?? "runtime";
|
|
16
|
+
this.subtype = input.subtype;
|
|
17
|
+
this.status = input.status;
|
|
18
|
+
this.code = input.code;
|
|
19
|
+
this.hint = input.hint;
|
|
20
|
+
this.details = input.details;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.EngineBacktestError = EngineBacktestError;
|
|
24
|
+
function isEngineBacktestError(value) {
|
|
25
|
+
return value instanceof EngineBacktestError;
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const BACKTEST_RUNTIME_PROTOCOL = 1;
|
|
2
|
+
export declare const DEFAULT_BACKTEST_WASM_MANIFEST_URL = "https://zwggllrrna54e2d6.public.blob.vercel-storage.com/engine-backtest/latest.json";
|
|
3
|
+
export declare const BLOB_RUNTIME_FILES: {
|
|
4
|
+
readonly wasm: "tradingfox-backtest.wasm";
|
|
5
|
+
readonly wasmExec: "wasm_exec.js";
|
|
6
|
+
readonly worker: "worker.mjs";
|
|
7
|
+
readonly client: "index.mjs";
|
|
8
|
+
readonly node: "node.mjs";
|
|
9
|
+
readonly nodeWorker: "worker-node.mjs";
|
|
10
|
+
readonly nodeWorkerPath: "worker-node-path.mjs";
|
|
11
|
+
};
|
|
12
|
+
export type BlobRuntimeFileKey = keyof typeof BLOB_RUNTIME_FILES;
|
|
13
|
+
export interface EngineBacktestBlobManifest {
|
|
14
|
+
readonly version: string;
|
|
15
|
+
readonly hash: string;
|
|
16
|
+
readonly protocol: number;
|
|
17
|
+
readonly engineSha?: string;
|
|
18
|
+
readonly packageVersion?: string;
|
|
19
|
+
readonly wasm: string;
|
|
20
|
+
readonly wasmExec: string;
|
|
21
|
+
readonly worker: string;
|
|
22
|
+
readonly client: string;
|
|
23
|
+
readonly node: string;
|
|
24
|
+
readonly nodeWorker: string;
|
|
25
|
+
readonly nodeWorkerPath: string;
|
|
26
|
+
}
|
|
27
|
+
export interface FetchRuntimeHooks {
|
|
28
|
+
readonly fetch?: typeof fetch;
|
|
29
|
+
readonly cacheDir?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function resolveBacktestWasmManifestUrl(env?: NodeJS.ProcessEnv): string;
|
|
32
|
+
export declare function resolveRuntimeCacheDir(hash: string, env?: NodeJS.ProcessEnv): string;
|
|
33
|
+
export declare function parseEngineBacktestBlobManifest(value: unknown): EngineBacktestBlobManifest;
|
|
34
|
+
export declare function ensureBlobRuntime(env?: NodeJS.ProcessEnv, hooks?: FetchRuntimeHooks): Promise<{
|
|
35
|
+
readonly manifest: EngineBacktestBlobManifest;
|
|
36
|
+
readonly directory: string;
|
|
37
|
+
readonly nodeEntry: string;
|
|
38
|
+
}>;
|
|
@@ -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;
|