@tradejs/cli 1.0.9 → 1.0.11

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 (77) hide show
  1. package/README.md +13 -4
  2. package/dist/cli.js +29531 -13520
  3. package/dist/lib/aiPocketSearch.js +1316 -0
  4. package/dist/lib/aiTrainCharts.js +711 -0
  5. package/dist/lib/aiTrainDataset.js +40 -0
  6. package/dist/lib/aiTrainEvaluationDump.js +76 -0
  7. package/dist/lib/aiTrainMetrics.js +148 -8
  8. package/dist/lib/aiTrainOptions.js +130 -0
  9. package/dist/lib/aiTrainQuarantine.js +199 -0
  10. package/dist/lib/aiTrainResearch.js +467 -0
  11. package/dist/lib/binanceMarketContextBackfill.js +841 -0
  12. package/dist/lib/binanceMarketData.js +392 -0
  13. package/dist/lib/coinMarketCapContextBackfill.js +1126 -0
  14. package/dist/lib/continuity.js +60 -0
  15. package/dist/lib/derivativesContextBackfill.js +490 -77
  16. package/dist/lib/executionCalibration.js +705 -0
  17. package/dist/lib/marketContextPrepare.js +4441 -0
  18. package/dist/lib/paritySummary.js +101 -0
  19. package/dist/lib/runBot.js +1 -0
  20. package/dist/lib/runEnvironment.js +2259 -0
  21. package/dist/lib/runFormatting.js +1657 -0
  22. package/dist/lib/runtimeDebugEvidence.js +526 -0
  23. package/dist/lib/runtimeModeConfig.js +53 -0
  24. package/dist/lib/runtimeParity.js +88 -11
  25. package/dist/lib/runtimeParityDetails.js +751 -0
  26. package/dist/lib/runtimeRedis.js +157 -0
  27. package/dist/lib/runtimeSignalsLoader.js +40 -6
  28. package/dist/lib/runtimeSignalsStorage.js +45 -22
  29. package/dist/lib/runtimeStrategyBacktest.js +134 -0
  30. package/dist/lib/runtimeTradeSync.js +263 -0
  31. package/dist/lib/telegramReports.js +9 -0
  32. package/dist/lib/tickerUniverseCache.js +116 -0
  33. package/dist/lib/timeWindow.js +3 -3
  34. package/dist/scripts/agentRun.js +14 -8
  35. package/dist/scripts/aiExport.js +1721 -7
  36. package/dist/scripts/aiExportSelect.js +16 -4
  37. package/dist/scripts/aiPocketSearch.js +4190 -0
  38. package/dist/scripts/aiTrain.js +1866 -87
  39. package/dist/scripts/backtest.js +4534 -803
  40. package/dist/scripts/binanceMarketIngest.js +565 -0
  41. package/dist/scripts/bot.js +16 -2
  42. package/dist/scripts/candlesMigrateProvider.js +32 -22
  43. package/dist/scripts/cleanDir.js +15 -2
  44. package/dist/scripts/cleanRedis.js +15 -2
  45. package/dist/scripts/cleanTests.js +38 -2
  46. package/dist/scripts/cleanupMarketContext.js +69 -0
  47. package/dist/scripts/continuity.js +148 -67
  48. package/dist/scripts/derivativesIngest.js +14 -4
  49. package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
  50. package/dist/scripts/doctor.js +14 -4
  51. package/dist/scripts/executionCalibration.js +792 -0
  52. package/dist/scripts/infraCommon.js +18 -5
  53. package/dist/scripts/infraDown.js +14 -4
  54. package/dist/scripts/infraInit.js +32 -9
  55. package/dist/scripts/infraUp.js +24 -6
  56. package/dist/scripts/marketWs.js +222 -0
  57. package/dist/scripts/migration.js +15 -2
  58. package/dist/scripts/mlExport.js +1698 -10
  59. package/dist/scripts/mlExportSelect.js +16 -4
  60. package/dist/scripts/mlInspect.js +13 -3
  61. package/dist/scripts/mlTrainLatestSelect.js +14 -4
  62. package/dist/scripts/replay.js +8867 -0
  63. package/dist/scripts/replayRunner.js +8870 -0
  64. package/dist/scripts/replayRuntimeEvidence.js +868 -0
  65. package/dist/scripts/researchAuto.js +286 -59
  66. package/dist/scripts/results.js +3 -3
  67. package/dist/scripts/runtimeEvidence.js +605 -0
  68. package/dist/scripts/runtimeParity.js +6400 -1043
  69. package/dist/scripts/serverHealth.js +588 -0
  70. package/dist/scripts/signals.js +5628 -511
  71. package/dist/scripts/signalsDaemon.js +6205 -0
  72. package/dist/scripts/signalsSummary.js +833 -248
  73. package/dist/scripts/test-ml.js +25 -3
  74. package/dist/scripts/test.js +26 -1
  75. package/dist/scripts/user-add.js +15 -2
  76. package/dist/workers/testerWorker.js +130 -21
  77. package/package.json +18 -12
