@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.
@@ -2434,7 +2434,7 @@ var import_userSettings = require("@tradejs/infra/userSettings");
2434
2434
  var HOUR_MS = 60 * 60 * 1e3;
2435
2435
  var DAY_MS2 = 24 * HOUR_MS;
2436
2436
  var DEFAULT_LOOKBACK_HOURS = 48;
2437
- var DEFAULT_INTERVALS = ["15m", "1h"];
2437
+ var SOURCE_INTERVALS = ["15m"];
2438
2438
  var coinalyzeIntervalMap = {
2439
2439
  "15m": "15min",
2440
2440
  "1h": "1hour"
@@ -2495,10 +2495,7 @@ var isDerivativesContextBackfillEnabled = (env) => {
2495
2495
  var shouldBackfillDerivativesContextForBacktest = (params) => !params.cacheOnly && (params.aiEnabled || params.mlEnabled) && isBacktestDerivativesContextEnabled();
2496
2496
  var shouldBackfillDerivativesContextForSignals = (params) => !params.cacheOnly && isSignalsDerivativesContextEnabled();
2497
2497
  var resolveDerivativesContextIntervals = () => {
2498
- const intervals = (0, import_indicators.normalizeDerivativesIntervals)(
2499
- process.env.DERIVATIVES_CONTEXT_INTERVALS
2500
- );
2501
- return intervals.length ? intervals : DEFAULT_INTERVALS;
2498
+ return [...SOURCE_INTERVALS];
2502
2499
  };
2503
2500
  var resolveDerivativesContextLookbackMs = () => {
2504
2501
  const hours = Number(process.env.DERIVATIVES_CONTEXT_LOOKBACK_HOURS);
@@ -2533,9 +2530,10 @@ var resolveDerivativesContextBackfillWindow = (params) => {
2533
2530
  };
2534
2531
  var resolveDerivativesContextIntervalBackfillWindow = (params) => {
2535
2532
  const intervalMs = derivativesIntervalMs(params.interval);
2533
+ const alignedToMs = params.closedOnly ? (0, import_indicators.getLastClosedDerivativesBarStartMs)(params.toMs, params.interval) : Math.floor(params.toMs / intervalMs) * intervalMs;
2536
2534
  return {
2537
2535
  fromMs: Math.floor(params.fromMs / intervalMs) * intervalMs,
2538
- toMs: Math.floor(params.toMs / intervalMs) * intervalMs,
2536
+ toMs: alignedToMs,
2539
2537
  intervalMs
2540
2538
  };
2541
2539
  };
@@ -2553,6 +2551,39 @@ var resolveDerivativesContextMissingFetchFromMs = (params) => {
2553
2551
  }
2554
2552
  return null;
2555
2553
  };
2554
+ var resolveDerivativesContextFetchFromMs = (params) => resolveDerivativesContextMissingFetchFromMs(params) ?? (params.refreshClosedTail ? params.toMs : null);
2555
+ var resolveDerivativesContextRequiredFetchFromMs = (params) => {
2556
+ const dataCoverageFromMs = params.dataCoverageKeyExists ? null : params.mode === "signals" ? params.fromMs : resolveDerivativesContextMissingCoverageFetchFromMs({
2557
+ ranges: params.dataCoverageRanges,
2558
+ fromMs: params.fromMs,
2559
+ toMs: params.toMs,
2560
+ intervalMs: params.intervalMs
2561
+ });
2562
+ const edgesFromMs = dataCoverageFromMs == null ? null : resolveDerivativesContextFetchFromMs({
2563
+ edges: params.edges,
2564
+ fromMs: params.fromMs,
2565
+ toMs: params.toMs,
2566
+ intervalMs: params.intervalMs,
2567
+ refreshClosedTail: params.mode === "signals"
2568
+ });
2569
+ const dataFetchFromMs = dataCoverageFromMs != null && edgesFromMs != null ? Math.max(dataCoverageFromMs, edgesFromMs) : null;
2570
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
2571
+ interval: params.interval,
2572
+ fromMs: params.fromMs,
2573
+ toMs: params.toMs,
2574
+ nowMs: params.nowMs
2575
+ });
2576
+ const liquidationFetchFromMs = confirmedLiquidationWindow ? resolveDerivativesContextMissingCoverageFetchFromMs({
2577
+ ranges: params.liquidationCoverageRanges,
2578
+ fromMs: confirmedLiquidationWindow.fromMs,
2579
+ toMs: confirmedLiquidationWindow.toMs,
2580
+ intervalMs: params.intervalMs
2581
+ }) : null;
2582
+ const fetchStarts = [dataFetchFromMs, liquidationFetchFromMs].filter(
2583
+ (value) => value != null
2584
+ );
2585
+ return fetchStarts.length ? Math.min(...fetchStarts) : null;
2586
+ };
2556
2587
  var countBackfillWindows = (params) => {
2557
2588
  let count = 0;
2558
2589
  let cursor = params.fromMs;
@@ -2616,6 +2647,14 @@ var groupDerivativesContextMissingFetchRanges = (ranges) => {
2616
2647
  }
2617
2648
  return [...groups.entries()].sort(([a], [b]) => a - b).map(([fromMs, items]) => ({ fromMs, items }));
2618
2649
  };
