@alphafox/cli 0.3.8 → 0.3.10

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.
@@ -39,8 +39,47 @@ const FUNDING_INTERVALS = [
39
39
  { interval: "8h", spacingMs: 28_800_000 },
40
40
  ];
41
41
 
42
+ /** Independent symbol×timeframe (and funding) fetches. Pagination stays serial. */
43
+ export const DEFAULT_TAPE_SERIES_CONCURRENCY = 4;
44
+ export const MAX_TAPE_SERIES_CONCURRENCY = 8;
45
+
42
46
  const exchangePromises = new Map();
43
47
 
48
+ export function resolveTapeSeriesConcurrency(value) {
49
+ if (typeof value === "number" && Number.isFinite(value) && value >= 1) {
50
+ return Math.min(
51
+ MAX_TAPE_SERIES_CONCURRENCY,
52
+ Math.max(1, Math.floor(value))
53
+ );
54
+ }
55
+ return DEFAULT_TAPE_SERIES_CONCURRENCY;
56
+ }
57
+
58
+ export async function mapWithConcurrency(items, concurrency, worker) {
59
+ const list = [...items];
60
+ if (list.length === 0) {
61
+ return [];
62
+ }
63
+ const limit = Math.min(
64
+ list.length,
65
+ Math.max(1, Math.floor(Number(concurrency)) || 1)
66
+ );
67
+ const results = new Array(list.length);
68
+ let nextIndex = 0;
69
+ async function runWorker() {
70
+ while (true) {
71
+ const index = nextIndex;
72
+ nextIndex += 1;
73
+ if (index >= list.length) {
74
+ return;
75
+ }
76
+ results[index] = await worker(list[index], index);
77
+ }
78
+ }
79
+ await Promise.all(Array.from({ length: limit }, () => runWorker()));
80
+ return results;
81
+ }
82
+
44
83
  export function effectiveTapeEndMs(
45
84
  requestedToMs,
46
85
  nowMs = Date.now(),
@@ -141,7 +180,13 @@ export async function loadTape(request, options = {}) {
141
180
 
142
181
  const exchangeDefinition = resolveRequestExchange(request);
143
182
  const onProgress = request.onProgress ?? options.onProgress;
144
- const cache = resolveTapeCache(options);
183
+ const cache = resolveTapeCache({
184
+ ...options,
185
+ cacheDir: options.cacheDir ?? request.cacheDir,
186
+ });
187
+ const seriesConcurrency = resolveTapeSeriesConcurrency(
188
+ options.seriesConcurrency ?? request.seriesConcurrency
189
+ );
145
190
  const dataQualityMode = request.dataQualityMode ?? "strict";
146
191
  const baseTimeframe = resolvePlanBaseTimeframe({
147
192
  baseTimeframe: request.baseTimeframe,
@@ -233,17 +278,36 @@ export async function loadTape(request, options = {}) {
233
278
  const buffers = {};
234
279
  const chartSeries = [];
235
280
  const series = [];
236
- const totalSeries = request.symbols.length * timeframes.length;
281
+ const seriesJobs = request.symbols.flatMap((symbol) =>
282
+ timeframes.map((timeframe) => ({ symbol, timeframe }))
283
+ );
284
+ const totalSeries = seriesJobs.length;
237
285
  const dataIssues = [];
238
286
  const coverageWarnings = [];
239
- let seriesDone = 0;
240
- let bufferSequence = 0;
241
- for (const symbol of request.symbols) {
242
- for (const timeframe of timeframes) {
287
+ const seriesFractions = new Array(totalSeries).fill(0);
288
+ let lastOhlcvDetail = "";
289
+ const reportOhlcv = (index, fraction, detail) => {
290
+ seriesFractions[index] = fraction;
291
+ lastOhlcvDetail = detail;
292
+ const completed =
293
+ seriesFractions.reduce((sum, value) => sum + value, 0) / totalSeries;
294
+ onProgress?.({
295
+ stage: "ohlcv",
296
+ detail: lastOhlcvDetail,
297
+ fraction:
298
+ MARKETS_PROGRESS_END +
299
+ (OHLCV_PROGRESS_END - MARKETS_PROGRESS_END) * completed,
300
+ });
301
+ };
302
+ const loadedSeries = await mapWithConcurrency(
303
+ seriesJobs,
304
+ seriesConcurrency,
305
+ async (job, index) => {
243
306
  request.signal?.throwIfAborted();
244
- let loaded;
307
+ const { symbol, timeframe } = job;
308
+ const detail = `${symbol} ${timeframe}`;
245
309
  try {
246
- loaded = await loadSeriesWithCache(
310
+ const loaded = await loadSeriesWithCache(
247
311
  exchange,
248
312
  exchangeDefinition,
249
313
  runtimeConfig,
@@ -255,46 +319,51 @@ export async function loadTape(request, options = {}) {
255
319
  requirementWarmups.get(`${symbol}\u0000${timeframe}`) ?? 0,
256
320
  timeframe === baseTimeframe,
257
321
  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
- },
322
+ (fraction) => reportOhlcv(index, fraction, detail),
268
323
  cacheUntilMs,
269
324
  cache,
270
325
  request.signal
271
326
  );
327
+ reportOhlcv(index, 1, detail);
328
+ return { ok: true, job, loaded };
272
329
  } catch (error) {
273
330
  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
- });
331
+ reportOhlcv(index, 1, detail);
332
+ return {
333
+ ok: false,
334
+ issues: toTapeDataIssues(error, symbol, timeframe),
335
+ };
294
336
  }
295
- series.push({ symbol, timeframe, buffer: bufferKey, rows: rows.length });
296
- seriesDone++;
297
337
  }
338
+ );
339
+ let bufferSequence = 0;
340
+ for (const result of loadedSeries) {
341
+ if (!result.ok) {
342
+ dataIssues.push(...result.issues);
343
+ continue;
344
+ }
345
+ if (result.loaded.softIssues.length > 0) {
346
+ coverageWarnings.push(
347
+ formatCoverageSoftWarning(
348
+ result.loaded.softIssues,
349
+ result.loaded.coverageRatio
350
+ )
351
+ );
352
+ }
353
+ const { symbol, timeframe } = result.job;
354
+ const { rows } = result.loaded;
355
+ const bufferKey = `k${bufferSequence++}`;
356
+ const buffer = encodeOhlcvColumns(rows);
357
+ buffers[bufferKey] = buffer;
358
+ if (timeframe === baseTimeframe) {
359
+ chartSeries.push({
360
+ symbol,
361
+ timeframe: baseTimeframe,
362
+ rows: rows.length,
363
+ buffer: buffer.slice(0),
364
+ });
365
+ }
366
+ series.push({ symbol, timeframe, buffer: bufferKey, rows: rows.length });
298
367
  }
299
368
  if (dataIssues.length > 0) {
300
369
  throw new TapeDataUnavailableError(dataIssues);
@@ -302,19 +371,24 @@ export async function loadTape(request, options = {}) {
302
371
 
303
372
  let fundingRates;
304
373
  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
- }
374
+ const fundingEntries = await mapWithConcurrency(
375
+ request.symbols,
376
+ seriesConcurrency,
377
+ async (symbol) => {
378
+ request.signal?.throwIfAborted();
379
+ const samples = await loadFundingHistory(
380
+ exchange,
381
+ exchangeDefinition,
382
+ runtimeConfig,
383
+ symbol,
384
+ request.fromMs,
385
+ tapeToMs,
386
+ request.signal
387
+ );
388
+ return [symbol, samples];
389
+ }
390
+ );
391
+ fundingRates = Object.fromEntries(fundingEntries);
318
392
  }
319
393
  request.signal?.throwIfAborted();
320
394
  onProgress?.({