@tradejs/cli 2.0.0 → 2.0.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.
package/dist/cli.js CHANGED
@@ -3005,7 +3005,7 @@ var init_aiTrainDataset = __esm({
3005
3005
  });
3006
3006
 
3007
3007
  // src/lib/aiTrainMetrics.ts
3008
- var divideOrNull, DAY_MS, DAYS_PER_WEEK, DAYS_PER_MONTH, getEvaluationPeriodDays, qualitySortKey, emptyRiskSummary, summarizeApprovedRisk, summarizeAiTrainEvaluations, getDirectionSortKey, getMonthKey, isDirectionMatchAtThreshold, summarizeAiTrainEvaluationsByDirection, summarizeAiTrainEvaluationsByMonth, summarizeAiTrainEvaluationsByQualityThreshold;
3008
+ var divideOrNull, DAY_MS, DAYS_PER_WEEK, DAYS_PER_MONTH, DAYS_PER_YEAR, getEvaluationPeriodDays, qualitySortKey, calculateTradePnlRiskRatios, emptyRiskSummary, summarizeApprovedRisk, summarizeAiTrainEvaluations, getDirectionSortKey, getMonthKey, isDirectionMatchAtThreshold, summarizeAiTrainEvaluationsByDirection, summarizeAiTrainEvaluationsByMonth, summarizeAiTrainEvaluationsByQualityThreshold;
3009
3009
  var init_aiTrainMetrics = __esm({
3010
3010
  "src/lib/aiTrainMetrics.ts"() {
3011
3011
  "use strict";
@@ -3018,6 +3018,7 @@ var init_aiTrainMetrics = __esm({
3018
3018
  DAY_MS = 24 * 60 * 60 * 1e3;
3019
3019
  DAYS_PER_WEEK = 7;
3020
3020
  DAYS_PER_MONTH = 30.4375;
3021
+ DAYS_PER_YEAR = 365;
3021
3022
  getEvaluationPeriodDays = (evaluations) => {
3022
3023
  let minTimestamp = null;
3023
3024
  let maxTimestamp = null;
@@ -3039,6 +3040,40 @@ var init_aiTrainMetrics = __esm({
3039
3040
  return Math.max((maxTimestamp - minTimestamp) / DAY_MS, 1);
3040
3041
  };
3041
3042
  qualitySortKey = (quality) => quality == null ? Number.POSITIVE_INFINITY : quality;
3043
+ calculateTradePnlRiskRatios = ({
3044
+ profits,
3045
+ totalProfit,
3046
+ maxDrawdown,
3047
+ periodDays
3048
+ }) => {
3049
+ if (!profits.length || periodDays == null || periodDays <= 0) {
3050
+ return {
3051
+ sharpeRatio: null,
3052
+ sortinoRatio: null,
3053
+ calmarRatio: null
3054
+ };
3055
+ }
3056
+ const meanProfit = totalProfit / profits.length;
3057
+ const variance = profits.reduce((sum2, profit) => {
3058
+ const diff = profit - meanProfit;
3059
+ return sum2 + diff * diff;
3060
+ }, 0) / profits.length;
3061
+ const stdDev = Math.sqrt(variance);
3062
+ const downsideDeviation = Math.sqrt(
3063
+ profits.reduce(
3064
+ (sum2, profit) => profit < 0 ? sum2 + profit * profit : sum2,
3065
+ 0
3066
+ ) / profits.length
3067
+ );
3068
+ const annualizationScale = Math.sqrt(
3069
+ profits.length / periodDays * DAYS_PER_YEAR
3070
+ );
3071
+ return {
3072
+ sharpeRatio: stdDev > 0 && annualizationScale > 0 ? meanProfit / stdDev * annualizationScale : null,
3073
+ sortinoRatio: downsideDeviation > 0 && annualizationScale > 0 ? meanProfit / downsideDeviation * annualizationScale : null,
3074
+ calmarRatio: maxDrawdown > 0 ? totalProfit / periodDays * DAYS_PER_YEAR / maxDrawdown : null
3075
+ };
3076
+ };
3042
3077
  emptyRiskSummary = () => ({
3043
3078
  trades: 0,
3044
3079
  totalProfit: 0,
@@ -3054,12 +3089,15 @@ var init_aiTrainMetrics = __esm({
3054
3089
  maxDrawdown: 0,
3055
3090
  maxDrawdownPctOfGrossProfit: null,
3056
3091
  maxDrawdownPctOfTotalProfit: null,
3092
+ sharpeRatio: null,
3093
+ sortinoRatio: null,
3094
+ calmarRatio: null,
3057
3095
  recoveryFactor: null,
3058
3096
  ulcerIndex: null,
3059
3097
  maxConsecutiveWins: 0,
3060
3098
  maxConsecutiveLosses: 0
3061
3099
  });
3062
- summarizeApprovedRisk = (evaluations) => {
3100
+ summarizeApprovedRisk = (evaluations, periodDays = getEvaluationPeriodDays(evaluations)) => {
3063
3101
  const approvedEvaluations = evaluations.filter((evaluation) => evaluation.aiApproved).sort((left, right) => {
3064
3102
  const leftTimestamp = typeof left.timestamp === "number" && Number.isFinite(left.timestamp) ? left.timestamp : Number.POSITIVE_INFINITY;
3065
3103
  const rightTimestamp = typeof right.timestamp === "number" && Number.isFinite(right.timestamp) ? right.timestamp : Number.POSITIVE_INFINITY;
@@ -3082,8 +3120,10 @@ var init_aiTrainMetrics = __esm({
3082
3120
  let currentLossStreak = 0;
3083
3121
  let maxConsecutiveWins = 0;
3084
3122
  let maxConsecutiveLosses = 0;
3123
+ const approvedProfits = [];
3085
3124
  for (const evaluation of approvedEvaluations) {
3086
3125
  const profit = evaluation.profit;
3126
+ approvedProfits.push(profit);
3087
3127
  if (profit > 0) {
3088
3128
  grossProfit += profit;
3089
3129
  wins += 1;
@@ -3111,6 +3151,12 @@ var init_aiTrainMetrics = __esm({
3111
3151
  const totalProfit = grossProfit - grossLoss;
3112
3152
  const avgWin = divideOrNull(grossProfit, wins);
3113
3153
  const avgLoss = divideOrNull(grossLoss, losses);
3154
+ const riskRatios = calculateTradePnlRiskRatios({
3155
+ profits: approvedProfits,
3156
+ totalProfit,
3157
+ maxDrawdown,
3158
+ periodDays
3159
+ });
3114
3160
  return {
3115
3161
  trades: approvedEvaluations.length,
3116
3162
  totalProfit,
@@ -3126,6 +3172,7 @@ var init_aiTrainMetrics = __esm({
3126
3172
  maxDrawdown,
3127
3173
  maxDrawdownPctOfGrossProfit: grossProfit > 0 ? maxDrawdown / grossProfit : null,
3128
3174
  maxDrawdownPctOfTotalProfit: totalProfit > 0 ? maxDrawdown / totalProfit : null,
3175
+ ...riskRatios,
3129
3176
  recoveryFactor: maxDrawdown > 0 ? totalProfit / maxDrawdown : null,
3130
3177
  ulcerIndex: Math.sqrt(drawdownSquares / approvedEvaluations.length),
3131
3178
  maxConsecutiveWins,
@@ -3224,7 +3271,7 @@ var init_aiTrainMetrics = __esm({
3224
3271
  avgApprovedTradesPerDay,
3225
3272
  avgApprovedTradesPerWeek,
3226
3273
  expectancyDelta,
3227
- approvedRisk: summarizeApprovedRisk(evaluations),
3274
+ approvedRisk: summarizeApprovedRisk(evaluations, periodDays),
3228
3275
  qualityBuckets: [...bucketMap.values()].sort(
3229
3276
  (a, b) => qualitySortKey(a.quality) - qualitySortKey(b.quality)
3230
3277
  )
@@ -6135,7 +6182,7 @@ var init_aiTrainEvaluationDump = __esm({
6135
6182
  });
6136
6183
 
6137
6184
  // src/lib/aiTrainResearch.ts
6138
- var import_node_child_process2, import_node_crypto2, import_promises4, import_node_path3, DAY_MS3, RESEARCH_CONTEXT_ENV_KEYS, normalizeForStableJson, fingerprintResearchValue, getTimestampRange, summarizeAiTrainCoverage, summarizeAiTrainRejectReasons, summarizeAiTrainTerminalWindows, readOptionalFile, getGitLineage, resolveGateFingerprint, buildAiTrainLineage;
6185
+ var import_node_child_process2, import_node_crypto2, import_promises4, import_node_path3, DAY_MS3, RESEARCH_CONTEXT_ENV_KEYS, DERIVATIVES_CONTEXT_DATA_MODEL, normalizeForStableJson, fingerprintResearchValue, getTimestampRange, summarizeAiTrainCoverage, summarizeAiTrainRejectReasons, summarizeAiTrainTerminalWindows, readOptionalFile, getGitLineage, resolveGateFingerprint, buildAiTrainLineage;
6139
6186
  var init_aiTrainResearch = __esm({
6140
6187
  "src/lib/aiTrainResearch.ts"() {
6141
6188
  "use strict";
@@ -6151,7 +6198,6 @@ var init_aiTrainResearch = __esm({
6151
6198
  "INTERVAL",
6152
6199
  "DERIVATIVES_CONTEXT_ENABLED",
6153
6200
  "DERIVATIVES_CONTEXT_TARGET_ENABLED",
6154
- "DERIVATIVES_CONTEXT_INTERVALS",
6155
6201
  "DERIVATIVES_CONTEXT_LOOKBACK_HOURS",
6156
6202
  "DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS",
6157
6203
  "DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY",
@@ -6165,6 +6211,12 @@ var init_aiTrainResearch = __esm({
6165
6211
  "COINMARKETCAP_CONTEXT_BACKFILL_MAX_DAYS",
6166
6212
  "COINMARKETCAP_CONTEXT_BACKFILL_WARMUP_DAYS"
6167
6213
  ];
6214
+ DERIVATIVES_CONTEXT_DATA_MODEL = {
6215
+ derivativesSourceIntervals: "15m",
6216
+ derivativesDerivedIntervals: "1h",
6217
+ derivativesHourlyFallback: "stored-1h",
6218
+ derivativesDataModelVersion: 2
6219
+ };
6168
6220
  normalizeForStableJson = (value) => {
6169
6221
  if (Array.isArray(value)) {
6170
6222
  return value.map(normalizeForStableJson);
@@ -6277,6 +6329,7 @@ var init_aiTrainResearch = __esm({
6277
6329
  const relativeCandidates = [
6278
6330
  `packages/strategies/src/${strategyName}/adapters/ai.ts`,
6279
6331
  `packages/strategies/src/${strategyName}/guardrails.ts`,
6332
+ `packages/strategies/src/${strategyName}/pockets.ts`,
6280
6333
  `packages/strategies/src/${strategyName}/config.ts`,
6281
6334
  "packages/node/src/ai.ts"
6282
6335
  ];
@@ -6323,7 +6376,8 @@ var init_aiTrainResearch = __esm({
6323
6376
  ...Object.fromEntries(
6324
6377
  RESEARCH_CONTEXT_ENV_KEYS.map((key) => [key, env[key] ?? null])
6325
6378
  ),
6326
- ...runContext
6379
+ ...runContext,
6380
+ ...DERIVATIVES_CONTEXT_DATA_MODEL
6327
6381
  };
6328
6382
  return {
6329
6383
  gitSha,
@@ -7403,6 +7457,9 @@ var init_aiTrain = __esm({
7403
7457
  import_chalk5.default.gray("CALENDAR/D"),
7404
7458
  import_chalk5.default.gray("WR"),
7405
7459
  import_chalk5.default.gray("PF"),
7460
+ import_chalk5.default.gray("SHARPE"),
7461
+ import_chalk5.default.gray("SORTINO"),
7462
+ import_chalk5.default.gray("CALMAR"),
7406
7463
  import_chalk5.default.gray("PNL"),
7407
7464
  import_chalk5.default.gray("MAX_DD"),
7408
7465
  import_chalk5.default.gray("TOP REJECT")
@@ -7417,6 +7474,9 @@ var init_aiTrain = __esm({
7417
7474
  colorizeMetricNumber(window2.approvedPerCalendarDay),
7418
7475
  colorizeRatio2(window2.outcome.approvedRisk.winRate),
7419
7476
  colorizeMetricNumber(window2.outcome.approvedRisk.profitFactor),
7477
+ colorizeMetricNumber(window2.outcome.approvedRisk.sharpeRatio),
7478
+ colorizeMetricNumber(window2.outcome.approvedRisk.sortinoRatio),
7479
+ colorizeMetricNumber(window2.outcome.approvedRisk.calmarRatio),
7420
7480
  colorizeProfit2(window2.outcome.approvedRisk.totalProfit),
7421
7481
  colorizeProfit2(-window2.outcome.approvedRisk.maxDrawdown),
7422
7482
  import_chalk5.default.gray(
@@ -7465,6 +7525,18 @@ var init_aiTrain = __esm({
7465
7525
  "profit_factor",
7466
7526
  colorizeMetricNumber(summary.approvedRisk.profitFactor)
7467
7527
  ],
7528
+ [
7529
+ "sharpe_ratio",
7530
+ colorizeMetricNumber(summary.approvedRisk.sharpeRatio)
7531
+ ],
7532
+ [
7533
+ "sortino_ratio",
7534
+ colorizeMetricNumber(summary.approvedRisk.sortinoRatio)
7535
+ ],
7536
+ [
7537
+ "calmar_ratio",
7538
+ colorizeMetricNumber(summary.approvedRisk.calmarRatio)
7539
+ ],
7468
7540
  [
7469
7541
  "payoff_ratio",
7470
7542
  colorizeMetricNumber(summary.approvedRisk.payoffRatio)
@@ -8591,7 +8663,7 @@ var init_binanceMarketContextBackfill = __esm({
8591
8663
  });
8592
8664
 
8593
8665
  // src/lib/derivativesContextBackfill.ts
8594
- var import_chalk7, import_progress4, import_async, import_constants5, import_indicators, import_timescale2, import_userSettings2, HOUR_MS, DAY_MS6, DEFAULT_LOOKBACK_HOURS, DEFAULT_INTERVALS, coinalyzeIntervalMap, derivativesIntervalMs, lastRequestTs, asInt2, parseList, parseBooleanFlag, normalizeSymbols, isDerivativesTargetContextEnabled, resolveDerivativesContextBackfillSymbols, chunkArray, isBacktestDerivativesContextEnabled, isSignalsDerivativesContextEnabled, isDerivativesContextBackfillEnabled, shouldBackfillDerivativesContextForBacktest, shouldBackfillDerivativesContextForSignals, resolveDerivativesContextIntervals, resolveDerivativesContextLookbackMs, resolveDerivativesContextBackfillWindow, resolveDerivativesContextIntervalBackfillWindow, resolveDerivativesContextMissingFetchFromMs, countBackfillWindows, buildBackfillWindows, hasDerivativesWindowCoverage, resolveDerivativesContextMissingCoverageFetchFromMs, groupDerivativesContextMissingFetchRanges, coverageKey, extendEdges, getCoinalyzeApiKey, getCoinalyzeBaseUrl, getRequestDelayMs, getRequestTimeoutMs, networkErrorCodes, getNestedErrorValue, getCoinalyzeErrorCause, isRetryableCoinalyzeFetchError, formatCoinalyzeRequestError, fetchJsonWithRateLimit, fetchCoinalyzeMarkets, selectBestMarket, buildMatches, normalizeMetricPoint, toSeriesMap, fetchMetricBatch, skippedBackfillResult2, backfillDerivativesContext, backfillDerivativesContextForBacktest, backfillDerivativesContextForSignals;
8666
+ var import_chalk7, import_progress4, import_async, import_constants5, import_indicators, import_timescale2, import_userSettings2, HOUR_MS, DAY_MS6, DEFAULT_LOOKBACK_HOURS, SOURCE_INTERVALS, coinalyzeIntervalMap, derivativesIntervalMs, lastRequestTs, asInt2, parseList, parseBooleanFlag, normalizeSymbols, isDerivativesTargetContextEnabled, resolveDerivativesContextBackfillSymbols, chunkArray, isBacktestDerivativesContextEnabled, isSignalsDerivativesContextEnabled, isDerivativesContextBackfillEnabled, shouldBackfillDerivativesContextForBacktest, shouldBackfillDerivativesContextForSignals, resolveDerivativesContextIntervals, resolveDerivativesContextLookbackMs, resolveDerivativesContextBackfillWindow, resolveDerivativesContextIntervalBackfillWindow, resolveDerivativesContextMissingFetchFromMs, resolveDerivativesContextFetchFromMs, resolveDerivativesContextRequiredFetchFromMs, countBackfillWindows, buildBackfillWindows, hasDerivativesWindowCoverage, resolveDerivativesContextMissingCoverageFetchFromMs, groupDerivativesContextMissingFetchRanges, getMissingClosedDerivativesSymbols, coverageKey, extendEdges, getCoinalyzeApiKey, getCoinalyzeBaseUrl, getRequestDelayMs, getRequestTimeoutMs, networkErrorCodes, getNestedErrorValue, getCoinalyzeErrorCause, isRetryableCoinalyzeFetchError, formatCoinalyzeRequestError, fetchJsonWithRateLimit, fetchCoinalyzeMarkets, selectBestMarket, buildMatches, normalizeMetricPoint, toSeriesMap, fetchMetricBatch, skippedBackfillResult2, backfillDerivativesContext, backfillDerivativesContextForBacktest, backfillDerivativesContextForSignals;
8595
8667
  var init_derivativesContextBackfill = __esm({
8596
8668
  "src/lib/derivativesContextBackfill.ts"() {
8597
8669
  "use strict";
@@ -8605,7 +8677,7 @@ var init_derivativesContextBackfill = __esm({
8605
8677
  HOUR_MS = 60 * 60 * 1e3;
8606
8678
  DAY_MS6 = 24 * HOUR_MS;
8607
8679
  DEFAULT_LOOKBACK_HOURS = 48;
8608
- DEFAULT_INTERVALS = ["15m", "1h"];
8680
+ SOURCE_INTERVALS = ["15m"];
8609
8681
  coinalyzeIntervalMap = {
8610
8682
  "15m": "15min",
8611
8683
  "1h": "1hour"
@@ -8666,10 +8738,7 @@ var init_derivativesContextBackfill = __esm({
8666
8738
  shouldBackfillDerivativesContextForBacktest = (params) => !params.cacheOnly && (params.aiEnabled || params.mlEnabled) && isBacktestDerivativesContextEnabled();
8667
8739
  shouldBackfillDerivativesContextForSignals = (params) => !params.cacheOnly && isSignalsDerivativesContextEnabled();
8668
8740
  resolveDerivativesContextIntervals = () => {
8669
- const intervals = (0, import_indicators.normalizeDerivativesIntervals)(
8670
- process.env.DERIVATIVES_CONTEXT_INTERVALS
8671
- );
8672
- return intervals.length ? intervals : DEFAULT_INTERVALS;
8741
+ return [...SOURCE_INTERVALS];
8673
8742
  };
8674
8743
  resolveDerivativesContextLookbackMs = () => {
8675
8744
  const hours = Number(process.env.DERIVATIVES_CONTEXT_LOOKBACK_HOURS);
@@ -8704,9 +8773,10 @@ var init_derivativesContextBackfill = __esm({
8704
8773
  };
8705
8774
  resolveDerivativesContextIntervalBackfillWindow = (params) => {
8706
8775
  const intervalMs2 = derivativesIntervalMs(params.interval);
8776
+ const alignedToMs = params.closedOnly ? (0, import_indicators.getLastClosedDerivativesBarStartMs)(params.toMs, params.interval) : Math.floor(params.toMs / intervalMs2) * intervalMs2;
8707
8777
  return {
8708
8778
  fromMs: Math.floor(params.fromMs / intervalMs2) * intervalMs2,
8709
- toMs: Math.floor(params.toMs / intervalMs2) * intervalMs2,
8779
+ toMs: alignedToMs,
8710
8780
  intervalMs: intervalMs2
8711
8781
  };
8712
8782
  };
@@ -8724,6 +8794,39 @@ var init_derivativesContextBackfill = __esm({
8724
8794
  }
8725
8795
  return null;
8726
8796
  };
8797
+ resolveDerivativesContextFetchFromMs = (params) => resolveDerivativesContextMissingFetchFromMs(params) ?? (params.refreshClosedTail ? params.toMs : null);
8798
+ resolveDerivativesContextRequiredFetchFromMs = (params) => {
8799
+ const dataCoverageFromMs = params.dataCoverageKeyExists ? null : params.mode === "signals" ? params.fromMs : resolveDerivativesContextMissingCoverageFetchFromMs({
8800
+ ranges: params.dataCoverageRanges,
8801
+ fromMs: params.fromMs,
8802
+ toMs: params.toMs,
8803
+ intervalMs: params.intervalMs
8804
+ });
8805
+ const edgesFromMs = dataCoverageFromMs == null ? null : resolveDerivativesContextFetchFromMs({
8806
+ edges: params.edges,
8807
+ fromMs: params.fromMs,
8808
+ toMs: params.toMs,
8809
+ intervalMs: params.intervalMs,
8810
+ refreshClosedTail: params.mode === "signals"
8811
+ });
8812
+ const dataFetchFromMs = dataCoverageFromMs != null && edgesFromMs != null ? Math.max(dataCoverageFromMs, edgesFromMs) : null;
8813
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
8814
+ interval: params.interval,
8815
+ fromMs: params.fromMs,
8816
+ toMs: params.toMs,
8817
+ nowMs: params.nowMs
8818
+ });
8819
+ const liquidationFetchFromMs = confirmedLiquidationWindow ? resolveDerivativesContextMissingCoverageFetchFromMs({
8820
+ ranges: params.liquidationCoverageRanges,
8821
+ fromMs: confirmedLiquidationWindow.fromMs,
8822
+ toMs: confirmedLiquidationWindow.toMs,
8823
+ intervalMs: params.intervalMs
8824
+ }) : null;
8825
+ const fetchStarts = [dataFetchFromMs, liquidationFetchFromMs].filter(
8826
+ (value) => value != null
8827
+ );
8828
+ return fetchStarts.length ? Math.min(...fetchStarts) : null;
8829
+ };
8727
8830
  countBackfillWindows = (params) => {
8728
8831
  let count = 0;
8729
8832
  let cursor = params.fromMs;
@@ -8787,6 +8890,14 @@ var init_derivativesContextBackfill = __esm({
8787
8890
  }
8788
8891
  return [...groups.entries()].sort(([a], [b]) => a - b).map(([fromMs, items]) => ({ fromMs, items }));
8789
8892
  };
8893
+ getMissingClosedDerivativesSymbols = (params) => {
8894
+ const availableSymbols = new Set(
8895
+ params.rows.filter((row) => row.ts.getTime() === params.expectedTimestamp).map((row) => row.symbol.trim().toUpperCase())
8896
+ );
8897
+ return params.symbols.filter(
8898
+ (symbol) => !availableSymbols.has(symbol.trim().toUpperCase())
8899
+ );
8900
+ };
8790
8901
  coverageKey = (params) => [
8791
8902
  params.symbol.trim().toUpperCase(),
8792
8903
  params.interval,
@@ -9051,7 +9162,8 @@ var init_derivativesContextBackfill = __esm({
9051
9162
  mode,
9052
9163
  startMs,
9053
9164
  endMs,
9054
- preloadStartMs: params.preloadStartMs
9165
+ preloadStartMs: params.preloadStartMs,
9166
+ nowMs: Date.now()
9055
9167
  });
9056
9168
  if (safeEndMs <= fromMs) {
9057
9169
  return skippedBackfillResult2();
@@ -9066,7 +9178,8 @@ var init_derivativesContextBackfill = __esm({
9066
9178
  ...resolveDerivativesContextIntervalBackfillWindow({
9067
9179
  fromMs,
9068
9180
  toMs: safeEndMs,
9069
- interval: interval5
9181
+ interval: interval5,
9182
+ closedOnly: mode === "signals"
9070
9183
  })
9071
9184
  })).filter((item) => item.toMs > item.fromMs);
9072
9185
  if (!intervalWindows.length) {
@@ -9084,15 +9197,33 @@ var init_derivativesContextBackfill = __esm({
9084
9197
  );
9085
9198
  const coverageKeysByInterval = /* @__PURE__ */ new Map();
9086
9199
  const coverageRangesByInterval = /* @__PURE__ */ new Map();
9200
+ const liquidationCoverageRangesByInterval = /* @__PURE__ */ new Map();
9201
+ const coverageNowMs = Date.now();
9087
9202
  await Promise.all(
9088
9203
  intervalWindows.map(async (window2) => {
9089
- const coverageRows = await (0, import_timescale2.getDerivativesBackfillCoverage)({
9090
- source: "coinalyze",
9091
- symbols,
9204
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
9092
9205
  interval: window2.interval,
9093
9206
  fromMs: window2.fromMs,
9094
- toMs: window2.toMs
9207
+ toMs: window2.toMs,
9208
+ nowMs: coverageNowMs
9095
9209
  });
9210
+ const [coverageRows, liquidationCoverageRows] = await Promise.all([
9211
+ (0, import_timescale2.getDerivativesBackfillCoverage)({
9212
+ source: "coinalyze",
9213
+ symbols,
9214
+ interval: window2.interval,
9215
+ fromMs: window2.fromMs,
9216
+ toMs: window2.toMs
9217
+ }),
9218
+ confirmedLiquidationWindow ? (0, import_timescale2.getDerivativesMetricCoverage)({
9219
+ source: "coinalyze",
9220
+ metric: "liquidation",
9221
+ symbols,
9222
+ interval: window2.interval,
9223
+ fromMs: confirmedLiquidationWindow.fromMs,
9224
+ toMs: confirmedLiquidationWindow.toMs
9225
+ }) : Promise.resolve([])
9226
+ ]);
9096
9227
  coverageKeysByInterval.set(
9097
9228
  window2.interval,
9098
9229
  new Set(
@@ -9114,6 +9245,17 @@ var init_derivativesContextBackfill = __esm({
9114
9245
  rangesBySymbol.set(symbol, ranges);
9115
9246
  }
9116
9247
  coverageRangesByInterval.set(window2.interval, rangesBySymbol);
9248
+ const liquidationRangesBySymbol = /* @__PURE__ */ new Map();
9249
+ for (const row of liquidationCoverageRows) {
9250
+ const symbol = row.symbol.toUpperCase();
9251
+ const ranges = liquidationRangesBySymbol.get(symbol) ?? [];
9252
+ ranges.push({ fromMs: row.fromMs, toMs: row.toMs });
9253
+ liquidationRangesBySymbol.set(symbol, ranges);
9254
+ }
9255
+ liquidationCoverageRangesByInterval.set(
9256
+ window2.interval,
9257
+ liquidationRangesBySymbol
9258
+ );
9117
9259
  })
9118
9260
  );
9119
9261
  const cachedWindows = intervalWindows.reduce(
@@ -9125,7 +9267,7 @@ var init_derivativesContextBackfill = __esm({
9125
9267
  }),
9126
9268
  0
9127
9269
  );
9128
- const allBackfillWindowsCached = intervalWindows.every((window2) => {
9270
+ const allBackfillWindowsCached = mode !== "signals" && intervalWindows.every((window2) => {
9129
9271
  const edgesBySymbol = edgesByInterval.get(window2.interval);
9130
9272
  const coverageKeys = coverageKeysByInterval.get(window2.interval);
9131
9273
  const backfillWindows = buildBackfillWindows({
@@ -9138,7 +9280,7 @@ var init_derivativesContextBackfill = __esm({
9138
9280
  (symbol) => backfillWindows.every((backfillWindow) => {
9139
9281
  const normalizedSymbol = symbol.toUpperCase();
9140
9282
  const coverageRanges = coverageRangesByInterval.get(window2.interval)?.get(normalizedSymbol) ?? [];
9141
- return hasDerivativesWindowCoverage({
9283
+ const dataCovered = hasDerivativesWindowCoverage({
9142
9284
  edges: edgesBySymbol?.get(normalizedSymbol),
9143
9285
  fromMs: backfillWindow.fromMs,
9144
9286
  toMs: backfillWindow.toMs
@@ -9155,6 +9297,19 @@ var init_derivativesContextBackfill = __esm({
9155
9297
  toMs: backfillWindow.toMs
9156
9298
  })
9157
9299
  );
9300
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
9301
+ interval: window2.interval,
9302
+ fromMs: backfillWindow.fromMs,
9303
+ toMs: backfillWindow.toMs,
9304
+ nowMs: coverageNowMs
9305
+ });
9306
+ const liquidationCovered = confirmedLiquidationWindow == null || resolveDerivativesContextMissingCoverageFetchFromMs({
9307
+ ranges: liquidationCoverageRangesByInterval.get(window2.interval)?.get(normalizedSymbol) ?? [],
9308
+ fromMs: confirmedLiquidationWindow.fromMs,
9309
+ toMs: confirmedLiquidationWindow.toMs,
9310
+ intervalMs: window2.intervalMs
9311
+ }) == null;
9312
+ return dataCovered && liquidationCovered;
9158
9313
  })
9159
9314
  );
9160
9315
  });
@@ -9239,6 +9394,11 @@ var init_derivativesContextBackfill = __esm({
9239
9394
  coverageKeysByInterval.set(interval5, coverageKeys);
9240
9395
  const coverageRangesBySymbol = coverageRangesByInterval.get(interval5) ?? /* @__PURE__ */ new Map();
9241
9396
  coverageRangesByInterval.set(interval5, coverageRangesBySymbol);
9397
+ const liquidationCoverageRangesBySymbol = liquidationCoverageRangesByInterval.get(interval5) ?? /* @__PURE__ */ new Map();
9398
+ liquidationCoverageRangesByInterval.set(
9399
+ interval5,
9400
+ liquidationCoverageRangesBySymbol
9401
+ );
9242
9402
  for (let batchIdx = 0; batchIdx < symbolBatches.length; batchIdx += 1) {
9243
9403
  const batch = symbolBatches[batchIdx];
9244
9404
  let cursor = window2.fromMs;
@@ -9251,30 +9411,26 @@ var init_derivativesContextBackfill = __esm({
9251
9411
  fromMs: cursor,
9252
9412
  toMs
9253
9413
  });
9254
- if (coverageKeys.has(key)) {
9255
- return null;
9256
- }
9257
9414
  const normalizedSymbol = item.symbol.toUpperCase();
9258
- const coverageFromMs = resolveDerivativesContextMissingCoverageFetchFromMs({
9259
- ranges: coverageRangesBySymbol.get(normalizedSymbol),
9260
- fromMs: cursor,
9261
- toMs,
9262
- intervalMs: intervalMs2
9263
- });
9264
- if (coverageFromMs == null) {
9265
- return null;
9266
- }
9267
- const edges = edgesBySymbol.get(normalizedSymbol);
9268
- const edgesFromMs = resolveDerivativesContextMissingFetchFromMs({
9269
- edges,
9270
- fromMs: cursor,
9271
- toMs,
9272
- intervalMs: intervalMs2
9273
- });
9274
- if (edgesFromMs == null) {
9275
- return null;
9276
- }
9277
- return { item, fromMs: Math.max(coverageFromMs, edgesFromMs) };
9415
+ const requiredFromMs = resolveDerivativesContextRequiredFetchFromMs(
9416
+ {
9417
+ mode,
9418
+ interval: interval5,
9419
+ intervalMs: intervalMs2,
9420
+ fromMs: cursor,
9421
+ toMs,
9422
+ nowMs: coverageNowMs,
9423
+ dataCoverageKeyExists: coverageKeys.has(key),
9424
+ dataCoverageRanges: coverageRangesBySymbol.get(normalizedSymbol),
9425
+ liquidationCoverageRanges: liquidationCoverageRangesBySymbol.get(normalizedSymbol),
9426
+ edges: edgesBySymbol.get(normalizedSymbol)
9427
+ }
9428
+ );
9429
+ if (requiredFromMs == null) return null;
9430
+ return {
9431
+ item,
9432
+ fromMs: requiredFromMs
9433
+ };
9278
9434
  }).filter(
9279
9435
  (item) => item != null
9280
9436
  );
@@ -9288,85 +9444,150 @@ var init_derivativesContextBackfill = __esm({
9288
9444
  const marketSymbols = missingBatch.map(
9289
9445
  (item) => item.marketSymbol
9290
9446
  );
9291
- const oiMap = await fetchMetricBatch({
9292
- endpoint: oiPath,
9293
- metric: "oi",
9294
- marketSymbols,
9295
- apiKey,
9296
- interval: interval5,
9297
- fromMs: group.fromMs,
9298
- toMs
9299
- });
9300
- const fundingMap = await fetchMetricBatch({
9301
- endpoint: fundingPath,
9302
- metric: "funding",
9303
- marketSymbols,
9304
- apiKey,
9305
- interval: interval5,
9306
- fromMs: group.fromMs,
9307
- toMs
9308
- });
9309
- const liqMap = await fetchMetricBatch({
9310
- endpoint: liqPath,
9311
- metric: "liq",
9312
- marketSymbols,
9313
- apiKey,
9314
- interval: interval5,
9315
- fromMs: group.fromMs,
9316
- toMs
9317
- });
9318
- const rows = missingBatch.flatMap((item) => {
9319
- const marketSymbol = item.marketSymbol.toUpperCase();
9320
- const points = (0, import_indicators.mergeCoinalyzeMetrics)({
9321
- symbol: item.symbol,
9322
- oiRaw: oiMap.get(marketSymbol) ?? [],
9323
- fundingRaw: fundingMap.get(marketSymbol) ?? [],
9324
- liqRaw: liqMap.get(marketSymbol) ?? []
9447
+ const maxClosedBarAttempts = mode === "signals" ? asInt2(
9448
+ process.env.DERIVATIVES_CONTEXT_CLOSED_BAR_MAX_ATTEMPTS,
9449
+ 3
9450
+ ) : 1;
9451
+ const closedBarRetryDelayMs = asInt2(
9452
+ process.env.DERIVATIVES_CONTEXT_CLOSED_BAR_RETRY_DELAY_MS,
9453
+ 2e3
9454
+ );
9455
+ let rows = [];
9456
+ let missingClosedSymbols = [];
9457
+ let liquidationRowsByMarket = /* @__PURE__ */ new Map();
9458
+ for (let attempt = 1; attempt <= maxClosedBarAttempts; attempt += 1) {
9459
+ const oiMap = await fetchMetricBatch({
9460
+ endpoint: oiPath,
9461
+ metric: "oi",
9462
+ marketSymbols,
9463
+ apiKey,
9464
+ interval: interval5,
9465
+ fromMs: group.fromMs,
9466
+ toMs
9325
9467
  });
9326
- return (0, import_indicators.coinalyzePointsToRows)(points, interval5, "coinalyze");
9327
- });
9468
+ const fundingMap = await fetchMetricBatch({
9469
+ endpoint: fundingPath,
9470
+ metric: "funding",
9471
+ marketSymbols,
9472
+ apiKey,
9473
+ interval: interval5,
9474
+ fromMs: group.fromMs,
9475
+ toMs
9476
+ });
9477
+ liquidationRowsByMarket = await fetchMetricBatch({
9478
+ endpoint: liqPath,
9479
+ metric: "liq",
9480
+ marketSymbols,
9481
+ apiKey,
9482
+ interval: interval5,
9483
+ fromMs: group.fromMs,
9484
+ toMs
9485
+ });
9486
+ rows = missingBatch.flatMap((item) => {
9487
+ const marketSymbol = item.marketSymbol.toUpperCase();
9488
+ const points = (0, import_indicators.mergeCoinalyzeMetrics)({
9489
+ symbol: item.symbol,
9490
+ oiRaw: oiMap.get(marketSymbol) ?? [],
9491
+ fundingRaw: fundingMap.get(marketSymbol) ?? [],
9492
+ liqRaw: liquidationRowsByMarket.get(marketSymbol) ?? []
9493
+ });
9494
+ return (0, import_indicators.coinalyzePointsToRows)(points, interval5, "coinalyze");
9495
+ });
9496
+ missingClosedSymbols = mode === "signals" ? getMissingClosedDerivativesSymbols({
9497
+ symbols: missingBatch.map((item) => item.symbol),
9498
+ rows,
9499
+ expectedTimestamp: toMs
9500
+ }) : [];
9501
+ if (!missingClosedSymbols.length) break;
9502
+ if (attempt < maxClosedBarAttempts) {
9503
+ await (0, import_async.delay)(closedBarRetryDelayMs);
9504
+ }
9505
+ }
9506
+ if (missingClosedSymbols.length) {
9507
+ throw new Error(
9508
+ `Coinalyze closed ${interval5} bar ${new Date(toMs).toISOString()} unavailable for ${missingClosedSymbols.join(",")}`
9509
+ );
9510
+ }
9328
9511
  if (rows.length) {
9329
9512
  await (0, import_timescale2.upsertDerivatives)(rows);
9330
9513
  totalRows += rows.length;
9331
9514
  }
9332
- const rowsCountBySymbol = /* @__PURE__ */ new Map();
9333
- for (const row of rows) {
9334
- const symbol = row.symbol.toUpperCase();
9335
- rowsCountBySymbol.set(
9336
- symbol,
9337
- (rowsCountBySymbol.get(symbol) ?? 0) + 1
9338
- );
9339
- }
9340
- const coverageRows = missingBatch.map((item) => {
9341
- const normalizedSymbol = item.symbol.toUpperCase();
9342
- const rowsCount = rowsCountBySymbol.get(normalizedSymbol) ?? 0;
9343
- return {
9344
- source: "coinalyze",
9345
- symbol: item.symbol,
9346
- interval: interval5,
9347
- fromMs: cursor,
9348
- toMs,
9349
- rowsCount
9350
- };
9515
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
9516
+ interval: interval5,
9517
+ fromMs: group.fromMs,
9518
+ toMs,
9519
+ nowMs: coverageNowMs
9351
9520
  });
9352
- await (0, import_timescale2.upsertDerivativesBackfillCoverage)(coverageRows);
9353
- for (const coverageRow of coverageRows) {
9354
- const symbol = coverageRow.symbol.toUpperCase();
9355
- edgesBySymbol.set(
9356
- symbol,
9357
- extendEdges(edgesBySymbol.get(symbol), cursor, toMs)
9358
- );
9359
- coverageKeys.add(
9360
- coverageKey({
9521
+ if (confirmedLiquidationWindow) {
9522
+ const metricCoverageRows = missingBatch.map((item) => {
9523
+ const marketSymbol = item.marketSymbol.toUpperCase();
9524
+ const eventRowsCount = (liquidationRowsByMarket.get(marketSymbol) ?? []).filter((point) => {
9525
+ const timestamp = (0, import_indicators.toCoinalyzeTimestampMs)(
9526
+ point.t ?? point.ts ?? point.time ?? point.timestamp
9527
+ );
9528
+ return timestamp != null && timestamp >= confirmedLiquidationWindow.fromMs && timestamp <= confirmedLiquidationWindow.toMs;
9529
+ }).length;
9530
+ return {
9531
+ source: "coinalyze",
9532
+ metric: "liquidation",
9533
+ symbol: item.symbol,
9534
+ interval: interval5,
9535
+ fromMs: confirmedLiquidationWindow.fromMs,
9536
+ toMs: confirmedLiquidationWindow.toMs,
9537
+ eventRowsCount
9538
+ };
9539
+ });
9540
+ await (0, import_timescale2.applyDerivativesMetricCoverage)(metricCoverageRows);
9541
+ for (const coverageRow of metricCoverageRows) {
9542
+ const symbol = coverageRow.symbol.toUpperCase();
9543
+ const ranges = liquidationCoverageRangesBySymbol.get(symbol) ?? [];
9544
+ ranges.push({
9545
+ fromMs: coverageRow.fromMs,
9546
+ toMs: coverageRow.toMs
9547
+ });
9548
+ liquidationCoverageRangesBySymbol.set(symbol, ranges);
9549
+ }
9550
+ }
9551
+ if (mode === "backtest") {
9552
+ const rowsCountBySymbol = /* @__PURE__ */ new Map();
9553
+ for (const row of rows) {
9554
+ const symbol = row.symbol.toUpperCase();
9555
+ rowsCountBySymbol.set(
9361
9556
  symbol,
9557
+ (rowsCountBySymbol.get(symbol) ?? 0) + 1
9558
+ );
9559
+ }
9560
+ const coverageRows = missingBatch.map((item) => {
9561
+ const normalizedSymbol = item.symbol.toUpperCase();
9562
+ const rowsCount = rowsCountBySymbol.get(normalizedSymbol) ?? 0;
9563
+ return {
9564
+ source: "coinalyze",
9565
+ symbol: item.symbol,
9362
9566
  interval: interval5,
9363
9567
  fromMs: cursor,
9364
- toMs
9365
- })
9366
- );
9367
- const coverageRanges = coverageRangesBySymbol.get(symbol) ?? [];
9368
- coverageRanges.push({ fromMs: cursor, toMs });
9369
- coverageRangesBySymbol.set(symbol, coverageRanges);
9568
+ toMs,
9569
+ rowsCount
9570
+ };
9571
+ });
9572
+ await (0, import_timescale2.upsertDerivativesBackfillCoverage)(coverageRows);
9573
+ for (const coverageRow of coverageRows) {
9574
+ const symbol = coverageRow.symbol.toUpperCase();
9575
+ edgesBySymbol.set(
9576
+ symbol,
9577
+ extendEdges(edgesBySymbol.get(symbol), cursor, toMs)
9578
+ );
9579
+ coverageKeys.add(
9580
+ coverageKey({
9581
+ symbol,
9582
+ interval: interval5,
9583
+ fromMs: cursor,
9584
+ toMs
9585
+ })
9586
+ );
9587
+ const coverageRanges = coverageRangesBySymbol.get(symbol) ?? [];
9588
+ coverageRanges.push({ fromMs: cursor, toMs });
9589
+ coverageRangesBySymbol.set(symbol, coverageRanges);
9590
+ }
9370
9591
  }
9371
9592
  }
9372
9593
  }
@@ -18915,11 +19136,11 @@ var init_derivativesIngest = __esm({
18915
19136
  import_userSettings4 = require("@tradejs/infra/userSettings");
18916
19137
  import_connectors9 = require("@tradejs/connectors");
18917
19138
  import_args12.default.example(
18918
- "yarn ts-node ./src/scripts/derivativesIngest --provider coinalyze --symbols BTCUSDT,ETHUSDT --intervals 15m,1h --days 120",
19139
+ "yarn ts-node ./src/scripts/derivativesIngest --provider coinalyze --symbols BTCUSDT,ETHUSDT --intervals 15m --days 120",
18919
19140
  "Ingest market features (derivatives/spread) into Timescale by provider"
18920
19141
  );
18921
19142
  import_args12.default.option(["s", "symbols"], "Comma-separated symbols", "BTCUSDT,ETHUSDT");
18922
- import_args12.default.option(["t", "intervals"], "Comma-separated intervals: 15m,1h", "15m,1h");
19143
+ import_args12.default.option(["t", "intervals"], "Comma-separated intervals: 15m,1h", "15m");
18923
19144
  import_args12.default.option(["d", "days"], "Lookback in days", 120);
18924
19145
  import_args12.default.option(
18925
19146
  ["p", "provider"],
@@ -18956,9 +19177,16 @@ var init_derivativesIngest = __esm({
18956
19177
  let totalSpreadRows = 0;
18957
19178
  for (const symbol of symbols) {
18958
19179
  for (const interval5 of intervals) {
19180
+ const lastClosedStartMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
19181
+ now2,
19182
+ interval5
19183
+ );
18959
19184
  let cursor = fromMs;
18960
- while (cursor < now2) {
18961
- const toMs = Math.min(now2, cursor + batchDays * 24 * 60 * 60 * 1e3);
19185
+ while (cursor < lastClosedStartMs) {
19186
+ const toMs = Math.min(
19187
+ lastClosedStartMs,
19188
+ cursor + batchDays * 24 * 60 * 60 * 1e3
19189
+ );
18962
19190
  process.stdout.write(
18963
19191
  `\r${import_chalk20.default.cyan(providerName)} ${import_chalk20.default.yellow(symbol)} ${interval5} ${new Date(cursor).toISOString()} .. ${new Date(toMs).toISOString()} `
18964
19192
  );
@@ -19018,7 +19246,7 @@ var init_derivativesIngestCoinalyzeAll = __esm({
19018
19246
  "1h": "1hour"
19019
19247
  };
19020
19248
  import_args13.default.example(
19021
- "yarn ts-node ./src/scripts/derivativesIngestCoinalyzeAll --days 120 --intervals 15m,1h",
19249
+ "yarn ts-node ./src/scripts/derivativesIngestCoinalyzeAll --days 120 --intervals 15m",
19022
19250
  "Fetch derivatives for all getTickers symbols matched to Coinalyze markets"
19023
19251
  );
19024
19252
  import_args13.default.option(["U", "user"], "User settings profile name from Redis", "root");
@@ -19026,7 +19254,7 @@ var init_derivativesIngestCoinalyzeAll = __esm({
19026
19254
  import_args13.default.option(["e", "exclude"], "Comma-separated exclude symbols");
19027
19255
  import_args13.default.option(["l", "tickersLimit"], "Tickers limit");
19028
19256
  import_args13.default.option(["c", "chunk"], "Chunk selector, e.g. 1/4");
19029
- import_args13.default.option(["i", "intervals"], "Intervals: 15m,1h", "15m,1h");
19257
+ import_args13.default.option(["i", "intervals"], "Intervals: 15m,1h", "15m");
19030
19258
  import_args13.default.option(["d", "days"], "Lookback in days", 120);
19031
19259
  import_args13.default.option(["b", "batchDays"], "Request chunk size in days", 120);
19032
19260
  import_args13.default.option(
@@ -19295,12 +19523,16 @@ var init_derivativesIngestCoinalyzeAll = __esm({
19295
19523
  let totalRows = 0;
19296
19524
  let failedWindows = 0;
19297
19525
  for (const interval5 of intervals) {
19526
+ const lastClosedStartMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(now2, interval5);
19298
19527
  for (let batchIdx = 0; batchIdx < symbolBatches.length; batchIdx += 1) {
19299
19528
  const batch = symbolBatches[batchIdx];
19300
19529
  const marketSymbols = batch.map((item) => item.marketSymbol);
19301
19530
  let cursor = fromMs;
19302
- while (cursor < now2) {
19303
- const toMs = Math.min(now2, cursor + batchDays * 24 * 60 * 60 * 1e3);
19531
+ while (cursor < lastClosedStartMs) {
19532
+ const toMs = Math.min(
19533
+ lastClosedStartMs,
19534
+ cursor + batchDays * 24 * 60 * 60 * 1e3
19535
+ );
19304
19536
  try {
19305
19537
  const oiMap = await fetchMetricBatch2({
19306
19538
  endpoint: oiPath,
@@ -19343,6 +19575,36 @@ var init_derivativesIngestCoinalyzeAll = __esm({
19343
19575
  await (0, import_timescale7.upsertDerivatives)(rows);
19344
19576
  totalRows += rows.length;
19345
19577
  }
19578
+ const confirmedLiquidationWindow = (0, import_indicators3.resolveCoinalyzeConfirmedIntradayCoverage)({
19579
+ interval: interval5,
19580
+ fromMs: cursor,
19581
+ toMs,
19582
+ nowMs: now2
19583
+ });
19584
+ if (confirmedLiquidationWindow) {
19585
+ await (0, import_timescale7.applyDerivativesMetricCoverage)(
19586
+ batch.map((item) => {
19587
+ const marketSymbol = item.marketSymbol.toUpperCase();
19588
+ const eventRowsCount = (liqMap.get(marketSymbol) ?? []).filter(
19589
+ (point) => {
19590
+ const timestamp = (0, import_indicators3.toCoinalyzeTimestampMs)(
19591
+ point.t ?? point.ts ?? point.time ?? point.timestamp
19592
+ );
19593
+ return timestamp != null && timestamp >= confirmedLiquidationWindow.fromMs && timestamp <= confirmedLiquidationWindow.toMs;
19594
+ }
19595
+ ).length;
19596
+ return {
19597
+ source: "coinalyze",
19598
+ metric: "liquidation",
19599
+ symbol: item.symbol,
19600
+ interval: interval5,
19601
+ fromMs: confirmedLiquidationWindow.fromMs,
19602
+ toMs: confirmedLiquidationWindow.toMs,
19603
+ eventRowsCount
19604
+ };
19605
+ })
19606
+ );
19607
+ }
19346
19608
  } catch (error) {
19347
19609
  failedWindows += 1;
19348
19610
  console.error(