2650
+ var getMissingClosedDerivativesSymbols = (params) => {
2651
+ const availableSymbols = new Set(
2652
+ params.rows.filter((row) => row.ts.getTime() === params.expectedTimestamp).map((row) => row.symbol.trim().toUpperCase())
2653
+ );
2654
+ return params.symbols.filter(
2655
+ (symbol) => !availableSymbols.has(symbol.trim().toUpperCase())
2656
+ );
2657
+ };
2619
2658
  var coverageKey = (params) => [
2620
2659
  params.symbol.trim().toUpperCase(),
2621
2660
  params.interval,
@@ -2880,7 +2919,8 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2880
2919
  mode,
2881
2920
  startMs,
2882
2921
  endMs,
2883
- preloadStartMs: params.preloadStartMs
2922
+ preloadStartMs: params.preloadStartMs,
2923
+ nowMs: Date.now()
2884
2924
  });
2885
2925
  if (safeEndMs <= fromMs) {
2886
2926
  return skippedBackfillResult2();
@@ -2895,7 +2935,8 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2895
2935
  ...resolveDerivativesContextIntervalBackfillWindow({
2896
2936
  fromMs,
2897
2937
  toMs: safeEndMs,
2898
- interval
2938
+ interval,
2939
+ closedOnly: mode === "signals"
2899
2940
  })
2900
2941
  })).filter((item) => item.toMs > item.fromMs);
2901
2942
  if (!intervalWindows.length) {
@@ -2913,15 +2954,33 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2913
2954
  );
2914
2955
  const coverageKeysByInterval = /* @__PURE__ */ new Map();
2915
2956
  const coverageRangesByInterval = /* @__PURE__ */ new Map();
