@alphafox/cli 0.2.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +42 -4
  2. package/dist/auth/loopback-callback.js +6 -6
  3. package/dist/catalog/command-tree.d.ts +1 -0
  4. package/dist/catalog/command-tree.js +5 -3
  5. package/dist/catalog/validate-body.d.ts +22 -0
  6. package/dist/catalog/validate-body.js +98 -0
  7. package/dist/commands/request-body.d.ts +21 -0
  8. package/dist/commands/request-body.js +92 -0
  9. package/dist/commands/run.js +113 -10
  10. package/dist/engine-backtest/errors.d.ts +18 -0
  11. package/dist/engine-backtest/errors.js +26 -0
  12. package/dist/engine-backtest/fetch-runtime.d.ts +38 -0
  13. package/dist/engine-backtest/fetch-runtime.js +170 -0
  14. package/dist/engine-backtest/native-import.d.ts +1 -0
  15. package/dist/engine-backtest/native-import.js +10 -0
  16. package/dist/engine-backtest/parse-args.d.ts +6 -0
  17. package/dist/engine-backtest/parse-args.js +281 -0
  18. package/dist/engine-backtest/persist.d.ts +32 -0
  19. package/dist/engine-backtest/persist.js +94 -0
  20. package/dist/engine-backtest/replay-timeframe.d.ts +16 -0
  21. package/dist/engine-backtest/replay-timeframe.js +48 -0
  22. package/dist/engine-backtest/resolve-packages.d.ts +27 -0
  23. package/dist/engine-backtest/resolve-packages.js +288 -0
  24. package/dist/engine-backtest/run-command.d.ts +38 -0
  25. package/dist/engine-backtest/run-command.js +540 -0
  26. package/dist/engine-backtest/types.d.ts +249 -0
  27. package/dist/engine-backtest/types.js +2 -0
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.js +16 -1
  30. package/dist/install/exec.d.ts +13 -0
  31. package/dist/install/exec.js +77 -0
  32. package/dist/install/package-root.d.ts +4 -0
  33. package/dist/install/package-root.js +59 -0
  34. package/dist/install/types.d.ts +69 -0
  35. package/dist/install/types.js +26 -0
  36. package/dist/install/wizard.d.ts +21 -0
  37. package/dist/install/wizard.js +330 -0
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/docs/.nojekyll +0 -0
  41. package/docs/alphafox-cli-installation-guide.md +105 -0
  42. package/docs/favicon.svg +4 -0
  43. package/docs/index.html +748 -0
  44. package/docs/logo/alphafox-mark.svg +4 -0
  45. package/docs/logo/alphafox-wordmark-black-en.svg +17 -0
  46. package/docs/logo/alphafox-wordmark-white-en.svg +16 -0
  47. package/docs/release-supply-chain.md +10 -2
  48. package/package.json +7 -2
  49. package/skills/account/SKILL.md +2 -2
  50. package/skills/admin/SKILL.md +2 -2
  51. package/skills/alphafox-shared/SKILL.md +22 -1
  52. package/skills/auth/SKILL.md +1 -1
  53. package/skills/engine-backtest/SKILL.md +71 -0
  54. package/skills/exchange/SKILL.md +2 -2
  55. package/skills/market/SKILL.md +1 -1
  56. package/skills/notification/SKILL.md +2 -2
  57. package/skills/strategy/SKILL.md +10 -2
  58. package/skills/trading/SKILL.md +6 -3
  59. package/vendor/backtest-runner/NOTICE.md +1 -0
  60. package/vendor/backtest-runner/README.md +97 -0
  61. package/vendor/backtest-runner/index.d.ts +423 -0
  62. package/vendor/backtest-runner/index.mjs +59 -0
  63. package/vendor/backtest-runner/lib/abortable.mjs +23 -0
  64. package/vendor/backtest-runner/lib/cache.mjs +159 -0
  65. package/vendor/backtest-runner/lib/coverage.mjs +238 -0
  66. package/vendor/backtest-runner/lib/encode.mjs +28 -0
  67. package/vendor/backtest-runner/lib/exchanges.mjs +143 -0
  68. package/vendor/backtest-runner/lib/proxy.mjs +78 -0
  69. package/vendor/backtest-runner/lib/scenario.mjs +66 -0
  70. package/vendor/backtest-runner/lib/series.mjs +370 -0
  71. package/vendor/backtest-runner/lib/tape-loader.mjs +614 -0
  72. package/vendor/backtest-runner/lib/timeframes.mjs +55 -0
  73. package/vendor/backtest-runner/package.json +13 -0
