@tradejs/cli 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2552,6 +2552,38 @@ var resolveDerivativesContextMissingFetchFromMs = (params) => {
2552
2552
  return null;
2553
2553
  };
2554
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
+ };
2555
2587
  var countBackfillWindows = (params) => {
2556
2588
  let count = 0;
2557
2589
  let cursor = params.fromMs;
@@ -2887,7 +2919,8 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2887
2919
  mode,
2888
2920
  startMs,
2889
2921
  endMs,
2890
- preloadStartMs: params.preloadStartMs
2922
+ preloadStartMs: params.preloadStartMs,
2923
+ nowMs: Date.now()
2891
2924
  });
2892
2925
  if (safeEndMs <= fromMs) {
2893
2926
  return skippedBackfillResult2();
@@ -2921,15 +2954,33 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2921
2954
  );
2922
2955
  const coverageKeysByInterval = /* @__PURE__ */ new Map();
2923
2956
  const coverageRangesByInterval = /* @__PURE__ */ new Map();
2957
+ const liquidationCoverageRangesByInterval = /* @__PURE__ */ new Map();
2958
+ const coverageNowMs = Date.now();
2924
2959
  await Promise.all(
2925
2960
  intervalWindows.map(async (window2) => {
2926
- const coverageRows = await (0, import_timescale2.getDerivativesBackfillCoverage)({
2927
- source: "coinalyze",
2928
- symbols,
2961
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
2929
2962
  interval: window2.interval,
2930
2963
  fromMs: window2.fromMs,
2931
- toMs: window2.toMs
2964
+ toMs: window2.toMs,
2965
+ nowMs: coverageNowMs
2932
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
+ ]);
2933
2984
  coverageKeysByInterval.set(
2934
2985
  window2.interval,
2935
2986
  new Set(
@@ -2951,6 +3002,17 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2951
3002
  rangesBySymbol.set(symbol, ranges);
2952
3003
  }
2953
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
+ );
2954
3016
  })
2955
3017
  );
2956
3018
  const cachedWindows = intervalWindows.reduce(
@@ -2975,7 +3037,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2975
3037
  (symbol) => backfillWindows.every((backfillWindow) => {
2976
3038
  const normalizedSymbol = symbol.toUpperCase();
2977
3039
  const coverageRanges = coverageRangesByInterval.get(window2.interval)?.get(normalizedSymbol) ?? [];
2978
- return hasDerivativesWindowCoverage({
3040
+ const dataCovered = hasDerivativesWindowCoverage({
2979
3041
  edges: edgesBySymbol?.get(normalizedSymbol),
2980
3042
  fromMs: backfillWindow.fromMs,
2981
3043
  toMs: backfillWindow.toMs
@@ -2992,6 +3054,19 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
2992
3054
  toMs: backfillWindow.toMs
2993
3055
  })
2994
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;
2995
3070
  })
2996
3071
  );
2997
3072
  });
@@ -3076,6 +3151,11 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
3076
3151
  coverageKeysByInterval.set(interval, coverageKeys);
3077
3152
  const coverageRangesBySymbol = coverageRangesByInterval.get(interval) ?? /* @__PURE__ */ new Map();
3078
3153
  coverageRangesByInterval.set(interval, coverageRangesBySymbol);
3154
+ const liquidationCoverageRangesBySymbol = liquidationCoverageRangesByInterval.get(interval) ?? /* @__PURE__ */ new Map();
3155
+ liquidationCoverageRangesByInterval.set(
3156
+ interval,
3157
+ liquidationCoverageRangesBySymbol
3158
+ );
3079
3159
  for (let batchIdx = 0; batchIdx < symbolBatches.length; batchIdx += 1) {
3080
3160
  const batch = symbolBatches[batchIdx];
3081
3161
  let cursor = window2.fromMs;
@@ -3088,33 +3168,25 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
3088
3168
  fromMs: cursor,
3089
3169
  toMs
3090
3170
  });
3091
- if (coverageKeys.has(key)) {
3092
- return null;
3093
- }
3094
3171
  const normalizedSymbol = item.symbol.toUpperCase();
