@alphafox/cli 0.3.2 → 0.3.4
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 +4 -3
- package/dist/auth/loopback-callback.js +6 -6
- package/dist/catalog/allowlist.d.ts +1 -1
- package/dist/catalog/allowlist.js +1 -1
- package/dist/catalog/generated/registry.json +338 -49
- package/dist/catalog/generated/schemas.json +1880 -779
- package/dist/catalog/omit.d.ts +6 -0
- package/dist/catalog/omit.js +11 -0
- package/dist/catalog/operations.d.ts +1 -1
- package/dist/catalog/operations.js +5 -2
- package/dist/commands/run.js +10 -3
- package/dist/engine-backtest/load-config.d.ts +4 -0
- package/dist/engine-backtest/load-config.js +44 -0
- package/dist/engine-backtest/parse-args.d.ts +3 -1
- package/dist/engine-backtest/parse-args.js +87 -3
- package/dist/engine-backtest/parse-axes.d.ts +3 -0
- package/dist/engine-backtest/parse-axes.js +167 -0
- package/dist/engine-backtest/persist.d.ts +60 -1
- package/dist/engine-backtest/persist.js +196 -1
- package/dist/engine-backtest/return-curve.d.ts +27 -0
- package/dist/engine-backtest/return-curve.js +80 -0
- package/dist/engine-backtest/run-command.d.ts +1 -4
- package/dist/engine-backtest/run-command.js +61 -45
- package/dist/engine-backtest/sweep-command.d.ts +13 -0
- package/dist/engine-backtest/sweep-command.js +749 -0
- package/dist/engine-backtest/sweep-kernel/caps.d.ts +26 -0
- package/dist/engine-backtest/sweep-kernel/caps.js +48 -0
- package/dist/engine-backtest/sweep-kernel/index.d.ts +13 -0
- package/dist/engine-backtest/sweep-kernel/index.js +34 -0
- package/dist/engine-backtest/sweep-kernel/plan.d.ts +22 -0
- package/dist/engine-backtest/sweep-kernel/plan.js +320 -0
- package/dist/engine-backtest/sweep-kernel/results.d.ts +72 -0
- package/dist/engine-backtest/sweep-kernel/results.js +150 -0
- package/dist/engine-backtest/sweep-kernel/session.d.ts +55 -0
- package/dist/engine-backtest/sweep-kernel/session.js +56 -0
- package/dist/engine-backtest/sweep-kernel/types.d.ts +36 -0
- package/dist/engine-backtest/sweep-kernel/types.js +2 -0
- package/dist/engine-backtest/types.d.ts +130 -0
- package/dist/install/exec.js +5 -1
- package/dist/install/wizard.js +26 -29
- package/dist/resolve-symbols/catalog.d.ts +3 -0
- package/dist/resolve-symbols/catalog.js +50 -0
- package/dist/resolve-symbols/errors.d.ts +18 -0
- package/dist/resolve-symbols/errors.js +26 -0
- package/dist/resolve-symbols/exchanges.d.ts +8 -0
- package/dist/resolve-symbols/exchanges.js +53 -0
- package/dist/resolve-symbols/match.d.ts +6 -0
- package/dist/resolve-symbols/match.js +250 -0
- package/dist/resolve-symbols/parse-args.d.ts +5 -0
- package/dist/resolve-symbols/parse-args.js +106 -0
- package/dist/resolve-symbols/run-command.d.ts +21 -0
- package/dist/resolve-symbols/run-command.js +149 -0
- package/dist/resolve-symbols/types.d.ts +37 -0
- package/dist/resolve-symbols/types.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/alphafox-cli-installation-guide.md +2 -2
- package/package.json +1 -1
- package/skills/account/SKILL.md +1 -1
- package/skills/admin/SKILL.md +1 -1
- package/skills/alphafox/SKILL.md +38 -0
- package/skills/alphafox-shared/SKILL.md +5 -1
- package/skills/auth/SKILL.md +1 -1
- package/skills/engine-backtest/SKILL.md +48 -6
- package/skills/exchange/SKILL.md +1 -1
- package/skills/market/SKILL.md +27 -4
- package/skills/notification/SKILL.md +1 -1
- package/skills/strategy/SKILL.md +7 -20
- package/skills/trading/SKILL.md +3 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-attached `/api/v1/backtests` jobs are not a CLI surface.
|
|
3
|
+
* Local Engine WASM (`engine-backtest run`) and `engine_backtest.*` stay.
|
|
4
|
+
* Match `backtests` / `backtests.*` only — never `engine_backtest.*`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isOmittedCatalogOperation(operationId: string): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isOmittedCatalogOperation = isOmittedCatalogOperation;
|
|
4
|
+
/**
|
|
5
|
+
* Chat-attached `/api/v1/backtests` jobs are not a CLI surface.
|
|
6
|
+
* Local Engine WASM (`engine-backtest run`) and `engine_backtest.*` stay.
|
|
7
|
+
* Match `backtests` / `backtests.*` only — never `engine_backtest.*`.
|
|
8
|
+
*/
|
|
9
|
+
function isOmittedCatalogOperation(operationId) {
|
|
10
|
+
return operationId === "backtests" || operationId.startsWith("backtests.");
|
|
11
|
+
}
|
|
@@ -102,6 +102,6 @@ export declare function buildCapabilityManifest(): {
|
|
|
102
102
|
errorSchema: string;
|
|
103
103
|
}[];
|
|
104
104
|
};
|
|
105
|
-
/** Resolve path templates like /api/v1/
|
|
105
|
+
/** Resolve path templates like /api/v1/engine-backtest/experiments/{experimentId}. */
|
|
106
106
|
export declare function resolveOperationPath(template: string, params: Record<string, string>): string;
|
|
107
107
|
export declare function checkGeneratedCatalogCompatibility(cliVersion: string): import("./compatibility").CompatibilityResult;
|
|
@@ -19,11 +19,12 @@ exports.checkGeneratedCatalogCompatibility = checkGeneratedCatalogCompatibility;
|
|
|
19
19
|
const compatibility_1 = require("./compatibility");
|
|
20
20
|
const registry_json_1 = __importDefault(require("./generated/registry.json"));
|
|
21
21
|
const schemas_json_1 = __importDefault(require("./generated/schemas.json"));
|
|
22
|
+
const omit_1 = require("./omit");
|
|
22
23
|
const generated = registry_json_1.default;
|
|
23
24
|
const generatedSchemas = schemas_json_1.default;
|
|
24
25
|
exports.CATALOG_SOURCE = generated.source;
|
|
25
26
|
exports.CATALOG_VERSION = generated.compatibility.contractVersion;
|
|
26
|
-
exports.CATALOG_OPERATIONS = generated.operations;
|
|
27
|
+
exports.CATALOG_OPERATIONS = generated.operations.filter((op) => !(0, omit_1.isOmittedCatalogOperation)(op.operationId));
|
|
27
28
|
exports.COMPATIBILITY_RANGE = generated.compatibility;
|
|
28
29
|
function getCompatibilityRange() {
|
|
29
30
|
return exports.COMPATIBILITY_RANGE;
|
|
@@ -63,6 +64,8 @@ function pathTemplateMatchesOp(template, actual, catchAll) {
|
|
|
63
64
|
return true;
|
|
64
65
|
}
|
|
65
66
|
function getOperationSchemaDocument(operationId) {
|
|
67
|
+
if ((0, omit_1.isOmittedCatalogOperation)(operationId))
|
|
68
|
+
return undefined;
|
|
66
69
|
return generatedSchemas[operationId];
|
|
67
70
|
}
|
|
68
71
|
function extractPathParamNames(template) {
|
|
@@ -103,7 +106,7 @@ function buildCapabilityManifest() {
|
|
|
103
106
|
})),
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
|
-
/** Resolve path templates like /api/v1/
|
|
109
|
+
/** Resolve path templates like /api/v1/engine-backtest/experiments/{experimentId}. */
|
|
107
110
|
function resolveOperationPath(template, params) {
|
|
108
111
|
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, key) => {
|
|
109
112
|
const value = params[key];
|
package/dist/commands/run.js
CHANGED
|
@@ -14,6 +14,7 @@ const store_1 = require("../keychain/store");
|
|
|
14
14
|
const confirmation_1 = require("../safety/confirmation");
|
|
15
15
|
const version_1 = require("../version");
|
|
16
16
|
const run_command_1 = require("../engine-backtest/run-command");
|
|
17
|
+
const run_command_2 = require("../resolve-symbols/run-command");
|
|
17
18
|
const validate_body_1 = require("../catalog/validate-body");
|
|
18
19
|
const types_1 = require("../install/types");
|
|
19
20
|
const wizard_1 = require("../install/wizard");
|
|
@@ -94,6 +95,8 @@ async function runCli(argv, env = process.env) {
|
|
|
94
95
|
"alphafox catalog",
|
|
95
96
|
"alphafox api METHOD PATH [--body JSON|--config @file]",
|
|
96
97
|
"alphafox engine-backtest run --experiment <uuid> --definition <id> --config @file --exchange <id> --range FROM..TO --initial-equity N",
|
|
98
|
+
"alphafox engine-backtest sweep --experiment <uuid> --definition <id> --config @file --axes @file --exchange <id> --range FROM..TO --initial-equity N --no-persist",
|
|
99
|
+
"alphafox resolve-symbols <query...> [--exchange binance]",
|
|
97
100
|
"alphafox <domain> <resource> <action> [flags]",
|
|
98
101
|
],
|
|
99
102
|
}, { format: flags.format, jq: flags.jq });
|
|
@@ -126,15 +129,19 @@ async function runCli(argv, env = process.env) {
|
|
|
126
129
|
const sub = args[0];
|
|
127
130
|
if (!sub ||
|
|
128
131
|
sub === "run" ||
|
|
132
|
+
sub === "sweep" ||
|
|
129
133
|
sub === "help" ||
|
|
130
134
|
sub === "--help" ||
|
|
131
135
|
sub === "-h") {
|
|
132
136
|
return await (0, run_command_1.cmdEngineBacktest)(args, flags, env);
|
|
133
137
|
}
|
|
134
|
-
// Hyphen built-in
|
|
138
|
+
// Hyphen built-in owns `run` and `sweep`. Underscore/hyphen catalog CRUD
|
|
135
139
|
// (engine_backtest.experiments.*) still goes through the typed tree.
|
|
136
140
|
return await cmdTyped(cmd, args, flags, env);
|
|
137
141
|
}
|
|
142
|
+
case "resolve-symbols":
|
|
143
|
+
case "resolve-symbol":
|
|
144
|
+
return await (0, run_command_2.cmdResolveSymbols)(args, flags, env);
|
|
138
145
|
default:
|
|
139
146
|
return await cmdTyped(cmd, args, flags, env);
|
|
140
147
|
}
|
|
@@ -174,8 +181,8 @@ async function cmdInstall(args, flags, env) {
|
|
|
174
181
|
(0, envelope_1.writeError)({
|
|
175
182
|
type: "usage",
|
|
176
183
|
subtype: "unknown_install_flag",
|
|
177
|
-
message:
|
|
178
|
-
hint: "
|
|
184
|
+
message: `未知的 install 参数:${parsed.unknown.join(" ")}`,
|
|
185
|
+
hint: "用法:alphafox install [--no-auth] [--dry-run]",
|
|
179
186
|
});
|
|
180
187
|
}
|
|
181
188
|
if (parsed.help) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadConfigValue = loadConfigValue;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
function loadConfigValue(raw, options = {}) {
|
|
8
|
+
const cwd = options.cwd ?? process.cwd();
|
|
9
|
+
const read = options.readFile ?? ((p) => (0, node_fs_1.readFileSync)(p, "utf8"));
|
|
10
|
+
if (raw.startsWith("@")) {
|
|
11
|
+
const rel = raw.slice(1);
|
|
12
|
+
if (!rel) {
|
|
13
|
+
throw new errors_1.EngineBacktestError({
|
|
14
|
+
type: "usage",
|
|
15
|
+
subtype: "missing_config",
|
|
16
|
+
message: "--config @path is empty",
|
|
17
|
+
status: 400,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const abs = (0, node_path_1.isAbsolute)(rel) ? rel : (0, node_path_1.resolve)(cwd, rel);
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(read(abs));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
throw new errors_1.EngineBacktestError({
|
|
26
|
+
type: "usage",
|
|
27
|
+
subtype: "invalid_config",
|
|
28
|
+
message: `Cannot read --config file ${abs}: ${err instanceof Error ? err.message : String(err)}`,
|
|
29
|
+
status: 400,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new errors_1.EngineBacktestError({
|
|
38
|
+
type: "usage",
|
|
39
|
+
subtype: "invalid_config",
|
|
40
|
+
message: "--config must be JSON or @path-to.json",
|
|
41
|
+
status: 400,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { EngineBacktestRunArgs, ExecutionModel, InclusiveUtcDateRange } from "./types";
|
|
1
|
+
import type { EngineBacktestRunArgs, EngineBacktestSweepArgs, ExecutionModel, InclusiveUtcDateRange } from "./types";
|
|
2
2
|
export declare const ENGINE_BACKTEST_RUN_USAGE: string[];
|
|
3
|
+
export declare const ENGINE_BACKTEST_SWEEP_USAGE: string[];
|
|
3
4
|
export declare function parseInclusiveUtcDateRange(rangeStart: string, rangeEnd: string): InclusiveUtcDateRange;
|
|
4
5
|
export declare function parseRangeFlag(raw: string): InclusiveUtcDateRange;
|
|
5
6
|
export declare function parseExecutionModelJson(raw: string): Partial<ExecutionModel>;
|
|
6
7
|
export declare function parseEngineBacktestRunArgs(args: readonly string[]): EngineBacktestRunArgs;
|
|
8
|
+
export declare function parseEngineBacktestSweepArgs(args: readonly string[]): EngineBacktestSweepArgs;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ENGINE_BACKTEST_RUN_USAGE = void 0;
|
|
3
|
+
exports.ENGINE_BACKTEST_SWEEP_USAGE = exports.ENGINE_BACKTEST_RUN_USAGE = void 0;
|
|
4
4
|
exports.parseInclusiveUtcDateRange = parseInclusiveUtcDateRange;
|
|
5
5
|
exports.parseRangeFlag = parseRangeFlag;
|
|
6
6
|
exports.parseExecutionModelJson = parseExecutionModelJson;
|
|
7
7
|
exports.parseEngineBacktestRunArgs = parseEngineBacktestRunArgs;
|
|
8
|
+
exports.parseEngineBacktestSweepArgs = parseEngineBacktestSweepArgs;
|
|
8
9
|
const errors_1 = require("./errors");
|
|
9
10
|
const replay_timeframe_1 = require("./replay-timeframe");
|
|
11
|
+
const sweep_kernel_1 = require("./sweep-kernel");
|
|
10
12
|
const DAY_MS = 86_400_000;
|
|
11
13
|
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
14
|
const TIERS = new Set(["free", "pro", "pro_max"]);
|
|
@@ -16,12 +18,17 @@ exports.ENGINE_BACKTEST_RUN_USAGE = [
|
|
|
16
18
|
"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
19
|
"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
20
|
];
|
|
19
|
-
|
|
21
|
+
exports.ENGINE_BACKTEST_SWEEP_USAGE = [
|
|
22
|
+
"alphafox engine-backtest sweep --experiment <uuid> --definition <id> --config @file.json --axes @axes.json --exchange <id> --range YYYY-MM-DD..YYYY-MM-DD --initial-equity N [--no-persist] [--mode neighborhood|range] [--search-mode standard|fast] [--concurrency N]",
|
|
23
|
+
];
|
|
24
|
+
const SWEEP_MODES = new Set(["neighborhood", "range"]);
|
|
25
|
+
const SEARCH_MODES = new Set(["standard", "fast"]);
|
|
26
|
+
function usage(message, subtype = "invalid_args", hint = exports.ENGINE_BACKTEST_RUN_USAGE[0]) {
|
|
20
27
|
throw new errors_1.EngineBacktestError({
|
|
21
28
|
type: "usage",
|
|
22
29
|
subtype,
|
|
23
30
|
message,
|
|
24
|
-
hint
|
|
31
|
+
hint,
|
|
25
32
|
status: 400,
|
|
26
33
|
});
|
|
27
34
|
}
|
|
@@ -279,3 +286,80 @@ function parseEngineBacktestRunArgs(args) {
|
|
|
279
286
|
replayTimeframe,
|
|
280
287
|
};
|
|
281
288
|
}
|
|
289
|
+
function parseEngineBacktestSweepArgs(args) {
|
|
290
|
+
let axesRaw;
|
|
291
|
+
let mode = "neighborhood";
|
|
292
|
+
let searchMode = "standard";
|
|
293
|
+
let concurrency = sweep_kernel_1.DEFAULT_SWEEP_CONCURRENCY;
|
|
294
|
+
let help = false;
|
|
295
|
+
const rest = [];
|
|
296
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
297
|
+
const a = args[i];
|
|
298
|
+
if (a === "--help" || a === "-h") {
|
|
299
|
+
help = true;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const eq = a.indexOf("=");
|
|
303
|
+
const flag = eq >= 0 ? a.slice(0, eq) : a;
|
|
304
|
+
const inline = eq >= 0 ? a.slice(eq + 1) : undefined;
|
|
305
|
+
const read = () => {
|
|
306
|
+
if (inline !== undefined)
|
|
307
|
+
return inline;
|
|
308
|
+
const taken = takeValue(args, i, flag);
|
|
309
|
+
i = taken.next;
|
|
310
|
+
return taken.value;
|
|
311
|
+
};
|
|
312
|
+
if (flag === "--axes") {
|
|
313
|
+
axesRaw = read();
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (flag === "--mode") {
|
|
317
|
+
const raw = read().trim();
|
|
318
|
+
if (!SWEEP_MODES.has(raw)) {
|
|
319
|
+
usage("--mode must be neighborhood|range", "invalid_sweep_mode", exports.ENGINE_BACKTEST_SWEEP_USAGE[0]);
|
|
320
|
+
}
|
|
321
|
+
mode = raw;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (flag === "--search-mode") {
|
|
325
|
+
const raw = read().trim();
|
|
326
|
+
if (!SEARCH_MODES.has(raw)) {
|
|
327
|
+
usage("--search-mode must be standard|fast", "invalid_search_mode", exports.ENGINE_BACKTEST_SWEEP_USAGE[0]);
|
|
328
|
+
}
|
|
329
|
+
searchMode = raw;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (flag === "--concurrency") {
|
|
333
|
+
const n = Number(read());
|
|
334
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
335
|
+
usage("--concurrency must be a positive integer", "invalid_concurrency", exports.ENGINE_BACKTEST_SWEEP_USAGE[0]);
|
|
336
|
+
}
|
|
337
|
+
concurrency = n;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
rest.push(a);
|
|
341
|
+
}
|
|
342
|
+
if (help) {
|
|
343
|
+
return {
|
|
344
|
+
help: true,
|
|
345
|
+
createExperiment: false,
|
|
346
|
+
dataQualityMode: "strict",
|
|
347
|
+
persist: true,
|
|
348
|
+
replayTimeframe: replay_timeframe_1.ENGINE_BACKTEST_DEFAULT_REPLAY_TIMEFRAME,
|
|
349
|
+
axesRaw,
|
|
350
|
+
mode,
|
|
351
|
+
searchMode,
|
|
352
|
+
concurrency,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (!axesRaw) {
|
|
356
|
+
usage("--axes is required (@file.json or JSON)", "missing_axes", exports.ENGINE_BACKTEST_SWEEP_USAGE[0]);
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
...parseEngineBacktestRunArgs(rest),
|
|
360
|
+
axesRaw,
|
|
361
|
+
mode,
|
|
362
|
+
searchMode,
|
|
363
|
+
concurrency,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSweepAxesDocument = parseSweepAxesDocument;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const NEIGHBORHOOD_STEPS = 3;
|
|
6
|
+
function usage(message, subtype) {
|
|
7
|
+
throw new errors_1.EngineBacktestError({
|
|
8
|
+
type: "usage",
|
|
9
|
+
subtype,
|
|
10
|
+
message,
|
|
11
|
+
hint: "Each --axes entry needs an explicit config path and either values or min/max/step.",
|
|
12
|
+
status: 400,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function parseSweepAxesDocument(raw, config, mode) {
|
|
16
|
+
const list = listAxes(raw);
|
|
17
|
+
if (list.length === 0) {
|
|
18
|
+
usage("--axes must contain at least one axis", "invalid_axes");
|
|
19
|
+
}
|
|
20
|
+
return list.map((item, index) => parseSweepAxis(item, config, mode, index));
|
|
21
|
+
}
|
|
22
|
+
function listAxes(raw) {
|
|
23
|
+
if (Array.isArray(raw)) {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
27
|
+
const axes = raw.axes;
|
|
28
|
+
if (Array.isArray(axes)) {
|
|
29
|
+
return axes;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
usage("--axes must be a JSON array or {\"axes\": [...]}", "invalid_axes");
|
|
33
|
+
}
|
|
34
|
+
function parseSweepAxis(raw, config, mode, index) {
|
|
35
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
36
|
+
usage(`Axis ${index} must be an object`, "invalid_axis");
|
|
37
|
+
}
|
|
38
|
+
const row = raw;
|
|
39
|
+
const path = parseAxisPath(row.path, index);
|
|
40
|
+
const current = readNumericCurrent(config, path, index);
|
|
41
|
+
const values = parseOptionalNumberList(row.values, `Axis ${index} values`);
|
|
42
|
+
const min = optionalFiniteNumber(row.min, `Axis ${index} min`);
|
|
43
|
+
const max = optionalFiniteNumber(row.max, `Axis ${index} max`);
|
|
44
|
+
const step = optionalFiniteNumber(row.step, `Axis ${index} step`);
|
|
45
|
+
const hasWindow = min !== undefined && max !== undefined && step !== undefined;
|
|
46
|
+
if (!values && !hasWindow) {
|
|
47
|
+
usage(`Axis ${index} must set values or min/max/step`, "invalid_axis_window");
|
|
48
|
+
}
|
|
49
|
+
if (hasWindow && !(step > 0)) {
|
|
50
|
+
usage(`Axis ${index} step must be > 0`, "invalid_axis_window");
|
|
51
|
+
}
|
|
52
|
+
const isInteger = typeof row.isInteger === "boolean"
|
|
53
|
+
? row.isInteger
|
|
54
|
+
: inferInteger(current, values, step);
|
|
55
|
+
const axis = {
|
|
56
|
+
path,
|
|
57
|
+
current,
|
|
58
|
+
isInteger,
|
|
59
|
+
...(min === undefined ? {} : { min }),
|
|
60
|
+
...(max === undefined ? {} : { max }),
|
|
61
|
+
...(typeof row.minExclusive === "boolean"
|
|
62
|
+
? { minExclusive: row.minExclusive }
|
|
63
|
+
: {}),
|
|
64
|
+
...(typeof row.maxExclusive === "boolean"
|
|
65
|
+
? { maxExclusive: row.maxExclusive }
|
|
66
|
+
: {}),
|
|
67
|
+
...(values ? { values } : {}),
|
|
68
|
+
...(values || step === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: {
|
|
71
|
+
window: mode === "neighborhood"
|
|
72
|
+
? {
|
|
73
|
+
min: current - NEIGHBORHOOD_STEPS * step,
|
|
74
|
+
max: current + NEIGHBORHOOD_STEPS * step,
|
|
75
|
+
step,
|
|
76
|
+
}
|
|
77
|
+
: { min: min, max: max, step },
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
return axis;
|
|
81
|
+
}
|
|
82
|
+
function parseAxisPath(raw, index) {
|
|
83
|
+
if (typeof raw === "string") {
|
|
84
|
+
const path = raw.split(".").filter((segment) => segment.length > 0);
|
|
85
|
+
if (path.length === 0) {
|
|
86
|
+
usage(`Axis ${index} path must not be empty`, "invalid_axis_path");
|
|
87
|
+
}
|
|
88
|
+
return path;
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(raw)) {
|
|
91
|
+
if (raw.length === 0 ||
|
|
92
|
+
raw.some((segment) => typeof segment !== "string" || segment.length === 0)) {
|
|
93
|
+
usage(`Axis ${index} path must be a non-empty array of segments`, "invalid_axis_path");
|
|
94
|
+
}
|
|
95
|
+
return raw;
|
|
96
|
+
}
|
|
97
|
+
usage(`Axis ${index} must identify an explicit config path`, "invalid_axis_path");
|
|
98
|
+
}
|
|
99
|
+
function readNumericCurrent(config, path, index) {
|
|
100
|
+
const value = getValueAtPath(config, path);
|
|
101
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
102
|
+
usage(`Axis ${index} current value at ${path.join(".")} must be a finite number in --config`, "invalid_axis_current");
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function parseOptionalNumberList(raw, label) {
|
|
107
|
+
if (raw === undefined) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
111
|
+
usage(`${label} must be a non-empty number array`, "invalid_axis_values");
|
|
112
|
+
}
|
|
113
|
+
const values = raw.map((value, valueIndex) => {
|
|
114
|
+
const n = Number(value);
|
|
115
|
+
if (!Number.isFinite(n)) {
|
|
116
|
+
usage(`${label}[${valueIndex}] must be a finite number`, "invalid_axis_values");
|
|
117
|
+
}
|
|
118
|
+
return n;
|
|
119
|
+
});
|
|
120
|
+
return values;
|
|
121
|
+
}
|
|
122
|
+
function optionalFiniteNumber(raw, label) {
|
|
123
|
+
if (raw === undefined) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const n = Number(raw);
|
|
127
|
+
if (!Number.isFinite(n)) {
|
|
128
|
+
usage(`${label} must be a finite number`, "invalid_axis_window");
|
|
129
|
+
}
|
|
130
|
+
return n;
|
|
131
|
+
}
|
|
132
|
+
function inferInteger(current, values, step) {
|
|
133
|
+
const candidates = [
|
|
134
|
+
current,
|
|
135
|
+
...(values ?? []),
|
|
136
|
+
...(step === undefined ? [] : [step]),
|
|
137
|
+
];
|
|
138
|
+
return candidates.every((value) => Number.isInteger(value));
|
|
139
|
+
}
|
|
140
|
+
function isPlainRecord(value) {
|
|
141
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
142
|
+
}
|
|
143
|
+
function parseArrayIndex(segment) {
|
|
144
|
+
if (!/^\d+$/.test(segment)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const index = Number(segment);
|
|
148
|
+
return Number.isInteger(index) && index >= 0 ? index : null;
|
|
149
|
+
}
|
|
150
|
+
function getValueAtPath(root, path) {
|
|
151
|
+
let current = root;
|
|
152
|
+
for (const segment of path) {
|
|
153
|
+
if (Array.isArray(current)) {
|
|
154
|
+
const index = parseArrayIndex(segment);
|
|
155
|
+
if (index === null) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
current = current[index];
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!isPlainRecord(current)) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
current = current[segment];
|
|
165
|
+
}
|
|
166
|
+
return current;
|
|
167
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProfileName } from "../config/profiles";
|
|
2
|
-
import type { CreateRunRequestBody, DataQualityMode, EngineBacktestMetrics, EngineBacktestScenario, ExecutionModel } from "./types";
|
|
2
|
+
import type { CreateRunRequestBody, CreateSweepRequestBody, DataQualityMode, EngineBacktestMetrics, EngineBacktestScenario, EngineBacktestSweepPointRow, ExecutionModel, PersistedSnapshot, SweepMode, SweepSearchMode } from "./types";
|
|
3
3
|
/** Aligned with `@alphafoxai/backtest-runner` DEFAULT_EXECUTION_MODEL. */
|
|
4
4
|
export declare const DEFAULT_EXECUTION_MODEL: ExecutionModel;
|
|
5
5
|
export declare const SNAPSHOT_SCHEMA_VERSION: 1;
|
|
@@ -23,6 +23,7 @@ export declare function buildCreateRunRequest(input: {
|
|
|
23
23
|
readonly exchangeId: string;
|
|
24
24
|
readonly dataQualityMode: DataQualityMode;
|
|
25
25
|
readonly engineVersion: string;
|
|
26
|
+
readonly equityCurve?: unknown;
|
|
26
27
|
}): CreateRunRequestBody;
|
|
27
28
|
/**
|
|
28
29
|
* Experiment detail URL. Locale prefix (`/zh`, `/en`) is optional — web
|
|
@@ -30,3 +31,61 @@ export declare function buildCreateRunRequest(input: {
|
|
|
30
31
|
* unprefixed `/dashboard/traders/backtest/{id}` path.
|
|
31
32
|
*/
|
|
32
33
|
export declare function experimentPageUrl(profile: ProfileName, experimentId: string): string;
|
|
34
|
+
/** Web experiment workspace opens the Sweep tab with `?tab=sweep`. */
|
|
35
|
+
export declare function experimentSweepPageUrl(profile: ProfileName, experimentId: string): string;
|
|
36
|
+
export declare const ENGINE_BACKTEST_SWEEP_MAX_UTF8_BYTES: number;
|
|
37
|
+
export declare const ENGINE_BACKTEST_SWEEP_ERROR_MAX_UTF8_BYTES: number;
|
|
38
|
+
export declare function shouldPersistSweepCompletion(input: {
|
|
39
|
+
readonly completed: boolean;
|
|
40
|
+
readonly cancelled: boolean;
|
|
41
|
+
}): boolean;
|
|
42
|
+
export declare function mintClientSweepId(randomUuid?: () => string): string;
|
|
43
|
+
export declare function sweepsPath(experimentId: string): string;
|
|
44
|
+
export declare function buildSweepBaseSnapshot(input: {
|
|
45
|
+
readonly definitionId: string;
|
|
46
|
+
readonly configSchemaVersion: number;
|
|
47
|
+
readonly config: unknown;
|
|
48
|
+
readonly exchangeId: string;
|
|
49
|
+
readonly rangeStart: string;
|
|
50
|
+
readonly rangeEnd: string;
|
|
51
|
+
readonly initialEquity: number;
|
|
52
|
+
readonly subscriptionTier: PersistedSnapshot["subscriptionTier"];
|
|
53
|
+
readonly dataQualityMode: DataQualityMode;
|
|
54
|
+
readonly symbols: readonly string[];
|
|
55
|
+
readonly timeframes: readonly string[];
|
|
56
|
+
readonly baseTimeframe: string;
|
|
57
|
+
readonly executionModel: ExecutionModel;
|
|
58
|
+
readonly positionSideDual: boolean;
|
|
59
|
+
readonly engineVersion: string;
|
|
60
|
+
}): PersistedSnapshot;
|
|
61
|
+
/**
|
|
62
|
+
* Build POST .../sweeps body from a completed local search.
|
|
63
|
+
* Field set matches web `buildCreateSweepRequestFromCompleted`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildCreateSweepRequest(input: {
|
|
66
|
+
readonly clientSweepId: string;
|
|
67
|
+
readonly snapshot: PersistedSnapshot;
|
|
68
|
+
readonly axes: ReadonlyArray<{
|
|
69
|
+
readonly path: readonly string[];
|
|
70
|
+
readonly current: number;
|
|
71
|
+
readonly values: readonly number[];
|
|
72
|
+
}>;
|
|
73
|
+
readonly mode: SweepMode;
|
|
74
|
+
readonly searchMode: SweepSearchMode;
|
|
75
|
+
readonly concurrency: number;
|
|
76
|
+
readonly requestedCombinationCount: number;
|
|
77
|
+
readonly sampled: boolean;
|
|
78
|
+
readonly points: readonly EngineBacktestSweepPointRow[];
|
|
79
|
+
readonly successfulCount: number;
|
|
80
|
+
readonly failedCount: number;
|
|
81
|
+
readonly liquidatedCount: number;
|
|
82
|
+
readonly elapsedMs: number;
|
|
83
|
+
readonly best: {
|
|
84
|
+
readonly coordinate: {
|
|
85
|
+
readonly values: readonly number[];
|
|
86
|
+
};
|
|
87
|
+
readonly returnPct: number;
|
|
88
|
+
} | null;
|
|
89
|
+
readonly engineVersion: string;
|
|
90
|
+
}): CreateSweepRequestBody;
|
|
91
|
+
export declare function assertSweepCreateRequestSize(body: CreateSweepRequestBody): void;
|