@@ -0,0 +1,370 @@
1
+ import { abortable } from "./abortable.mjs";
2
+ import {
3
+ missingCachedOhlcvRanges,
4
+ mergeCachedOhlcvRanges,
5
+ ohlcvSeriesCacheKey,
6
+ } from "./cache.mjs";
7
+ import {
8
+ evaluateOhlcvCoverage,
9
+ TapeDataUnavailableError,
10
+ } from "./coverage.mjs";
11
+ import {
12
+ ENGINE_BACKTEST_WARMUP_CANDLES,
13
+ TIMEFRAME_MS,
14
+ } from "./timeframes.mjs";
15
+
16
+ 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
+ export function isClosedCandle(timestampMs, timeframe, toMs) {
22
+ const stepMs = TIMEFRAME_MS[timeframe];
23
+ if (!stepMs) {
24
+ throw new Error(`不支持的 timeframe:${timeframe}`);
25
+ }
26
+ return timestampMs + stepMs <= toMs;
27
+ }
28
+
29
+ export function ohlcvSeriesStartMs(fromMs, timeframe, market) {
30
+ const stepMs = TIMEFRAME_MS[timeframe];
31
+ if (!stepMs) {
32
+ throw new Error(`不支持的 timeframe:${timeframe}`);
33
+ }
34
+ const warmupStartMs = fromMs - WARMUP_CANDLES * stepMs;
35
+ const marketCreatedMs = market?.created;
36
+ return typeof marketCreatedMs === "number" &&
37
+ Number.isFinite(marketCreatedMs) &&
38
+ marketCreatedMs > 0
39
+ ? Math.max(warmupStartMs, marketCreatedMs)
40
+ : warmupStartMs;
41
+ }
42
+
43
+ export async function loadSeriesWithCache(
44
+ exchange,
45
+ exchangeDefinition,
46
+ runtimeConfig,
47
+ symbol,
48
+ timeframe,
49
+ market,
50
+ fromMs,
51
+ toMs,
52
+ minWarmupCandles,
53
+ requireFullReplayCoverage,
54
+ dataQualityMode,
55
+ onFraction,
56
+ cacheUntilMs,
57
+ cache,
58
+ signal
59
+ ) {
60
+ signal?.throwIfAborted();
61
+ const stepMs = TIMEFRAME_MS[timeframe];
62
+ if (!stepMs) {
63
+ throw new Error(`不支持的 timeframe:${timeframe}`);
64
+ }
65
+ const sinceMs = ohlcvSeriesStartMs(fromMs, timeframe, market);
66
+ const closedEndMs = Math.min(Math.max(cacheUntilMs, sinceMs), toMs);
67
+ const cacheable = closedEndMs > sinceMs;
68
+ const cacheKey = ohlcvSeriesCacheKey(
69
+ exchangeDefinition.ccxtId,
70
+ symbol,
71
+ timeframe
72
+ );
73
+ const totalSpan = Math.max(1, toMs - sinceMs);
74
+ const closedShare = (closedEndMs - sinceMs) / totalSpan;
75
+ const closedSpan = Math.max(1, closedEndMs - sinceMs);
76
+
77
+ const evaluate = (rows) =>
78
+ evaluateOhlcvCoverage({
79
+ symbol,
80
+ timeframe,
81
+ rows,
82
+ stepMs,
83
+ fromMs,
84
+ toMs,
85
+ minWarmupCandles,
86
+ requireFullReplayCoverage,
87
+ mode: dataQualityMode,
88
+ });
89
+ const fetchClosedSegment = (range, reportProgress) =>
90
+ fetchClosedOhlcvRange({
91
+ exchange,
92
+ exchangeDefinition,
93
+ runtimeConfig,
94
+ symbol,
95
+ timeframe,
96
+ sinceMs: range.sinceMs,
97
+ fromMs: Math.min(range.untilMs, Math.max(range.sinceMs, fromMs)),
98
+ toMs: range.untilMs,
99
+ allowEmpty: true,
100
+ onFraction: reportProgress
101
+ ? (fraction) =>
102
+ onFraction(
103
+ ((range.sinceMs -
104
+ sinceMs +
105
+ fraction * (range.untilMs - range.sinceMs)) /
106
+ closedSpan) *
107
+ closedShare
108
+ )
109
+ : undefined,
110
+ signal,
111
+ });
112
+ const fetchLiveTail = () =>
113
+ closedEndMs < toMs
114
+ ? fetchClosedOhlcvRange({
115
+ exchange,
116
+ exchangeDefinition,
117
+ runtimeConfig,
118
+ symbol,
119
+ timeframe,
120
+ sinceMs: closedEndMs,
121
+ fromMs: Math.max(fromMs, closedEndMs),
122
+ toMs,
123
+ allowEmpty: true,
124
+ onFraction: (fraction) =>
125
+ onFraction(closedShare + fraction * (1 - closedShare)),
126
+ signal,
127
+ })
128
+ : Promise.resolve([]);
129
+
130
+ const cachedRange =
131
+ cacheable && cache && !cache.disabled
132
+ ? await abortable(cache.read(cacheKey), signal)
133
+ : null;
134
+ signal?.throwIfAborted();
135
+ const cachedRangeContributed =
136
+ cachedRange !== null &&
137
+ cachedRange.untilMs >= sinceMs &&
138
+ cachedRange.sinceMs <= closedEndMs;
139
+ const missingRanges = cacheable
140
+ ? missingCachedOhlcvRanges(cachedRange, sinceMs, closedEndMs)
141
+ : [];
142
+ let mergedRange = cachedRange;
143
+ let fetchedClosedRows = false;
144
+ for (const missingRange of missingRanges) {
145
+ onFraction(((missingRange.sinceMs - sinceMs) / closedSpan) * closedShare);
146
+ const fetchedRows = await fetchClosedSegment(missingRange, true);
147
+ mergedRange = mergeCachedOhlcvRanges(mergedRange, {
148
+ ...missingRange,
149
+ rows: fetchedRows,
150
+ });
151
+ fetchedClosedRows = true;
152
+ }
153
+ if (cacheable && missingRanges.length === 0) onFraction(closedShare);
154
+
155
+ let closedRows = rowsForClosedRange(
156
+ mergedRange,
157
+ sinceMs,
158
+ closedEndMs,
159
+ timeframe
160
+ );
161
+ const liveRows = await fetchLiveTail();
162
+ signal?.throwIfAborted();
163
+
164
+ let rows = concatMonotonicRows(closedRows ?? [], liveRows);
165
+ let decision = evaluate(rows);
166
+ if (!decision.accepted && cachedRangeContributed) {
167
+ const freshRange = { sinceMs, untilMs: closedEndMs };
168
+ closedRows = await fetchClosedSegment(freshRange, false);
169
+ mergedRange = mergeCachedOhlcvRanges(mergedRange, {
170
+ ...freshRange,
171
+ rows: closedRows,
172
+ });
173
+ fetchedClosedRows = true;
174
+ rows = concatMonotonicRows(closedRows, liveRows);
175
+ decision = evaluate(rows);
176
+ }
177
+ if (!decision.accepted) {
178
+ throw new TapeDataUnavailableError(decision.blockingIssues);
179
+ }
180
+ if (
181
+ cacheable &&
182
+ fetchedClosedRows &&
183
+ mergedRange !== null &&
184
+ mergedRange.rows.length > 0 &&
185
+ cache &&
186
+ !cache.disabled
187
+ ) {
188
+ await abortable(cache.write(cacheKey, mergedRange), signal);
189
+ }
190
+ signal?.throwIfAborted();
191
+ onFraction(1);
192
+ return {
193
+ rows,
194
+ softIssues: decision.acceptedSoftIssues,
195
+ coverageRatio: decision.report.coverageRatio,
196
+ };
197
+ }
198
+
199
+ function rowsForClosedRange(range, sinceMs, untilMs, timeframe) {
200
+ return (range?.rows ?? []).filter(
201
+ (row) => row[0] >= sinceMs && isClosedCandle(row[0], timeframe, untilMs)
202
+ );
203
+ }
204
+
205
+ function concatMonotonicRows(head, tail) {
206
+ if (head.length === 0) return [...tail];
207
+ const lastHeadTimestamp = head[head.length - 1][0];
208
+ return [...head, ...tail.filter((row) => row[0] > lastHeadTimestamp)];
209
+ }
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
+ }