@alphafox/cli 0.3.20 → 0.3.22
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/dist/commands/run.js +2 -2
- package/dist/engine-backtest/parse-args.js +10 -2
- package/dist/engine-backtest/run-command.js +4 -1
- package/dist/engine-backtest/sweep-command.js +2 -2
- package/dist/engine-backtest/types.d.ts +5 -1
- package/dist/skills-manifest.json +41 -41
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/account/SKILL.md +1 -1
- package/skills/admin/SKILL.md +1 -1
- package/skills/alphafox/SKILL.md +1 -1
- package/skills/alphafox-shared/SKILL.md +1 -1
- package/skills/auth/SKILL.md +1 -1
- package/skills/cache/SKILL.md +1 -1
- package/skills/engine-backtest/SKILL.md +3 -1
- package/skills/exchange/SKILL.md +1 -1
- package/skills/market/SKILL.md +1 -1
- package/skills/notification/SKILL.md +1 -1
- package/skills/strategy/SKILL.md +26 -6
- package/skills/trading/SKILL.md +3 -1
- package/vendor/backtest-runner/README.md +2 -0
- package/vendor/backtest-runner/index.d.ts +8 -2
- package/vendor/backtest-runner/index.mjs +5 -2
- package/vendor/backtest-runner/lib/series.mjs +5 -171
- package/vendor/backtest-runner/lib/tape-loader-concurrency.mjs +72 -0
- package/vendor/backtest-runner/lib/tape-loader-range-plan.mjs +77 -0
- package/vendor/backtest-runner/lib/tape-loader-range.mjs +222 -0
- package/vendor/backtest-runner/lib/tape-loader.mjs +13 -41
package/dist/commands/run.js
CHANGED
|
@@ -101,8 +101,8 @@ async function runCli(argv, env = process.env) {
|
|
|
101
101
|
"alphafox schema [operationId]",
|
|
102
102
|
"alphafox catalog",
|
|
103
103
|
"alphafox api METHOD PATH [--body JSON|--config @file]",
|
|
104
|
-
"alphafox engine-backtest run --experiment <uuid> --definition <id> --config @file --exchange <id> --range FROM..TO --initial-equity N",
|
|
105
|
-
"alphafox engine-backtest sweep --experiment <uuid> --definition <id> --config @file --axes @file --exchange <id> --range FROM..TO --initial-equity N --no-persist",
|
|
104
|
+
"alphafox engine-backtest run --experiment <uuid> --definition <id> --config @file --exchange <id> --range FROM..TO --initial-equity N [--verbose]",
|
|
105
|
+
"alphafox engine-backtest sweep --experiment <uuid> --definition <id> --config @file --axes @file --exchange <id> --range FROM..TO --initial-equity N --no-persist [--verbose]",
|
|
106
106
|
"alphafox cache status|clean [--tape|--runtime|--all]",
|
|
107
107
|
"alphafox resolve-symbols <query...> [--exchange binance] [--asset-class equity_perp]",
|
|
108
108
|
"alphafox <domain> <resource> <action> [flags]",
|
|
@@ -16,11 +16,11 @@ const TIERS = new Set(["free", "pro", "pro_max"]);
|
|
|
16
16
|
const DATA_QUALITY = new Set(["strict", "basic"]);
|
|
17
17
|
const PRICE_PATHS = new Set(["ohlc_path_4", "close_only"]);
|
|
18
18
|
exports.ENGINE_BACKTEST_RUN_USAGE = [
|
|
19
|
-
"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]",
|
|
19
|
+
"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] [--verbose]",
|
|
20
20
|
"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",
|
|
21
21
|
];
|
|
22
22
|
exports.ENGINE_BACKTEST_SWEEP_USAGE = [
|
|
23
|
-
"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
|
+
"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] [--verbose]",
|
|
24
24
|
];
|
|
25
25
|
const SWEEP_MODES = new Set(["neighborhood", "range"]);
|
|
26
26
|
const SEARCH_MODES = new Set(["standard", "fast"]);
|
|
@@ -112,6 +112,7 @@ function parseEngineBacktestRunArgs(args) {
|
|
|
112
112
|
let configSchemaVersion;
|
|
113
113
|
let executionModelOverride;
|
|
114
114
|
let persist = true;
|
|
115
|
+
let verbose = false;
|
|
115
116
|
let help = false;
|
|
116
117
|
let replayTimeframe = replay_timeframe_1.ENGINE_BACKTEST_DEFAULT_REPLAY_TIMEFRAME;
|
|
117
118
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -128,6 +129,10 @@ function parseEngineBacktestRunArgs(args) {
|
|
|
128
129
|
persist = false;
|
|
129
130
|
continue;
|
|
130
131
|
}
|
|
132
|
+
if (a === "--verbose") {
|
|
133
|
+
verbose = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
131
136
|
if (!a.startsWith("--")) {
|
|
132
137
|
usage(`Unexpected argument: ${a}`, "unexpected_arg");
|
|
133
138
|
}
|
|
@@ -227,6 +232,7 @@ function parseEngineBacktestRunArgs(args) {
|
|
|
227
232
|
tier,
|
|
228
233
|
dataQualityMode,
|
|
229
234
|
persist,
|
|
235
|
+
verbose,
|
|
230
236
|
replayTimeframe,
|
|
231
237
|
};
|
|
232
238
|
}
|
|
@@ -284,6 +290,7 @@ function parseEngineBacktestRunArgs(args) {
|
|
|
284
290
|
configSchemaVersion,
|
|
285
291
|
executionModelOverride,
|
|
286
292
|
persist,
|
|
293
|
+
verbose,
|
|
287
294
|
replayTimeframe,
|
|
288
295
|
};
|
|
289
296
|
}
|
|
@@ -346,6 +353,7 @@ function parseEngineBacktestSweepArgs(args) {
|
|
|
346
353
|
createExperiment: false,
|
|
347
354
|
dataQualityMode: types_1.DEFAULT_DATA_QUALITY_MODE,
|
|
348
355
|
persist: true,
|
|
356
|
+
verbose: false,
|
|
349
357
|
replayTimeframe: replay_timeframe_1.ENGINE_BACKTEST_DEFAULT_REPLAY_TIMEFRAME,
|
|
350
358
|
axesRaw,
|
|
351
359
|
mode,
|
|
@@ -222,7 +222,9 @@ async function executeEngineBacktestRun(args, flags, env = process.env, deps = {
|
|
|
222
222
|
wasm = wasmLoaded.module;
|
|
223
223
|
runner = runnerLoaded.module;
|
|
224
224
|
}
|
|
225
|
-
const client = wasm.createNodeBacktestClient(
|
|
225
|
+
const client = wasm.createNodeBacktestClient({
|
|
226
|
+
verbose: args.verbose,
|
|
227
|
+
});
|
|
226
228
|
try {
|
|
227
229
|
if (typeof client.init === "function") {
|
|
228
230
|
await client.init();
|
|
@@ -449,6 +451,7 @@ function engineBacktestHelpData() {
|
|
|
449
451
|
"runs.create is write (not high-risk-write); --yes is not required",
|
|
450
452
|
"Do not pass --token; use alphafox auth login",
|
|
451
453
|
"--replay-timeframe defaults to 1m (min 1m). Indicator series still download their native plan timeframes.",
|
|
454
|
+
"Raw Engine worker logs, including per-trade output, are hidden by default. Use --verbose to forward them.",
|
|
452
455
|
"--data-quality defaults to basic: hard tape failures still stop, soft gaps finish the run and appear as coverageNotice (prefix_gap less severe, internal_gap more severe). Use --data-quality strict to fail on any gap.",
|
|
453
456
|
],
|
|
454
457
|
};
|
|
@@ -85,7 +85,7 @@ async function executeEngineBacktestSweep(args, flags, env = process.env, deps =
|
|
|
85
85
|
const { wasm, runner } = await loadRuntime(deps, env);
|
|
86
86
|
const clients = [];
|
|
87
87
|
try {
|
|
88
|
-
const client = wasm.createNodeBacktestClient();
|
|
88
|
+
const client = wasm.createNodeBacktestClient({ verbose: args.verbose });
|
|
89
89
|
clients.push(client);
|
|
90
90
|
if (typeof client.init === "function") {
|
|
91
91
|
await client.init();
|
|
@@ -221,7 +221,7 @@ async function executeEngineBacktestSweep(args, flags, env = process.env, deps =
|
|
|
221
221
|
}
|
|
222
222
|
emitProgress("tape", 1);
|
|
223
223
|
while (clients.length < concurrency) {
|
|
224
|
-
const extra = wasm.createNodeBacktestClient();
|
|
224
|
+
const extra = wasm.createNodeBacktestClient({ verbose: args.verbose });
|
|
225
225
|
clients.push(extra);
|
|
226
226
|
if (typeof extra.init === "function") {
|
|
227
227
|
await extra.init();
|
|
@@ -50,6 +50,8 @@ export interface EngineBacktestRunArgs {
|
|
|
50
50
|
readonly configSchemaVersion?: number;
|
|
51
51
|
readonly executionModelOverride?: Partial<ExecutionModel>;
|
|
52
52
|
readonly persist: boolean;
|
|
53
|
+
/** Forward raw Engine WASM worker stdout/stderr. Disabled by default. */
|
|
54
|
+
readonly verbose: boolean;
|
|
53
55
|
/** Replay/download bar. Defaults to 1m; never finer. */
|
|
54
56
|
readonly replayTimeframe: string;
|
|
55
57
|
}
|
|
@@ -256,7 +258,9 @@ export interface BacktestClientLike {
|
|
|
256
258
|
terminate(): void;
|
|
257
259
|
}
|
|
258
260
|
export interface BacktestWasmModule {
|
|
259
|
-
createNodeBacktestClient(options?:
|
|
261
|
+
createNodeBacktestClient(options?: {
|
|
262
|
+
readonly verbose?: boolean;
|
|
263
|
+
}): BacktestClientLike;
|
|
260
264
|
}
|
|
261
265
|
export interface BacktestRunnerModule {
|
|
262
266
|
loadTape(request: {
|
|
@@ -1,153 +1,153 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "@alphafox/cli",
|
|
4
|
-
"packageVersion": "0.3.
|
|
4
|
+
"packageVersion": "0.3.22",
|
|
5
5
|
"contractVersion": "2026-08-31",
|
|
6
|
-
"bundleHash": "
|
|
6
|
+
"bundleHash": "0ff8e818dd5a2f06d7d0df63d681bdf04163b9b09d8626da99c033a97aa8d438",
|
|
7
7
|
"skills": [
|
|
8
8
|
{
|
|
9
9
|
"name": "alphafox",
|
|
10
|
-
"version": "0.3.
|
|
10
|
+
"version": "0.3.22",
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "SKILL.md",
|
|
14
|
-
"sha256": "
|
|
14
|
+
"sha256": "454ddd309258874eb35bf567cd9ac3f524ecd212cd0a76599b8ae96ca50b9658",
|
|
15
15
|
"size": 7950
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
|
-
"hash": "
|
|
18
|
+
"hash": "10dc9fcb503d2e09dda7284818fcebd1223d97fb3c8e95e7757ea6db86a239b2"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"name": "alphafox-account",
|
|
22
|
-
"version": "0.3.
|
|
22
|
+
"version": "0.3.22",
|
|
23
23
|
"files": [
|
|
24
24
|
{
|
|
25
25
|
"path": "SKILL.md",
|
|
26
|
-
"sha256": "
|
|
26
|
+
"sha256": "ab83a22f7960d1b94d1f3874cd464c705c9eafb77b3b132ad3d2c092b343227b",
|
|
27
27
|
"size": 784
|
|
28
28
|
}
|
|
29
29
|
],
|
|
30
|
-
"hash": "
|
|
30
|
+
"hash": "b0de2c06865dd85be5655358e2fe20bd54dd2db29af73c225e43e3e5c4464649"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"name": "alphafox-admin",
|
|
34
|
-
"version": "0.3.
|
|
34
|
+
"version": "0.3.22",
|
|
35
35
|
"files": [
|
|
36
36
|
{
|
|
37
37
|
"path": "SKILL.md",
|
|
38
|
-
"sha256": "
|
|
38
|
+
"sha256": "a30aa7f40d67e32068f909481906a548da713f05dfb8562eebb05bc09d90bca7",
|
|
39
39
|
"size": 2078
|
|
40
40
|
}
|
|
41
41
|
],
|
|
42
|
-
"hash": "
|
|
42
|
+
"hash": "e0aa20827c3299dc996c74ef27b7708bd32275e3a61362b07be367fc565e735a"
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"name": "alphafox-auth",
|
|
46
|
-
"version": "0.3.
|
|
46
|
+
"version": "0.3.22",
|
|
47
47
|
"files": [
|
|
48
48
|
{
|
|
49
49
|
"path": "SKILL.md",
|
|
50
|
-
"sha256": "
|
|
50
|
+
"sha256": "eab220c69f31de56f7bdfde7722723c2bc9a667539f62bc8faae7234642412be",
|
|
51
51
|
"size": 2371
|
|
52
52
|
}
|
|
53
53
|
],
|
|
54
|
-
"hash": "
|
|
54
|
+
"hash": "02c24ece7d90d42afd81c249184925ec970602e0226deafd290ff1d7d2e2dfa9"
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
"name": "alphafox-cache",
|
|
58
|
-
"version": "0.3.
|
|
58
|
+
"version": "0.3.22",
|
|
59
59
|
"files": [
|
|
60
60
|
{
|
|
61
61
|
"path": "SKILL.md",
|
|
62
|
-
"sha256": "
|
|
62
|
+
"sha256": "ebdf0007dfb04f2e2285b2e4cf738e6b70c46b40f67b22f2ec024be79e1ff895",
|
|
63
63
|
"size": 1530
|
|
64
64
|
}
|
|
65
65
|
],
|
|
66
|
-
"hash": "
|
|
66
|
+
"hash": "7ebf4b0457e8c61fdd28601dbd95ef3b36eb2d4935e1f38448273e329ae69013"
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
"name": "alphafox-engine-backtest",
|
|
70
|
-
"version": "0.3.
|
|
70
|
+
"version": "0.3.22",
|
|
71
71
|
"files": [
|
|
72
72
|
{
|
|
73
73
|
"path": "SKILL.md",
|
|
74
|
-
"sha256": "
|
|
75
|
-
"size":
|
|
74
|
+
"sha256": "2a30ee16622c6a4cf1f9f40519be544d0f089a62eba4ffdd48d726f153760de3",
|
|
75
|
+
"size": 9792
|
|
76
76
|
}
|
|
77
77
|
],
|
|
78
|
-
"hash": "
|
|
78
|
+
"hash": "1aecca4608c0253089e69c1d58746984c2621ab5eccbb45c4e962a7530936663"
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
"name": "alphafox-exchange",
|
|
82
|
-
"version": "0.3.
|
|
82
|
+
"version": "0.3.22",
|
|
83
83
|
"files": [
|
|
84
84
|
{
|
|
85
85
|
"path": "SKILL.md",
|
|
86
|
-
"sha256": "
|
|
86
|
+
"sha256": "a065c2af77150a140838580053d688defaafa2c1093c565f9759b2e4f211e3cd",
|
|
87
87
|
"size": 744
|
|
88
88
|
}
|
|
89
89
|
],
|
|
90
|
-
"hash": "
|
|
90
|
+
"hash": "0dd29f8547bdbcfe89789877b32e84f130e88c886cd07cab2785d515ff807a3c"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
"name": "alphafox-market",
|
|
94
|
-
"version": "0.3.
|
|
94
|
+
"version": "0.3.22",
|
|
95
95
|
"files": [
|
|
96
96
|
{
|
|
97
97
|
"path": "SKILL.md",
|
|
98
|
-
"sha256": "
|
|
98
|
+
"sha256": "ca9d73c21724cf2e9b2f4ab9128f5ca883c70530ee4dc7268ea05c3a9d226083",
|
|
99
99
|
"size": 3079
|
|
100
100
|
}
|
|
101
101
|
],
|
|
102
|
-
"hash": "
|
|
102
|
+
"hash": "e5582689e8c8ca0e2ffcb334ba454b12400e9591070c9728b31e39db42c276a0"
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
"name": "alphafox-notification",
|
|
106
|
-
"version": "0.3.
|
|
106
|
+
"version": "0.3.22",
|
|
107
107
|
"files": [
|
|
108
108
|
{
|
|
109
109
|
"path": "SKILL.md",
|
|
110
|
-
"sha256": "
|
|
110
|
+
"sha256": "05485ee0d3bec411583f57ecb711555a5353d50fbf76ab92aaa45e2de4abdf20",
|
|
111
111
|
"size": 699
|
|
112
112
|
}
|
|
113
113
|
],
|
|
114
|
-
"hash": "
|
|
114
|
+
"hash": "a139b26ee840f4f6c3bf46e92809ef5db1af29b446e572b5254808a639f86f27"
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
"name": "alphafox-shared",
|
|
118
|
-
"version": "0.3.
|
|
118
|
+
"version": "0.3.22",
|
|
119
119
|
"files": [
|
|
120
120
|
{
|
|
121
121
|
"path": "SKILL.md",
|
|
122
|
-
"sha256": "
|
|
122
|
+
"sha256": "b7bbdf683b12975538c26e38a05dce89d0c0f1a19a5cc73569e2c709fbb5d04b",
|
|
123
123
|
"size": 6920
|
|
124
124
|
}
|
|
125
125
|
],
|
|
126
|
-
"hash": "
|
|
126
|
+
"hash": "7c7750ba8bf2d2dbf51c8fb134ac958076773f9572f9082497925bb8b255de13"
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"name": "alphafox-strategy",
|
|
130
|
-
"version": "0.3.
|
|
130
|
+
"version": "0.3.22",
|
|
131
131
|
"files": [
|
|
132
132
|
{
|
|
133
133
|
"path": "SKILL.md",
|
|
134
|
-
"sha256": "
|
|
135
|
-
"size":
|
|
134
|
+
"sha256": "01540ff5e1076eab22515991199be407263d942bc7608628c377b81eb0157cd7",
|
|
135
|
+
"size": 7185
|
|
136
136
|
}
|
|
137
137
|
],
|
|
138
|
-
"hash": "
|
|
138
|
+
"hash": "e2e0e0a6bae21e5f97ed6ba42d66e90cf51c76b6601cdad606630c3743441909"
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
"name": "alphafox-trading",
|
|
142
|
-
"version": "0.3.
|
|
142
|
+
"version": "0.3.22",
|
|
143
143
|
"files": [
|
|
144
144
|
{
|
|
145
145
|
"path": "SKILL.md",
|
|
146
|
-
"sha256": "
|
|
147
|
-
"size":
|
|
146
|
+
"sha256": "4b862b2f01534e0fda2c6667bc39a972f2cd9692f8967a0496b941411b745967",
|
|
147
|
+
"size": 4921
|
|
148
148
|
}
|
|
149
149
|
],
|
|
150
|
-
"hash": "
|
|
150
|
+
"hash": "d8c0388da25fd4964d6ca8d4668ec0a0778669e0fbc22e85952b7eca80869205"
|
|
151
151
|
}
|
|
152
152
|
]
|
|
153
153
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare const CLI_NAME = "alphafox";
|
|
2
2
|
export declare const CLI_PACKAGE = "@alphafox/cli";
|
|
3
|
-
export declare const CLI_VERSION = "0.3.
|
|
3
|
+
export declare const CLI_VERSION = "0.3.22";
|
|
4
4
|
export { CATALOG_VERSION as CLI_CONTRACT_VERSION } from "./catalog/operations";
|
package/dist/version.js
CHANGED
|
@@ -3,6 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CLI_CONTRACT_VERSION = exports.CLI_VERSION = exports.CLI_PACKAGE = exports.CLI_NAME = void 0;
|
|
4
4
|
exports.CLI_NAME = "alphafox";
|
|
5
5
|
exports.CLI_PACKAGE = "@alphafox/cli";
|
|
6
|
-
exports.CLI_VERSION = "0.3.
|
|
6
|
+
exports.CLI_VERSION = "0.3.22";
|
|
7
7
|
var operations_1 = require("./catalog/operations");
|
|
8
8
|
Object.defineProperty(exports, "CLI_CONTRACT_VERSION", { enumerable: true, get: function () { return operations_1.CATALOG_VERSION; } });
|
package/package.json
CHANGED
package/skills/account/SKILL.md
CHANGED
package/skills/admin/SKILL.md
CHANGED
package/skills/alphafox/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox
|
|
3
3
|
description: AlphaFox CLI entry router. Use for any AlphaFox request — install, update, login, whoami, 回测, engine backtest, 清理回测缓存 / 历史数据, strategy definitions, create/list/start/stop a running strategy (trader), ticker/标的 resolve (美股 or crypto), market data, exchange connectors, wallet, subscriptions, notifications, or admin. After 回测 or 运行策略, include the dashboard URL from the domain skill. When the user asks 排行榜, include https://www.alphafox.app/zh/dashboard/leaderboard. After a successful install and login, present the 新人引导 in this file (Lite square 带单员 + classic strategies). If a CLI command prints `[alphafox] update available`, ask the user「检测到新的版本,是否需要我帮你升级?」and only then run `alphafox update --format json --no-input`. After a large backtest, if tape cache is large, ask「回测下载的历史数据比较大,要不要我帮你清理本地缓存?」then open `alphafox-cache`. Start here, then open the routed domain skill. Do not guess alphafox-engine-backtest vs alphafox-strategy vs alphafox-trading from memory.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.22
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# AlphaFox
|
package/skills/auth/SKILL.md
CHANGED
package/skills/cache/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox-engine-backtest
|
|
3
3
|
description: Local Engine WASM backtest (alphafox engine-backtest run|sweep) vs catalog experiment CRUD. After a persisted run, include https://www.alphafox.app/zh/dashboard/traders/backtest/{experimentId}.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.22
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Engine Backtest
|
|
@@ -33,6 +33,8 @@ alphafox schema engine_backtest.experiments.byId.sweeps.create --format json --n
|
|
|
33
33
|
|
|
34
34
|
The tape runner ships inside the CLI (plus `ccxt` for public-market pulls). The wasm / Node host is downloaded from the public Vercel Blob manifest (`engine-backtest/latest.json`) into `~/.cache/alphafox/engine-backtest/<hash>/` on first run.
|
|
35
35
|
|
|
36
|
+
Before planning or running, follow the complete parameter review in `alphafox-strategy`, including when the operator already supplied `--config` or pasted a full command. Do not run the backtest until every applicable parameter, default status, short explanation, proposed value, and value source has been shown and the operator explicitly confirms the final proposal. User overrides win; unresolved required values stop the flow.
|
|
37
|
+
|
|
36
38
|
Local overrides, in order:
|
|
37
39
|
|
|
38
40
|
1. `ALPHAFOX_BACKTEST_WASM_DIR` / `ALPHAFOX_BACKTEST_RUNNER_DIR`
|
package/skills/exchange/SKILL.md
CHANGED
package/skills/market/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox-market
|
|
3
3
|
description: Market data and ticker resolution for US equity perps, RWAs, and crypto on the same perp catalog. Use when the user names 美股, NVDA, AAPL, BTC, or any 标的. Keep the operator's asset class via symbolMetadata — do not rewrite NVDA into a crypto coin.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.22
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Market
|
package/skills/strategy/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox-strategy
|
|
3
3
|
description: Strategy definitions — list types, read a definition's contract, and validate config. Creating a running strategy is creating a trader; use alphafox-trading for that. Local Engine backtest is alphafox-engine-backtest.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.22
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Strategy definitions
|
|
@@ -31,18 +31,38 @@ Match the name the operator used against list `id` / `name` / `display`. If they
|
|
|
31
31
|
1. `id`, `category` (`COPY` / `DCA` / `GRID` / `TREND` / `OTHERS`), `status`
|
|
32
32
|
2. English `description` (mechanism). Prefer it over marketing `display.description`
|
|
33
33
|
3. `capabilities` and `commonModules` (event-driven vs polling, signal sources, manual sync)
|
|
34
|
-
4. `
|
|
34
|
+
4. The effective `configSchema`, including composed common modules, strategy parameters, defaults, constraints, conditional branches, and each field's `display`
|
|
35
35
|
5. `capabilities.actionDefinitions` (what a manual action changes)
|
|
36
36
|
|
|
37
37
|
Explain the type from those fields. Missing a layer → say unknown; do not fill from memory. Decision logic (`simple-long`, grid `mode` `neutral`/`long`/`short`) is an internal parameter, not a new definition id.
|
|
38
38
|
|
|
39
39
|
## Configure with the human
|
|
40
40
|
|
|
41
|
-
The human
|
|
41
|
+
The human confirms the complete parameter set. You write JSON only after that review.
|
|
42
42
|
|
|
43
43
|
1. Confirm the definition from `byId.get` in the operator's language (what it is, what drives it, how positions change).
|
|
44
|
-
2.
|
|
45
|
-
3.
|
|
44
|
+
2. Use the effective `configSchema` returned by `byId.get` as the sole parameter contract. It already composes the definition's `commonModules` with `strategyConfigSchema` and may contain definition-specific customization. Walk every applicable parameter, not only required fields or familiar knobs.
|
|
45
|
+
3. Resolve conditional schemas in dependency order. Ask for a discriminator or enable/disable choice first, then enumerate every parameter in the selected branch. Do not present inactive branch fields as active settings. For arrays or maps, explain the collection and confirm every field of each configured item.
|
|
46
|
+
4. Build a grouped or numbered review table in the operator's language. Every applicable parameter must have:
|
|
47
|
+
|
|
48
|
+
- full JSON path;
|
|
49
|
+
- short explanation from localized `display.description`, falling back to the schema's description; if neither exists, say the definition provides no explanation — do not guess;
|
|
50
|
+
- type plus enum, range, or other relevant constraints;
|
|
51
|
+
- default status and source;
|
|
52
|
+
- proposed value and value source.
|
|
53
|
+
|
|
54
|
+
5. Determine the proposed value in this precedence order:
|
|
55
|
+
|
|
56
|
+
1. **user override** — any value the operator explicitly supplied, including values already present in a provided config;
|
|
57
|
+
2. **schema default** — the field's JSON Schema `default`;
|
|
58
|
+
3. **product default** — only a default explicitly documented by this skill or the live product contract. Currently `common.execution.leverage` is `10`, matching the website form; raw Engine omission means 1x;
|
|
59
|
+
4. **no default** — required fields remain unresolved and must be answered; optional fields are proposed as “不设置 / omit”. Do not guess a value or describe omission as a default.
|
|
60
|
+
|
|
61
|
+
A user override always wins, even when it equals neither default. Preserve explicit `false`, `0`, empty arrays, and empty strings when the schema allows them; they are not missing values.
|
|
62
|
+
|
|
63
|
+
6. When there are many parameters, present them in logical groups or numbered chunks so the review remains readable. After all groups are visible, ask the operator to reply **confirm all** / “全部确认”, or override paths/numbers. One overall confirmation is sufficient, but it must cover every displayed parameter. Apply overrides, show the affected rows again, and repeat until no required value is unresolved and the operator explicitly confirms the final proposal.
|
|
64
|
+
7. An earlier request such as “创建策略”, “运行回测”, or a pasted command/config is input to the proposal, not confirmation of the review. Do not validate, create, or backtest until the complete parameter review is explicitly confirmed.
|
|
65
|
+
8. Write `strategy-config.json` as the trader object:
|
|
46
66
|
|
|
47
67
|
```json
|
|
48
68
|
{
|
|
@@ -53,7 +73,7 @@ The human answers knobs. You write JSON.
|
|
|
53
73
|
|
|
54
74
|
`common` is shared risk / SLTP / execution / market. `strategy` is this type's parameters and decision logic. Do not use top-level `settings`, `policyId`, or `policyParams`. `configSchemaVersion` comes from the definition (`byId.get` / list); keep it off this file.
|
|
55
75
|
|
|
56
|
-
|
|
76
|
+
9. Keys and enums come from the definition schema, not from this skill. Do not ship a default `grid.json` / `dca.json`. Reuse this source file for `engine-backtest --config` and as `trading.traders.create` `config`.
|
|
57
77
|
|
|
58
78
|
## Validate config
|
|
59
79
|
|
package/skills/trading/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox-trading
|
|
3
3
|
description: Running strategies (traders) — create, list, start, and stop. A trader is a live or paper strategy instance (grid, dca, copy, …), not a person. Default Engine create uses autoStart true (创建即开始). Use autoStart false only when the user asks to create without starting. After create or start, include https://www.alphafox.app/zh/dashboard/traders/{traderId}.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.22
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Running strategies (traders)
|
|
@@ -32,6 +32,8 @@ alphafox api GET /api/v1/trading/traders --format json --no-input
|
|
|
32
32
|
|
|
33
33
|
Read `alphafox schema <operationId>` first. Body may only include documented `request.body` fields. Large / nested bodies use `--config @file`.
|
|
34
34
|
|
|
35
|
+
Before composing or dry-running any create request, follow the complete parameter review in `alphafox-strategy`, including when the operator already supplied a config. Do not create a trader until every applicable parameter, default status, short explanation, proposed value, and value source has been shown and the operator explicitly confirms the final proposal. User overrides win; unresolved required values stop the flow.
|
|
36
|
+
|
|
35
37
|
```bash
|
|
36
38
|
alphafox schema trading.traders.create --format json --no-input
|
|
37
39
|
alphafox trading traders create --config @./create-trader.json --dry-run --format json --no-input
|
|
@@ -80,6 +80,8 @@ const scenario = assembleScenario({
|
|
|
80
80
|
|
|
81
81
|
与 web `tape-loader.series.ts` 对齐:Binance 1500、OKX 100、Bybit / Bitget 1000、Hyperliquid 5000。Bitget 另受约 89 天请求跨度限制;Hyperliquid 分页带 `until` 窗口。
|
|
82
82
|
|
|
83
|
+
长区间会拆成最多 8 个可独立下载的时间窗口;不同 `symbol × timeframe` 与同一序列的时间窗口共用一个全局 8 请求池,避免嵌套并发放大。相邻窗口保留一根重叠 K 线,合并时按时间去重;同时间戳内容不一致会抛出 `invalid_ohlcv`,不会静默选取其中一份。
|
|
84
|
+
|
|
83
85
|
## 数据质量
|
|
84
86
|
|
|
85
87
|
- `basic`(默认):硬失败(缺市场、空序列、非法 K 线、拉数失败)仍抛错;软缺口进入 `coverageIssues` 与 `coverageWarnings`。起始缺口(`prefix_gap`)较轻,中间缺口(`internal_gap`)较重。
|
|
@@ -316,7 +316,7 @@ export interface TapeProxyOptions {
|
|
|
316
316
|
export interface TapeLoadOptions extends TapeProxyOptions {
|
|
317
317
|
readonly cache?: FileTapeCache | false | "disable";
|
|
318
318
|
readonly cacheDir?: string;
|
|
319
|
-
/**
|
|
319
|
+
/** Shared series and time-window request budget; default and hard cap are 8. */
|
|
320
320
|
readonly seriesConcurrency?: number;
|
|
321
321
|
readonly nowMs?: number;
|
|
322
322
|
readonly onProgress?: (progress: TapeLoadProgress) => void;
|
|
@@ -353,7 +353,7 @@ export interface TapeLoadResult {
|
|
|
353
353
|
};
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
export const DEFAULT_TAPE_SERIES_CONCURRENCY:
|
|
356
|
+
export const DEFAULT_TAPE_SERIES_CONCURRENCY: 8;
|
|
357
357
|
export const MAX_TAPE_SERIES_CONCURRENCY: 8;
|
|
358
358
|
|
|
359
359
|
export function resolveTapeSeriesConcurrency(value?: number): number;
|
|
@@ -364,6 +364,12 @@ export function mapWithConcurrency<T, R>(
|
|
|
364
364
|
worker: (item: T, index: number) => Promise<R>
|
|
365
365
|
): Promise<R[]>;
|
|
366
366
|
|
|
367
|
+
export function limitTapeOhlcvConcurrency(
|
|
368
|
+
exchange: Pick<TapeRuntimeExchange, "fetchOHLCV">,
|
|
369
|
+
concurrency: number,
|
|
370
|
+
signal?: AbortSignal
|
|
371
|
+
): Pick<TapeRuntimeExchange, "fetchOHLCV">;
|
|
372
|
+
|
|
367
373
|
export function loadTape(
|
|
368
374
|
request: TapeLoadRequest,
|
|
369
375
|
options?: TapeLoadOptions
|
|
@@ -55,10 +55,13 @@ export {
|
|
|
55
55
|
export {
|
|
56
56
|
DEFAULT_TAPE_SERIES_CONCURRENCY,
|
|
57
57
|
MAX_TAPE_SERIES_CONCURRENCY,
|
|
58
|
-
|
|
58
|
+
limitTapeOhlcvConcurrency,
|
|
59
59
|
mapWithConcurrency,
|
|
60
|
-
resolveTapeCache,
|
|
61
60
|
resolveTapeSeriesConcurrency,
|
|
61
|
+
} from "./lib/tape-loader-concurrency.mjs";
|
|
62
|
+
export {
|
|
63
|
+
loadTape,
|
|
64
|
+
resolveTapeCache,
|
|
62
65
|
effectiveTapeEndMs,
|
|
63
66
|
inferFundingIntervals,
|
|
64
67
|
classifyTapeSymbolsForPreflight,
|
|
@@ -12,19 +12,14 @@ import {
|
|
|
12
12
|
ENGINE_BACKTEST_WARMUP_CANDLES,
|
|
13
13
|
TIMEFRAME_MS,
|
|
14
14
|
} from "./timeframes.mjs";
|
|
15
|
+
import {
|
|
16
|
+
fetchClosedOhlcvRange,
|
|
17
|
+
isClosedCandle,
|
|
18
|
+
} from "./tape-loader-range.mjs";
|
|
15
19
|
|
|
16
20
|
const WARMUP_CANDLES = ENGINE_BACKTEST_WARMUP_CANDLES;
|
|
17
|
-
const BITGET_OHLCV_MAX_REQUEST_SPAN_MS = 89 * 86_400_000;
|
|
18
|
-
|
|
19
|
-
export { TIMEFRAME_MS };
|
|
20
21
|
|
|
21
|
-
export
|
|
22
|
-
const stepMs = TIMEFRAME_MS[timeframe];
|
|
23
|
-
if (!stepMs) {
|
|
24
|
-
throw new Error(`不支持的 timeframe:${timeframe}`);
|
|
25
|
-
}
|
|
26
|
-
return timestampMs + stepMs <= toMs;
|
|
27
|
-
}
|
|
22
|
+
export { fetchClosedOhlcvRange, isClosedCandle, TIMEFRAME_MS };
|
|
28
23
|
|
|
29
24
|
export function ohlcvSeriesStartMs(fromMs, timeframe, market) {
|
|
30
25
|
const stepMs = TIMEFRAME_MS[timeframe];
|
|
@@ -207,164 +202,3 @@ function concatMonotonicRows(head, tail) {
|
|
|
207
202
|
const lastHeadTimestamp = head[head.length - 1][0];
|
|
208
203
|
return [...head, ...tail.filter((row) => row[0] > lastHeadTimestamp)];
|
|
209
204
|
}
|
|
210
|
-
|
|
211
|
-
export async function fetchClosedOhlcvRange(request) {
|
|
212
|
-
const stepMs = TIMEFRAME_MS[request.timeframe];
|
|
213
|
-
if (!stepMs) {
|
|
214
|
-
throw new Error(`不支持的 timeframe:${request.timeframe}`);
|
|
215
|
-
}
|
|
216
|
-
const pageLimit = ohlcvPageLimitForTimeframe(
|
|
217
|
-
request.exchangeDefinition,
|
|
218
|
-
request.runtimeConfig,
|
|
219
|
-
request.timeframe
|
|
220
|
-
);
|
|
221
|
-
const rows = [];
|
|
222
|
-
const totalSpan = Math.max(1, request.toMs - request.sinceMs);
|
|
223
|
-
let cursor = request.sinceMs;
|
|
224
|
-
let emptyPrefixLowerMs = null;
|
|
225
|
-
let nonEmptyPrefixUpperMs = null;
|
|
226
|
-
let prefixResolved = false;
|
|
227
|
-
|
|
228
|
-
while (cursor < request.toMs) {
|
|
229
|
-
request.signal?.throwIfAborted();
|
|
230
|
-
const fetchSinceMs =
|
|
231
|
-
request.exchangeDefinition.ccxtId === "bitget"
|
|
232
|
-
? Math.max(0, cursor - 1)
|
|
233
|
-
: cursor;
|
|
234
|
-
const requestParams = {
|
|
235
|
-
...request.runtimeConfig.requestParams,
|
|
236
|
-
};
|
|
237
|
-
if (request.exchangeDefinition.ccxtId === "hyperliquid") {
|
|
238
|
-
requestParams.until = Math.min(
|
|
239
|
-
request.toMs,
|
|
240
|
-
fetchSinceMs + (pageLimit - 1) * stepMs
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
const page = await abortable(
|
|
244
|
-
request.exchange.fetchOHLCV(
|
|
245
|
-
request.symbol,
|
|
246
|
-
request.timeframe,
|
|
247
|
-
fetchSinceMs,
|
|
248
|
-
pageLimit,
|
|
249
|
-
requestParams
|
|
250
|
-
),
|
|
251
|
-
request.signal
|
|
252
|
-
);
|
|
253
|
-
request.signal?.throwIfAborted();
|
|
254
|
-
|
|
255
|
-
if (!prefixResolved && rows.length === 0 && emptyPrefixLowerMs !== null) {
|
|
256
|
-
if (page.length === 0) {
|
|
257
|
-
emptyPrefixLowerMs = cursor;
|
|
258
|
-
if (cursor >= request.fromMs) break;
|
|
259
|
-
} else {
|
|
260
|
-
nonEmptyPrefixUpperMs = cursor;
|
|
261
|
-
}
|
|
262
|
-
const upperMs = nonEmptyPrefixUpperMs ?? request.fromMs;
|
|
263
|
-
if (
|
|
264
|
-
nonEmptyPrefixUpperMs !== null &&
|
|
265
|
-
upperMs - emptyPrefixLowerMs <= stepMs
|
|
266
|
-
) {
|
|
267
|
-
cursor = upperMs;
|
|
268
|
-
prefixResolved = true;
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
cursor = midpointCursorMs(emptyPrefixLowerMs, upperMs, stepMs);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (page.length === 0) {
|
|
276
|
-
if (!prefixResolved && rows.length === 0 && cursor < request.fromMs) {
|
|
277
|
-
emptyPrefixLowerMs = cursor;
|
|
278
|
-
cursor = midpointCursorMs(cursor, request.fromMs, stepMs);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
prefixResolved = true;
|
|
284
|
-
|
|
285
|
-
for (const raw of page) {
|
|
286
|
-
if (raw.length < 6) {
|
|
287
|
-
throw new TapeDataUnavailableError([
|
|
288
|
-
{
|
|
289
|
-
code: "invalid_ohlcv",
|
|
290
|
-
symbol: request.symbol,
|
|
291
|
-
timeframe: request.timeframe,
|
|
292
|
-
},
|
|
293
|
-
]);
|
|
294
|
-
}
|
|
295
|
-
const timestamp = Number(raw[0]);
|
|
296
|
-
if (!Number.isFinite(timestamp)) {
|
|
297
|
-
throw new TapeDataUnavailableError([
|
|
298
|
-
{
|
|
299
|
-
code: "invalid_ohlcv",
|
|
300
|
-
symbol: request.symbol,
|
|
301
|
-
timeframe: request.timeframe,
|
|
302
|
-
},
|
|
303
|
-
]);
|
|
304
|
-
}
|
|
305
|
-
if (!isClosedCandle(timestamp, request.timeframe, request.toMs)) {
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
if (rows.length > 0 && timestamp <= rows[rows.length - 1][0]) {
|
|
309
|
-
throw new TapeDataUnavailableError([
|
|
310
|
-
{
|
|
311
|
-
code: "non_monotonic",
|
|
312
|
-
symbol: request.symbol,
|
|
313
|
-
timeframe: request.timeframe,
|
|
314
|
-
expected: rows[rows.length - 1][0] + stepMs,
|
|
315
|
-
actual: timestamp,
|
|
316
|
-
timestamp,
|
|
317
|
-
},
|
|
318
|
-
]);
|
|
319
|
-
}
|
|
320
|
-
rows.push([
|
|
321
|
-
timestamp,
|
|
322
|
-
Number(raw[1]),
|
|
323
|
-
Number(raw[2]),
|
|
324
|
-
Number(raw[3]),
|
|
325
|
-
Number(raw[4]),
|
|
326
|
-
Number(raw[5]),
|
|
327
|
-
]);
|
|
328
|
-
}
|
|
329
|
-
const lastTimestamp = Number(page[page.length - 1][0]);
|
|
330
|
-
const nextCursor = lastTimestamp + stepMs;
|
|
331
|
-
if (nextCursor <= cursor) {
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
cursor = nextCursor;
|
|
335
|
-
request.onFraction?.(Math.min(1, (cursor - request.sinceMs) / totalSpan));
|
|
336
|
-
}
|
|
337
|
-
if (rows.length === 0 && !request.allowEmpty) {
|
|
338
|
-
throw new TapeDataUnavailableError([
|
|
339
|
-
{
|
|
340
|
-
code: "ohlcv_missing",
|
|
341
|
-
symbol: request.symbol,
|
|
342
|
-
timeframe: request.timeframe,
|
|
343
|
-
},
|
|
344
|
-
]);
|
|
345
|
-
}
|
|
346
|
-
return rows;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function ohlcvPageLimitForTimeframe(exchange, runtimeConfig, timeframe) {
|
|
350
|
-
if (exchange.ccxtId !== "bitget") {
|
|
351
|
-
return runtimeConfig.ohlcvPageLimit;
|
|
352
|
-
}
|
|
353
|
-
const stepMs = TIMEFRAME_MS[timeframe];
|
|
354
|
-
return Math.max(
|
|
355
|
-
1,
|
|
356
|
-
Math.min(
|
|
357
|
-
runtimeConfig.ohlcvPageLimit,
|
|
358
|
-
Math.floor(BITGET_OHLCV_MAX_REQUEST_SPAN_MS / stepMs)
|
|
359
|
-
)
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function midpointCursorMs(lowerMs, upperMs, stepMs) {
|
|
364
|
-
if (upperMs - lowerMs <= stepMs) {
|
|
365
|
-
return upperMs;
|
|
366
|
-
}
|
|
367
|
-
const midpointMs = lowerMs + Math.floor((upperMs - lowerMs) / 2);
|
|
368
|
-
const alignedMs = Math.floor(midpointMs / stepMs) * stepMs;
|
|
369
|
-
return Math.min(upperMs, Math.max(lowerMs + 1, alignedMs));
|
|
370
|
-
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const DEFAULT_TAPE_SERIES_CONCURRENCY = 8;
|
|
2
|
+
export const MAX_TAPE_SERIES_CONCURRENCY = 8;
|
|
3
|
+
|
|
4
|
+
export function resolveTapeSeriesConcurrency(value) {
|
|
5
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 1) {
|
|
6
|
+
return Math.min(
|
|
7
|
+
MAX_TAPE_SERIES_CONCURRENCY,
|
|
8
|
+
Math.max(1, Math.floor(value))
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return DEFAULT_TAPE_SERIES_CONCURRENCY;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function mapWithConcurrency(items, concurrency, worker) {
|
|
15
|
+
const list = [...items];
|
|
16
|
+
if (list.length === 0) return [];
|
|
17
|
+
|
|
18
|
+
const limit = Math.min(
|
|
19
|
+
list.length,
|
|
20
|
+
Math.max(1, Math.floor(Number(concurrency)) || 1)
|
|
21
|
+
);
|
|
22
|
+
const results = new Array(list.length);
|
|
23
|
+
let nextIndex = 0;
|
|
24
|
+
async function runWorker() {
|
|
25
|
+
while (true) {
|
|
26
|
+
const index = nextIndex;
|
|
27
|
+
nextIndex += 1;
|
|
28
|
+
if (index >= list.length) return;
|
|
29
|
+
results[index] = await worker(list[index], index);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
await Promise.all(Array.from({ length: limit }, () => runWorker()));
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Shares one request budget across series workers and their time windows. */
|
|
37
|
+
export function limitTapeOhlcvConcurrency(exchange, concurrency, signal) {
|
|
38
|
+
const limit = resolveTapeSeriesConcurrency(concurrency);
|
|
39
|
+
const queue = [];
|
|
40
|
+
let active = 0;
|
|
41
|
+
|
|
42
|
+
const drain = () => {
|
|
43
|
+
while (active < limit) {
|
|
44
|
+
const task = queue.shift();
|
|
45
|
+
if (!task) return;
|
|
46
|
+
active += 1;
|
|
47
|
+
void task().finally(() => {
|
|
48
|
+
active -= 1;
|
|
49
|
+
drain();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const schedule = (operation) =>
|
|
54
|
+
new Promise((resolve, reject) => {
|
|
55
|
+
queue.push(async () => {
|
|
56
|
+
try {
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
resolve(await operation());
|
|
59
|
+
} catch (error) {
|
|
60
|
+
reject(error);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
drain();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
fetchOHLCV: (symbol, timeframe, since, pageLimit, params) =>
|
|
68
|
+
schedule(() =>
|
|
69
|
+
exchange.fetchOHLCV(symbol, timeframe, since, pageLimit, params)
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { TapeDataUnavailableError } from "./coverage.mjs";
|
|
2
|
+
import { MAX_TAPE_SERIES_CONCURRENCY } from "./tape-loader-concurrency.mjs";
|
|
3
|
+
|
|
4
|
+
export function planOhlcvTimeSegments(request, stepMs, pageLimit) {
|
|
5
|
+
const candleCount = Math.max(
|
|
6
|
+
0,
|
|
7
|
+
Math.ceil((request.toMs - request.sinceMs) / stepMs)
|
|
8
|
+
);
|
|
9
|
+
const segmentCount = Math.min(
|
|
10
|
+
MAX_TAPE_SERIES_CONCURRENCY,
|
|
11
|
+
Math.max(1, Math.ceil(candleCount / pageLimit))
|
|
12
|
+
);
|
|
13
|
+
if (segmentCount === 1) {
|
|
14
|
+
return [{ sinceMs: request.sinceMs, untilMs: request.toMs }];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const candlesPerSegment = Math.ceil(candleCount / segmentCount);
|
|
18
|
+
return Array.from({ length: segmentCount }, (_, index) => {
|
|
19
|
+
const logicalStartMs = request.sinceMs + index * candlesPerSegment * stepMs;
|
|
20
|
+
return {
|
|
21
|
+
// Overlap preserves bars whose exchange open time is not split-aligned.
|
|
22
|
+
sinceMs:
|
|
23
|
+
index === 0
|
|
24
|
+
? request.sinceMs
|
|
25
|
+
: Math.max(request.sinceMs, logicalStartMs - stepMs),
|
|
26
|
+
untilMs: Math.min(
|
|
27
|
+
request.toMs,
|
|
28
|
+
request.sinceMs + (index + 1) * candlesPerSegment * stepMs
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
}).filter((segment) => segment.untilMs > segment.sinceMs);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createSegmentRequest(request, segment, onFraction) {
|
|
35
|
+
return {
|
|
36
|
+
...request,
|
|
37
|
+
sinceMs: segment.sinceMs,
|
|
38
|
+
toMs: segment.untilMs,
|
|
39
|
+
fromMs: Math.min(
|
|
40
|
+
segment.untilMs,
|
|
41
|
+
Math.max(segment.sinceMs, request.fromMs)
|
|
42
|
+
),
|
|
43
|
+
allowEmpty: true,
|
|
44
|
+
onFraction,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function mergeOhlcvTimeSegments(request, segments) {
|
|
49
|
+
const rowsByTimestamp = new Map();
|
|
50
|
+
for (const rows of segments) {
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
if (row[0] < request.sinceMs) continue;
|
|
53
|
+
const existing = rowsByTimestamp.get(row[0]);
|
|
54
|
+
if (existing && !sameOhlcvRow(existing, row)) {
|
|
55
|
+
throw new TapeDataUnavailableError([
|
|
56
|
+
{
|
|
57
|
+
code: "invalid_ohlcv",
|
|
58
|
+
symbol: request.symbol,
|
|
59
|
+
timeframe: request.timeframe,
|
|
60
|
+
timestamp: row[0],
|
|
61
|
+
message: "concurrent OHLCV windows returned conflicting candles",
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
rowsByTimestamp.set(row[0], row);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...rowsByTimestamp.values()].sort((left, right) => left[0] - right[0]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function averageProgress(values) {
|
|
72
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sameOhlcvRow(left, right) {
|
|
76
|
+
return left.every((value, index) => value === right[index]);
|
|
77
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { abortable } from "./abortable.mjs";
|
|
2
|
+
import { TapeDataUnavailableError } from "./coverage.mjs";
|
|
3
|
+
import {
|
|
4
|
+
averageProgress,
|
|
5
|
+
createSegmentRequest,
|
|
6
|
+
mergeOhlcvTimeSegments,
|
|
7
|
+
planOhlcvTimeSegments,
|
|
8
|
+
} from "./tape-loader-range-plan.mjs";
|
|
9
|
+
import { TIMEFRAME_MS } from "./timeframes.mjs";
|
|
10
|
+
|
|
11
|
+
const BITGET_OHLCV_MAX_REQUEST_SPAN_MS = 89 * 86_400_000;
|
|
12
|
+
|
|
13
|
+
export function isClosedCandle(timestampMs, timeframe, toMs) {
|
|
14
|
+
return timestampMs + timeframeStepMs(timeframe) <= toMs;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function fetchClosedOhlcvRange(request) {
|
|
18
|
+
const stepMs = timeframeStepMs(request.timeframe);
|
|
19
|
+
const pageLimit = ohlcvPageLimitForTimeframe(
|
|
20
|
+
request.exchangeDefinition,
|
|
21
|
+
request.runtimeConfig,
|
|
22
|
+
request.timeframe
|
|
23
|
+
);
|
|
24
|
+
const segments = planOhlcvTimeSegments(request, stepMs, pageLimit);
|
|
25
|
+
if (segments.length === 1) {
|
|
26
|
+
return fetchClosedOhlcvTimeSegment(request, pageLimit);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const progress = new Array(segments.length).fill(0);
|
|
30
|
+
const segmentRows = await Promise.all(
|
|
31
|
+
segments.map((segment, index) =>
|
|
32
|
+
fetchClosedOhlcvTimeSegment(
|
|
33
|
+
createSegmentRequest(request, segment, (fraction) => {
|
|
34
|
+
progress[index] = fraction;
|
|
35
|
+
request.onFraction?.(averageProgress(progress));
|
|
36
|
+
}),
|
|
37
|
+
pageLimit
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
const rows = mergeOhlcvTimeSegments(request, segmentRows);
|
|
42
|
+
if (rows.length === 0 && !request.allowEmpty) {
|
|
43
|
+
throw missingOhlcvError(request);
|
|
44
|
+
}
|
|
45
|
+
request.onFraction?.(1);
|
|
46
|
+
return rows;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fetchClosedOhlcvTimeSegment(request, pageLimit) {
|
|
50
|
+
const stepMs = timeframeStepMs(request.timeframe);
|
|
51
|
+
const rows = [];
|
|
52
|
+
const totalSpan = Math.max(1, request.toMs - request.sinceMs);
|
|
53
|
+
let cursor = request.sinceMs;
|
|
54
|
+
let emptyPrefixLowerMs = null;
|
|
55
|
+
let nonEmptyPrefixUpperMs = null;
|
|
56
|
+
let prefixResolved = false;
|
|
57
|
+
|
|
58
|
+
while (cursor < request.toMs) {
|
|
59
|
+
request.signal?.throwIfAborted();
|
|
60
|
+
const fetchSinceMs = resolveFetchSinceMs(request, cursor);
|
|
61
|
+
const page = await fetchOhlcvPage(request, fetchSinceMs, pageLimit, stepMs);
|
|
62
|
+
request.signal?.throwIfAborted();
|
|
63
|
+
|
|
64
|
+
const prefix = resolveEmptyPrefix({
|
|
65
|
+
request,
|
|
66
|
+
page,
|
|
67
|
+
rows,
|
|
68
|
+
cursor,
|
|
69
|
+
stepMs,
|
|
70
|
+
emptyPrefixLowerMs,
|
|
71
|
+
nonEmptyPrefixUpperMs,
|
|
72
|
+
prefixResolved,
|
|
73
|
+
});
|
|
74
|
+
if (prefix) {
|
|
75
|
+
({ cursor, emptyPrefixLowerMs, nonEmptyPrefixUpperMs, prefixResolved } =
|
|
76
|
+
prefix);
|
|
77
|
+
if (prefix.done) break;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (page.length === 0) break;
|
|
81
|
+
prefixResolved = true;
|
|
82
|
+
appendPageRows(request, rows, page, stepMs);
|
|
83
|
+
const nextCursor = Number(page[page.length - 1][0]) + stepMs;
|
|
84
|
+
if (nextCursor <= cursor) break;
|
|
85
|
+
cursor = nextCursor;
|
|
86
|
+
request.onFraction?.(Math.min(1, (cursor - request.sinceMs) / totalSpan));
|
|
87
|
+
}
|
|
88
|
+
if (rows.length === 0 && !request.allowEmpty) {
|
|
89
|
+
throw missingOhlcvError(request);
|
|
90
|
+
}
|
|
91
|
+
return rows;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveEmptyPrefix(input) {
|
|
95
|
+
if (input.prefixResolved || input.rows.length > 0) return null;
|
|
96
|
+
if (input.page.length === 0 && input.cursor >= input.request.fromMs) {
|
|
97
|
+
return { ...input, done: true };
|
|
98
|
+
}
|
|
99
|
+
if (input.page.length > 0 && input.emptyPrefixLowerMs === null) return null;
|
|
100
|
+
|
|
101
|
+
const emptyPrefixLowerMs =
|
|
102
|
+
input.page.length === 0 ? input.cursor : input.emptyPrefixLowerMs;
|
|
103
|
+
const nonEmptyPrefixUpperMs =
|
|
104
|
+
input.page.length > 0 ? input.cursor : input.nonEmptyPrefixUpperMs;
|
|
105
|
+
const upperMs = nonEmptyPrefixUpperMs ?? input.request.fromMs;
|
|
106
|
+
const prefixResolved =
|
|
107
|
+
nonEmptyPrefixUpperMs !== null &&
|
|
108
|
+
upperMs - (emptyPrefixLowerMs ?? upperMs) <= input.stepMs;
|
|
109
|
+
return {
|
|
110
|
+
cursor: prefixResolved
|
|
111
|
+
? upperMs
|
|
112
|
+
: midpointCursorMs(
|
|
113
|
+
emptyPrefixLowerMs ?? input.cursor,
|
|
114
|
+
upperMs,
|
|
115
|
+
input.stepMs
|
|
116
|
+
),
|
|
117
|
+
emptyPrefixLowerMs,
|
|
118
|
+
nonEmptyPrefixUpperMs,
|
|
119
|
+
prefixResolved,
|
|
120
|
+
done: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function fetchOhlcvPage(request, fetchSinceMs, pageLimit, stepMs) {
|
|
125
|
+
const params = { ...request.runtimeConfig.requestParams };
|
|
126
|
+
if (request.exchangeDefinition.ccxtId === "hyperliquid") {
|
|
127
|
+
params.until = Math.min(
|
|
128
|
+
request.toMs,
|
|
129
|
+
fetchSinceMs + (pageLimit - 1) * stepMs
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return abortable(
|
|
133
|
+
request.exchange.fetchOHLCV(
|
|
134
|
+
request.symbol,
|
|
135
|
+
request.timeframe,
|
|
136
|
+
fetchSinceMs,
|
|
137
|
+
pageLimit,
|
|
138
|
+
params
|
|
139
|
+
),
|
|
140
|
+
request.signal
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function appendPageRows(request, rows, page, stepMs) {
|
|
145
|
+
for (const raw of page) {
|
|
146
|
+
const timestamp = Number(raw[0]);
|
|
147
|
+
if (raw.length < 6 || !Number.isFinite(timestamp)) {
|
|
148
|
+
throw invalidOhlcvError(request);
|
|
149
|
+
}
|
|
150
|
+
if (!isClosedCandle(timestamp, request.timeframe, request.toMs)) continue;
|
|
151
|
+
if (rows.length > 0 && timestamp <= rows[rows.length - 1][0]) {
|
|
152
|
+
throw new TapeDataUnavailableError([
|
|
153
|
+
{
|
|
154
|
+
code: "non_monotonic",
|
|
155
|
+
symbol: request.symbol,
|
|
156
|
+
timeframe: request.timeframe,
|
|
157
|
+
expected: rows[rows.length - 1][0] + stepMs,
|
|
158
|
+
actual: timestamp,
|
|
159
|
+
timestamp,
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
rows.push([
|
|
164
|
+
timestamp,
|
|
165
|
+
Number(raw[1]),
|
|
166
|
+
Number(raw[2]),
|
|
167
|
+
Number(raw[3]),
|
|
168
|
+
Number(raw[4]),
|
|
169
|
+
Number(raw[5]),
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveFetchSinceMs(request, cursor) {
|
|
175
|
+
return request.exchangeDefinition.ccxtId === "bitget"
|
|
176
|
+
? Math.max(0, cursor - 1)
|
|
177
|
+
: cursor;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function ohlcvPageLimitForTimeframe(exchange, runtimeConfig, timeframe) {
|
|
181
|
+
if (exchange.ccxtId !== "bitget") return runtimeConfig.ohlcvPageLimit;
|
|
182
|
+
return Math.max(
|
|
183
|
+
1,
|
|
184
|
+
Math.min(
|
|
185
|
+
runtimeConfig.ohlcvPageLimit,
|
|
186
|
+
Math.floor(BITGET_OHLCV_MAX_REQUEST_SPAN_MS / timeframeStepMs(timeframe))
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function timeframeStepMs(timeframe) {
|
|
192
|
+
const stepMs = TIMEFRAME_MS[timeframe];
|
|
193
|
+
if (!stepMs) throw new Error(`不支持的 timeframe:${timeframe}`);
|
|
194
|
+
return stepMs;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function midpointCursorMs(lowerMs, upperMs, stepMs) {
|
|
198
|
+
if (upperMs - lowerMs <= stepMs) return upperMs;
|
|
199
|
+
const midpointMs = lowerMs + Math.floor((upperMs - lowerMs) / 2);
|
|
200
|
+
const alignedMs = Math.floor(midpointMs / stepMs) * stepMs;
|
|
201
|
+
return Math.min(upperMs, Math.max(lowerMs + 1, alignedMs));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function invalidOhlcvError(request) {
|
|
205
|
+
return new TapeDataUnavailableError([
|
|
206
|
+
{
|
|
207
|
+
code: "invalid_ohlcv",
|
|
208
|
+
symbol: request.symbol,
|
|
209
|
+
timeframe: request.timeframe,
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function missingOhlcvError(request) {
|
|
215
|
+
return new TapeDataUnavailableError([
|
|
216
|
+
{
|
|
217
|
+
code: "ohlcv_missing",
|
|
218
|
+
symbol: request.symbol,
|
|
219
|
+
timeframe: request.timeframe,
|
|
220
|
+
},
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
@@ -19,9 +19,15 @@ import {
|
|
|
19
19
|
buildCcxtConstructorOptions,
|
|
20
20
|
ccxtExchangeClientCacheKey,
|
|
21
21
|
} from "./proxy.mjs";
|
|
22
|
-
import { loadSeriesWithCache
|
|
22
|
+
import { loadSeriesWithCache } from "./series.mjs";
|
|
23
|
+
import {
|
|
24
|
+
limitTapeOhlcvConcurrency,
|
|
25
|
+
mapWithConcurrency,
|
|
26
|
+
resolveTapeSeriesConcurrency,
|
|
27
|
+
} from "./tape-loader-concurrency.mjs";
|
|
23
28
|
import {
|
|
24
29
|
ENGINE_BACKTEST_BASE_TIMEFRAME,
|
|
30
|
+
TIMEFRAME_MS,
|
|
25
31
|
baseTimeframeStepMs,
|
|
26
32
|
resolvePlanBaseTimeframe,
|
|
27
33
|
} from "./timeframes.mjs";
|
|
@@ -40,47 +46,8 @@ const FUNDING_INTERVALS = [
|
|
|
40
46
|
{ interval: "8h", spacingMs: 28_800_000 },
|
|
41
47
|
];
|
|
42
48
|
|
|
43
|
-
/** Independent symbol×timeframe (and funding) fetches. Pagination stays serial. */
|
|
44
|
-
export const DEFAULT_TAPE_SERIES_CONCURRENCY = 4;
|
|
45
|
-
export const MAX_TAPE_SERIES_CONCURRENCY = 8;
|
|
46
|
-
|
|
47
49
|
const exchangePromises = new Map();
|
|
48
50
|
|
|
49
|
-
export function resolveTapeSeriesConcurrency(value) {
|
|
50
|
-
if (typeof value === "number" && Number.isFinite(value) && value >= 1) {
|
|
51
|
-
return Math.min(
|
|
52
|
-
MAX_TAPE_SERIES_CONCURRENCY,
|
|
53
|
-
Math.max(1, Math.floor(value))
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
return DEFAULT_TAPE_SERIES_CONCURRENCY;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function mapWithConcurrency(items, concurrency, worker) {
|
|
60
|
-
const list = [...items];
|
|
61
|
-
if (list.length === 0) {
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
const limit = Math.min(
|
|
65
|
-
list.length,
|
|
66
|
-
Math.max(1, Math.floor(Number(concurrency)) || 1)
|
|
67
|
-
);
|
|
68
|
-
const results = new Array(list.length);
|
|
69
|
-
let nextIndex = 0;
|
|
70
|
-
async function runWorker() {
|
|
71
|
-
while (true) {
|
|
72
|
-
const index = nextIndex;
|
|
73
|
-
nextIndex += 1;
|
|
74
|
-
if (index >= list.length) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
results[index] = await worker(list[index], index);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
await Promise.all(Array.from({ length: limit }, () => runWorker()));
|
|
81
|
-
return results;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
51
|
export function effectiveTapeEndMs(
|
|
85
52
|
requestedToMs,
|
|
86
53
|
nowMs = Date.now(),
|
|
@@ -287,6 +254,11 @@ export async function loadTape(request, options = {}) {
|
|
|
287
254
|
const dataIssues = [];
|
|
288
255
|
const coverageWarnings = [];
|
|
289
256
|
const coverageIssues = [];
|
|
257
|
+
const ohlcvExchange = limitTapeOhlcvConcurrency(
|
|
258
|
+
exchange,
|
|
259
|
+
seriesConcurrency,
|
|
260
|
+
request.signal
|
|
261
|
+
);
|
|
290
262
|
const seriesFractions = new Array(totalSeries).fill(0);
|
|
291
263
|
let lastOhlcvDetail = "";
|
|
292
264
|
const reportOhlcv = (index, fraction, detail) => {
|
|
@@ -311,7 +283,7 @@ export async function loadTape(request, options = {}) {
|
|
|
311
283
|
const detail = `${symbol} ${timeframe}`;
|
|
312
284
|
try {
|
|
313
285
|
const loaded = await loadSeriesWithCache(
|
|
314
|
-
|
|
286
|
+
ohlcvExchange,
|
|
315
287
|
exchangeDefinition,
|
|
316
288
|
runtimeConfig,
|
|
317
289
|
symbol,
|