@@ -32,12 +32,19 @@ var derivativesContextBackfill_exports = {};
32
32
  __export(derivativesContextBackfill_exports, {
33
33
  backfillDerivativesContextForBacktest: () => backfillDerivativesContextForBacktest,
34
34
  backfillDerivativesContextForSignals: () => backfillDerivativesContextForSignals,
35
+ formatCoinalyzeRequestError: () => formatCoinalyzeRequestError,
36
+ groupDerivativesContextMissingFetchRanges: () => groupDerivativesContextMissingFetchRanges,
37
+ hasDerivativesContextCoverageRange: () => hasDerivativesContextCoverageRange,
35
38
  isBacktestDerivativesContextEnabled: () => isBacktestDerivativesContextEnabled,
39
+ isDerivativesTargetContextEnabled: () => isDerivativesTargetContextEnabled,
36
40
  isSignalsDerivativesContextEnabled: () => isSignalsDerivativesContextEnabled,
37
41
  resolveDerivativesContextBackfillSymbols: () => resolveDerivativesContextBackfillSymbols,
38
42
  resolveDerivativesContextBackfillWindow: () => resolveDerivativesContextBackfillWindow,
43
+ resolveDerivativesContextIntervalBackfillWindow: () => resolveDerivativesContextIntervalBackfillWindow,
39
44
  resolveDerivativesContextIntervals: () => resolveDerivativesContextIntervals,
40
45
  resolveDerivativesContextLookbackMs: () => resolveDerivativesContextLookbackMs,
46
+ resolveDerivativesContextMissingCoverageFetchFromMs: () => resolveDerivativesContextMissingCoverageFetchFromMs,
47
+ resolveDerivativesContextMissingFetchFromMs: () => resolveDerivativesContextMissingFetchFromMs,
41
48
  shouldBackfillDerivativesContextForBacktest: () => shouldBackfillDerivativesContextForBacktest,
42
49
  shouldBackfillDerivativesContextForSignals: () => shouldBackfillDerivativesContextForSignals
43
50
  });
@@ -57,12 +64,20 @@ var coinalyzeIntervalMap = {
57
64
  "15m": "15min",
58
65
  "1h": "1hour"
59
66
  };
67
+ var derivativesIntervalMs = (interval) => interval === "1h" ? HOUR_MS : 15 * 60 * 1e3;
60
68
  var lastRequestTs = 0;
61
69
  var asInt = (value, fallback) => {
62
70
  const parsed = Number.parseInt(String(value ?? ""), 10);
63
71
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
64
72
  };
65
73
  var parseList = (value) => String(value ?? "").split(",").map((item) => item.trim().toUpperCase()).filter(Boolean);