3095
- const coverageFromMs = mode === "signals" ? cursor : resolveDerivativesContextMissingCoverageFetchFromMs({
3096
- ranges: coverageRangesBySymbol.get(normalizedSymbol),
3097
- fromMs: cursor,
3098
- toMs,
3099
- intervalMs
3100
- });
3101
- if (coverageFromMs == null) {
3102
- return null;
3103
- }
3104
- const edges = edgesBySymbol.get(normalizedSymbol);
3105
- const edgesFromMs = resolveDerivativesContextFetchFromMs({
3106
- edges,
3107
- fromMs: cursor,
3108
- toMs,
3109
- intervalMs,
3110
- refreshClosedTail: mode === "signals"
3111
- });
3112
- if (edgesFromMs == null) {
3113
- return null;
3114
- }
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;
3115
3187
  return {
3116
3188
  item,
3117
- fromMs: Math.max(coverageFromMs, edgesFromMs)
3189
+ fromMs: requiredFromMs
3118
3190
  };
3119
3191
  }).filter(
3120
3192
  (item) => item != null
@@ -3139,6 +3211,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
3139
3211
  );
3140
3212
  let rows = [];
3141
3213
  let missingClosedSymbols = [];
3214
+ let liquidationRowsByMarket = /* @__PURE__ */ new Map();
3142
3215
  for (let attempt = 1; attempt <= maxClosedBarAttempts; attempt += 1) {
3143
3216
  const oiMap = await fetchMetricBatch({
3144
3217
  endpoint: oiPath,
@@ -3158,7 +3231,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
3158
3231
  fromMs: group.fromMs,
3159
3232
  toMs
3160
3233
  });
3161
- const liqMap = await fetchMetricBatch({
3234
+ liquidationRowsByMarket = await fetchMetricBatch({
3162
3235
  endpoint: liqPath,
3163
3236
  metric: "liq",
3164
3237
  marketSymbols,
@@ -3173,7 +3246,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
3173
3246
  symbol: item.symbol,
3174
3247
  oiRaw: oiMap.get(marketSymbol) ?? [],
3175
3248
  fundingRaw: fundingMap.get(marketSymbol) ?? [],
3176
- liqRaw: liqMap.get(marketSymbol) ?? []
3249
+ liqRaw: liquidationRowsByMarket.get(marketSymbol) ?? []
3177
3250
  });
3178
3251
  return (0, import_indicators.coinalyzePointsToRows)(points, interval, "coinalyze");
3179
3252
  });
@@ -3196,6 +3269,42 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
3196
3269
  await (0, import_timescale2.upsertDerivatives)(rows);
3197
3270
  totalRows += rows.length;
3198
3271
  }