2957
+ const liquidationCoverageRangesByInterval = /* @__PURE__ */ new Map();
2958
+ const coverageNowMs = Date.now();
2916
2959
  await Promise.all(
2917
2960
  intervalWindows.map(async (window2) => {
2918
- const coverageRows = await (0, import_timescale2.getDerivativesBackfillCoverage)({
2919
- source: "coinalyze",
2920
- symbols,
2961
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
2921
2962
  interval: window2.interval,
2922
2963
  fromMs: window2.fromMs,
2923
- toMs: window2.toMs
2964
+ toMs: window2.toMs,
2965
+ nowMs: coverageNowMs
2924
2966
  });
2967
+ const [coverageRows, liquidationCoverageRows] = await Promise.all([
2968
+ (0, import_timescale2.getDerivativesBackfillCoverage)({
2969
+ source: "coinalyze",
2970
+ symbols,
2971
+ interval: window2.interval,
2972
+ fromMs: window2.fromMs,
2973
+ toMs: window2.toMs
2974
+ }),
2975
+ confirmedLiquidationWindow ? (0, import_timescale2.getDerivativesMetricCoverage)({
2976
+ source: "coinalyze",
2977
+ metric: "liquidation",
2978
+ symbols,
2979
+ interval: window2.interval,
2980
+ fromMs: confirmedLiquidationWindow.fromMs,
2981
+ toMs: confirmedLiquidationWindow.toMs
2982
+ }) : Promise.resolve([])
2983
+ ]);
2925
2984
  coverageKeysByInterval.set(
2926
2985
  window2.interval,
2927
2986
  new Set(
@@ -2943,6 +3002,17 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2943
3002
  rangesBySymbol.set(symbol, ranges);
2944
3003
  }
2945
3004
  coverageRangesByInterval.set(window2.interval, rangesBySymbol);
3005
+ const liquidationRangesBySymbol = /* @__PURE__ */ new Map();
3006
+ for (const row of liquidationCoverageRows) {
3007
+ const symbol = row.symbol.toUpperCase();
3008
+ const ranges = liquidationRangesBySymbol.get(symbol) ?? [];
3009
+ ranges.push({ fromMs: row.fromMs, toMs: row.toMs });
3010
+ liquidationRangesBySymbol.set(symbol, ranges);
3011
+ }
3012
+ liquidationCoverageRangesByInterval.set(
3013
+ window2.interval,
3014
+ liquidationRangesBySymbol
3015
+ );
2946
3016
  })
2947
3017
  );
2948
3018
  const cachedWindows = intervalWindows.reduce(
@@ -2954,7 +3024,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2954
3024
  }),
2955
3025
  0
2956
3026
  );