74
+ var parseBooleanFlag = (value, fallback = false) => {
75
+ const normalized = String(value ?? "").trim().toLowerCase();
76
+ if (!normalized) return fallback;
77
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
78
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
79
+ return fallback;
80
+ };
66
81
  var normalizeSymbols = (symbols) => [
67
82
  ...new Set(
68
83
  symbols.map(
@@ -70,7 +85,13 @@ var normalizeSymbols = (symbols) => [
70
85
  ).filter(Boolean)
71
86
  )
72
87
  ];
73
- var resolveDerivativesContextBackfillSymbols = (_requestedSymbols = []) => [...import_constants.DERIVATIVES_CONTEXT_REFERENCE_SYMBOLS];
88
+ var isDerivativesTargetContextEnabled = () => parseBooleanFlag(process.env.DERIVATIVES_CONTEXT_TARGET_ENABLED, false);
89
+ var resolveDerivativesContextBackfillSymbols = (requestedSymbols = []) => {
90
+ const referenceSymbols = (0, import_constants.resolveDerivativesContextReferenceSymbols)(
91
+ process.env.DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS
92
+ );
93
+ return isDerivativesTargetContextEnabled() ? normalizeSymbols([...referenceSymbols, ...requestedSymbols]) : referenceSymbols;
94
+ };
74
95
  var chunkArray = (items, size) => {
75
96
  const chunks = [];
76
97
  for (let i = 0; i < items.length; i += size) {
@@ -84,7 +105,7 @@ var isBacktestDerivativesContextEnabled = () => {
84
105
  var isSignalsDerivativesContextEnabled = () => isDerivativesContextBackfillEnabled("CRON");
85
106
  var isDerivativesContextBackfillEnabled = (env) => {
86
107
  const normalized = String(process.env.DERIVATIVES_CONTEXT_ENABLED ?? "").trim().toLowerCase();
87
- if (!normalized) return false;
108
+ if (!normalized) return true;
88
109
  if (["1", "true", "yes", "on"].includes(normalized)) {
89
110
  return true;
90
111
  }
@@ -110,11 +131,18 @@ var resolveDerivativesContextLookbackMs = () => {
110
131
  return normalizedHours * HOUR_MS;
111
132
  };
112
133
  var resolveDerivativesContextBackfillWindow = (params) => {
113
- const { startMs, endMs, preloadStartMs, nowMs = Date.now() } = params;
134
+ const {
135
+ mode = "signals",
136
+ startMs,
137
+ endMs,
138
+ preloadStartMs,
139
+ nowMs = Date.now()
140
+ } = params;
114
141
  const safeEndMs = Math.min(endMs, nowMs);
115
142
  const safeStartMs = Math.max(0, Math.min(startMs, safeEndMs));
116
143
  const lookbackFromMs = safeStartMs - resolveDerivativesContextLookbackMs();
117
- const requestedFromMs = preloadStartMs != null ? Math.max(Number(preloadStartMs), lookbackFromMs) : lookbackFromMs;
144
+ const defaultFromMs = mode === "backtest" ? safeStartMs : lookbackFromMs;
145
+ const requestedFromMs = mode === "signals" ? defaultFromMs : preloadStartMs ?? defaultFromMs;
118
146
  const fromMs = Math.max(
119
147
  0,
120
148
  Math.min(
@@ -128,6 +156,104 @@ var resolveDerivativesContextBackfillWindow = (params) => {
128
156
  testStartMs: Math.trunc(safeStartMs)
129
157
  };
130
158
  };
159
+ var resolveDerivativesContextIntervalBackfillWindow = (params) => {
160
+ const intervalMs = derivativesIntervalMs(params.interval);
161
+ return {
162
+ fromMs: Math.floor(params.fromMs / intervalMs) * intervalMs,
163
+ toMs: Math.floor(params.toMs / intervalMs) * intervalMs,
164
+ intervalMs
165
+ };
166
+ };
167
+ var resolveDerivativesContextMissingFetchFromMs = (params) => {
168
+ const min = params.edges?.min;
169
+ const max = params.edges?.max;
170
+ if (min == null || max == null) {
171
+ return params.fromMs;
172
+ }
173
+ if (min > params.fromMs) {
174
+ return params.fromMs;
175
+ }
176
+ if (max < params.toMs) {
177
+ return Math.max(params.fromMs, max + params.intervalMs);
178
+ }
179
+ return null;
180
+ };
181
+ var countBackfillWindows = (params) => {
182
+ let count = 0;
183
+ let cursor = params.fromMs;
184
+ while (cursor < params.toMs) {
185
+ const toMs = Math.min(params.toMs, cursor + params.batchMs);
186
+ count += 1;
187
+ cursor = toMs + params.intervalMs;
188
+ }
189
+ return count;
190
+ };
191
+ var buildBackfillWindows = (params) => {
192
+ const windows = [];
193
+ let cursor = params.fromMs;
194
+ while (cursor < params.toMs) {
195
+ const toMs = Math.min(params.toMs, cursor + params.batchMs);
196
+ windows.push({ fromMs: cursor, toMs });
197
+ cursor = toMs + params.intervalMs;
198
+ }
199
+ return windows;
200
+ };
201
+ var hasDerivativesWindowCoverage = (params) => params.edges?.min != null && params.edges?.max != null && params.edges.min <= params.fromMs && params.edges.max >= params.toMs;
202
+ var hasDerivativesContextCoverageRange = (ranges, fromMs, toMs) => (ranges ?? []).some(
203
+ (range) => Number.isFinite(range.fromMs) && Number.isFinite(range.toMs) && range.fromMs <= fromMs && range.toMs >= toMs
204
+ );
205
+ var resolveDerivativesContextMissingCoverageFetchFromMs = (params) => {
206
+ const ranges = (params.ranges ?? []).filter(
207
+ (range) => Number.isFinite(range.fromMs) && Number.isFinite(range.toMs)
208
+ ).sort((a, b) => a.fromMs - b.fromMs);
209
+ let coveredUntil;
210
+ for (const range of ranges) {
211
+ if (range.toMs < params.fromMs) {
212
+ continue;
213
+ }
214
+ if (coveredUntil == null) {
215
+ if (range.fromMs > params.fromMs) {
216
+ return params.fromMs;
217
+ }
218
+ coveredUntil = range.toMs;
219
+ if (coveredUntil >= params.toMs) {
220
+ return null;
221
+ }
222
+ continue;
223
+ }
224
+ if (range.fromMs > coveredUntil + params.intervalMs) {
225
+ break;
226
+ }
227
+ coveredUntil = Math.max(coveredUntil, range.toMs);
228
+ if (coveredUntil >= params.toMs) {
229
+ return null;
230
+ }
231
+ }
232
+ if (coveredUntil == null) {
233
+ return params.fromMs;
234
+ }
235
+ return Math.max(params.fromMs, coveredUntil + params.intervalMs);
236
+ };
237
+ var groupDerivativesContextMissingFetchRanges = (ranges) => {
238
+ const groups = /* @__PURE__ */ new Map();
239
+ for (const range of ranges) {
240
+ const fromMs = Math.trunc(range.fromMs);
241
+ const items = groups.get(fromMs) ?? [];
242
+ items.push(range.item);
243
+ groups.set(fromMs, items);
244
+ }
245
+ return [...groups.entries()].sort(([a], [b]) => a - b).map(([fromMs, items]) => ({ fromMs, items }));
246
+ };
247
+ var coverageKey = (params) => [
248
+ params.symbol.trim().toUpperCase(),
249
+ params.interval,
250
+ Math.trunc(params.fromMs),
251
+ Math.trunc(params.toMs)
252
+ ].join(":");
253
+ var extendEdges = (current, fromMs, toMs) => ({
254
+ min: current?.min == null ? fromMs : Math.min(current.min, Math.trunc(fromMs)),
255
+ max: current?.max == null ? toMs : Math.max(current.max, Math.trunc(toMs))
256
+ });
131
257
  var getCoinalyzeApiKey = async (userName) => {
132
258
  const settings = await (0, import_userSettings.getUserSettings)(userName);
133
259
  return settings.COINALYZE_API_KEY.trim() || String(process.env.COINALYZE_API_KEY ?? "").trim();
@@ -141,10 +267,67 @@ var getRequestDelayMs = () => Math.max(
141
267
  )
142
268
  );
143
269
  var getRequestTimeoutMs = () => Math.max(5e3, asInt(process.env.COINALYZE_REQUEST_TIMEOUT_MS, 45e3));
270
+ var networkErrorCodes = /* @__PURE__ */ new Set([
271
+ "UND_ERR_CONNECT_TIMEOUT",
272
+ "ETIMEDOUT",
273
+ "ECONNRESET",
274
+ "ECONNREFUSED",
275
+ "ENOTFOUND",
276
+ "EAI_AGAIN",
277
+ "EHOSTUNREACH"
278
+ ]);
279
+ var getNestedErrorValue = (error, key) => {
280
+ let current = error;
281
+ const seen = /* @__PURE__ */ new Set();
282
+ while (current && typeof current === "object" && !seen.has(current)) {
283
+ seen.add(current);
284
+ const record = current;
285
+ const value = record[key];
286
+ if (typeof value === "string" && value.trim()) {
287
+ return value.trim();
288
+ }
289
+ current = record.cause;
290
+ }
291
+ return null;
292
+ };
293
+ var getCoinalyzeErrorCause = (error) => {
294
+ const code = getNestedErrorValue(error, "code");
295
+ const message = getNestedErrorValue(error, "message") ?? String(error || "unknown error");
296
+ return code ? `${code}: ${message}` : message;
297
+ };
298
+ var isRetryableCoinalyzeFetchError = (error) => {
299
+ const name = getNestedErrorValue(error, "name");
300
+ const code = getNestedErrorValue(error, "code");
301
+ const message = getNestedErrorValue(error, "message")?.toLowerCase() ?? "";
302
+ return name === "AbortError" || code != null && networkErrorCodes.has(code) || message.includes("aborted") || message.includes("fetch failed");
303
+ };
304
+ var formatCoinalyzeRequestError = ({
305
+ url,
306
+ error,
307
+ attempts,
308
+ timeoutMs
309
+ }) => {
310
+ const endpoint = (() => {
311
+ try {
312
+ const parsed = new URL(url);
313
+ return `${parsed.origin}${parsed.pathname}`;
314
+ } catch {
315
+ return url;
316
+ }
317
+ })();
318
+ return [
319
+ `Coinalyze request failed: ${endpoint}`,
320
+ `cause=${getCoinalyzeErrorCause(error)}`,
321
+ `attempts=${attempts}`,
322
+ `timeout=${timeoutMs}ms`,
323
+ "Set COINALYZE_REQUEST_TIMEOUT_MS/COINALYZE_MAX_RETRIES to retry longer, or DERIVATIVES_CONTEXT_ENABLED=live to skip derivatives backfill during backtests."
324
+ ].join("; ");
325
+ };
144
326
  var fetchJsonWithRateLimit = async (url, apiKey) => {
145
327
  const requestDelayMs = getRequestDelayMs();
146
328
  const requestTimeoutMs = getRequestTimeoutMs();
147
329
  const maxRetries = asInt(process.env.COINALYZE_MAX_RETRIES, 4);
330
+ let lastError = null;
148
331
  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
149
332
  const waitMs = Math.max(0, lastRequestTs + requestDelayMs - Date.now());
150
333
  if (waitMs > 0) {
@@ -165,12 +348,19 @@ var fetchJsonWithRateLimit = async (url, apiKey) => {
165
348
  });
166
349
  } catch (error) {
167
350
  clearTimeout(timer);
168
- const abortError = error instanceof Error && (error.name === "AbortError" || String(error.message).toLowerCase().includes("aborted"));
169
- if (attempt < maxRetries && abortError) {
351
+ lastError = error;
352
+ if (attempt < maxRetries && isRetryableCoinalyzeFetchError(error)) {
170
353
  await (0, import_async.delay)(Math.min(12e3, 800 * 2 ** attempt));
171
354
  continue;
172
355
  }
173
- throw error;
356
+ throw new Error(
357
+ formatCoinalyzeRequestError({
358
+ url,
359
+ error,
360
+ attempts: attempt + 1,
361
+ timeoutMs: requestTimeoutMs
362
+ })
363
+ );
174
364
  } finally {
175
365
  clearTimeout(timer);
176
366
  }
@@ -187,7 +377,14 @@ var fetchJsonWithRateLimit = async (url, apiKey) => {
187
377
  }
188
378
  throw new Error(`Coinalyze ${response.status}: ${text}`);
189
379
  }
190
- throw new Error("Coinalyze request failed after retries");
380
+ throw new Error(
381
+ formatCoinalyzeRequestError({
382
+ url,
383
+ error: lastError,
384
+ attempts: maxRetries + 1,
385
+ timeoutMs: requestTimeoutMs
386
+ })
387
+ );
191
388
  };
192
389
  var fetchCoinalyzeMarkets = async (apiKey) => {
193
390
  const raw = await fetchJsonWithRateLimit(
@@ -292,7 +489,7 @@ var skippedBackfillResult = () => ({
292
489
  failedWindows: 0,
293
490
  skippedWindows: 0
294
491
  });
295
- var backfillDerivativesContext = async (params, enabled) => {
492
+ var backfillDerivativesContext = async (params, enabled, mode) => {
296
493
  const { userName, startMs, endMs } = params;
297
494
  const requestedSymbols = normalizeSymbols(params.symbols);
298
495
  const symbols = resolveDerivativesContextBackfillSymbols(requestedSymbols);
@@ -303,17 +500,12 @@ var backfillDerivativesContext = async (params, enabled) => {
303
500
  if (!intervals.length) {
304
501
  return skippedBackfillResult();
305
502
  }
306
- const apiKey = await getCoinalyzeApiKey(userName);
307
- if (!apiKey) {
308
- throw new Error(
309
- `Missing COINALYZE_API_KEY for derivatives context backfill (user=${userName})`
310
- );
311
- }
312
503
  const {
313
504
  fromMs,
314
505
  toMs: safeEndMs,
315
506
  testStartMs: safeStartMs
316
507
  } = resolveDerivativesContextBackfillWindow({
508
+ mode,
317
509
  startMs,
318
510
  endMs,
319
511
  preloadStartMs: params.preloadStartMs
@@ -321,6 +513,129 @@ var backfillDerivativesContext = async (params, enabled) => {
321
513
  if (safeEndMs <= fromMs) {
322
514
  return skippedBackfillResult();
323
515
  }
516
+ const batchDays = asInt(
517
+ process.env.DERIVATIVES_CONTEXT_BACKFILL_BATCH_DAYS,
518
+ 30
519
+ );
520
+ const batchMs = batchDays * DAY_MS;
521
+ const intervalWindows = intervals.map((interval) => ({
522
+ interval,
523
+ ...resolveDerivativesContextIntervalBackfillWindow({
524
+ fromMs,
525
+ toMs: safeEndMs,
526
+ interval
527
+ })
528
+ })).filter((item) => item.toMs > item.fromMs);
529
+ if (!intervalWindows.length) {
530
+ return skippedBackfillResult();
531
+ }
532
+ await (0, import_timescale.waitForDbReady)();
533
+ const edgesByInterval = /* @__PURE__ */ new Map();
534
+ await Promise.all(
535
+ intervalWindows.map(async ({ interval }) => {
536
+ edgesByInterval.set(
537
+ interval,
538
+ await (0, import_timescale.getDerivativesDataEdgesForSymbols)(symbols, interval)
539
+ );
540
+ })
541
+ );
542
+ const coverageKeysByInterval = /* @__PURE__ */ new Map();
543
+ const coverageRangesByInterval = /* @__PURE__ */ new Map();
544
+ await Promise.all(
545
+ intervalWindows.map(async (window) => {
546
+ const coverageRows = await (0, import_timescale.getDerivativesBackfillCoverage)({
547
+ source: "coinalyze",
548
+ symbols,
549
+ interval: window.interval,
550
+ fromMs: window.fromMs,
551
+ toMs: window.toMs
552
+ });
553
+ coverageKeysByInterval.set(
554
+ window.interval,
555
+ new Set(
556
+ coverageRows.map(
557
+ (row) => coverageKey({
558
+ symbol: row.symbol,
559
+ interval: row.interval,
560
+ fromMs: row.fromMs,
561
+ toMs: row.toMs
562
+ })
563
+ )
564
+ )
565
+ );
566
+ const rangesBySymbol = /* @__PURE__ */ new Map();
567
+ for (const row of coverageRows) {
568
+ const symbol = row.symbol.toUpperCase();
569
+ const ranges = rangesBySymbol.get(symbol) ?? [];
570
+ ranges.push({ fromMs: row.fromMs, toMs: row.toMs });
571
+ rangesBySymbol.set(symbol, ranges);
572
+ }
573
+ coverageRangesByInterval.set(window.interval, rangesBySymbol);
574
+ })
575
+ );
576
+ const cachedWindows = intervalWindows.reduce(
577
+ (sum, window) => sum + countBackfillWindows({
578
+ fromMs: window.fromMs,
579
+ toMs: window.toMs,
580
+ batchMs,
581
+ intervalMs: window.intervalMs
582
+ }),
583
+ 0
584
+ );
585
+ const allBackfillWindowsCached = intervalWindows.every((window) => {
586
+ const edgesBySymbol = edgesByInterval.get(window.interval);
587
+ const coverageKeys = coverageKeysByInterval.get(window.interval);
588
+ const backfillWindows = buildBackfillWindows({
589
+ fromMs: window.fromMs,
590
+ toMs: window.toMs,
591
+ batchMs,
592
+ intervalMs: window.intervalMs
593
+ });
594
+ return symbols.every(
595
+ (symbol) => backfillWindows.every((backfillWindow) => {
596
+ const normalizedSymbol = symbol.toUpperCase();
597
+ const coverageRanges = coverageRangesByInterval.get(window.interval)?.get(normalizedSymbol) ?? [];
598
+ return hasDerivativesWindowCoverage({
599
+ edges: edgesBySymbol?.get(normalizedSymbol),
600
+ fromMs: backfillWindow.fromMs,
601
+ toMs: backfillWindow.toMs
602
+ }) || resolveDerivativesContextMissingCoverageFetchFromMs({
603
+ ranges: coverageRanges,
604
+ fromMs: backfillWindow.fromMs,
605
+ toMs: backfillWindow.toMs,
606
+ intervalMs: window.intervalMs
607
+ }) == null || coverageKeys?.has(
608
+ coverageKey({
609
+ symbol: normalizedSymbol,
610
+ interval: window.interval,
611
+ fromMs: backfillWindow.fromMs,
612
+ toMs: backfillWindow.toMs
613
+ })
614
+ );
615
+ })
616
+ );
617
+ });
618
+ if (allBackfillWindowsCached) {
619
+ console.log(
620
+ import_chalk.default.gray(
621
+ `derivatives context backfill: cached symbols=${symbols.length}, requestedSymbols=${requestedSymbols.length}, intervals=${intervals.join(",")}, window=${new Date(fromMs).toISOString()}..${new Date(safeEndMs).toISOString()}`
622
+ )
623
+ );
624
+ return {
625
+ skipped: false,
626
+ rows: 0,
627
+ matchedSymbols: symbols.length,
628
+ unmatchedSymbols: 0,
629
+ failedWindows: 0,
630
+ skippedWindows: cachedWindows
631
+ };
632
+ }
633
+ const apiKey = await getCoinalyzeApiKey(userName);
634
+ if (!apiKey) {
635
+ throw new Error(
636
+ `Missing COINALYZE_API_KEY for derivatives context backfill (user=${userName})`
637
+ );
638
+ }
324
639
  const exchangePriority = parseList(
325
640
  process.env.COINALYZE_EXCHANGE_PRIORITY || process.env.DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY || "A,6,0"
326
641
  );
@@ -335,21 +650,24 @@ var backfillDerivativesContext = async (params, enabled) => {
335
650
  );
336
651
  if (!matches.length) {
337
652
  throw new Error(
338
- "No matched derivatives reference symbols between BTC/ETH and Coinalyze"
653
+ "No matched derivatives context symbols between requested symbols and Coinalyze"
339
654
  );
340
655
  }
341
- await (0, import_timescale.waitForDbReady)();
342
- const batchDays = asInt(
343
- process.env.DERIVATIVES_CONTEXT_BACKFILL_BATCH_DAYS,
344
- 30
345
- );
346
656
  const symbolBatchSize = asInt(
347
657
  process.env.DERIVATIVES_CONTEXT_BACKFILL_SYMBOL_BATCH_SIZE,
348
658
  8
349
659
  );
350
660
  const symbolBatches = chunkArray(matches, symbolBatchSize);
351
- const windowsPerPair = Math.ceil((safeEndMs - fromMs) / (batchDays * DAY_MS));
352
- const totalWindows = symbolBatches.length * intervals.length * windowsPerPair;
661
+ const windowsPerPair = intervalWindows.reduce(
662
+ (sum, window) => sum + countBackfillWindows({
663
+ fromMs: window.fromMs,
664
+ toMs: window.toMs,
665
+ batchMs,
666
+ intervalMs: window.intervalMs
667
+ }),
668
+ 0
669
+ );
670
+ const totalWindows = symbolBatches.length * windowsPerPair;
353
671
  const bar = new import_progress.default(
354
672
  ":current/:total [:bar][:percent] :eta(s) :batch rows=:rows fail=:fail skip=:skip",
355
673
  {
@@ -365,68 +683,148 @@ var backfillDerivativesContext = async (params, enabled) => {
365
683
  let skippedWindows = 0;
366
684
  console.log(
367
685
  import_chalk.default.cyan(
368
- `derivatives context backfill: referenceSymbols=${matches.length}, requestedSymbols=${requestedSymbols.length}, unmatched=${unmatched.length}, intervals=${intervals.join(",")}, window=${new Date(fromMs).toISOString()}..${new Date(safeEndMs).toISOString()}, testStart=${new Date(safeStartMs).toISOString()}`
686
+ `derivatives context backfill: symbols=${matches.length}, requestedSymbols=${requestedSymbols.length}, unmatched=${unmatched.length}, intervals=${intervals.join(",")}, window=${new Date(fromMs).toISOString()}..${new Date(safeEndMs).toISOString()}, testStart=${new Date(safeStartMs).toISOString()}`
369
687
  )
370
688
  );
371
- for (const interval of intervals) {
372
- const edgesBySymbol = await (0, import_timescale.getDerivativesDataEdgesForSymbols)(
689
+ for (const window of intervalWindows) {
690
+ const { interval, intervalMs } = window;
691
+ const edgesBySymbol = edgesByInterval.get(interval) ?? await (0, import_timescale.getDerivativesDataEdgesForSymbols)(
373
692
  matches.map((item) => item.symbol),
374
693
  interval
375
694
  );
695
+ const coverageKeys = coverageKeysByInterval.get(interval) ?? /* @__PURE__ */ new Set();
696
+ coverageKeysByInterval.set(interval, coverageKeys);
697
+ const coverageRangesBySymbol = coverageRangesByInterval.get(interval) ?? /* @__PURE__ */ new Map();
698
+ coverageRangesByInterval.set(interval, coverageRangesBySymbol);
376
699
  for (let batchIdx = 0; batchIdx < symbolBatches.length; batchIdx += 1) {
377
700
  const batch = symbolBatches[batchIdx];
378
- let cursor = fromMs;
379
- while (cursor < safeEndMs) {
380
- const toMs = Math.min(safeEndMs, cursor + batchDays * DAY_MS);
381
- const missingBatch = batch.filter((item) => {
382
- const edges = edgesBySymbol.get(item.symbol.toUpperCase());
383
- return edges?.min == null || edges?.max == null || edges.min > cursor || edges.max < toMs;
384
- });
701
+ let cursor = window.fromMs;
702
+ while (cursor < window.toMs) {
703
+ const toMs = Math.min(window.toMs, cursor + batchMs);
704
+ const missingRanges = batch.map((item) => {
705
+ const key = coverageKey({
706
+ symbol: item.symbol,
707
+ interval,
708
+ fromMs: cursor,
709
+ toMs
710
+ });
711
+ if (coverageKeys.has(key)) {
712
+ return null;
713
+ }
714
+ const normalizedSymbol = item.symbol.toUpperCase();
715
+ const coverageFromMs = resolveDerivativesContextMissingCoverageFetchFromMs({
716
+ ranges: coverageRangesBySymbol.get(normalizedSymbol),
717
+ fromMs: cursor,
718
+ toMs,
719
+ intervalMs
720
+ });
721
+ if (coverageFromMs == null) {
722
+ return null;
723
+ }
724
+ const edges = edgesBySymbol.get(normalizedSymbol);
725
+ const edgesFromMs = resolveDerivativesContextMissingFetchFromMs({
726
+ edges,
727
+ fromMs: cursor,
728
+ toMs,
729
+ intervalMs
730
+ });
731
+ if (edgesFromMs == null) {
732
+ return null;
733
+ }
734
+ return { item, fromMs: Math.max(coverageFromMs, edgesFromMs) };
735
+ }).filter(
736
+ (item) => item != null
737
+ );
385
738
  try {
386
- if (!missingBatch.length) {
739
+ if (!missingRanges.length) {
387
740
  skippedWindows += 1;
388
741
  } else {
389
- const marketSymbols = missingBatch.map((item) => item.marketSymbol);
390
- const oiMap = await fetchMetricBatch({
391
- endpoint: oiPath,
392
- metric: "oi",
393
- marketSymbols,
394
- apiKey,
395
- interval,
396
- fromMs: cursor,
397
- toMs
398
- });
399
- const fundingMap = await fetchMetricBatch({
400
- endpoint: fundingPath,
401
- metric: "funding",
402
- marketSymbols,
403
- apiKey,
404
- interval,
405
- fromMs: cursor,
406
- toMs
407
- });
408
- const liqMap = await fetchMetricBatch({
409
- endpoint: liqPath,
410
- metric: "liq",
411
- marketSymbols,
412
- apiKey,
413
- interval,
414
- fromMs: cursor,
415
- toMs
416
- });
417
- const rows = missingBatch.flatMap((item) => {
418
- const marketSymbol = item.marketSymbol.toUpperCase();
419
- const points = (0, import_indicators.mergeCoinalyzeMetrics)({
420
- symbol: item.symbol,
421
- oiRaw: oiMap.get(marketSymbol) ?? [],
422
- fundingRaw: fundingMap.get(marketSymbol) ?? [],
423
- liqRaw: liqMap.get(marketSymbol) ?? []
742
+ const groupedMissingRanges = groupDerivativesContextMissingFetchRanges(missingRanges);
743
+ for (const group of groupedMissingRanges) {
744
+ const missingBatch = group.items;
745
+ const marketSymbols = missingBatch.map(
746
+ (item) => item.marketSymbol
747
+ );
748
+ const oiMap = await fetchMetricBatch({
749
+ endpoint: oiPath,
750
+ metric: "oi",
751
+ marketSymbols,
752
+ apiKey,
753
+ interval,
754
+ fromMs: group.fromMs,
755
+ toMs
756
+ });
757
+ const fundingMap = await fetchMetricBatch({
758
+ endpoint: fundingPath,
759
+ metric: "funding",
760
+ marketSymbols,
761
+ apiKey,
762
+ interval,
763
+ fromMs: group.fromMs,
764
+ toMs
424
765
  });
425
- return (0, import_indicators.coinalyzePointsToRows)(points, interval, "coinalyze");
426
- });
427
- if (rows.length) {
428
- await (0, import_timescale.upsertDerivatives)(rows);
429
- totalRows += rows.length;
766
+ const liqMap = await fetchMetricBatch({
767
+ endpoint: liqPath,
768
+ metric: "liq",
769
+ marketSymbols,
770
+ apiKey,
771
+ interval,
772
+ fromMs: group.fromMs,
773
+ toMs
774
+ });
775
+ const rows = missingBatch.flatMap((item) => {
776
+ const marketSymbol = item.marketSymbol.toUpperCase();
777
+ const points = (0, import_indicators.mergeCoinalyzeMetrics)({
778
+ symbol: item.symbol,
779
+ oiRaw: oiMap.get(marketSymbol) ?? [],
780
+ fundingRaw: fundingMap.get(marketSymbol) ?? [],
781
+ liqRaw: liqMap.get(marketSymbol) ?? []
782
+ });
783
+ return (0, import_indicators.coinalyzePointsToRows)(points, interval, "coinalyze");
784
+ });
785
+ if (rows.length) {
786
+ await (0, import_timescale.upsertDerivatives)(rows);
787
+ totalRows += rows.length;
788
+ }
789
+ const rowsCountBySymbol = /* @__PURE__ */ new Map();
790
+ for (const row of rows) {
791
+ const symbol = row.symbol.toUpperCase();
792
+ rowsCountBySymbol.set(
793
+ symbol,
794
+ (rowsCountBySymbol.get(symbol) ?? 0) + 1
795
+ );
796
+ }
797
+ const coverageRows = missingBatch.map((item) => {
798
+ const normalizedSymbol = item.symbol.toUpperCase();
799
+ const rowsCount = rowsCountBySymbol.get(normalizedSymbol) ?? 0;
800
+ return {
801
+ source: "coinalyze",
802
+ symbol: item.symbol,
803
+ interval,
804
+ fromMs: cursor,
805
+ toMs,
806
+ rowsCount
807
+ };
808
+ });
809
+ await (0, import_timescale.upsertDerivativesBackfillCoverage)(coverageRows);
810
+ for (const coverageRow of coverageRows) {
811
+ const symbol = coverageRow.symbol.toUpperCase();
812
+ edgesBySymbol.set(
813
+ symbol,
814
+ extendEdges(edgesBySymbol.get(symbol), cursor, toMs)
815
+ );
816
+ coverageKeys.add(
817
+ coverageKey({
818
+ symbol,
819
+ interval,
820
+ fromMs: cursor,
821
+ toMs
822
+ })
823
+ );
824
+ const coverageRanges = coverageRangesBySymbol.get(symbol) ?? [];
825
+ coverageRanges.push({ fromMs: cursor, toMs });
826
+ coverageRangesBySymbol.set(symbol, coverageRanges);
827
+ }
430
828
  }
431
829
  }
432
830
  } catch (error) {
@@ -446,7 +844,7 @@ var backfillDerivativesContext = async (params, enabled) => {
446
844
  skip: skippedWindows
447
845
  });
448
846
  }
449
- cursor = toMs + 1;
847
+ cursor = toMs + intervalMs;
450
848
  }
451
849
  }
452
850
  }
@@ -470,18 +868,33 @@ var backfillDerivativesContext = async (params, enabled) => {
470
868
  skippedWindows
471
869
  };
472
870
  };
473
- var backfillDerivativesContextForBacktest = async (params) => backfillDerivativesContext(params, isBacktestDerivativesContextEnabled());
474
- var backfillDerivativesContextForSignals = async (params) => backfillDerivativesContext(params, isSignalsDerivativesContextEnabled());
871
+ var backfillDerivativesContextForBacktest = async (params) => backfillDerivativesContext(
872
+ params,
873
+ isBacktestDerivativesContextEnabled(),
874
+ "backtest"
875
+ );
876
+ var backfillDerivativesContextForSignals = async (params) => backfillDerivativesContext(
877
+ params,
878
+ isSignalsDerivativesContextEnabled(),
879
+ "signals"
880
+ );
475
881
  // Annotate the CommonJS export names for ESM import in node:
476
882
  0 && (module.exports = {
477
883
  backfillDerivativesContextForBacktest,
478
884
  backfillDerivativesContextForSignals,
885
+ formatCoinalyzeRequestError,
886
+ groupDerivativesContextMissingFetchRanges,
887
+ hasDerivativesContextCoverageRange,
479
888
  isBacktestDerivativesContextEnabled,
889
+ isDerivativesTargetContextEnabled,
480
890
  isSignalsDerivativesContextEnabled,
481
891
  resolveDerivativesContextBackfillSymbols,
482
892
  resolveDerivativesContextBackfillWindow,
893
+ resolveDerivativesContextIntervalBackfillWindow,
483
894
  resolveDerivativesContextIntervals,
484
895
  resolveDerivativesContextLookbackMs,
896
+ resolveDerivativesContextMissingCoverageFetchFromMs,
897
+ resolveDerivativesContextMissingFetchFromMs,
485
898
  shouldBackfillDerivativesContextForBacktest,
486
899
  shouldBackfillDerivativesContextForSignals
487
900
  });