3272
+ const confirmedLiquidationWindow = (0, import_indicators.resolveCoinalyzeConfirmedIntradayCoverage)({
3273
+ interval,
3274
+ fromMs: group.fromMs,
3275
+ toMs,
3276
+ nowMs: coverageNowMs
3277
+ });
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
+ }
3199
3308
  if (mode === "backtest") {
3200
3309
  const rowsCountBySymbol = /* @__PURE__ */ new Map();
3201
3310
  for (const row of rows) {
@@ -23,6 +23,7 @@ __export(runtimeTradeSync_exports, {
23
23
  EXCHANGE_HISTORY_MAX_RANGE_MS: () => EXCHANGE_HISTORY_MAX_RANGE_MS,
24
24
  consumeClosedPnlMatch: () => consumeClosedPnlMatch,
25
25
  formatRuntimeTradeSyncError: () => formatRuntimeTradeSyncError,
26
+ isRuntimeTradeSyncFallbackClose: () => isRuntimeTradeSyncFallbackClose,
26
27
  loadClosedPnlRows: () => loadClosedPnlRows,
27
28
  splitExchangeHistoryTimeRange: () => splitExchangeHistoryTimeRange,
28
29
  syncRuntimeTrades: () => syncRuntimeTrades
@@ -32,7 +33,9 @@ var import_constants = require("@tradejs/core/constants");
32
33
  var import_time = require("@tradejs/core/time");
33
34
  var import_redis = require("@tradejs/infra/redis");
34
35
  var CLOSED_PNL_LIMIT = 100;
36
+ var CLOSED_PNL_RECONCILIATION_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
35
37
  var EXCHANGE_HISTORY_MAX_RANGE_MS = 7 * 24 * 60 * 60 * 1e3;
38
+ var isRuntimeTradeSyncFallbackClose = (trade) => trade.status === "closed" && trade.exitTimestamp === trade.lastSyncedAt && trade.exitPrice == null && trade.actualExitPrice == null && trade.closeFee == null && trade.fundingFee == null;
36
39
  var splitExchangeHistoryTimeRange = ({
37
40
  startTime,
38
41
  endTime,
@@ -161,7 +164,7 @@ var syncRuntimeTrades = async ({
161
164
  );
162
165
  const closedPnlRows = await loadClosedPnlRows({
163
166
  connector,
164
- startTime,
167
+ startTime: Math.max(0, startTime - CLOSED_PNL_RECONCILIATION_LOOKBACK_MS),
165
168
  endTime,
166
169
  callbacks: closedPnlCallbacks
167
170
  });
@@ -173,14 +176,14 @@ var syncRuntimeTrades = async ({
173
176
  }
174
177
  const syncedTrades = [];
175
178
  for (const trade of trades) {
176
- if (trade.status !== "active") {
179
+ if (trade.status !== "active" && !isRuntimeTradeSyncFallbackClose(trade)) {
177
180
  syncedTrades.push(trade);
178
181
  continue;
179
182
  }
180
183
  const openPosition = openPositionsBySymbol.get(trade.symbol);
181
184
  const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
182
185
  const isCurrentActiveTrade = activeOrderId === trade.orderId;
183
- if (!openPositionsReliable) {
186
+ if (trade.status === "active" && !openPositionsReliable) {
184
187
  syncedTrades.push({
185
188
  ...trade,
186
189
  status: "active",
@@ -188,7 +191,7 @@ var syncRuntimeTrades = async ({
188
191
  });
189
192
  continue;
190
193
  }
191
- if (isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
194
+ if (trade.status === "active" && isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
192
195
  const nextTrade2 = {
193
196
  ...trade,
194
197
  status: "active",
@@ -216,21 +219,24 @@ var syncRuntimeTrades = async ({
216
219
  continue;
217
220
  }
218
221
  const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
222
+ if (!matchedClosedPnl) {
223
+ continue;
224
+ }
219
225
  const nextTrade = {
220
226
  ...trade,
221
227
  status: "closed",
222
- currentPrice: matchedClosedPnl?.exitPrice ?? trade.currentPrice ?? null,
223
- currentPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
224
- closedPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
225
- actualEntryPrice: matchedClosedPnl?.entryPrice ?? trade.actualEntryPrice ?? null,
226
- exitPrice: matchedClosedPnl?.exitPrice ?? trade.exitPrice ?? null,
227
- actualExitPrice: matchedClosedPnl?.exitPrice ?? trade.actualExitPrice ?? null,
228
- exitTimestamp: matchedClosedPnl?.closedAt ?? trade.exitTimestamp ?? endTime,
228
+ currentPrice: matchedClosedPnl.exitPrice ?? trade.currentPrice ?? null,
229
+ currentPnl: matchedClosedPnl.closedPnl,
230
+ closedPnl: matchedClosedPnl.closedPnl,
231
+ actualEntryPrice: matchedClosedPnl.entryPrice ?? trade.actualEntryPrice ?? null,
232
+ exitPrice: matchedClosedPnl.exitPrice ?? trade.exitPrice ?? null,
233
+ actualExitPrice: matchedClosedPnl.exitPrice ?? trade.actualExitPrice ?? null,
234
+ exitTimestamp: matchedClosedPnl.closedAt,
229
235
  exitType: trade.exitType ?? null,
230
- openFee: matchedClosedPnl?.openFee ?? trade.openFee ?? null,
231
- closeFee: matchedClosedPnl?.closeFee ?? trade.closeFee ?? null,
232
- fundingFee: matchedClosedPnl?.fundingFee ?? trade.fundingFee ?? null,
233
- totalFee: matchedClosedPnl?.totalFee ?? trade.totalFee ?? null,
236
+ openFee: matchedClosedPnl.openFee ?? trade.openFee ?? null,
237
+ closeFee: matchedClosedPnl.closeFee ?? trade.closeFee ?? null,
238
+ fundingFee: matchedClosedPnl.fundingFee ?? trade.fundingFee ?? null,
239
+ totalFee: matchedClosedPnl.totalFee ?? trade.totalFee ?? null,
234
240
  lastSyncedAt: endTime
235
241
  };
236
242
  await Promise.all([
@@ -257,6 +263,7 @@ var syncRuntimeTrades = async ({
257
263
  EXCHANGE_HISTORY_MAX_RANGE_MS,
258
264
  consumeClosedPnlMatch,
259
265
  formatRuntimeTradeSyncError,
266
+ isRuntimeTradeSyncFallbackClose,
260
267
  loadClosedPnlRows,
261
268
  splitExchangeHistoryTimeRange,
262
269
  syncRuntimeTrades
@@ -1697,7 +1697,8 @@ var getStrategyChoices = async () => {
1697
1697
  "MaStrategy",
1698
1698
  "AdaptiveMomentumRibbon",
1699
1699
  "TrendLine",
1700
- "VolumeDivergence"
1700
+ "VolumeDivergence",
1701
+ "Grid"
1701
1702
  ];
1702
1703
  };
1703
1704
  var selectStrategy = async (promptLabel = "Select strategy", options) => {
@@ -54,7 +54,8 @@ var getStrategyChoices = async () => {
54
54
  "MaStrategy",
55
55
  "AdaptiveMomentumRibbon",
56
56
  "TrendLine",
57
- "VolumeDivergence"
57
+ "VolumeDivergence",
58
+ "Grid"
58
59
  ];
59
60
  };
60
61
  var selectStrategy = async (promptLabel = "Select strategy", options) => {
@@ -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
  )
@@ -2263,6 +2310,23 @@ var addDirectionalDerivedFeatures = (features) => {
2263
2310
  }
2264
2311
  features["derived.directIndicatorSupportCount"] = supportCount;
2265
2312
  };
2313
+ var addSignalRiskDistanceFeatures = ({
2314
+ features,
2315
+ signal
2316
+ }) => {
2317
+ const currentPrice = isFiniteNumber(signal.prices?.currentPrice) ? signal.prices.currentPrice : null;
2318
+ const stopLossPrice = isFiniteNumber(signal.prices?.stopLossPrice) ? signal.prices.stopLossPrice : null;
2319
+ const takeProfitPrice = isFiniteNumber(signal.prices?.takeProfitPrice) ? signal.prices.takeProfitPrice : null;
2320
+ if (currentPrice == null || currentPrice === 0) {
2321
+ return;
2322
+ }
2323
+ if (stopLossPrice != null) {
2324
+ features["derived.stopDistanceBps"] = Math.abs(currentPrice - stopLossPrice) / Math.abs(currentPrice) * 1e4;
2325
+ }
2326
+ if (takeProfitPrice != null) {
2327
+ features["derived.takeProfitDistanceBps"] = Math.abs(takeProfitPrice - currentPrice) / Math.abs(currentPrice) * 1e4;
2328
+ }
2329
+ };
2266
2330
  var collectAiPocketFeatures = ({
2267
2331
  payload,
2268
2332
  gateContext,
@@ -2290,6 +2354,7 @@ var collectAiPocketFeatures = ({
2290
2354
  maxDepth: 8,
2291
2355
  shouldSkipPath: featureProfile === "compact" ? isCompactFeaturePathSkipped : void 0
2292
2356
  });
2357
+ addSignalRiskDistanceFeatures({ features, signal });
2293
2358
  addDirectionalDerivedFeatures(features);
2294
2359
  return features;
2295
2360
  };