@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.
Files changed (104) hide show
  1. package/README.md +42 -4
  2. package/dist/auth/browser-login.d.ts +30 -0
  3. package/dist/auth/browser-login.js +193 -0
  4. package/dist/auth/loopback-callback.d.ts +32 -0
  5. package/dist/auth/loopback-callback.js +175 -0
  6. package/dist/auth/open-browser.d.ts +15 -0
  7. package/dist/auth/open-browser.js +54 -0
  8. package/dist/auth/refresh.d.ts +27 -2
  9. package/dist/auth/refresh.js +52 -15
  10. package/dist/catalog/allowlist.d.ts +20 -4
  11. package/dist/catalog/allowlist.js +126 -25
  12. package/dist/catalog/command-tree.d.ts +35 -0
  13. package/dist/catalog/command-tree.js +119 -0
  14. package/dist/catalog/compatibility.d.ts +23 -0
  15. package/dist/catalog/compatibility.js +58 -0
  16. package/dist/catalog/generated/registry.json +6137 -0
  17. package/dist/catalog/generated/schemas.json +31036 -0
  18. package/dist/catalog/operations.d.ts +76 -3
  19. package/dist/catalog/operations.js +87 -213
  20. package/dist/catalog/validate-body.d.ts +22 -0
  21. package/dist/catalog/validate-body.js +98 -0
  22. package/dist/commands/request-body.d.ts +21 -0
  23. package/dist/commands/request-body.js +92 -0
  24. package/dist/commands/run.js +283 -162
  25. package/dist/config/profiles.js +3 -3
  26. package/dist/engine-backtest/errors.d.ts +18 -0
  27. package/dist/engine-backtest/errors.js +26 -0
  28. package/dist/engine-backtest/fetch-runtime.d.ts +38 -0
  29. package/dist/engine-backtest/fetch-runtime.js +170 -0
  30. package/dist/engine-backtest/native-import.d.ts +1 -0
  31. package/dist/engine-backtest/native-import.js +10 -0
  32. package/dist/engine-backtest/parse-args.d.ts +6 -0
  33. package/dist/engine-backtest/parse-args.js +281 -0
  34. package/dist/engine-backtest/persist.d.ts +32 -0
  35. package/dist/engine-backtest/persist.js +94 -0
  36. package/dist/engine-backtest/replay-timeframe.d.ts +16 -0
  37. package/dist/engine-backtest/replay-timeframe.js +48 -0
  38. package/dist/engine-backtest/resolve-packages.d.ts +27 -0
  39. package/dist/engine-backtest/resolve-packages.js +288 -0
  40. package/dist/engine-backtest/run-command.d.ts +38 -0
  41. package/dist/engine-backtest/run-command.js +540 -0
  42. package/dist/engine-backtest/types.d.ts +249 -0
  43. package/dist/engine-backtest/types.js +2 -0
  44. package/dist/envelope.d.ts +3 -0
  45. package/dist/envelope.js +43 -3
  46. package/dist/http/client.js +18 -15
  47. package/dist/index.d.ts +16 -5
  48. package/dist/index.js +47 -1
  49. package/dist/install/exec.d.ts +13 -0
  50. package/dist/install/exec.js +73 -0
  51. package/dist/install/package-root.d.ts +4 -0
  52. package/dist/install/package-root.js +59 -0
  53. package/dist/install/types.d.ts +69 -0
  54. package/dist/install/types.js +26 -0
  55. package/dist/install/wizard.d.ts +21 -0
  56. package/dist/install/wizard.js +332 -0
  57. package/dist/keychain/linux-secret-service.d.ts +11 -0
  58. package/dist/keychain/linux-secret-service.js +93 -0
  59. package/dist/keychain/store.d.ts +20 -1
  60. package/dist/keychain/store.js +94 -6
  61. package/dist/keychain/windows-credential.d.ts +13 -0
  62. package/dist/keychain/windows-credential.js +176 -0
  63. package/dist/safety/confirmation.d.ts +10 -3
  64. package/dist/safety/confirmation.js +27 -4
  65. package/dist/version.d.ts +2 -2
  66. package/dist/version.js +3 -2
  67. package/docs/.nojekyll +0 -0
  68. package/docs/agents/domain.md +51 -0
  69. package/docs/agents/issue-tracker.md +156 -0
  70. package/docs/agents/triage-labels.md +18 -0
  71. package/docs/alphafox-cli-installation-guide.md +105 -0
  72. package/docs/e2e-staging.md +76 -6
  73. package/docs/favicon.svg +4 -0
  74. package/docs/index.html +748 -0
  75. package/docs/logo/alphafox-mark.svg +4 -0
  76. package/docs/logo/alphafox-wordmark-black-en.svg +17 -0
  77. package/docs/logo/alphafox-wordmark-white-en.svg +16 -0
  78. package/docs/release-supply-chain.md +107 -26
  79. package/package.json +10 -3
  80. package/skills/account/SKILL.md +8 -6
  81. package/skills/admin/SKILL.md +9 -4
  82. package/skills/alphafox-shared/SKILL.md +44 -14
  83. package/skills/auth/SKILL.md +21 -6
  84. package/skills/engine-backtest/SKILL.md +71 -0
  85. package/skills/exchange/SKILL.md +10 -3
  86. package/skills/market/SKILL.md +9 -4
  87. package/skills/notification/SKILL.md +8 -3
  88. package/skills/strategy/SKILL.md +23 -9
  89. package/skills/trading/SKILL.md +18 -5
  90. package/vendor/backtest-runner/NOTICE.md +1 -0
  91. package/vendor/backtest-runner/README.md +97 -0
  92. package/vendor/backtest-runner/index.d.ts +423 -0
  93. package/vendor/backtest-runner/index.mjs +59 -0
  94. package/vendor/backtest-runner/lib/abortable.mjs +23 -0
  95. package/vendor/backtest-runner/lib/cache.mjs +159 -0
  96. package/vendor/backtest-runner/lib/coverage.mjs +238 -0
  97. package/vendor/backtest-runner/lib/encode.mjs +28 -0
  98. package/vendor/backtest-runner/lib/exchanges.mjs +143 -0
  99. package/vendor/backtest-runner/lib/proxy.mjs +78 -0
  100. package/vendor/backtest-runner/lib/scenario.mjs +66 -0
  101. package/vendor/backtest-runner/lib/series.mjs +370 -0
  102. package/vendor/backtest-runner/lib/tape-loader.mjs +614 -0
  103. package/vendor/backtest-runner/lib/timeframes.mjs +55 -0
  104. package/vendor/backtest-runner/package.json +13 -0