2957
- const allBackfillWindowsCached = intervalWindows.every((window2) => {
3027
+ const allBackfillWindowsCached = mode !== "signals" && intervalWindows.every((window2) => {
2958
3028
  const edgesBySymbol = edgesByInterval.get(window2.interval);
2959
3029
  const coverageKeys = coverageKeysByInterval.get(window2.interval);
2960
3030
  const backfillWindows = buildBackfillWindows({
@@ -2967,7 +3037,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2967
3037
  (symbol) => backfillWindows.every((backfillWindow) => {
2968
3038
  const normalizedSymbol = symbol.toUpperCase();
2969
3039
  const coverageRanges = coverageRangesByInterval.get(window2.interval)?.get(normalizedSymbol) ?? [];
2970
- return hasDerivativesWindowCoverage({
3040
+ const dataCovered = hasDerivativesWindowCoverage({
2971
3041
  edges: edgesBySymbol?.get(normalizedSymbol),
2972
3042
  fromMs: backfillWindow.fromMs,
2973
3043
  toMs: backfillWindow.toMs
@@ -2984,6 +3054,19 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2984
3054
  toMs: backfillWindow.toMs
2985
3055
  })
2986
3056
  );
3057
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
3058
+ interval: window2.interval,
3059
+ fromMs: backfillWindow.fromMs,
3060
+ toMs: backfillWindow.toMs,
3061
+ nowMs: coverageNowMs
3062
+ });
3063
+ const liquidationCovered = confirmedLiquidationWindow == null || resolveDerivativesContextMissingCoverageFetchFromMs({
3064
+ ranges: liquidationCoverageRangesByInterval.get(window2.interval)?.get(normalizedSymbol) ?? [],
3065
+ fromMs: confirmedLiquidationWindow.fromMs,
3066
+ toMs: confirmedLiquidationWindow.toMs,
3067
+ intervalMs: window2.intervalMs
3068
+ }) == null;
3069
+ return dataCovered && liquidationCovered;
2987
3070
  })
2988
3071
  );
2989
3072
  });
@@ -3068,6 +3151,11 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
3068
3151
  coverageKeysByInterval.set(interval, coverageKeys);
3069
3152
  const coverageRangesBySymbol = coverageRangesByInterval.get(interval) ?? /* @__PURE__ */ new Map();
3070
3153
  coverageRangesByInterval.set(interval, coverageRangesBySymbol);
3154
+ const liquidationCoverageRangesBySymbol = liquidationCoverageRangesByInterval.get(interval) ?? /* @__PURE__ */ new Map();
3155
+ liquidationCoverageRangesByInterval.set(
3156
+ interval,
3157
+ liquidationCoverageRangesBySymbol
3158
+ );
3071
3159
  for (let batchIdx = 0; batchIdx < symbolBatches.length; batchIdx += 1) {
3072
3160
  const batch = symbolBatches[batchIdx];
3073
3161
  let cursor = window2.fromMs;
@@ -3080,30 +3168,26 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
3080
3168
  fromMs: cursor,
3081
3169
  toMs
3082
3170
  });
3083
- if (coverageKeys.has(key)) {
3084
- return null;
3085
- }
3086
3171
  const normalizedSymbol = item.symbol.toUpperCase();
3087
- const coverageFromMs = resolveDerivativesContextMissingCoverageFetchFromMs({
3088
- ranges: coverageRangesBySymbol.get(normalizedSymbol),
3089
- fromMs: cursor,
3090
- toMs,
3091
- intervalMs
3092
- });
3093
- if (coverageFromMs == null) {
3094
- return null;
3095
- }
3096
- const edges = edgesBySymbol.get(normalizedSymbol);
3097
- const edgesFromMs = resolveDerivativesContextMissingFetchFromMs({
3098
- edges,
3099
- fromMs: cursor,
3100
- toMs,
3101
- intervalMs
3102
- });
3103
- if (edgesFromMs == null) {
3104
- return null;
3105
- }
3106
- return { item, fromMs: Math.max(coverageFromMs, edgesFromMs) };
3172
+ const requiredFromMs = resolveDerivativesContextRequiredFetchFromMs(
3173
+ {
3174
+ mode,
3175
+ interval,
3176
+ intervalMs,
3177
+ fromMs: cursor,
3178
+ toMs,
3179
+ nowMs: coverageNowMs,
3180
+ dataCoverageKeyExists: coverageKeys.has(key),
3181
+ dataCoverageRanges: coverageRangesBySymbol.get(normalizedSymbol),
3182
+ liquidationCoverageRanges: liquidationCoverageRangesBySymbol.get(normalizedSymbol),
3183
+ edges: edgesBySymbol.get(normalizedSymbol)
3184
+ }
3185
+ );
3186
+ if (requiredFromMs == null) return null;
3187
+ return {
3188
+ item,
3189
+ fromMs: requiredFromMs
3190
+ };
3107
3191
  }).filter(
3108
3192
  (item) => item != null
3109
3193
  );
@@ -3117,85 +3201,150 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
3117
3201
  const marketSymbols = missingBatch.map(
3118
3202
  (item) => item.marketSymbol
3119
3203
  );
