@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,423 @@
1
+ export const TAPE_SERIES_COLUMNS: readonly string[];
2
+
3
+ export function encodeOhlcvColumns(
4
+ rows: ReadonlyArray<readonly [number, number, number, number, number, number]>
5
+ ): ArrayBuffer;
6
+
7
+ export const DEFAULT_EXECUTION_MODEL: {
8
+ readonly pricePath: "ohlc_path_4";
9
+ readonly makerFeeRate: 0.0002;
10
+ readonly takerFeeRate: 0.0005;
11
+ readonly slippageRate: 0.0001;
12
+ };
13
+
14
+ export type TapeQuoteAsset = "USDT" | "USDC";
15
+ export type TapeCcxtId =
16
+ | "binanceusdm"
17
+ | "okx"
18
+ | "bybit"
19
+ | "bitget"
20
+ | "hyperliquid";
21
+ export type TapeExchangeId =
22
+ | "binance_perp_usdt"
23
+ | "okx_perp_usdt"
24
+ | "bybit_perp_usdt"
25
+ | "bitget_perp_usdt"
26
+ | "hyperliquid_perp_usdc";
27
+
28
+ export interface TapeExchangeDefinition {
29
+ readonly id: TapeExchangeId | string;
30
+ readonly label: string;
31
+ readonly ccxtId: TapeCcxtId | string;
32
+ readonly marketType: "swap";
33
+ readonly quoteAsset: TapeQuoteAsset;
34
+ }
35
+
36
+ export const TAPE_EXCHANGES: readonly TapeExchangeDefinition[];
37
+ export const PUBLIC_MARKET_EXCHANGE_BINANCE: "binance_perp_usdt";
38
+ export const PUBLIC_MARKET_EXCHANGE_OKX: "okx_perp_usdt";
39
+ export const PUBLIC_MARKET_EXCHANGE_BYBIT: "bybit_perp_usdt";
40
+ export const PUBLIC_MARKET_EXCHANGE_BITGET: "bitget_perp_usdt";
41
+ export const PUBLIC_MARKET_EXCHANGE_HYPERLIQUID: "hyperliquid_perp_usdc";
42
+ export const HYPERLIQUID_PUBLIC_MARKET_DEXES: readonly string[];
43
+
44
+ export function resolveTapeExchange(
45
+ exchangeId: string | TapeExchangeDefinition
46
+ ): TapeExchangeDefinition;
47
+
48
+ export interface TapeExchangeRuntimeConfig {
49
+ readonly ohlcvPageLimit: number;
50
+ readonly fundingPageLimit: number;
51
+ readonly requestParams: Readonly<Record<string, unknown>>;
52
+ readonly constructorOptions?: Readonly<Record<string, unknown>>;
53
+ readonly unsupportedTimeframes?: readonly string[];
54
+ }
55
+
56
+ export function tapeExchangeRuntimeConfig(
57
+ exchange: TapeExchangeDefinition
58
+ ): TapeExchangeRuntimeConfig;
59
+
60
+ export const DEFAULT_TAPE_CACHE_DIR: string;
61
+ export const CACHE_KEY_VERSION: "closed-series-v2";
62
+ export const CACHE_MAX_AGE_MS: number;
63
+
64
+ export type CachedOhlcvRow = readonly [
65
+ number,
66
+ number,
67
+ number,
68
+ number,
69
+ number,
70
+ number,
71
+ ];
72
+
73
+ export interface CachedOhlcvRange {
74
+ readonly sinceMs: number;
75
+ readonly untilMs: number;
76
+ readonly rows: CachedOhlcvRow[];
77
+ }
78
+
79
+ export interface OhlcvRangeSegment {
80
+ readonly sinceMs: number;
81
+ readonly untilMs: number;
82
+ }
83
+
84
+ export interface FileTapeCache {
85
+ readonly disabled: boolean;
86
+ readonly rootDir?: string;
87
+ read(key: string): Promise<CachedOhlcvRange | null>;
88
+ write(key: string, range: CachedOhlcvRange): Promise<void>;
89
+ }
90
+
91
+ export function ohlcvSeriesCacheKey(
92
+ exchangeId: string,
93
+ symbol: string,
94
+ timeframe: string
95
+ ): string;
96
+ export function missingCachedOhlcvRanges(
97
+ cached: CachedOhlcvRange | null,
98
+ sinceMs: number,
99
+ untilMs: number
100
+ ): OhlcvRangeSegment[];
101
+ export function mergeCachedOhlcvRanges(
102
+ cached: CachedOhlcvRange | null,
103
+ addition: CachedOhlcvRange
104
+ ): CachedOhlcvRange;
105
+ export function createFileTapeCache(
106
+ rootDir?: string,
107
+ options?: { maxAgeMs?: number; nowMs?: number }
108
+ ): FileTapeCache;
109
+ export function createDisabledTapeCache(): FileTapeCache;
110
+
111
+ export type TapeDataQualityMode = "strict" | "basic";
112
+ export type TapeDataIssueCode =
113
+ | "market_missing"
114
+ | "ohlcv_missing"
115
+ | "invalid_ohlcv"
116
+ | "non_monotonic"
117
+ | "internal_gap"
118
+ | "prefix_gap"
119
+ | "suffix_gap"
120
+ | "warmup_insufficient"
121
+ | "load_failed"
122
+ | "coverage_insufficient";
123
+
124
+ export interface TapeDataIssue {
125
+ readonly code: TapeDataIssueCode;
126
+ readonly symbol: string;
127
+ readonly timeframe: string;
128
+ readonly expected?: number;
129
+ readonly actual?: number;
130
+ readonly timestamp?: number;
131
+ readonly message?: string;
132
+ }
133
+
134
+ export class TapeDataUnavailableError extends Error {
135
+ readonly issues: readonly TapeDataIssue[];
136
+ constructor(issues: readonly TapeDataIssue[]);
137
+ }
138
+
139
+ export function isTapeDataUnavailableError(
140
+ value: unknown
141
+ ): value is TapeDataUnavailableError;
142
+
143
+ export type EngineBacktestPrecisionMode = "DECIMAL_PLACES" | "TICK_SIZE";
144
+ export type EngineBacktestSubscriptionTier = "free" | "pro" | "pro_max";
145
+ export type EngineBacktestPricePath = "ohlc_path_4" | "close_only";
146
+
147
+ export interface EngineBacktestExecutionModel {
148
+ readonly pricePath: EngineBacktestPricePath;
149
+ readonly makerFeeRate: number;
150
+ readonly takerFeeRate: number;
151
+ readonly slippageRate: number;
152
+ }
153
+
154
+ export interface EngineBacktestTapeMarket {
155
+ readonly id: string;
156
+ readonly symbol: string;
157
+ readonly base?: string;
158
+ readonly quote?: string;
159
+ readonly settle?: string;
160
+ readonly contractSize: number;
161
+ readonly linear: boolean;
162
+ readonly precisionMode: EngineBacktestPrecisionMode;
163
+ readonly precisionAmount?: number;
164
+ readonly precisionPrice?: number;
165
+ readonly amountMin?: number;
166
+ readonly amountMax?: number;
167
+ readonly priceMin?: number;
168
+ readonly costMin?: number;
169
+ }
170
+
171
+ export interface EngineBacktestTapeSeriesRef {
172
+ readonly symbol: string;
173
+ readonly timeframe: string;
174
+ readonly buffer: string;
175
+ readonly rows: number;
176
+ }
177
+
178
+ export interface EngineBacktestFundingSample {
179
+ readonly timestamp: number;
180
+ readonly rate: number;
181
+ readonly interval: string;
182
+ }
183
+
184
+ export interface EngineBacktestTapeInput {
185
+ readonly from: string;
186
+ readonly to: string;
187
+ readonly baseTimeframe: string;
188
+ readonly markets: Readonly<Record<string, EngineBacktestTapeMarket>>;
189
+ readonly series: readonly EngineBacktestTapeSeriesRef[];
190
+ readonly fundingRates?: Readonly<
191
+ Record<string, readonly EngineBacktestFundingSample[]>
192
+ >;
193
+ readonly lowLiquiditySymbols?: readonly string[];
194
+ }
195
+
196
+ export interface EngineBacktestScenarioTrader {
197
+ readonly id?: string;
198
+ readonly name?: string;
199
+ readonly strategyDefinitionId: string;
200
+ readonly configSchemaVersion: number;
201
+ readonly subscriptionTier: EngineBacktestSubscriptionTier;
202
+ readonly config: unknown;
203
+ }
204
+
205
+ export interface EngineBacktestScenario {
206
+ readonly version: 1;
207
+ readonly runId: string;
208
+ readonly trader: EngineBacktestScenarioTrader;
209
+ readonly exchange: {
210
+ readonly positionSideDual: boolean;
211
+ readonly initialEquity: number;
212
+ };
213
+ readonly executionModel: EngineBacktestExecutionModel;
214
+ readonly tape: EngineBacktestTapeInput;
215
+ }
216
+
217
+ export interface EngineBacktestSeriesRequirement {
218
+ readonly symbol: string;
219
+ readonly timeframe: string;
220
+ readonly minWarmupCandles: number;
221
+ }
222
+
223
+ export interface EngineBacktestAuxiliaryDataRequirement {
224
+ readonly kind: string;
225
+ readonly parameters?: Record<string, unknown>;
226
+ }
227
+
228
+ export interface TapeLoadProgress {
229
+ readonly stage: "markets" | "ohlcv" | "validation";
230
+ readonly detail: string;
231
+ readonly fraction: number;
232
+ }
233
+
234
+ export interface TapeMarketsSnapshot {
235
+ readonly markets: Readonly<Record<string, TapeSymbolMarket>>;
236
+ readonly precisionMode?: number;
237
+ readonly nowMs?: number;
238
+ }
239
+
240
+ export interface TapeSymbolMarket {
241
+ readonly id?: string;
242
+ readonly symbol?: string;
243
+ readonly created?: number;
244
+ readonly base?: string;
245
+ readonly quote?: string;
246
+ readonly settle?: string;
247
+ readonly active?: boolean;
248
+ readonly type?: string;
249
+ readonly swap?: boolean;
250
+ readonly linear?: boolean;
251
+ readonly inverse?: boolean;
252
+ readonly contractSize?: number;
253
+ readonly precision?: { readonly amount?: number; readonly price?: number };
254
+ readonly limits?: {
255
+ readonly amount?: { readonly min?: number; readonly max?: number };
256
+ readonly price?: { readonly min?: number };
257
+ readonly cost?: { readonly min?: number };
258
+ };
259
+ }
260
+
261
+ export type TapeOhlcvFetcher = (
262
+ symbol: string,
263
+ timeframe: string,
264
+ since?: number,
265
+ limit?: number,
266
+ params?: Record<string, unknown>
267
+ ) => Promise<ReadonlyArray<readonly number[]>>;
268
+
269
+ export type TapeFundingFetcher = (
270
+ symbol: string,
271
+ since?: number,
272
+ limit?: number,
273
+ params?: Record<string, unknown>
274
+ ) => Promise<ReadonlyArray<{ timestamp?: number; fundingRate?: number }>>;
275
+
276
+ export interface TapeLoadRequest {
277
+ readonly exchangeId?: string;
278
+ readonly exchange?: TapeExchangeDefinition;
279
+ readonly symbols: readonly string[];
280
+ readonly baseTimeframe?: string;
281
+ readonly timeframes: readonly string[];
282
+ readonly seriesRequirements?: readonly EngineBacktestSeriesRequirement[];
283
+ readonly needsFunding?: boolean;
284
+ readonly auxiliaryDataRequirements?: readonly EngineBacktestAuxiliaryDataRequirement[];
285
+ readonly fromMs: number;
286
+ readonly toMs: number;
287
+ readonly dataQualityMode?: TapeDataQualityMode;
288
+ readonly signal?: AbortSignal;
289
+ readonly onProgress?: (progress: TapeLoadProgress) => void;
290
+ readonly nowMs?: number;
291
+ readonly ohlcvFetcher?: TapeOhlcvFetcher;
292
+ readonly fundingFetcher?: TapeFundingFetcher;
293
+ readonly marketsLoader?: () => Promise<TapeMarketsSnapshot>;
294
+ }
295
+
296
+ export interface TapeProxyOptions {
297
+ readonly httpsProxy?: string;
298
+ readonly httpProxy?: string;
299
+ readonly agent?: unknown;
300
+ }
301
+
302
+ export interface TapeLoadOptions extends TapeProxyOptions {
303
+ readonly cache?: FileTapeCache | false | "disable";
304
+ readonly cacheDir?: string;
305
+ readonly nowMs?: number;
306
+ readonly onProgress?: (progress: TapeLoadProgress) => void;
307
+ readonly createExchange?: (
308
+ exchange: TapeExchangeDefinition,
309
+ constructorOptions: Record<string, unknown>
310
+ ) => Promise<TapeRuntimeExchange> | TapeRuntimeExchange;
311
+ }
312
+
313
+ export interface TapeRuntimeExchange {
314
+ markets?: Readonly<Record<string, TapeSymbolMarket>>;
315
+ precisionMode?: number;
316
+ loadMarkets(): Promise<Readonly<Record<string, TapeSymbolMarket>>>;
317
+ fetchTime(): Promise<number>;
318
+ fetchOHLCV: TapeOhlcvFetcher;
319
+ fetchFundingRateHistory?: TapeFundingFetcher;
320
+ }
321
+
322
+ export interface TapeChartSeries {
323
+ readonly symbol: string;
324
+ readonly timeframe: string;
325
+ readonly rows: number;
326
+ readonly buffer: ArrayBuffer;
327
+ }
328
+
329
+ export interface TapeLoadResult {
330
+ readonly tape: EngineBacktestTapeInput;
331
+ readonly buffers: Record<string, ArrayBuffer>;
332
+ readonly coverageWarnings: readonly string[];
333
+ readonly chartData?: {
334
+ readonly exchangeId: string;
335
+ readonly series: readonly TapeChartSeries[];
336
+ };
337
+ }
338
+
339
+ export function loadTape(
340
+ request: TapeLoadRequest,
341
+ options?: TapeLoadOptions
342
+ ): Promise<TapeLoadResult>;
343
+
344
+ export interface AssembleScenarioInput {
345
+ readonly runId: string;
346
+ readonly definitionId: string;
347
+ readonly configSchemaVersion: number;
348
+ readonly config: unknown;
349
+ readonly subscriptionTier: EngineBacktestSubscriptionTier;
350
+ readonly initialEquity: number;
351
+ readonly traderName?: string;
352
+ readonly traderId?: string;
353
+ readonly tape?: EngineBacktestTapeInput;
354
+ readonly preparedTape?: { readonly tape: EngineBacktestTapeInput };
355
+ readonly prepared?: { readonly tape: EngineBacktestTapeInput };
356
+ readonly executionModel?: Partial<EngineBacktestExecutionModel>;
357
+ }
358
+
359
+ export function assembleScenario(
360
+ input: AssembleScenarioInput
361
+ ): EngineBacktestScenario;
362
+
363
+ export const TIMEFRAME_MS: Readonly<Record<string, number>>;
364
+ export const ENGINE_BACKTEST_BASE_TIMEFRAME: string;
365
+ export const ENGINE_BACKTEST_WARMUP_CANDLES: number;
366
+
367
+ export function resolvePlanBaseTimeframe(input: {
368
+ readonly baseTimeframe?: string | null;
369
+ readonly timeframes?: readonly string[] | null;
370
+ }): string;
371
+
372
+ export function abortable<T>(
373
+ operation: Promise<T>,
374
+ signal?: AbortSignal
375
+ ): Promise<T>;
376
+
377
+ export function resolveCcxtProxyOptions(
378
+ options?: TapeProxyOptions,
379
+ env?: NodeJS.ProcessEnv
380
+ ): { httpsProxy?: string; httpProxy?: string; agent?: unknown };
381
+
382
+ export function buildCcxtConstructorOptions(
383
+ exchange: TapeExchangeDefinition,
384
+ options?: TapeProxyOptions & { runtimeConfig?: TapeExchangeRuntimeConfig }
385
+ ): Record<string, unknown>;
386
+
387
+ export function isClosedCandle(
388
+ timestampMs: number,
389
+ timeframe: string,
390
+ toMs: number
391
+ ): boolean;
392
+
393
+ export function ohlcvSeriesStartMs(
394
+ fromMs: number,
395
+ timeframe: string,
396
+ market?: Pick<TapeSymbolMarket, "created">
397
+ ): number;
398
+
399
+ export function effectiveTapeEndMs(
400
+ requestedToMs: number,
401
+ nowMs?: number,
402
+ baseTimeframe?: string
403
+ ): number;
404
+
405
+ export function inferFundingIntervals(
406
+ samples: readonly Omit<EngineBacktestFundingSample, "interval">[]
407
+ ): EngineBacktestFundingSample[];
408
+
409
+ export function classifyTapeSymbolsForPreflight(
410
+ symbols: readonly string[],
411
+ markets: Readonly<Record<string, TapeSymbolMarket>>,
412
+ quoteAsset: TapeQuoteAsset
413
+ ): {
414
+ readonly availableSymbols: readonly string[];
415
+ readonly missingSymbols: readonly string[];
416
+ };
417
+
418
+ export function resolveEngineBacktestPrecisionMode(
419
+ precisionMode: number | undefined,
420
+ exchangeLabel: string
421
+ ): EngineBacktestPrecisionMode;
422
+
423
+ export function resolveTapeCache(options?: TapeLoadOptions): FileTapeCache;
@@ -0,0 +1,59 @@
1
+ export { TAPE_SERIES_COLUMNS, encodeOhlcvColumns } from "./lib/encode.mjs";
2
+ export {
3
+ DEFAULT_EXECUTION_MODEL,
4
+ assembleScenario,
5
+ } from "./lib/scenario.mjs";
6
+ export {
7
+ TAPE_EXCHANGES,
8
+ PUBLIC_MARKET_EXCHANGE_BINANCE,
9
+ PUBLIC_MARKET_EXCHANGE_OKX,
10
+ PUBLIC_MARKET_EXCHANGE_BYBIT,
11
+ PUBLIC_MARKET_EXCHANGE_BITGET,
12
+ PUBLIC_MARKET_EXCHANGE_HYPERLIQUID,
13
+ HYPERLIQUID_PUBLIC_MARKET_DEXES,
14
+ resolveTapeExchange,
15
+ tapeExchangeRuntimeConfig,
16
+ } from "./lib/exchanges.mjs";
17
+ export {
18
+ DEFAULT_TAPE_CACHE_DIR,
19
+ CACHE_KEY_VERSION,
20
+ CACHE_MAX_AGE_MS,
21
+ ohlcvSeriesCacheKey,
22
+ missingCachedOhlcvRanges,
23
+ mergeCachedOhlcvRanges,
24
+ createFileTapeCache,
25
+ createDisabledTapeCache,
26
+ } from "./lib/cache.mjs";
27
+ export {
28
+ TapeDataUnavailableError,
29
+ isTapeDataUnavailableError,
30
+ evaluateOhlcvCoverage,
31
+ analyzeOhlcvCoverage,
32
+ } from "./lib/coverage.mjs";
33
+ export {
34
+ TIMEFRAME_MS,
35
+ ENGINE_BACKTEST_BASE_TIMEFRAME,
36
+ ENGINE_BACKTEST_WARMUP_CANDLES,
37
+ resolvePlanBaseTimeframe,
38
+ } from "./lib/timeframes.mjs";
39
+ export {
40
+ abortable,
41
+ } from "./lib/abortable.mjs";
42
+ export {
43
+ resolveCcxtProxyOptions,
44
+ buildCcxtConstructorOptions,
45
+ } from "./lib/proxy.mjs";
46
+ export {
47
+ isClosedCandle,
48
+ ohlcvSeriesStartMs,
49
+ fetchClosedOhlcvRange,
50
+ loadSeriesWithCache,
51
+ } from "./lib/series.mjs";
52
+ export {
53
+ loadTape,
54
+ resolveTapeCache,
55
+ effectiveTapeEndMs,
56
+ inferFundingIntervals,
57
+ classifyTapeSymbolsForPreflight,
58
+ resolveEngineBacktestPrecisionMode,
59
+ } from "./lib/tape-loader.mjs";
@@ -0,0 +1,23 @@
1
+ export function abortError(signal) {
2
+ if (signal?.reason !== undefined) {
3
+ return signal.reason;
4
+ }
5
+ if (typeof DOMException === "function") {
6
+ return new DOMException("The operation was aborted", "AbortError");
7
+ }
8
+ const error = new Error("The operation was aborted");
9
+ error.name = "AbortError";
10
+ return error;
11
+ }
12
+
13
+ export function abortable(operation, signal) {
14
+ if (!signal) return operation;
15
+ signal.throwIfAborted();
16
+ return new Promise((resolve, reject) => {
17
+ const onAbort = () => reject(abortError(signal));
18
+ signal.addEventListener("abort", onAbort, { once: true });
19
+ operation.then(resolve, reject).finally(() => {
20
+ signal.removeEventListener("abort", onAbort);
21
+ });
22
+ });
23
+ }
@@ -0,0 +1,159 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ export const CACHE_KEY_VERSION = "closed-series-v2";
6
+ export const CACHE_MAX_AGE_MS = 30 * 86_400_000;
7
+ export const DEFAULT_TAPE_CACHE_DIR = path.join(
8
+ os.homedir(),
9
+ ".alphafox",
10
+ "cache",
11
+ "engine-backtest"
12
+ );
13
+
14
+ export function ohlcvSeriesCacheKey(exchangeId, symbol, timeframe) {
15
+ return `${CACHE_KEY_VERSION}|${exchangeId}|${symbol}|${timeframe}`;
16
+ }
17
+
18
+ export function missingCachedOhlcvRanges(cached, sinceMs, untilMs) {
19
+ if (untilMs <= sinceMs) return [];
20
+ if (!cached || cached.untilMs < sinceMs || cached.sinceMs > untilMs) {
21
+ return [{ sinceMs, untilMs }];
22
+ }
23
+
24
+ const missing = [];
25
+ if (sinceMs < cached.sinceMs) {
26
+ missing.push({ sinceMs, untilMs: Math.min(untilMs, cached.sinceMs) });
27
+ }
28
+ if (untilMs > cached.untilMs) {
29
+ missing.push({ sinceMs: Math.max(sinceMs, cached.untilMs), untilMs });
30
+ }
31
+ return missing.filter((range) => range.untilMs > range.sinceMs);
32
+ }
33
+
34
+ export function mergeCachedOhlcvRanges(cached, addition) {
35
+ if (
36
+ !cached ||
37
+ addition.untilMs < cached.sinceMs ||
38
+ addition.sinceMs > cached.untilMs
39
+ ) {
40
+ return addition;
41
+ }
42
+
43
+ const rowsByTimestamp = new Map();
44
+ for (const row of cached.rows) rowsByTimestamp.set(row[0], row);
45
+ for (const row of addition.rows) rowsByTimestamp.set(row[0], row);
46
+ return {
47
+ sinceMs: Math.min(cached.sinceMs, addition.sinceMs),
48
+ untilMs: Math.max(cached.untilMs, addition.untilMs),
49
+ rows: [...rowsByTimestamp.values()].sort(
50
+ (left, right) => left[0] - right[0]
51
+ ),
52
+ };
53
+ }
54
+
55
+ export function createDisabledTapeCache() {
56
+ return {
57
+ disabled: true,
58
+ async read() {
59
+ return null;
60
+ },
61
+ async write() {},
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Filesystem closed-series cache. Semantics match the web IndexedDB cache:
67
+ * one rolling range per exchange+symbol+timeframe; hits only fetch gaps.
68
+ *
69
+ * @param {string} [rootDir]
70
+ * @param {{ maxAgeMs?: number, nowMs?: number }} [options]
71
+ */
72
+ export function createFileTapeCache(
73
+ rootDir = DEFAULT_TAPE_CACHE_DIR,
74
+ options = {}
75
+ ) {
76
+ const resolvedRoot = path.resolve(rootDir);
77
+ const maxAgeMs = options.maxAgeMs ?? CACHE_MAX_AGE_MS;
78
+ const now = () => options.nowMs ?? Date.now();
79
+
80
+ return {
81
+ disabled: false,
82
+ rootDir: resolvedRoot,
83
+ async read(key) {
84
+ try {
85
+ const raw = await fs.readFile(filePathForKey(resolvedRoot, key), "utf8");
86
+ const record = JSON.parse(raw);
87
+ const fresh =
88
+ record &&
89
+ Number.isFinite(record.storedAt) &&
90
+ now() - record.storedAt <= maxAgeMs;
91
+ if (
92
+ !fresh ||
93
+ !Number.isFinite(record.sinceMs) ||
94
+ !Number.isFinite(record.untilMs) ||
95
+ record.untilMs <= record.sinceMs ||
96
+ !Array.isArray(record.rows) ||
97
+ !record.rows.every(isBasicOhlcvRow)
98
+ ) {
99
+ return null;
100
+ }
101
+ return {
102
+ sinceMs: record.sinceMs,
103
+ untilMs: record.untilMs,
104
+ rows: record.rows,
105
+ };
106
+ } catch (error) {
107
+ if (error && error.code === "ENOENT") {
108
+ return null;
109
+ }
110
+ // Cache unavailability must not block a run.
111
+ return null;
112
+ }
113
+ },
114
+ async write(key, range) {
115
+ try {
116
+ const filePath = filePathForKey(resolvedRoot, key);
117
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
118
+ const record = {
119
+ key,
120
+ sinceMs: range.sinceMs,
121
+ untilMs: range.untilMs,
122
+ rows: range.rows,
123
+ storedAt: now(),
124
+ };
125
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
126
+ await fs.writeFile(tmp, JSON.stringify(record), "utf8");
127
+ await fs.rename(tmp, filePath);
128
+ } catch {
129
+ // Best-effort cache; ignore write failures.
130
+ }
131
+ },
132
+ };
133
+ }
134
+
135
+ function isBasicOhlcvRow(row) {
136
+ return (
137
+ Array.isArray(row) &&
138
+ row.length === 6 &&
139
+ row.every(Number.isFinite)
140
+ );
141
+ }
142
+
143
+ export function filePathForKey(rootDir, key) {
144
+ const parts = key.split("|");
145
+ if (parts.length >= 4) {
146
+ const version = parts[0];
147
+ const exchangeId = parts[1];
148
+ const timeframe = parts[parts.length - 1];
149
+ const symbol = parts.slice(2, -1).join("|");
150
+ return path.join(
151
+ rootDir,
152
+ version,
153
+ exchangeId,
154
+ encodeURIComponent(symbol),
155
+ `${timeframe}.json`
156
+ );
157
+ }
158
+ return path.join(rootDir, `${encodeURIComponent(key)}.json`);
159
+ }