@@ -0,0 +1,614 @@
1
+ import { abortable } from "./abortable.mjs";
2
+ import {
3
+ createDisabledTapeCache,
4
+ createFileTapeCache,
5
+ DEFAULT_TAPE_CACHE_DIR,
6
+ } from "./cache.mjs";
7
+ import {
8
+ formatCoverageSoftWarning,
9
+ isTapeDataUnavailableError,
10
+ TapeDataUnavailableError,
11
+ } from "./coverage.mjs";
12
+ import { encodeOhlcvColumns } from "./encode.mjs";
13
+ import {
14
+ resolveTapeExchange,
15
+ tapeExchangeRuntimeConfig,
16
+ } from "./exchanges.mjs";
17
+ import {
18
+ buildCcxtConstructorOptions,
19
+ ccxtExchangeClientCacheKey,
20
+ } from "./proxy.mjs";
21
+ import { loadSeriesWithCache, TIMEFRAME_MS } from "./series.mjs";
22
+ import {
23
+ ENGINE_BACKTEST_BASE_TIMEFRAME,
24
+ baseTimeframeStepMs,
25
+ resolvePlanBaseTimeframe,
26
+ } from "./timeframes.mjs";
27
+
28
+ const DAY_MS = 86_400_000;
29
+ const MARKETS_PROGRESS_END = 0.05;
30
+ const OHLCV_PROGRESS_END = 0.95;
31
+ const FUNDING_INTERVAL_TOLERANCE_MS = 5 * 60_000;
32
+ const CCXT_PRECISION_MODE_DECIMAL_PLACES = 2;
33
+ const CCXT_PRECISION_MODE_SIGNIFICANT_DIGITS = 3;
34
+ const CCXT_PRECISION_MODE_TICK_SIZE = 4;
35
+ const FUNDING_INTERVALS = [
36
+ { interval: "1h", spacingMs: 3_600_000 },
37
+ { interval: "2h", spacingMs: 7_200_000 },
38
+ { interval: "4h", spacingMs: 14_400_000 },
39
+ { interval: "8h", spacingMs: 28_800_000 },
40
+ ];
41
+
42
+ const exchangePromises = new Map();
43
+
44
+ export function effectiveTapeEndMs(
45
+ requestedToMs,
46
+ nowMs = Date.now(),
47
+ baseTimeframe = ENGINE_BACKTEST_BASE_TIMEFRAME
48
+ ) {
49
+ const baseStepMs = baseTimeframeStepMs(baseTimeframe);
50
+ return Math.min(requestedToMs, Math.floor(nowMs / baseStepMs) * baseStepMs);
51
+ }
52
+
53
+ export function resolveEngineBacktestPrecisionMode(precisionMode, exchangeLabel) {
54
+ if (precisionMode === CCXT_PRECISION_MODE_DECIMAL_PLACES) {
55
+ return "DECIMAL_PLACES";
56
+ }
57
+ if (precisionMode === CCXT_PRECISION_MODE_TICK_SIZE) {
58
+ return "TICK_SIZE";
59
+ }
60
+ const modeLabel =
61
+ precisionMode === CCXT_PRECISION_MODE_SIGNIFICANT_DIGITS
62
+ ? "SIGNIFICANT_DIGITS"
63
+ : String(precisionMode);
64
+ throw new Error(
65
+ `${exchangeLabel} 使用不支持的 CCXT precisionMode:${modeLabel}`
66
+ );
67
+ }
68
+
69
+ export function classifyTapeSymbolsForPreflight(symbols, markets, quoteAsset) {
70
+ const availableSymbols = [];
71
+ const missingSymbols = [];
72
+ for (const symbol of new Set(symbols)) {
73
+ if (isLinearSwapMarket(markets[symbol], quoteAsset)) {
74
+ availableSymbols.push(symbol);
75
+ } else {
76
+ missingSymbols.push(symbol);
77
+ }
78
+ }
79
+ return { availableSymbols, missingSymbols };
80
+ }
81
+
82
+ export function inferFundingIntervals(samples) {
83
+ if (samples.length < 2) {
84
+ return samples.map((sample) => ({ ...sample, interval: "8h" }));
85
+ }
86
+
87
+ return samples.map((sample, index) => {
88
+ const nextSample = samples[index + 1];
89
+ const previousSample = samples[index - 1];
90
+ const spacingMs = nextSample
91
+ ? nextSample.timestamp - sample.timestamp
92
+ : previousSample
93
+ ? sample.timestamp - previousSample.timestamp
94
+ : Number.NaN;
95
+ return {
96
+ ...sample,
97
+ interval: fundingIntervalFromSpacing(spacingMs),
98
+ };
99
+ });
100
+ }
101
+
102
+ export function resolveTapeCache(options = {}) {
103
+ if (
104
+ options.cache === false ||
105
+ options.cache === "disable" ||
106
+ options.cache?.disabled === true
107
+ ) {
108
+ return createDisabledTapeCache();
109
+ }
110
+ if (options.cache && typeof options.cache.read === "function") {
111
+ return options.cache;
112
+ }
113
+ if (options.cacheDir) {
114
+ return createFileTapeCache(options.cacheDir);
115
+ }
116
+ return createFileTapeCache(DEFAULT_TAPE_CACHE_DIR);
117
+ }
118
+
119
+ /**
120
+ * Load a closed-candle tape and columnar buffers.
121
+ * Inject `ohlcvFetcher` / `fundingFetcher` / `marketsLoader` to avoid ccxt.
122
+ *
123
+ * @param {import("../index.d.ts").TapeLoadRequest} request
124
+ * @param {import("../index.d.ts").TapeLoadOptions} [options]
125
+ */
126
+ export async function loadTape(request, options = {}) {
127
+ request.signal?.throwIfAborted();
128
+ if (!request || request.symbols?.length === 0) {
129
+ throw new Error("回测至少需要一个 symbol");
130
+ }
131
+ if (request.toMs <= request.fromMs) {
132
+ throw new Error("回测区间结束时间必须晚于开始时间");
133
+ }
134
+ if ((request.auxiliaryDataRequirements?.length ?? 0) > 0) {
135
+ throw new Error(
136
+ `Unsupported backtest auxiliary data requirements: ${request.auxiliaryDataRequirements
137
+ .map((requirement) => requirement.kind)
138
+ .join(", ")}`
139
+ );
140
+ }
141
+
142
+ const exchangeDefinition = resolveRequestExchange(request);
143
+ const onProgress = request.onProgress ?? options.onProgress;
144
+ const cache = resolveTapeCache(options);
145
+ const dataQualityMode = request.dataQualityMode ?? "strict";
146
+ const baseTimeframe = resolvePlanBaseTimeframe({
147
+ baseTimeframe: request.baseTimeframe,
148
+ timeframes: [
149
+ ...request.timeframes,
150
+ ...(request.seriesRequirements ?? []).map(
151
+ (requirement) => requirement.timeframe
152
+ ),
153
+ ],
154
+ });
155
+ const runtimeConfig = tapeExchangeRuntimeConfig(exchangeDefinition);
156
+ const timeframes = normalizeTimeframes(
157
+ [
158
+ ...request.timeframes,
159
+ ...(request.seriesRequirements ?? []).map(
160
+ (requirement) => requirement.timeframe
161
+ ),
162
+ ],
163
+ baseTimeframe,
164
+ runtimeConfig,
165
+ exchangeDefinition.label
166
+ );
167
+ const requirementWarmups = new Map(
168
+ (request.seriesRequirements ?? []).map((requirement) => [
169
+ `${requirement.symbol}\u0000${requirement.timeframe}`,
170
+ requirement.minWarmupCandles,
171
+ ])
172
+ );
173
+
174
+ onProgress?.({
175
+ stage: "markets",
176
+ detail: `加载 ${exchangeDefinition.label} 市场元数据`,
177
+ fraction: 0,
178
+ });
179
+
180
+ const injected = usesInjectedRuntime(request);
181
+ if (injected) {
182
+ assertInjectedRuntime(request);
183
+ }
184
+
185
+ const exchange = injected
186
+ ? createInjectedExchange(request)
187
+ : await abortable(
188
+ getTapeExchange(exchangeDefinition, options),
189
+ request.signal
190
+ );
191
+ const marketsSnapshot = await abortable(
192
+ exchange.loadMarkets(),
193
+ request.signal
194
+ );
195
+ const precisionMode = resolveEngineBacktestPrecisionMode(
196
+ exchange.precisionMode,
197
+ exchangeDefinition.label
198
+ );
199
+ const exchangeNowMs =
200
+ request.nowMs ??
201
+ options.nowMs ??
202
+ (await abortable(exchange.fetchTime(), request.signal));
203
+ const tapeToMs = effectiveTapeEndMs(
204
+ request.toMs,
205
+ exchangeNowMs,
206
+ baseTimeframe
207
+ );
208
+ const cacheUntilMs = Math.min(
209
+ tapeToMs,
210
+ Math.floor(exchangeNowMs / DAY_MS) * DAY_MS
211
+ );
212
+ if (tapeToMs <= request.fromMs) {
213
+ throw new Error("回测区间内还没有已收盘的 K 线");
214
+ }
215
+ request.signal?.throwIfAborted();
216
+
217
+ const markets = {};
218
+ for (const symbol of request.symbols) {
219
+ markets[symbol] = tapeMarketFromCcxt(
220
+ symbol,
221
+ exchange.markets?.[symbol] ?? marketsSnapshot?.[symbol],
222
+ exchangeDefinition.label,
223
+ precisionMode,
224
+ exchangeDefinition.quoteAsset
225
+ );
226
+ }
227
+ onProgress?.({
228
+ stage: "markets",
229
+ detail: `${exchangeDefinition.label} 市场元数据已加载`,
230
+ fraction: MARKETS_PROGRESS_END,
231
+ });
232
+
233
+ const buffers = {};
234
+ const chartSeries = [];
235
+ const series = [];
236
+ const totalSeries = request.symbols.length * timeframes.length;
237
+ const dataIssues = [];
238
+ const coverageWarnings = [];
239
+ let seriesDone = 0;
240
+ let bufferSequence = 0;
241
+ for (const symbol of request.symbols) {
242
+ for (const timeframe of timeframes) {
243
+ request.signal?.throwIfAborted();
244
+ let loaded;
245
+ try {
246
+ loaded = await loadSeriesWithCache(
247
+ exchange,
248
+ exchangeDefinition,
249
+ runtimeConfig,
250
+ symbol,
251
+ timeframe,
252
+ exchange.markets?.[symbol] ?? marketsSnapshot?.[symbol],
253
+ request.fromMs,
254
+ tapeToMs,
255
+ requirementWarmups.get(`${symbol}\u0000${timeframe}`) ?? 0,
256
+ timeframe === baseTimeframe,
257
+ dataQualityMode,
258
+ (fraction) => {
259
+ onProgress?.({
260
+ stage: "ohlcv",
261
+ detail: `${symbol} ${timeframe}`,
262
+ fraction:
263
+ MARKETS_PROGRESS_END +
264
+ (OHLCV_PROGRESS_END - MARKETS_PROGRESS_END) *
265
+ ((seriesDone + fraction) / totalSeries),
266
+ });
267
+ },
268
+ cacheUntilMs,
269
+ cache,
270
+ request.signal
271
+ );
272
+ } catch (error) {
273
+ request.signal?.throwIfAborted();
274
+ dataIssues.push(...toTapeDataIssues(error, symbol, timeframe));
275
+ seriesDone++;
276
+ continue;
277
+ }
278
+ if (loaded.softIssues.length > 0) {
279
+ coverageWarnings.push(
280
+ formatCoverageSoftWarning(loaded.softIssues, loaded.coverageRatio)
281
+ );
282
+ }
283
+ const { rows } = loaded;
284
+ const bufferKey = `k${bufferSequence++}`;
285
+ const buffer = encodeOhlcvColumns(rows);
286
+ buffers[bufferKey] = buffer;
287
+ if (timeframe === baseTimeframe) {
288
+ chartSeries.push({
289
+ symbol,
290
+ timeframe: baseTimeframe,
291
+ rows: rows.length,
292
+ buffer: buffer.slice(0),
293
+ });
294
+ }
295
+ series.push({ symbol, timeframe, buffer: bufferKey, rows: rows.length });
296
+ seriesDone++;
297
+ }
298
+ }
299
+ if (dataIssues.length > 0) {
300
+ throw new TapeDataUnavailableError(dataIssues);
301
+ }
302
+
303
+ let fundingRates;
304
+ if (request.needsFunding) {
305
+ fundingRates = {};
306
+ for (const symbol of request.symbols) {
307
+ request.signal?.throwIfAborted();
308
+ fundingRates[symbol] = await loadFundingHistory(
309
+ exchange,
310
+ exchangeDefinition,
311
+ runtimeConfig,
312
+ symbol,
313
+ request.fromMs,
314
+ tapeToMs,
315
+ request.signal
316
+ );
317
+ }
318
+ }
319
+ request.signal?.throwIfAborted();
320
+ onProgress?.({
321
+ stage: "validation",
322
+ detail: "行情数据校验完成",
323
+ fraction: 1,
324
+ });
325
+
326
+ return {
327
+ tape: {
328
+ from: new Date(request.fromMs).toISOString(),
329
+ to: new Date(tapeToMs).toISOString(),
330
+ baseTimeframe,
331
+ markets,
332
+ series,
333
+ ...(fundingRates ? { fundingRates } : {}),
334
+ lowLiquiditySymbols: [],
335
+ },
336
+ buffers,
337
+ coverageWarnings,
338
+ chartData: {
339
+ exchangeId: exchangeDefinition.id,
340
+ series: chartSeries,
341
+ },
342
+ };
343
+ }
344
+
345
+ function resolveRequestExchange(request) {
346
+ if (request.exchange) {
347
+ return resolveTapeExchange(request.exchange);
348
+ }
349
+ return resolveTapeExchange(request.exchangeId);
350
+ }
351
+
352
+ function usesInjectedRuntime(request) {
353
+ return Boolean(
354
+ request.ohlcvFetcher || request.fundingFetcher || request.marketsLoader
355
+ );
356
+ }
357
+
358
+ function assertInjectedRuntime(request) {
359
+ if (!request.marketsLoader) {
360
+ throw new Error(
361
+ "loadTape requires marketsLoader when using injected fetchers (refusing to construct a live ccxt client)"
362
+ );
363
+ }
364
+ if (!request.ohlcvFetcher) {
365
+ throw new Error(
366
+ "loadTape requires ohlcvFetcher when using injected fetchers (refusing to construct a live ccxt client)"
367
+ );
368
+ }
369
+ if (request.needsFunding && !request.fundingFetcher) {
370
+ throw new Error(
371
+ "loadTape requires fundingFetcher when needsFunding is set with injected fetchers"
372
+ );
373
+ }
374
+ }
375
+
376
+ function createInjectedExchange(request) {
377
+ let markets = {};
378
+ let precisionMode = CCXT_PRECISION_MODE_DECIMAL_PLACES;
379
+ let nowMs;
380
+ return {
381
+ get markets() {
382
+ return markets;
383
+ },
384
+ get precisionMode() {
385
+ return precisionMode;
386
+ },
387
+ async loadMarkets() {
388
+ const snapshot = await request.marketsLoader();
389
+ if (!snapshot || typeof snapshot !== "object" || !snapshot.markets) {
390
+ throw new Error("marketsLoader must return { markets }");
391
+ }
392
+ markets = snapshot.markets;
393
+ if (snapshot.precisionMode !== undefined) {
394
+ precisionMode = snapshot.precisionMode;
395
+ }
396
+ if (snapshot.nowMs !== undefined) {
397
+ nowMs = snapshot.nowMs;
398
+ }
399
+ return markets;
400
+ },
401
+ async fetchTime() {
402
+ return nowMs ?? Date.now();
403
+ },
404
+ async fetchOHLCV(symbol, timeframe, since, limit, params) {
405
+ return request.ohlcvFetcher(symbol, timeframe, since, limit, params);
406
+ },
407
+ async fetchFundingRateHistory(symbol, since, limit, params) {
408
+ if (!request.fundingFetcher) {
409
+ throw new Error("fundingFetcher is required when needsFunding is set");
410
+ }
411
+ return request.fundingFetcher(symbol, since, limit, params);
412
+ },
413
+ };
414
+ }
415
+
416
+ async function getTapeExchange(exchangeDefinition, options) {
417
+ const runtimeConfig = tapeExchangeRuntimeConfig(exchangeDefinition);
418
+ const constructorOptions = buildCcxtConstructorOptions(exchangeDefinition, {
419
+ ...options,
420
+ runtimeConfig,
421
+ });
422
+ if (typeof options.createExchange === "function") {
423
+ const exchange = await options.createExchange(
424
+ exchangeDefinition,
425
+ constructorOptions
426
+ );
427
+ await exchange.loadMarkets();
428
+ return exchange;
429
+ }
430
+
431
+ const cacheKey = ccxtExchangeClientCacheKey(exchangeDefinition.id, options);
432
+ const cached = exchangePromises.get(cacheKey);
433
+ if (cached) {
434
+ return cached;
435
+ }
436
+
437
+ const pending = (async () => {
438
+
439
+ const ccxtModule = await import("ccxt");
440
+ const namespace = (ccxtModule.default ?? ccxtModule);
441
+ const ExchangeCtor = namespace[exchangeDefinition.ccxtId];
442
+ if (typeof ExchangeCtor !== "function") {
443
+ throw new Error(`ccxt 不包含 ${exchangeDefinition.ccxtId} 交易所实现`);
444
+ }
445
+ const exchange = new ExchangeCtor(constructorOptions);
446
+ await exchange.loadMarkets();
447
+ return exchange;
448
+ })().catch((error) => {
449
+ if (exchangePromises.get(cacheKey) === pending) {
450
+ exchangePromises.delete(cacheKey);
451
+ }
452
+ throw error;
453
+ });
454
+ exchangePromises.set(cacheKey, pending);
455
+ return pending;
456
+ }
457
+
458
+ function normalizeTimeframes(
459
+ timeframes,
460
+ baseTimeframe,
461
+ runtimeConfig,
462
+ exchangeLabel
463
+ ) {
464
+ const set = new Set([baseTimeframe, ...timeframes]);
465
+ const unsupported = new Set(runtimeConfig.unsupportedTimeframes ?? []);
466
+ for (const timeframe of set) {
467
+ if (!TIMEFRAME_MS[timeframe]) {
468
+ throw new Error(`不支持的 timeframe:${timeframe}`);
469
+ }
470
+ if (unsupported.has(timeframe)) {
471
+ throw new Error(
472
+ `${exchangeLabel} 数据源不提供 ${timeframe} K 线,无法回测该周期的策略`
473
+ );
474
+ }
475
+ }
476
+ return [...set].sort(
477
+ (left, right) => (TIMEFRAME_MS[left] ?? 0) - (TIMEFRAME_MS[right] ?? 0)
478
+ );
479
+ }
480
+
481
+ function tapeMarketFromCcxt(
482
+ symbol,
483
+ market,
484
+ exchangeLabel,
485
+ precisionMode,
486
+ quoteAsset
487
+ ) {
488
+ if (!market) {
489
+ throw new Error(`${exchangeLabel} 不存在市场 ${symbol}`);
490
+ }
491
+ if (!isLinearSwapMarket(market, quoteAsset)) {
492
+ throw new Error(
493
+ `${exchangeLabel} 市场 ${symbol} 不是可交易的 ${quoteAsset} 线性永续合约`
494
+ );
495
+ }
496
+ if (!market.id) {
497
+ throw new Error(`${exchangeLabel} 市场 ${symbol} 缺少交易所 id`);
498
+ }
499
+ return {
500
+ id: market.id,
501
+ symbol,
502
+ base: market.base,
503
+ quote: market.quote,
504
+ settle: market.settle,
505
+ contractSize: market.contractSize ?? 1,
506
+ linear: true,
507
+ precisionMode,
508
+ precisionAmount: market.precision?.amount,
509
+ precisionPrice: market.precision?.price,
510
+ amountMin: market.limits?.amount?.min,
511
+ amountMax: market.limits?.amount?.max,
512
+ priceMin: market.limits?.price?.min,
513
+ costMin: market.limits?.cost?.min,
514
+ };
515
+ }
516
+
517
+ function isLinearSwapMarket(market, quoteAsset) {
518
+ return Boolean(
519
+ market &&
520
+ market.active !== false &&
521
+ (market.type === "swap" || market.swap === true) &&
522
+ market.inverse !== true &&
523
+ (market.linear === true || market.settle === quoteAsset) &&
524
+ (market.quote === quoteAsset || market.settle === quoteAsset) &&
525
+ market.symbol
526
+ );
527
+ }
528
+
529
+ async function loadFundingHistory(
530
+ exchange,
531
+ exchangeDefinition,
532
+ runtimeConfig,
533
+ symbol,
534
+ fromMs,
535
+ toMs,
536
+ signal
537
+ ) {
538
+ signal?.throwIfAborted();
539
+ const sinceMs = fromMs - TIMEFRAME_MS["8h"];
540
+ const samples = [];
541
+ let cursor = sinceMs;
542
+ while (cursor < toMs) {
543
+ signal?.throwIfAborted();
544
+ const page = await abortable(
545
+ exchange.fetchFundingRateHistory(
546
+ symbol,
547
+ cursor,
548
+ runtimeConfig.fundingPageLimit,
549
+ { ...runtimeConfig.requestParams }
550
+ ),
551
+ signal
552
+ );
553
+ signal?.throwIfAborted();
554
+ if (page.length === 0) {
555
+ break;
556
+ }
557
+ let advanced = false;
558
+ for (const entry of page) {
559
+ const timestamp = entry.timestamp;
560
+ const rate = entry.fundingRate;
561
+ if (typeof timestamp !== "number" || typeof rate !== "number") {
562
+ continue;
563
+ }
564
+ if (timestamp >= toMs) {
565
+ continue;
566
+ }
567
+ if (
568
+ samples.length > 0 &&
569
+ timestamp <= samples[samples.length - 1].timestamp
570
+ ) {
571
+ continue;
572
+ }
573
+ samples.push({ timestamp, rate });
574
+ advanced = true;
575
+ }
576
+ const lastTimestamp = page[page.length - 1]?.timestamp;
577
+ if (
578
+ typeof lastTimestamp !== "number" ||
579
+ lastTimestamp + 1 <= cursor ||
580
+ !advanced
581
+ ) {
582
+ break;
583
+ }
584
+ cursor = lastTimestamp + 1;
585
+ }
586
+ if (samples.length === 0) {
587
+ throw new Error(
588
+ `${exchangeDefinition.label} 区间内没有 ${symbol} funding 历史数据(DCA funding filter 需要该数据)`
589
+ );
590
+ }
591
+ return inferFundingIntervals(samples);
592
+ }
593
+
594
+ function fundingIntervalFromSpacing(spacingMs) {
595
+ const match = FUNDING_INTERVALS.find(
596
+ (candidate) =>
597
+ Math.abs(candidate.spacingMs - spacingMs) <= FUNDING_INTERVAL_TOLERANCE_MS
598
+ );
599
+ return match?.interval ?? "8h";
600
+ }
601
+
602
+ function toTapeDataIssues(error, symbol, timeframe) {
603
+ if (isTapeDataUnavailableError(error)) {
604
+ return [...error.issues];
605
+ }
606
+ return [
607
+ {
608
+ code: "load_failed",
609
+ symbol,
610
+ timeframe,
611
+ message: error instanceof Error ? error.message : String(error),
612
+ },
613
+ ];
614
+ }
@@ -0,0 +1,55 @@
1
+ export const ENGINE_BACKTEST_BASE_TIMEFRAME = "1m";
2
+ export const ENGINE_BACKTEST_WARMUP_CANDLES = 500;
3
+
4
+ /** Timeframes the tape loader can fetch natively. */
5
+ export const TIMEFRAME_MS = Object.freeze({
6
+ "1m": 60_000,
7
+ "3m": 180_000,
8
+ "5m": 300_000,
9
+ "15m": 900_000,
10
+ "30m": 1_800_000,
11
+ "1h": 3_600_000,
12
+ "2h": 7_200_000,
13
+ "4h": 14_400_000,
14
+ "6h": 21_600_000,
15
+ "8h": 28_800_000,
16
+ "12h": 43_200_000,
17
+ "1d": 86_400_000,
18
+ "3d": 259_200_000,
19
+ "1w": 604_800_000,
20
+ });
21
+
22
+ /** Plan resolution also accepts 1M; series fetch still rejects it. */
23
+ const PLAN_TIMEFRAME_MS = Object.freeze({
24
+ ...TIMEFRAME_MS,
25
+ "1M": 2_592_000_000,
26
+ });
27
+
28
+ export function resolvePlanBaseTimeframe(input) {
29
+ const explicit = input.baseTimeframe?.trim();
30
+ if (explicit && PLAN_TIMEFRAME_MS[explicit]) {
31
+ return explicit;
32
+ }
33
+ const candidates = (input.timeframes ?? [])
34
+ .map((value) => value.trim())
35
+ .filter((value) => PLAN_TIMEFRAME_MS[value]);
36
+ if (candidates.length === 0) {
37
+ return ENGINE_BACKTEST_BASE_TIMEFRAME;
38
+ }
39
+ let best = candidates[0];
40
+ for (const timeframe of candidates) {
41
+ const bestMs = PLAN_TIMEFRAME_MS[best];
42
+ const nextMs = PLAN_TIMEFRAME_MS[timeframe];
43
+ if (nextMs < bestMs || (nextMs === bestMs && timeframe < best)) {
44
+ best = timeframe;
45
+ }
46
+ }
47
+ return best;
48
+ }
49
+
50
+ export function baseTimeframeStepMs(baseTimeframe) {
51
+ return (
52
+ PLAN_TIMEFRAME_MS[baseTimeframe] ??
53
+ PLAN_TIMEFRAME_MS[ENGINE_BACKTEST_BASE_TIMEFRAME]
54
+ );
55
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@alphafox/backtest-runner",
3
+ "private": true,
4
+ "type": "module",
5
+ "main": "./index.mjs",
6
+ "types": "./index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "default": "./index.mjs"
11
+ }
12
+ }
13
+ }