3120
- const oiMap = await fetchMetricBatch({
3121
- endpoint: oiPath,
3122
- metric: "oi",
3123
- marketSymbols,
3124
- apiKey,
3125
- interval,
3126
- fromMs: group.fromMs,
3127
- toMs
3128
- });
3129
- const fundingMap = await fetchMetricBatch({
3130
- endpoint: fundingPath,
3131
- metric: "funding",
3132
- marketSymbols,
3133
- apiKey,
3134
- interval,
3135
- fromMs: group.fromMs,
3136
- toMs
3137
- });
3138
- const liqMap = await fetchMetricBatch({
3139
- endpoint: liqPath,
3140
- metric: "liq",
3141
- marketSymbols,
3142
- apiKey,
3143
- interval,
3144
- fromMs: group.fromMs,
3145
- toMs
3146
- });
3147
- const rows = missingBatch.flatMap((item) => {
3148
- const marketSymbol = item.marketSymbol.toUpperCase();
3149
- const points = (0, import_indicators.mergeCoinalyzeMetrics)({
3150
- symbol: item.symbol,
3151
- oiRaw: oiMap.get(marketSymbol) ?? [],
3152
- fundingRaw: fundingMap.get(marketSymbol) ?? [],
3153
- liqRaw: liqMap.get(marketSymbol) ?? []
3204
+ const maxClosedBarAttempts = mode === "signals" ? asInt2(
3205
+ process.env.DERIVATIVES_CONTEXT_CLOSED_BAR_MAX_ATTEMPTS,
3206
+ 3
3207
+ ) : 1;
3208
+ const closedBarRetryDelayMs = asInt2(
3209
+ process.env.DERIVATIVES_CONTEXT_CLOSED_BAR_RETRY_DELAY_MS,
3210
+ 2e3
3211
+ );
3212
+ let rows = [];
3213
+ let missingClosedSymbols = [];
3214
+ let liquidationRowsByMarket = /* @__PURE__ */ new Map();
3215
+ for (let attempt = 1; attempt <= maxClosedBarAttempts; attempt += 1) {
3216
+ const oiMap = await fetchMetricBatch({
3217
+ endpoint: oiPath,
3218
+ metric: "oi",
3219
+ marketSymbols,
3220
+ apiKey,
3221
+ interval,
3222
+ fromMs: group.fromMs,
3223
+ toMs
3154
3224
  });
3155
- return (0, import_indicators.coinalyzePointsToRows)(points, interval, "coinalyze");
3156
- });
3225
+ const fundingMap = await fetchMetricBatch({
3226
+ endpoint: fundingPath,
3227
+ metric: "funding",
3228
+ marketSymbols,
3229
+ apiKey,
3230
+ interval,
3231
+ fromMs: group.fromMs,
3232
+ toMs
3233
+ });
3234
+ liquidationRowsByMarket = await fetchMetricBatch({
3235
+ endpoint: liqPath,
3236
+ metric: "liq",
3237
+ marketSymbols,
3238
+ apiKey,
3239
+ interval,
3240
+ fromMs: group.fromMs,
3241
+ toMs
3242
+ });
3243
+ rows = missingBatch.flatMap((item) => {
3244
+ const marketSymbol = item.marketSymbol.toUpperCase();
3245
+ const points = (0, import_indicators.mergeCoinalyzeMetrics)({
3246
+ symbol: item.symbol,
3247
+ oiRaw: oiMap.get(marketSymbol) ?? [],
3248
+ fundingRaw: fundingMap.get(marketSymbol) ?? [],
3249
+ liqRaw: liquidationRowsByMarket.get(marketSymbol) ?? []
3250
+ });
3251
+ return (0, import_indicators.coinalyzePointsToRows)(points, interval, "coinalyze");
3252
+ });
3253
+ missingClosedSymbols = mode === "signals" ? getMissingClosedDerivativesSymbols({
3254
+ symbols: missingBatch.map((item) => item.symbol),
3255
+ rows,
3256
+ expectedTimestamp: toMs
3257
+ }) : [];
3258
+ if (!missingClosedSymbols.length) break;
3259
+ if (attempt < maxClosedBarAttempts) {
3260
+ await (0, import_async.delay)(closedBarRetryDelayMs);
3261
+ }
3262
+ }
3263
+ if (missingClosedSymbols.length) {
3264
+ throw new Error(
3265
+ `Coinalyze closed ${interval} bar ${new Date(toMs).toISOString()} unavailable for ${missingClosedSymbols.join(",")}`
3266
+ );
3267
+ }
3157
3268
  if (rows.length) {
3158
3269
  await (0, import_timescale2.upsertDerivatives)(rows);
3159
3270
  totalRows += rows.length;
3160
3271
  }
3161
- const rowsCountBySymbol = /* @__PURE__ */ new Map();
3162
- for (const row of rows) {
3163
- const symbol = row.symbol.toUpperCase();
3164
- rowsCountBySymbol.set(
3165
- symbol,
3166
- (rowsCountBySymbol.get(symbol) ?? 0) + 1
3167
- );
3168
- }
3169
- const coverageRows = missingBatch.map((item) => {
3170
- const normalizedSymbol = item.symbol.toUpperCase();
3171
- const rowsCount = rowsCountBySymbol.get(normalizedSymbol) ?? 0;
3172
- return {
3173
- source: "coinalyze",
3174
- symbol: item.symbol,
3175
- interval,
3176
- fromMs: cursor,
3177
- toMs,
3178
- rowsCount
3179
- };
3272
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
3273
+ interval,
3274
+ fromMs: group.fromMs,
3275
+ toMs,
3276
+ nowMs: coverageNowMs
3180
3277
  });
3181
- await (0, import_timescale2.upsertDerivativesBackfillCoverage)(coverageRows);
3182
- for (const coverageRow of coverageRows) {
3183
- const symbol = coverageRow.symbol.toUpperCase();
3184
- edgesBySymbol.set(
3185
- symbol,
3186
- extendEdges(edgesBySymbol.get(symbol), cursor, toMs)
3187
- );
3188
- coverageKeys.add(
3189
- coverageKey({
3278
+ if (confirmedLiquidationWindow) {
3279
+ const metricCoverageRows = missingBatch.map((item) => {
3280
+ const marketSymbol = item.marketSymbol.toUpperCase();
3281
+ const eventRowsCount = (liquidationRowsByMarket.get(marketSymbol) ?? []).filter((point) => {
3282
+ const timestamp = (0, import_indicators.toCoinalyzeTimestampMs)(
3283
+ point.t ?? point.ts ?? point.time ?? point.timestamp
3284
+ );
3285
+ return timestamp != null && timestamp >= confirmedLiquidationWindow.fromMs && timestamp <= confirmedLiquidationWindow.toMs;
3286
+ }).length;
3287
+ return {
3288
+ source: "coinalyze",
3289
+ metric: "liquidation",
3290
+ symbol: item.symbol,
3291
+ interval,
3292
+ fromMs: confirmedLiquidationWindow.fromMs,
3293
+ toMs: confirmedLiquidationWindow.toMs,
3294
+ eventRowsCount
3295
+ };
3296
+ });
3297
+ await (0, import_timescale2.applyDerivativesMetricCoverage)(metricCoverageRows);
3298
+ for (const coverageRow of metricCoverageRows) {
3299
+ const symbol = coverageRow.symbol.toUpperCase();
3300
+ const ranges = liquidationCoverageRangesBySymbol.get(symbol) ?? [];
3301
+ ranges.push({
3302
+ fromMs: coverageRow.fromMs,
3303
+ toMs: coverageRow.toMs
3304
+ });
3305
+ liquidationCoverageRangesBySymbol.set(symbol, ranges);
3306
+ }
3307
+ }
3308
+ if (mode === "backtest") {
3309
+ const rowsCountBySymbol = /* @__PURE__ */ new Map();
3310
+ for (const row of rows) {
3311
+ const symbol = row.symbol.toUpperCase();
3312
+ rowsCountBySymbol.set(
3190
3313
  symbol,
3314
+ (rowsCountBySymbol.get(symbol) ?? 0) + 1
3315
+ );
3316
+ }
3317
+ const coverageRows = missingBatch.map((item) => {
3318
+ const normalizedSymbol = item.symbol.toUpperCase();
3319
+ const rowsCount = rowsCountBySymbol.get(normalizedSymbol) ?? 0;
3320
+ return {
3321
+ source: "coinalyze",
3322
+ symbol: item.symbol,
3191
3323
  interval,
3192
3324
  fromMs: cursor,
3193
- toMs
3194
- })
3195
- );
3196
- const coverageRanges = coverageRangesBySymbol.get(symbol) ?? [];
3197
- coverageRanges.push({ fromMs: cursor, toMs });
3198
- coverageRangesBySymbol.set(symbol, coverageRanges);
3325
+ toMs,
3326
+ rowsCount
3327
+ };
3328
+ });
3329
+ await (0, import_timescale2.upsertDerivativesBackfillCoverage)(coverageRows);
3330
+ for (const coverageRow of coverageRows) {
3331
+ const symbol = coverageRow.symbol.toUpperCase();
3332
+ edgesBySymbol.set(
3333
+ symbol,
3334
+ extendEdges(edgesBySymbol.get(symbol), cursor, toMs)
3335
+ );
3336
+ coverageKeys.add(
3337
+ coverageKey({
3338
+ symbol,
3339
+ interval,
3340
+ fromMs: cursor,
3341
+ toMs
3342
+ })
3343
+ );
3344
+ const coverageRanges = coverageRangesBySymbol.get(symbol) ?? [];
3345
+ coverageRanges.push({ fromMs: cursor, toMs });
3346
+ coverageRangesBySymbol.set(symbol, coverageRanges);
3347
+ }
3199
3348
  }
3200
3349
  }
3201
3350
  }
@@ -1663,6 +1663,7 @@ var divideOrNull = (num, denom) => {
1663
1663
  var DAY_MS = 24 * 60 * 60 * 1e3;
1664
1664
  var DAYS_PER_WEEK = 7;
1665
1665
  var DAYS_PER_MONTH = 30.4375;
1666
+ var DAYS_PER_YEAR = 365;
1666
1667
  var getEvaluationPeriodDays = (evaluations) => {
1667
1668
  let minTimestamp = null;
1668
1669
  let maxTimestamp = null;
@@ -1684,6 +1685,40 @@ var getEvaluationPeriodDays = (evaluations) => {
1684
1685
  return Math.max((maxTimestamp - minTimestamp) / DAY_MS, 1);
1685
1686
  };
1686
1687
  var qualitySortKey = (quality) => quality == null ? Number.POSITIVE_INFINITY : quality;
1688
+ var calculateTradePnlRiskRatios = ({
1689
+ profits,
1690
+ totalProfit,
1691
+ maxDrawdown,
1692
+ periodDays
1693
+ }) => {
1694
+ if (!profits.length || periodDays == null || periodDays <= 0) {
1695
+ return {
1696
+ sharpeRatio: null,
1697
+ sortinoRatio: null,
1698
+ calmarRatio: null
1699
+ };
1700
+ }
1701
+ const meanProfit = totalProfit / profits.length;
1702
+ const variance = profits.reduce((sum, profit) => {
1703
+ const diff = profit - meanProfit;
1704
+ return sum + diff * diff;
1705
+ }, 0) / profits.length;
1706
+ const stdDev = Math.sqrt(variance);
1707
+ const downsideDeviation = Math.sqrt(
1708
+ profits.reduce(
1709
+ (sum, profit) => profit < 0 ? sum + profit * profit : sum,
1710
+ 0
1711
+ ) / profits.length
1712
+ );
1713
+ const annualizationScale = Math.sqrt(
1714
+ profits.length / periodDays * DAYS_PER_YEAR
1715
+ );
1716
+ return {
1717
+ sharpeRatio: stdDev > 0 && annualizationScale > 0 ? meanProfit / stdDev * annualizationScale : null,
1718
+ sortinoRatio: downsideDeviation > 0 && annualizationScale > 0 ? meanProfit / downsideDeviation * annualizationScale : null,
1719
+ calmarRatio: maxDrawdown > 0 ? totalProfit / periodDays * DAYS_PER_YEAR / maxDrawdown : null
1720
+ };
1721
+ };
1687
1722
  var emptyRiskSummary = () => ({
1688
1723
  trades: 0,
1689
1724
  totalProfit: 0,
@@ -1699,12 +1734,15 @@ var emptyRiskSummary = () => ({
1699
1734
  maxDrawdown: 0,
1700
1735
  maxDrawdownPctOfGrossProfit: null,
1701
1736
  maxDrawdownPctOfTotalProfit: null,
1737
+ sharpeRatio: null,
1738
+ sortinoRatio: null,
1739
+ calmarRatio: null,
1702
1740
  recoveryFactor: null,
1703
1741
  ulcerIndex: null,
1704
1742
  maxConsecutiveWins: 0,
1705
1743
  maxConsecutiveLosses: 0
1706
1744
  });
1707
- var summarizeApprovedRisk = (evaluations) => {
1745
+ var summarizeApprovedRisk = (evaluations, periodDays = getEvaluationPeriodDays(evaluations)) => {
1708
1746
  const approvedEvaluations = evaluations.filter((evaluation) => evaluation.aiApproved).sort((left, right) => {
1709
1747
  const leftTimestamp = typeof left.timestamp === "number" && Number.isFinite(left.timestamp) ? left.timestamp : Number.POSITIVE_INFINITY;
1710
1748
  const rightTimestamp = typeof right.timestamp === "number" && Number.isFinite(right.timestamp) ? right.timestamp : Number.POSITIVE_INFINITY;
@@ -1727,8 +1765,10 @@ var summarizeApprovedRisk = (evaluations) => {
1727
1765
  let currentLossStreak = 0;
1728
1766
  let maxConsecutiveWins = 0;
1729
1767
  let maxConsecutiveLosses = 0;
1768
+ const approvedProfits = [];
1730
1769
  for (const evaluation of approvedEvaluations) {
1731
1770
  const profit = evaluation.profit;
1771
+ approvedProfits.push(profit);
1732
1772
  if (profit > 0) {
1733
1773
  grossProfit += profit;
1734
1774
  wins += 1;
@@ -1756,6 +1796,12 @@ var summarizeApprovedRisk = (evaluations) => {
1756
1796
  const totalProfit = grossProfit - grossLoss;
1757
1797
  const avgWin = divideOrNull(grossProfit, wins);
1758
1798
  const avgLoss = divideOrNull(grossLoss, losses);
1799
+ const riskRatios = calculateTradePnlRiskRatios({
1800
+ profits: approvedProfits,
1801
+ totalProfit,
1802
+ maxDrawdown,
1803
+ periodDays
1804
+ });
1759
1805
  return {
1760
1806
  trades: approvedEvaluations.length,
1761
1807
  totalProfit,
@@ -1771,6 +1817,7 @@ var summarizeApprovedRisk = (evaluations) => {
1771
1817
  maxDrawdown,
1772
1818
  maxDrawdownPctOfGrossProfit: grossProfit > 0 ? maxDrawdown / grossProfit : null,
1773
1819
  maxDrawdownPctOfTotalProfit: totalProfit > 0 ? maxDrawdown / totalProfit : null,
1820
+ ...riskRatios,
1774
1821
  recoveryFactor: maxDrawdown > 0 ? totalProfit / maxDrawdown : null,
1775
1822
  ulcerIndex: Math.sqrt(drawdownSquares / approvedEvaluations.length),
1776
1823
  maxConsecutiveWins,
@@ -1869,7 +1916,7 @@ var summarizeAiTrainEvaluations = (evaluations) => {
1869
1916
  avgApprovedTradesPerDay,
1870
1917
  avgApprovedTradesPerWeek,
1871
1918
  expectancyDelta,
1872
- approvedRisk: summarizeApprovedRisk(evaluations),
1919
+ approvedRisk: summarizeApprovedRisk(evaluations, periodDays),
1873
1920
  qualityBuckets: [...bucketMap.values()].sort(
1874
1921
  (a, b) => qualitySortKey(a.quality) - qualitySortKey(b.quality)
1875
1922
  )