backtest-kit 6.9.0 → 6.11.0

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/build/index.cjs CHANGED
@@ -4147,53 +4147,6 @@ const toProfitLossDto = (signal, priceClose) => {
4147
4147
  };
4148
4148
  };
4149
4149
 
4150
- /**
4151
- * Converts markdown content to plain text with minimal formatting
4152
- * @param content - Markdown string to convert
4153
- * @returns Plain text representation
4154
- */
4155
- const toPlainString = (content) => {
4156
- if (!content) {
4157
- return "";
4158
- }
4159
- let text = content;
4160
- // Remove code blocks
4161
- text = text.replace(/```[\s\S]*?```/g, "");
4162
- text = text.replace(/`([^`]+)`/g, "$1");
4163
- // Remove images
4164
- text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
4165
- // Convert links to text only (keep link text, remove URL)
4166
- text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
4167
- // Remove headers (convert to plain text)
4168
- text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
4169
- // Remove bold and italic markers
4170
- text = text.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
4171
- text = text.replace(/\*\*(.+?)\*\*/g, "$1");
4172
- text = text.replace(/\*(.+?)\*/g, "$1");
4173
- text = text.replace(/___(.+?)___/g, "$1");
4174
- text = text.replace(/__(.+?)__/g, "$1");
4175
- text = text.replace(/_(.+?)_/g, "$1");
4176
- // Remove strikethrough
4177
- text = text.replace(/~~(.+?)~~/g, "$1");
4178
- // Convert lists to plain text with bullets
4179
- text = text.replace(/^\s*[-*+]\s+/gm, "• ");
4180
- text = text.replace(/^\s*\d+\.\s+/gm, "• ");
4181
- // Remove blockquotes
4182
- text = text.replace(/^\s*>\s+/gm, "");
4183
- // Remove horizontal rules
4184
- text = text.replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "");
4185
- // Remove HTML tags
4186
- text = text.replace(/<[^>]+>/g, "");
4187
- // Remove excessive whitespace and normalize line breaks
4188
- text = text.replace(/\n[\s\n]*\n/g, "\n");
4189
- text = text.replace(/[ \t]+/g, " ");
4190
- // Remove all newline characters
4191
- text = text.replace(/\n/g, " ");
4192
- // Remove excessive spaces after newline removal
4193
- text = text.replace(/\s+/g, " ");
4194
- return text.trim();
4195
- };
4196
-
4197
4150
  /**
4198
4151
  * Returns the total closed state of a position using costBasisAtClose snapshots.
4199
4152
  *
@@ -4828,6 +4781,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
4828
4781
  originalPriceOpen: publicSignal.originalPriceOpen,
4829
4782
  scheduledAt: publicSignal.scheduledAt,
4830
4783
  pendingAt: publicSignal.pendingAt,
4784
+ note: publicSignal.note,
4831
4785
  });
4832
4786
  continue;
4833
4787
  }
@@ -4855,6 +4809,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
4855
4809
  originalPriceOpen: publicSignal.originalPriceOpen,
4856
4810
  scheduledAt: publicSignal.scheduledAt,
4857
4811
  pendingAt: publicSignal.pendingAt,
4812
+ note: publicSignal.note,
4858
4813
  });
4859
4814
  continue;
4860
4815
  }
@@ -4881,6 +4836,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
4881
4836
  originalPriceOpen: publicSignal.originalPriceOpen,
4882
4837
  scheduledAt: publicSignal.scheduledAt,
4883
4838
  pendingAt: publicSignal.pendingAt,
4839
+ note: publicSignal.note,
4884
4840
  });
4885
4841
  continue;
4886
4842
  }
@@ -4908,6 +4864,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
4908
4864
  originalPriceOpen: publicSignal.originalPriceOpen,
4909
4865
  scheduledAt: publicSignal.scheduledAt,
4910
4866
  pendingAt: publicSignal.pendingAt,
4867
+ note: publicSignal.note,
4911
4868
  });
4912
4869
  continue;
4913
4870
  }
@@ -4935,6 +4892,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
4935
4892
  originalPriceOpen: publicSignal.originalPriceOpen,
4936
4893
  scheduledAt: publicSignal.scheduledAt,
4937
4894
  pendingAt: publicSignal.pendingAt,
4895
+ note: publicSignal.note,
4938
4896
  });
4939
4897
  continue;
4940
4898
  }
@@ -4964,6 +4922,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
4964
4922
  originalPriceOpen: publicSignal.originalPriceOpen,
4965
4923
  scheduledAt: publicSignal.scheduledAt,
4966
4924
  pendingAt: publicSignal.pendingAt,
4925
+ note: publicSignal.note,
4967
4926
  });
4968
4927
  continue;
4969
4928
  }
@@ -5062,7 +5021,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
5062
5021
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
5063
5022
  const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
5064
5023
  const signal = await Promise.race([
5065
- self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
5024
+ self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when, currentPrice),
5066
5025
  functoolsKit.sleep(timeoutMs).then(() => TIMEOUT_SYMBOL),
5067
5026
  ]);
5068
5027
  if (typeof signal === "symbol") {
@@ -5092,7 +5051,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
5092
5051
  cost: signal.cost || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST,
5093
5052
  priceOpen: signal.priceOpen, // Используем priceOpen из сигнала
5094
5053
  position: signal.position,
5095
- note: toPlainString(signal.note),
5054
+ note: signal.note || "",
5096
5055
  priceTakeProfit: signal.priceTakeProfit,
5097
5056
  priceStopLoss: signal.priceStopLoss,
5098
5057
  minuteEstimatedTime: signal.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
@@ -5118,7 +5077,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
5118
5077
  cost: signal.cost || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST,
5119
5078
  priceOpen: signal.priceOpen,
5120
5079
  position: signal.position,
5121
- note: toPlainString(signal.note),
5080
+ note: signal.note || "",
5122
5081
  priceTakeProfit: signal.priceTakeProfit,
5123
5082
  priceStopLoss: signal.priceStopLoss,
5124
5083
  minuteEstimatedTime: signal.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
@@ -5143,7 +5102,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
5143
5102
  cost: signal.cost || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST,
5144
5103
  priceOpen: currentPrice,
5145
5104
  ...structuredClone(signal),
5146
- note: toPlainString(signal.note),
5105
+ note: signal.note || "",
5147
5106
  minuteEstimatedTime: signal.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
5148
5107
  symbol: self.params.execution.context.symbol,
5149
5108
  exchangeName: self.params.method.context.exchangeName,
@@ -5890,6 +5849,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
5890
5849
  totalPartials: scheduled._partial?.length ?? 0,
5891
5850
  originalPriceOpen: scheduled.priceOpen,
5892
5851
  pnl: toProfitLossDto(scheduled, scheduled.priceOpen),
5852
+ note: scheduled.note,
5893
5853
  });
5894
5854
  return null;
5895
5855
  }
@@ -6658,6 +6618,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
6658
6618
  totalPartials: scheduled._partial?.length ?? 0,
6659
6619
  originalPriceOpen: scheduled.priceOpen,
6660
6620
  pnl: toProfitLossDto(scheduled, averagePrice),
6621
+ note: scheduled.note,
6661
6622
  });
6662
6623
  }
6663
6624
  await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, averagePrice, closeTimestamp, self.params.execution.context.backtest);
@@ -6736,6 +6697,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
6736
6697
  totalPartials: scheduled._partial?.length ?? 0,
6737
6698
  originalPriceOpen: scheduled.priceOpen,
6738
6699
  pnl: toProfitLossDto(scheduled, scheduled.priceOpen),
6700
+ note: scheduled.note,
6739
6701
  });
6740
6702
  return false;
6741
6703
  }
@@ -6823,6 +6785,7 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
6823
6785
  totalPartials: closedSignal._partial?.length ?? 0,
6824
6786
  originalPriceOpen: closedSignal.priceOpen,
6825
6787
  pnl: toProfitLossDto(closedSignal, averagePrice),
6788
+ note: closedSignal.note,
6826
6789
  });
6827
6790
  await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
6828
6791
  await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
@@ -6923,6 +6886,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
6923
6886
  totalPartials: activatedSignal._partial?.length ?? 0,
6924
6887
  originalPriceOpen: activatedSignal.priceOpen,
6925
6888
  pnl: toProfitLossDto(activatedSignal, averagePrice),
6889
+ note: activatedSignal.note,
6926
6890
  });
6927
6891
  return { outcome: "pending" };
6928
6892
  }
@@ -6954,6 +6918,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
6954
6918
  pendingAt: publicSignalForCommit.pendingAt,
6955
6919
  totalEntries: publicSignalForCommit.totalEntries,
6956
6920
  totalPartials: publicSignalForCommit.totalPartials,
6921
+ note: publicSignalForCommit.note,
6957
6922
  });
6958
6923
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
6959
6924
  await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
@@ -7990,6 +7955,90 @@ class ClientStrategy {
7990
7955
  }
7991
7956
  return this._pendingSignal._fall.pnlCost;
7992
7957
  }
7958
+ /**
7959
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
7960
+ *
7961
+ * Measures how much PnL% the position has given back from its best point.
7962
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
7963
+ * Zero when called at the exact moment the peak was set, or when current PnL >= peak PnL.
7964
+ *
7965
+ * Returns null if no pending signal exists.
7966
+ *
7967
+ * @param symbol - Trading pair symbol
7968
+ * @param currentPrice - Current market price
7969
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
7970
+ */
7971
+ async getPositionHighestProfitDistancePnlPercentage(symbol, currentPrice) {
7972
+ this.params.logger.debug("ClientStrategy getPositionHighestProfitDistancePnlPercentage", { symbol, currentPrice });
7973
+ if (!this._pendingSignal) {
7974
+ return null;
7975
+ }
7976
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
7977
+ return Math.max(0, this._pendingSignal._peak.pnlPercentage - currentPnl.pnlPercentage);
7978
+ }
7979
+ /**
7980
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
7981
+ *
7982
+ * Measures how much PnL cost the position has given back from its best point.
7983
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
7984
+ * Zero when called at the exact moment the peak was set, or when current PnL >= peak PnL.
7985
+ *
7986
+ * Returns null if no pending signal exists.
7987
+ *
7988
+ * @param symbol - Trading pair symbol
7989
+ * @param currentPrice - Current market price
7990
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
7991
+ */
7992
+ async getPositionHighestProfitDistancePnlCost(symbol, currentPrice) {
7993
+ this.params.logger.debug("ClientStrategy getPositionHighestProfitDistancePnlCost", { symbol, currentPrice });
7994
+ if (!this._pendingSignal) {
7995
+ return null;
7996
+ }
7997
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
7998
+ return Math.max(0, this._pendingSignal._peak.pnlCost - currentPnl.pnlCost);
7999
+ }
8000
+ /**
8001
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
8002
+ *
8003
+ * Measures how much the position has recovered from its deepest loss point.
8004
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
8005
+ * Zero when called at the exact moment the trough was set, or when current PnL <= trough PnL.
8006
+ *
8007
+ * Returns null if no pending signal exists.
8008
+ *
8009
+ * @param symbol - Trading pair symbol
8010
+ * @param currentPrice - Current market price
8011
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
8012
+ */
8013
+ async getPositionHighestMaxDrawdownPnlPercentage(symbol, currentPrice) {
8014
+ this.params.logger.debug("ClientStrategy getPositionHighestMaxDrawdownPnlPercentage", { symbol, currentPrice });
8015
+ if (!this._pendingSignal) {
8016
+ return null;
8017
+ }
8018
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
8019
+ return Math.max(0, currentPnl.pnlPercentage - this._pendingSignal._fall.pnlPercentage);
8020
+ }
8021
+ /**
8022
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
8023
+ *
8024
+ * Measures how much the position has recovered from its deepest loss point.
8025
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
8026
+ * Zero when called at the exact moment the trough was set, or when current PnL <= trough PnL.
8027
+ *
8028
+ * Returns null if no pending signal exists.
8029
+ *
8030
+ * @param symbol - Trading pair symbol
8031
+ * @param currentPrice - Current market price
8032
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
8033
+ */
8034
+ async getPositionHighestMaxDrawdownPnlCost(symbol, currentPrice) {
8035
+ this.params.logger.debug("ClientStrategy getPositionHighestMaxDrawdownPnlCost", { symbol, currentPrice });
8036
+ if (!this._pendingSignal) {
8037
+ return null;
8038
+ }
8039
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
8040
+ return Math.max(0, currentPnl.pnlCost - this._pendingSignal._fall.pnlCost);
8041
+ }
7993
8042
  /**
7994
8043
  * Performs a single tick of strategy execution.
7995
8044
  *
@@ -8054,6 +8103,7 @@ class ClientStrategy {
8054
8103
  totalPartials: cancelledSignal._partial?.length ?? 0,
8055
8104
  originalPriceOpen: cancelledSignal.priceOpen,
8056
8105
  pnl: toProfitLossDto(cancelledSignal, currentPrice),
8106
+ note: cancelledSignal.note,
8057
8107
  });
8058
8108
  // Call onCancel callback
8059
8109
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
@@ -8107,6 +8157,7 @@ class ClientStrategy {
8107
8157
  totalPartials: closedSignal._partial?.length ?? 0,
8108
8158
  originalPriceOpen: closedSignal.priceOpen,
8109
8159
  pnl: toProfitLossDto(closedSignal, currentPrice),
8160
+ note: closedSignal.note,
8110
8161
  });
8111
8162
  // Call onClose callback
8112
8163
  await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
@@ -8188,6 +8239,7 @@ class ClientStrategy {
8188
8239
  totalPartials: activatedSignal._partial?.length ?? 0,
8189
8240
  originalPriceOpen: activatedSignal.priceOpen,
8190
8241
  pnl: toProfitLossDto(activatedSignal, currentPrice),
8242
+ note: activatedSignal.note,
8191
8243
  });
8192
8244
  return await RETURN_IDLE_FN(this, currentPrice);
8193
8245
  }
@@ -8218,6 +8270,7 @@ class ClientStrategy {
8218
8270
  pendingAt: publicSignalForCommit.pendingAt,
8219
8271
  totalEntries: publicSignalForCommit.totalEntries,
8220
8272
  totalPartials: publicSignalForCommit.totalPartials,
8273
+ note: publicSignalForCommit.note,
8221
8274
  });
8222
8275
  // Call onOpen callback
8223
8276
  await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
@@ -8353,6 +8406,7 @@ class ClientStrategy {
8353
8406
  totalPartials: cancelledSignal._partial?.length ?? 0,
8354
8407
  originalPriceOpen: cancelledSignal.priceOpen,
8355
8408
  pnl: toProfitLossDto(cancelledSignal, currentPrice),
8409
+ note: cancelledSignal.note,
8356
8410
  });
8357
8411
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
8358
8412
  const cancelledResult = {
@@ -8407,6 +8461,7 @@ class ClientStrategy {
8407
8461
  totalPartials: closedSignal._partial?.length ?? 0,
8408
8462
  originalPriceOpen: closedSignal.priceOpen,
8409
8463
  pnl: toProfitLossDto(closedSignal, currentPrice),
8464
+ note: closedSignal.note,
8410
8465
  });
8411
8466
  await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
8412
8467
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
@@ -9956,6 +10011,8 @@ class MergeRisk {
9956
10011
  }
9957
10012
  }
9958
10013
 
10014
+ /** Default interval for strategies that do not specify one */
10015
+ const STRATEGY_DEFAULT_INTERVAL = "1m";
9959
10016
  /**
9960
10017
  * If syncSubject listener or any registered action throws, it means the signal was not properly synchronized
9961
10018
  * to the exchange (e.g. limit order failed to fill).
@@ -10341,7 +10398,7 @@ class StrategyConnectionService {
10341
10398
  * @returns Configured ClientStrategy instance
10342
10399
  */
10343
10400
  this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$t(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
10344
- const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
10401
+ const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
10345
10402
  return new ClientStrategy({
10346
10403
  symbol,
10347
10404
  interval,
@@ -11097,6 +11154,90 @@ class StrategyConnectionService {
11097
11154
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11098
11155
  return await strategy.getPositionMaxDrawdownPnlCost(symbol);
11099
11156
  };
11157
+ /**
11158
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
11159
+ *
11160
+ * Resolves current price via priceMetaService and delegates to
11161
+ * ClientStrategy.getPositionHighestProfitDistancePnlPercentage().
11162
+ * Returns null if no pending signal exists.
11163
+ *
11164
+ * @param backtest - Whether running in backtest mode
11165
+ * @param symbol - Trading pair symbol
11166
+ * @param context - Execution context with strategyName, exchangeName, frameName
11167
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
11168
+ */
11169
+ this.getPositionHighestProfitDistancePnlPercentage = async (backtest, symbol, context) => {
11170
+ this.loggerService.log("strategyConnectionService getPositionHighestProfitDistancePnlPercentage", {
11171
+ symbol,
11172
+ context,
11173
+ });
11174
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11175
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11176
+ return await strategy.getPositionHighestProfitDistancePnlPercentage(symbol, currentPrice);
11177
+ };
11178
+ /**
11179
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
11180
+ *
11181
+ * Resolves current price via priceMetaService and delegates to
11182
+ * ClientStrategy.getPositionHighestProfitDistancePnlCost().
11183
+ * Returns null if no pending signal exists.
11184
+ *
11185
+ * @param backtest - Whether running in backtest mode
11186
+ * @param symbol - Trading pair symbol
11187
+ * @param context - Execution context with strategyName, exchangeName, frameName
11188
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
11189
+ */
11190
+ this.getPositionHighestProfitDistancePnlCost = async (backtest, symbol, context) => {
11191
+ this.loggerService.log("strategyConnectionService getPositionHighestProfitDistancePnlCost", {
11192
+ symbol,
11193
+ context,
11194
+ });
11195
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11196
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11197
+ return await strategy.getPositionHighestProfitDistancePnlCost(symbol, currentPrice);
11198
+ };
11199
+ /**
11200
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
11201
+ *
11202
+ * Resolves current price via priceMetaService and delegates to
11203
+ * ClientStrategy.getPositionHighestMaxDrawdownPnlPercentage().
11204
+ * Returns null if no pending signal exists.
11205
+ *
11206
+ * @param backtest - Whether running in backtest mode
11207
+ * @param symbol - Trading pair symbol
11208
+ * @param context - Execution context with strategyName, exchangeName, frameName
11209
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
11210
+ */
11211
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
11212
+ this.loggerService.log("strategyConnectionService getPositionHighestMaxDrawdownPnlPercentage", {
11213
+ symbol,
11214
+ context,
11215
+ });
11216
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11217
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11218
+ return await strategy.getPositionHighestMaxDrawdownPnlPercentage(symbol, currentPrice);
11219
+ };
11220
+ /**
11221
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
11222
+ *
11223
+ * Resolves current price via priceMetaService and delegates to
11224
+ * ClientStrategy.getPositionHighestMaxDrawdownPnlCost().
11225
+ * Returns null if no pending signal exists.
11226
+ *
11227
+ * @param backtest - Whether running in backtest mode
11228
+ * @param symbol - Trading pair symbol
11229
+ * @param context - Execution context with strategyName, exchangeName, frameName
11230
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
11231
+ */
11232
+ this.getPositionHighestMaxDrawdownPnlCost = async (backtest, symbol, context) => {
11233
+ this.loggerService.log("strategyConnectionService getPositionHighestMaxDrawdownPnlCost", {
11234
+ symbol,
11235
+ context,
11236
+ });
11237
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11238
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11239
+ return await strategy.getPositionHighestMaxDrawdownPnlCost(symbol, currentPrice);
11240
+ };
11100
11241
  /**
11101
11242
  * Disposes the ClientStrategy instance for the given context.
11102
11243
  *
@@ -15258,6 +15399,82 @@ class StrategyCoreService {
15258
15399
  await this.validate(context);
15259
15400
  return await this.strategyConnectionService.getPositionMaxDrawdownPnlCost(backtest, symbol, context);
15260
15401
  };
15402
+ /**
15403
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
15404
+ *
15405
+ * Delegates to StrategyConnectionService.getPositionHighestProfitDistancePnlPercentage().
15406
+ * Returns null if no pending signal exists.
15407
+ *
15408
+ * @param backtest - Whether running in backtest mode
15409
+ * @param symbol - Trading pair symbol
15410
+ * @param context - Execution context with strategyName, exchangeName, frameName
15411
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
15412
+ */
15413
+ this.getPositionHighestProfitDistancePnlPercentage = async (backtest, symbol, context) => {
15414
+ this.loggerService.log("strategyCoreService getPositionHighestProfitDistancePnlPercentage", {
15415
+ symbol,
15416
+ context,
15417
+ });
15418
+ await this.validate(context);
15419
+ return await this.strategyConnectionService.getPositionHighestProfitDistancePnlPercentage(backtest, symbol, context);
15420
+ };
15421
+ /**
15422
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
15423
+ *
15424
+ * Delegates to StrategyConnectionService.getPositionHighestProfitDistancePnlCost().
15425
+ * Returns null if no pending signal exists.
15426
+ *
15427
+ * @param backtest - Whether running in backtest mode
15428
+ * @param symbol - Trading pair symbol
15429
+ * @param context - Execution context with strategyName, exchangeName, frameName
15430
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
15431
+ */
15432
+ this.getPositionHighestProfitDistancePnlCost = async (backtest, symbol, context) => {
15433
+ this.loggerService.log("strategyCoreService getPositionHighestProfitDistancePnlCost", {
15434
+ symbol,
15435
+ context,
15436
+ });
15437
+ await this.validate(context);
15438
+ return await this.strategyConnectionService.getPositionHighestProfitDistancePnlCost(backtest, symbol, context);
15439
+ };
15440
+ /**
15441
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
15442
+ *
15443
+ * Delegates to StrategyConnectionService.getPositionHighestMaxDrawdownPnlPercentage().
15444
+ * Returns null if no pending signal exists.
15445
+ *
15446
+ * @param backtest - Whether running in backtest mode
15447
+ * @param symbol - Trading pair symbol
15448
+ * @param context - Execution context with strategyName, exchangeName, frameName
15449
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
15450
+ */
15451
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
15452
+ this.loggerService.log("strategyCoreService getPositionHighestMaxDrawdownPnlPercentage", {
15453
+ symbol,
15454
+ context,
15455
+ });
15456
+ await this.validate(context);
15457
+ return await this.strategyConnectionService.getPositionHighestMaxDrawdownPnlPercentage(backtest, symbol, context);
15458
+ };
15459
+ /**
15460
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
15461
+ *
15462
+ * Delegates to StrategyConnectionService.getPositionHighestMaxDrawdownPnlCost().
15463
+ * Returns null if no pending signal exists.
15464
+ *
15465
+ * @param backtest - Whether running in backtest mode
15466
+ * @param symbol - Trading pair symbol
15467
+ * @param context - Execution context with strategyName, exchangeName, frameName
15468
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
15469
+ */
15470
+ this.getPositionHighestMaxDrawdownPnlCost = async (backtest, symbol, context) => {
15471
+ this.loggerService.log("strategyCoreService getPositionHighestMaxDrawdownPnlCost", {
15472
+ symbol,
15473
+ context,
15474
+ });
15475
+ await this.validate(context);
15476
+ return await this.strategyConnectionService.getPositionHighestMaxDrawdownPnlCost(backtest, symbol, context);
15477
+ };
15261
15478
  }
15262
15479
  }
15263
15480
 
@@ -15900,8 +16117,8 @@ class StrategySchemaService {
15900
16117
  if (strategySchema.actions?.some((value) => typeof value !== "string")) {
15901
16118
  throw new Error(`strategy schema validation failed: invalid actions for strategyName=${strategySchema.strategyName} actions=[${strategySchema.actions}]`);
15902
16119
  }
15903
- if (typeof strategySchema.interval !== "string") {
15904
- throw new Error(`strategy schema validation failed: missing interval for strategyName=${strategySchema.strategyName}`);
16120
+ if (strategySchema.interval && typeof strategySchema.interval !== "string") {
16121
+ throw new Error(`strategy schema validation failed: invalid interval for strategyName=${strategySchema.strategyName}`);
15905
16122
  }
15906
16123
  if (typeof strategySchema.getSignal !== "function") {
15907
16124
  throw new Error(`strategy schema validation failed: missing getSignal for strategyName=${strategySchema.strategyName}`);
@@ -17847,6 +18064,53 @@ class WalkerCommandService {
17847
18064
  }
17848
18065
  }
17849
18066
 
18067
+ /**
18068
+ * Converts markdown content to plain text with minimal formatting
18069
+ * @param content - Markdown string to convert
18070
+ * @returns Plain text representation
18071
+ */
18072
+ const toPlainString = (content) => {
18073
+ if (!content) {
18074
+ return "";
18075
+ }
18076
+ let text = content;
18077
+ // Remove code blocks
18078
+ text = text.replace(/```[\s\S]*?```/g, "");
18079
+ text = text.replace(/`([^`]+)`/g, "$1");
18080
+ // Remove images
18081
+ text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
18082
+ // Convert links to text only (keep link text, remove URL)
18083
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
18084
+ // Remove headers (convert to plain text)
18085
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
18086
+ // Remove bold and italic markers
18087
+ text = text.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
18088
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1");
18089
+ text = text.replace(/\*(.+?)\*/g, "$1");
18090
+ text = text.replace(/___(.+?)___/g, "$1");
18091
+ text = text.replace(/__(.+?)__/g, "$1");
18092
+ text = text.replace(/_(.+?)_/g, "$1");
18093
+ // Remove strikethrough
18094
+ text = text.replace(/~~(.+?)~~/g, "$1");
18095
+ // Convert lists to plain text with bullets
18096
+ text = text.replace(/^\s*[-*+]\s+/gm, "• ");
18097
+ text = text.replace(/^\s*\d+\.\s+/gm, "• ");
18098
+ // Remove blockquotes
18099
+ text = text.replace(/^\s*>\s+/gm, "");
18100
+ // Remove horizontal rules
18101
+ text = text.replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "");
18102
+ // Remove HTML tags
18103
+ text = text.replace(/<[^>]+>/g, "");
18104
+ // Remove excessive whitespace and normalize line breaks
18105
+ text = text.replace(/\n[\s\n]*\n/g, "\n");
18106
+ text = text.replace(/[ \t]+/g, " ");
18107
+ // Remove all newline characters
18108
+ text = text.replace(/\n/g, " ");
18109
+ // Remove excessive spaces after newline removal
18110
+ text = text.replace(/\s+/g, " ");
18111
+ return text.trim();
18112
+ };
18113
+
17850
18114
  /**
17851
18115
  * Column configuration for backtest markdown reports.
17852
18116
  *
@@ -18522,7 +18786,7 @@ const partial_columns = [
18522
18786
  {
18523
18787
  key: "note",
18524
18788
  label: "Note",
18525
- format: (data) => data.note || "",
18789
+ format: (data) => toPlainString(data.note ?? "N/A"),
18526
18790
  isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
18527
18791
  },
18528
18792
  {
@@ -18682,7 +18946,7 @@ const breakeven_columns = [
18682
18946
  {
18683
18947
  key: "note",
18684
18948
  label: "Note",
18685
- format: (data) => data.note || "",
18949
+ format: (data) => toPlainString(data.note ?? "N/A"),
18686
18950
  isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
18687
18951
  },
18688
18952
  {
@@ -34913,6 +35177,10 @@ const GET_POSITION_MAX_DRAWDOWN_PRICE_METHOD_NAME = "strategy.getPositionMaxDraw
34913
35177
  const GET_POSITION_MAX_DRAWDOWN_TIMESTAMP_METHOD_NAME = "strategy.getPositionMaxDrawdownTimestamp";
34914
35178
  const GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlPercentage";
34915
35179
  const GET_POSITION_MAX_DRAWDOWN_PNL_COST_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlCost";
35180
+ const GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionHighestProfitDistancePnlPercentage";
35181
+ const GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST_METHOD_NAME = "strategy.getPositionHighestProfitDistancePnlCost";
35182
+ const GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionHighestMaxDrawdownPnlPercentage";
35183
+ const GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST_METHOD_NAME = "strategy.getPositionHighestMaxDrawdownPnlCost";
34916
35184
  const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
34917
35185
  const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
34918
35186
  const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
@@ -36547,6 +36815,122 @@ async function getPositionMaxDrawdownPnlCost(symbol) {
36547
36815
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36548
36816
  return await backtest.strategyCoreService.getPositionMaxDrawdownPnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
36549
36817
  }
36818
+ /**
36819
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
36820
+ *
36821
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
36822
+ * Returns null if no pending signal exists.
36823
+ *
36824
+ * @param symbol - Trading pair symbol
36825
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
36826
+ *
36827
+ * @example
36828
+ * ```typescript
36829
+ * import { getPositionHighestProfitDistancePnlPercentage } from "backtest-kit";
36830
+ *
36831
+ * const dist = await getPositionHighestProfitDistancePnlPercentage("BTCUSDT");
36832
+ * // e.g. 1.5 (gave back 1.5% from peak)
36833
+ * ```
36834
+ */
36835
+ async function getPositionHighestProfitDistancePnlPercentage(symbol) {
36836
+ backtest.loggerService.info(GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE_METHOD_NAME, { symbol });
36837
+ if (!ExecutionContextService.hasContext()) {
36838
+ throw new Error("getPositionHighestProfitDistancePnlPercentage requires an execution context");
36839
+ }
36840
+ if (!MethodContextService.hasContext()) {
36841
+ throw new Error("getPositionHighestProfitDistancePnlPercentage requires a method context");
36842
+ }
36843
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36844
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36845
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlPercentage(isBacktest, symbol, { exchangeName, frameName, strategyName });
36846
+ }
36847
+ /**
36848
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
36849
+ *
36850
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
36851
+ * Returns null if no pending signal exists.
36852
+ *
36853
+ * @param symbol - Trading pair symbol
36854
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
36855
+ *
36856
+ * @example
36857
+ * ```typescript
36858
+ * import { getPositionHighestProfitDistancePnlCost } from "backtest-kit";
36859
+ *
36860
+ * const dist = await getPositionHighestProfitDistancePnlCost("BTCUSDT");
36861
+ * // e.g. 3.2 (gave back $3.2 from peak)
36862
+ * ```
36863
+ */
36864
+ async function getPositionHighestProfitDistancePnlCost(symbol) {
36865
+ backtest.loggerService.info(GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST_METHOD_NAME, { symbol });
36866
+ if (!ExecutionContextService.hasContext()) {
36867
+ throw new Error("getPositionHighestProfitDistancePnlCost requires an execution context");
36868
+ }
36869
+ if (!MethodContextService.hasContext()) {
36870
+ throw new Error("getPositionHighestProfitDistancePnlCost requires a method context");
36871
+ }
36872
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36873
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36874
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
36875
+ }
36876
+ /**
36877
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
36878
+ *
36879
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
36880
+ * Returns null if no pending signal exists.
36881
+ *
36882
+ * @param symbol - Trading pair symbol
36883
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
36884
+ *
36885
+ * @example
36886
+ * ```typescript
36887
+ * import { getPositionHighestMaxDrawdownPnlPercentage } from "backtest-kit";
36888
+ *
36889
+ * const dist = await getPositionHighestMaxDrawdownPnlPercentage("BTCUSDT");
36890
+ * // e.g. 2.1 (recovered 2.1% from trough)
36891
+ * ```
36892
+ */
36893
+ async function getPositionHighestMaxDrawdownPnlPercentage(symbol) {
36894
+ backtest.loggerService.info(GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME, { symbol });
36895
+ if (!ExecutionContextService.hasContext()) {
36896
+ throw new Error("getPositionHighestMaxDrawdownPnlPercentage requires an execution context");
36897
+ }
36898
+ if (!MethodContextService.hasContext()) {
36899
+ throw new Error("getPositionHighestMaxDrawdownPnlPercentage requires a method context");
36900
+ }
36901
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36902
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36903
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlPercentage(isBacktest, symbol, { exchangeName, frameName, strategyName });
36904
+ }
36905
+ /**
36906
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
36907
+ *
36908
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
36909
+ * Returns null if no pending signal exists.
36910
+ *
36911
+ * @param symbol - Trading pair symbol
36912
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
36913
+ *
36914
+ * @example
36915
+ * ```typescript
36916
+ * import { getPositionHighestMaxDrawdownPnlCost } from "backtest-kit";
36917
+ *
36918
+ * const dist = await getPositionHighestMaxDrawdownPnlCost("BTCUSDT");
36919
+ * // e.g. 4.8 (recovered $4.8 from trough)
36920
+ * ```
36921
+ */
36922
+ async function getPositionHighestMaxDrawdownPnlCost(symbol) {
36923
+ backtest.loggerService.info(GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST_METHOD_NAME, { symbol });
36924
+ if (!ExecutionContextService.hasContext()) {
36925
+ throw new Error("getPositionHighestMaxDrawdownPnlCost requires an execution context");
36926
+ }
36927
+ if (!MethodContextService.hasContext()) {
36928
+ throw new Error("getPositionHighestMaxDrawdownPnlCost requires a method context");
36929
+ }
36930
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36931
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36932
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
36933
+ }
36550
36934
  /**
36551
36935
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
36552
36936
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -38216,6 +38600,10 @@ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "BacktestUtils.getP
38216
38600
  const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "BacktestUtils.getPositionMaxDrawdownTimestamp";
38217
38601
  const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "BacktestUtils.getPositionMaxDrawdownPnlPercentage";
38218
38602
  const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "BacktestUtils.getPositionMaxDrawdownPnlCost";
38603
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE = "BacktestUtils.getPositionHighestProfitDistancePnlPercentage";
38604
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST = "BacktestUtils.getPositionHighestProfitDistancePnlCost";
38605
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE = "BacktestUtils.getPositionHighestMaxDrawdownPnlPercentage";
38606
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST = "BacktestUtils.getPositionHighestMaxDrawdownPnlCost";
38219
38607
  const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
38220
38608
  const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
38221
38609
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
@@ -39485,6 +39873,118 @@ class BacktestUtils {
39485
39873
  }
39486
39874
  return await backtest.strategyCoreService.getPositionMaxDrawdownPnlCost(true, symbol, context);
39487
39875
  };
39876
+ /**
39877
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
39878
+ *
39879
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
39880
+ * Returns null if no pending signal exists.
39881
+ *
39882
+ * @param symbol - Trading pair symbol
39883
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39884
+ * @returns drawdown distance in PnL% (≥ 0) or null if no active position
39885
+ */
39886
+ this.getPositionHighestProfitDistancePnlPercentage = async (symbol, context) => {
39887
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE, {
39888
+ symbol,
39889
+ context,
39890
+ });
39891
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
39892
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
39893
+ {
39894
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39895
+ riskName &&
39896
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
39897
+ riskList &&
39898
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
39899
+ actions &&
39900
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
39901
+ }
39902
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlPercentage(true, symbol, context);
39903
+ };
39904
+ /**
39905
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
39906
+ *
39907
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
39908
+ * Returns null if no pending signal exists.
39909
+ *
39910
+ * @param symbol - Trading pair symbol
39911
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39912
+ * @returns drawdown distance in PnL cost (≥ 0) or null if no active position
39913
+ */
39914
+ this.getPositionHighestProfitDistancePnlCost = async (symbol, context) => {
39915
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST, {
39916
+ symbol,
39917
+ context,
39918
+ });
39919
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
39920
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
39921
+ {
39922
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39923
+ riskName &&
39924
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
39925
+ riskList &&
39926
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
39927
+ actions &&
39928
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
39929
+ }
39930
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlCost(true, symbol, context);
39931
+ };
39932
+ /**
39933
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
39934
+ *
39935
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
39936
+ * Returns null if no pending signal exists.
39937
+ *
39938
+ * @param symbol - Trading pair symbol
39939
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39940
+ * @returns recovery distance in PnL% (≥ 0) or null if no active position
39941
+ */
39942
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (symbol, context) => {
39943
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE, {
39944
+ symbol,
39945
+ context,
39946
+ });
39947
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
39948
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
39949
+ {
39950
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39951
+ riskName &&
39952
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
39953
+ riskList &&
39954
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
39955
+ actions &&
39956
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
39957
+ }
39958
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlPercentage(true, symbol, context);
39959
+ };
39960
+ /**
39961
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
39962
+ *
39963
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
39964
+ * Returns null if no pending signal exists.
39965
+ *
39966
+ * @param symbol - Trading pair symbol
39967
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39968
+ * @returns recovery distance in PnL cost (≥ 0) or null if no active position
39969
+ */
39970
+ this.getPositionHighestMaxDrawdownPnlCost = async (symbol, context) => {
39971
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST, {
39972
+ symbol,
39973
+ context,
39974
+ });
39975
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
39976
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
39977
+ {
39978
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39979
+ riskName &&
39980
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
39981
+ riskList &&
39982
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
39983
+ actions &&
39984
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
39985
+ }
39986
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlCost(true, symbol, context);
39987
+ };
39488
39988
  /**
39489
39989
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
39490
39990
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -40610,6 +41110,10 @@ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "LiveUtils.getPositionM
40610
41110
  const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "LiveUtils.getPositionMaxDrawdownTimestamp";
40611
41111
  const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "LiveUtils.getPositionMaxDrawdownPnlPercentage";
40612
41112
  const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "LiveUtils.getPositionMaxDrawdownPnlCost";
41113
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE = "LiveUtils.getPositionHighestProfitDistancePnlPercentage";
41114
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST = "LiveUtils.getPositionHighestProfitDistancePnlCost";
41115
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE = "LiveUtils.getPositionHighestMaxDrawdownPnlPercentage";
41116
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST = "LiveUtils.getPositionHighestMaxDrawdownPnlCost";
40613
41117
  const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
40614
41118
  const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
40615
41119
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
@@ -42006,6 +42510,134 @@ class LiveUtils {
42006
42510
  frameName: "",
42007
42511
  });
42008
42512
  };
42513
+ /**
42514
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
42515
+ *
42516
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
42517
+ * Returns null if no pending signal exists.
42518
+ *
42519
+ * @param symbol - Trading pair symbol
42520
+ * @param context - Execution context with strategyName and exchangeName
42521
+ * @returns drawdown distance in PnL% (≥ 0) or null if no active position
42522
+ */
42523
+ this.getPositionHighestProfitDistancePnlPercentage = async (symbol, context) => {
42524
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE, {
42525
+ symbol,
42526
+ context,
42527
+ });
42528
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
42529
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
42530
+ {
42531
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42532
+ riskName &&
42533
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
42534
+ riskList &&
42535
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
42536
+ actions &&
42537
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
42538
+ }
42539
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlPercentage(false, symbol, {
42540
+ strategyName: context.strategyName,
42541
+ exchangeName: context.exchangeName,
42542
+ frameName: "",
42543
+ });
42544
+ };
42545
+ /**
42546
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
42547
+ *
42548
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
42549
+ * Returns null if no pending signal exists.
42550
+ *
42551
+ * @param symbol - Trading pair symbol
42552
+ * @param context - Execution context with strategyName and exchangeName
42553
+ * @returns drawdown distance in PnL cost (≥ 0) or null if no active position
42554
+ */
42555
+ this.getPositionHighestProfitDistancePnlCost = async (symbol, context) => {
42556
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST, {
42557
+ symbol,
42558
+ context,
42559
+ });
42560
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
42561
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
42562
+ {
42563
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42564
+ riskName &&
42565
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
42566
+ riskList &&
42567
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
42568
+ actions &&
42569
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
42570
+ }
42571
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlCost(false, symbol, {
42572
+ strategyName: context.strategyName,
42573
+ exchangeName: context.exchangeName,
42574
+ frameName: "",
42575
+ });
42576
+ };
42577
+ /**
42578
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
42579
+ *
42580
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
42581
+ * Returns null if no pending signal exists.
42582
+ *
42583
+ * @param symbol - Trading pair symbol
42584
+ * @param context - Execution context with strategyName and exchangeName
42585
+ * @returns recovery distance in PnL% (≥ 0) or null if no active position
42586
+ */
42587
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (symbol, context) => {
42588
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE, {
42589
+ symbol,
42590
+ context,
42591
+ });
42592
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
42593
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
42594
+ {
42595
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42596
+ riskName &&
42597
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
42598
+ riskList &&
42599
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
42600
+ actions &&
42601
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
42602
+ }
42603
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlPercentage(false, symbol, {
42604
+ strategyName: context.strategyName,
42605
+ exchangeName: context.exchangeName,
42606
+ frameName: "",
42607
+ });
42608
+ };
42609
+ /**
42610
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
42611
+ *
42612
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
42613
+ * Returns null if no pending signal exists.
42614
+ *
42615
+ * @param symbol - Trading pair symbol
42616
+ * @param context - Execution context with strategyName and exchangeName
42617
+ * @returns recovery distance in PnL cost (≥ 0) or null if no active position
42618
+ */
42619
+ this.getPositionHighestMaxDrawdownPnlCost = async (symbol, context) => {
42620
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST, {
42621
+ symbol,
42622
+ context,
42623
+ });
42624
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
42625
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
42626
+ {
42627
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42628
+ riskName &&
42629
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
42630
+ riskList &&
42631
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
42632
+ actions &&
42633
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
42634
+ }
42635
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlCost(false, symbol, {
42636
+ strategyName: context.strategyName,
42637
+ exchangeName: context.exchangeName,
42638
+ frameName: "",
42639
+ });
42640
+ };
42009
42641
  /**
42010
42642
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
42011
42643
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -51053,6 +51685,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
51053
51685
  pnlEntries: data.signal.pnl.pnlEntries,
51054
51686
  scheduledAt: data.signal.scheduledAt,
51055
51687
  currentPrice: data.currentPrice,
51688
+ note: data.signal.note,
51056
51689
  createdAt: data.createdAt,
51057
51690
  };
51058
51691
  }
@@ -51082,6 +51715,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
51082
51715
  duration: durationMin,
51083
51716
  scheduledAt: data.signal.scheduledAt,
51084
51717
  pendingAt: data.signal.pendingAt,
51718
+ note: data.signal.note,
51085
51719
  createdAt: data.createdAt,
51086
51720
  };
51087
51721
  }
@@ -51118,6 +51752,7 @@ const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
51118
51752
  pnlPriceClose: data.data.pnl.priceClose,
51119
51753
  pnlCost: data.data.pnl.pnlCost,
51120
51754
  pnlEntries: data.data.pnl.pnlEntries,
51755
+ note: data.data.note,
51121
51756
  scheduledAt: data.data.scheduledAt,
51122
51757
  pendingAt: data.data.pendingAt,
51123
51758
  createdAt: data.timestamp,
@@ -51153,6 +51788,7 @@ const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
51153
51788
  pnlPriceClose: data.data.pnl.priceClose,
51154
51789
  pnlCost: data.data.pnl.pnlCost,
51155
51790
  pnlEntries: data.data.pnl.pnlEntries,
51791
+ note: data.data.note,
51156
51792
  scheduledAt: data.data.scheduledAt,
51157
51793
  pendingAt: data.data.pendingAt,
51158
51794
  createdAt: data.timestamp,
@@ -51187,6 +51823,7 @@ const CREATE_BREAKEVEN_NOTIFICATION_FN = (data) => ({
51187
51823
  pnlPriceClose: data.data.pnl.priceClose,
51188
51824
  pnlCost: data.data.pnl.pnlCost,
51189
51825
  pnlEntries: data.data.pnl.pnlEntries,
51826
+ note: data.data.note,
51190
51827
  scheduledAt: data.data.scheduledAt,
51191
51828
  pendingAt: data.data.pendingAt,
51192
51829
  createdAt: data.timestamp,
@@ -51228,6 +51865,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51228
51865
  pnlEntries: data.pnl.pnlEntries,
51229
51866
  scheduledAt: data.scheduledAt,
51230
51867
  pendingAt: data.pendingAt,
51868
+ note: data.note,
51231
51869
  createdAt: data.timestamp,
51232
51870
  };
51233
51871
  }
@@ -51260,6 +51898,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51260
51898
  pnlEntries: data.pnl.pnlEntries,
51261
51899
  scheduledAt: data.scheduledAt,
51262
51900
  pendingAt: data.pendingAt,
51901
+ note: data.note,
51263
51902
  createdAt: data.timestamp,
51264
51903
  };
51265
51904
  }
@@ -51291,6 +51930,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51291
51930
  pnlEntries: data.pnl.pnlEntries,
51292
51931
  scheduledAt: data.scheduledAt,
51293
51932
  pendingAt: data.pendingAt,
51933
+ note: data.note,
51294
51934
  createdAt: data.timestamp,
51295
51935
  };
51296
51936
  }
@@ -51323,6 +51963,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51323
51963
  pnlEntries: data.pnl.pnlEntries,
51324
51964
  scheduledAt: data.scheduledAt,
51325
51965
  pendingAt: data.pendingAt,
51966
+ note: data.note,
51326
51967
  createdAt: data.timestamp,
51327
51968
  };
51328
51969
  }
@@ -51355,6 +51996,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51355
51996
  pnlEntries: data.pnl.pnlEntries,
51356
51997
  scheduledAt: data.scheduledAt,
51357
51998
  pendingAt: data.pendingAt,
51999
+ note: data.note,
51358
52000
  createdAt: data.timestamp,
51359
52001
  };
51360
52002
  }
@@ -51387,6 +52029,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51387
52029
  pnlEntries: data.pnl.pnlEntries,
51388
52030
  scheduledAt: data.scheduledAt,
51389
52031
  pendingAt: data.pendingAt,
52032
+ note: data.note,
51390
52033
  createdAt: data.timestamp,
51391
52034
  };
51392
52035
  }
@@ -51420,6 +52063,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51420
52063
  pnlEntries: data.pnl.pnlEntries,
51421
52064
  scheduledAt: data.scheduledAt,
51422
52065
  pendingAt: data.pendingAt,
52066
+ note: data.note,
51423
52067
  createdAt: data.timestamp,
51424
52068
  };
51425
52069
  }
@@ -51443,6 +52087,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51443
52087
  pnlPriceClose: data.pnl.priceClose,
51444
52088
  pnlCost: data.pnl.pnlCost,
51445
52089
  pnlEntries: data.pnl.pnlEntries,
52090
+ note: data.note,
51446
52091
  createdAt: data.timestamp,
51447
52092
  };
51448
52093
  }
@@ -51466,6 +52111,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51466
52111
  pnlPriceClose: data.pnl.priceClose,
51467
52112
  pnlCost: data.pnl.pnlCost,
51468
52113
  pnlEntries: data.pnl.pnlEntries,
52114
+ note: data.note,
51469
52115
  createdAt: data.timestamp,
51470
52116
  };
51471
52117
  }
@@ -51507,6 +52153,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
51507
52153
  totalPartials: data.totalPartials,
51508
52154
  scheduledAt: data.scheduledAt,
51509
52155
  pendingAt: data.pendingAt,
52156
+ note: data.signal.note,
51510
52157
  createdAt: data.timestamp,
51511
52158
  };
51512
52159
  }
@@ -51539,6 +52186,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
51539
52186
  scheduledAt: data.scheduledAt,
51540
52187
  pendingAt: data.pendingAt,
51541
52188
  closeReason: data.closeReason,
52189
+ note: data.signal.note,
51542
52190
  createdAt: data.timestamp,
51543
52191
  };
51544
52192
  }
@@ -53038,6 +53686,7 @@ const CACHE_METHOD_NAME_FILE = "CacheUtils.file";
53038
53686
  const CACHE_METHOD_NAME_FILE_CLEAR = "CacheUtils.file.clear";
53039
53687
  const CACHE_METHOD_NAME_DISPOSE = "CacheUtils.dispose";
53040
53688
  const CACHE_METHOD_NAME_CLEAR = "CacheUtils.clear";
53689
+ const CACHE_METHOD_NAME_RESET_COUNTER = "CacheUtils.resetCounter";
53041
53690
  const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
53042
53691
  const MS_PER_MINUTE$1 = 60000;
53043
53692
  const INTERVAL_MINUTES$1 = {
@@ -53289,7 +53938,7 @@ class CacheFileInstance {
53289
53938
  /**
53290
53939
  * Clears the index counter.
53291
53940
  */
53292
- static clearCounter() {
53941
+ static resetCounter() {
53293
53942
  CacheFileInstance._indexCounter = 0;
53294
53943
  }
53295
53944
  /**
@@ -53544,11 +54193,17 @@ class CacheUtils {
53544
54193
  */
53545
54194
  this.clear = () => {
53546
54195
  backtest.loggerService.info(CACHE_METHOD_NAME_CLEAR);
53547
- {
53548
- this._getFnInstance.clear();
53549
- this._getFileInstance.clear();
53550
- }
53551
- CacheFileInstance.clearCounter();
54196
+ this._getFnInstance.clear();
54197
+ this._getFileInstance.clear();
54198
+ };
54199
+ /**
54200
+ * Resets the CacheFileInstance index counter to zero.
54201
+ * This is useful when process.cwd() changes between strategy iterations to ensure
54202
+ * that new CacheFileInstance objects start with index 0 and do not collide with old instances.
54203
+ */
54204
+ this.resetCounter = () => {
54205
+ backtest.loggerService.info(CACHE_METHOD_NAME_RESET_COUNTER);
54206
+ CacheFileInstance.resetCounter();
53552
54207
  };
53553
54208
  }
53554
54209
  }
@@ -53572,10 +54227,12 @@ const INTERVAL_METHOD_NAME_RUN = "IntervalFnInstance.run";
53572
54227
  const INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN = "IntervalFileInstance.run";
53573
54228
  const INTERVAL_METHOD_NAME_FN = "IntervalUtils.fn";
53574
54229
  const INTERVAL_METHOD_NAME_FN_CLEAR = "IntervalUtils.fn.clear";
54230
+ const INTERVAL_METHOD_NAME_FN_GC = "IntervalUtils.fn.gc";
53575
54231
  const INTERVAL_METHOD_NAME_FILE = "IntervalUtils.file";
53576
54232
  const INTERVAL_METHOD_NAME_FILE_CLEAR = "IntervalUtils.file.clear";
53577
54233
  const INTERVAL_METHOD_NAME_DISPOSE = "IntervalUtils.dispose";
53578
54234
  const INTERVAL_METHOD_NAME_CLEAR = "IntervalUtils.clear";
54235
+ const INTERVAL_METHOD_NAME_RESET_COUNTER = "IntervalUtils.resetCounter";
53579
54236
  const MS_PER_MINUTE = 60000;
53580
54237
  const INTERVAL_MINUTES = {
53581
54238
  "1m": 1,
@@ -53642,45 +54299,51 @@ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, isBacktest) => {
53642
54299
  *
53643
54300
  * State is kept in memory; use `IntervalFileInstance` for persistence across restarts.
53644
54301
  *
54302
+ * @template F - Concrete function type
54303
+ *
53645
54304
  * @example
53646
54305
  * ```typescript
53647
54306
  * const instance = new IntervalFnInstance(mySignalFn, "1h");
53648
- * await instance.run("BTCUSDT"); // → ISignalIntervalDto | null (fn called)
53649
- * await instance.run("BTCUSDT"); // → null (skipped, same interval)
54307
+ * await instance.run("BTCUSDT"); // → T | null (fn called)
54308
+ * await instance.run("BTCUSDT"); // → null (skipped, same interval)
53650
54309
  * // After 1 hour passes:
53651
- * await instance.run("BTCUSDT"); // → ISignalIntervalDto | null (fn called again)
54310
+ * await instance.run("BTCUSDT"); // → T | null (fn called again)
53652
54311
  * ```
53653
54312
  */
53654
54313
  class IntervalFnInstance {
53655
54314
  /**
53656
54315
  * Creates a new IntervalFnInstance.
53657
54316
  *
53658
- * @param fn - Signal function to fire once per interval
54317
+ * @param fn - Function to fire once per interval
53659
54318
  * @param interval - Candle interval that controls the firing boundary
54319
+ * @param key - Optional key generator for argument-based state separation.
54320
+ * Default: `([symbol]) => symbol`
53660
54321
  */
53661
- constructor(fn, interval) {
54322
+ constructor(fn, interval, key = ([symbol]) => symbol) {
53662
54323
  this.fn = fn;
53663
54324
  this.interval = interval;
53664
- /** Stores the last aligned timestamp per context+symbol key. */
54325
+ this.key = key;
54326
+ /** Stores the last aligned timestamp per context+symbol+args key. */
53665
54327
  this._stateMap = new Map();
53666
54328
  /**
53667
54329
  * Execute the signal function with once-per-interval enforcement.
53668
54330
  *
53669
54331
  * Algorithm:
53670
54332
  * 1. Align the current execution context `when` to the interval boundary.
53671
- * 2. If the stored aligned timestamp for this context+symbol equals the current one → return `null`.
53672
- * 3. Otherwise call `fn`. If it returns a non-null signal, record the aligned timestamp and return
54333
+ * 2. Build state key from context + key generator result.
54334
+ * 3. If the stored aligned timestamp for this key equals the current one return `null`.
54335
+ * 4. Otherwise call `fn`. If it returns a non-null signal, record the aligned timestamp and return
53673
54336
  * the signal. If it returns `null`, leave state unchanged so the next call retries.
53674
54337
  *
53675
54338
  * Requires active method context and execution context.
53676
54339
  *
53677
- * @param symbol - Trading pair symbol (e.g. "BTCUSDT")
53678
- * @returns The signal returned by `fn` on the first non-null fire, `null` on all subsequent calls
54340
+ * @param args - Arguments forwarded to the wrapped function
54341
+ * @returns The value returned by `fn` on the first non-null fire, `null` on all subsequent calls
53679
54342
  * within the same interval or when `fn` itself returned `null`
53680
54343
  * @throws Error if method context, execution context, or interval is missing
53681
54344
  */
53682
- this.run = async (symbol) => {
53683
- backtest.loggerService.debug(INTERVAL_METHOD_NAME_RUN, { symbol });
54345
+ this.run = async (...args) => {
54346
+ backtest.loggerService.debug(INTERVAL_METHOD_NAME_RUN, { args });
53684
54347
  const step = INTERVAL_MINUTES[this.interval];
53685
54348
  {
53686
54349
  if (!MethodContextService.hasContext()) {
@@ -53694,15 +54357,16 @@ class IntervalFnInstance {
53694
54357
  }
53695
54358
  }
53696
54359
  const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
53697
- const key = `${contextKey}:${symbol}`;
53698
54360
  const currentWhen = backtest.executionContextService.context.when;
53699
54361
  const currentAligned = align(currentWhen.getTime(), this.interval);
53700
- if (this._stateMap.get(key) === currentAligned) {
54362
+ const argKey = this.key(args);
54363
+ const stateKey = `${contextKey}:${argKey}`;
54364
+ if (this._stateMap.get(stateKey) === currentAligned) {
53701
54365
  return null;
53702
54366
  }
53703
- const result = await this.fn(symbol, currentWhen);
54367
+ const result = await this.fn.apply(null, args);
53704
54368
  if (result !== null) {
53705
- this._stateMap.set(key, currentAligned);
54369
+ this._stateMap.set(stateKey, currentAligned);
53706
54370
  }
53707
54371
  return result;
53708
54372
  };
@@ -53723,6 +54387,28 @@ class IntervalFnInstance {
53723
54387
  }
53724
54388
  }
53725
54389
  };
54390
+ /**
54391
+ * Garbage collect expired state entries.
54392
+ *
54393
+ * Removes all entries whose aligned timestamp differs from the current interval boundary.
54394
+ * Call this periodically to free memory from stale state entries.
54395
+ *
54396
+ * Requires active execution context to get current time.
54397
+ *
54398
+ * @returns Number of entries removed
54399
+ */
54400
+ this.gc = () => {
54401
+ const currentWhen = backtest.executionContextService.context.when;
54402
+ const currentAligned = align(currentWhen.getTime(), this.interval);
54403
+ let removed = 0;
54404
+ for (const [key, storedAligned] of this._stateMap.entries()) {
54405
+ if (storedAligned !== currentAligned) {
54406
+ this._stateMap.delete(key);
54407
+ removed++;
54408
+ }
54409
+ }
54410
+ return removed;
54411
+ };
53726
54412
  }
53727
54413
  }
53728
54414
  /**
@@ -53736,13 +54422,13 @@ class IntervalFnInstance {
53736
54422
  *
53737
54423
  * Fired state survives process restarts — unlike `IntervalFnInstance` which is in-memory only.
53738
54424
  *
53739
- * @template T - Async function type: `(symbol: string, ...args) => Promise<ISignalIntervalDto | null>`
54425
+ * @template F - Concrete async function type
53740
54426
  *
53741
54427
  * @example
53742
54428
  * ```typescript
53743
54429
  * const instance = new IntervalFileInstance(fetchSignal, "1h", "mySignal");
53744
- * await instance.run("BTCUSDT"); // → ISignalIntervalDto | null (fn called, result written to disk)
53745
- * await instance.run("BTCUSDT"); // → null (record exists, already fired)
54430
+ * await instance.run("BTCUSDT"); // → R | null (fn called, result written to disk)
54431
+ * await instance.run("BTCUSDT"); // → null (record exists, already fired)
53746
54432
  * ```
53747
54433
  */
53748
54434
  class IntervalFileInstance {
@@ -53757,7 +54443,7 @@ class IntervalFileInstance {
53757
54443
  * Resets the index counter to zero.
53758
54444
  * Call this when clearing all instances (e.g. on `IntervalUtils.clear()`).
53759
54445
  */
53760
- static clearCounter() {
54446
+ static resetCounter() {
53761
54447
  IntervalFileInstance._indexCounter = 0;
53762
54448
  }
53763
54449
  /**
@@ -53766,26 +54452,30 @@ class IntervalFileInstance {
53766
54452
  * @param fn - Async signal function to fire once per interval
53767
54453
  * @param interval - Candle interval that controls the firing boundary
53768
54454
  * @param name - Human-readable bucket name used as the directory prefix
54455
+ * @param key - Dynamic key generator; receives `[symbol, alignMs, ...rest]`.
54456
+ * Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
53769
54457
  */
53770
- constructor(fn, interval, name) {
54458
+ constructor(fn, interval, name, key = ([symbol, alignMs]) => `${symbol}_${alignMs}`) {
53771
54459
  this.fn = fn;
53772
54460
  this.interval = interval;
53773
54461
  this.name = name;
54462
+ this.key = key;
53774
54463
  /**
53775
- * Execute the async signal function with persistent once-per-interval enforcement.
54464
+ * Execute the async function with persistent once-per-interval enforcement.
53776
54465
  *
53777
54466
  * Algorithm:
53778
54467
  * 1. Build bucket = `${name}_${interval}_${index}` — fixed per instance, used as directory name.
53779
- * 2. Align execution context `when` to interval boundary → `alignedTs`.
53780
- * 3. Build entity key = `${symbol}_${alignedTs}`.
54468
+ * 2. Align execution context `when` to interval boundary → `alignedMs`.
54469
+ * 3. Build entity key from the key generator (receives `[symbol, alignedMs, ...rest]`).
53781
54470
  * 4. Try to read from `PersistIntervalAdapter` using (bucket, entityKey).
53782
54471
  * 5. On hit — return `null` (interval already fired).
53783
54472
  * 6. On miss — call `fn`. If non-null, write to disk and return result. If null, skip write and return null.
53784
54473
  *
53785
54474
  * Requires active method context and execution context.
53786
54475
  *
53787
- * @param args - Arguments forwarded to the wrapped function (first must be `symbol: string`)
53788
- * @returns The signal on the first non-null fire, `null` if already fired this interval
54476
+ * @param symbol - Trading pair symbol (e.g. "BTCUSDT")
54477
+ * @param args - Additional arguments forwarded to the wrapped function
54478
+ * @returns The value on the first non-null fire, `null` if already fired this interval
53789
54479
  * or if `fn` itself returned `null`
53790
54480
  * @throws Error if method context, execution context, or interval is missing
53791
54481
  */
@@ -53803,11 +54493,11 @@ class IntervalFileInstance {
53803
54493
  throw new Error(`IntervalFileInstance unknown interval=${this.interval}`);
53804
54494
  }
53805
54495
  }
53806
- const [symbol] = args;
54496
+ const [symbol, ...rest] = args;
53807
54497
  const { when } = backtest.executionContextService.context;
53808
- const alignedTs = align(when.getTime(), this.interval);
54498
+ const alignedMs = align(when.getTime(), this.interval);
53809
54499
  const bucket = `${this.name}_${this.interval}_${this.index}`;
53810
- const entityKey = `${symbol}_${alignedTs}`;
54500
+ const entityKey = this.key([symbol, alignedMs, ...rest]);
53811
54501
  const cached = await PersistIntervalAdapter.readIntervalData(bucket, entityKey);
53812
54502
  if (cached !== null) {
53813
54503
  return null;
@@ -53843,8 +54533,8 @@ IntervalFileInstance._indexCounter = 0;
53843
54533
  * import { Interval } from "./classes/Interval";
53844
54534
  *
53845
54535
  * const fireOncePerHour = Interval.fn(mySignalFn, { interval: "1h" });
53846
- * await fireOncePerHour("BTCUSDT", when); // fn called — returns its result
53847
- * await fireOncePerHour("BTCUSDT", when); // returns null (same interval)
54536
+ * await fireOncePerHour("BTCUSDT"); // fn called — returns its result
54537
+ * await fireOncePerHour("BTCUSDT"); // returns null (same interval)
53848
54538
  * ```
53849
54539
  */
53850
54540
  class IntervalUtils {
@@ -53853,12 +54543,12 @@ class IntervalUtils {
53853
54543
  * Memoized factory to get or create an `IntervalFnInstance` for a function.
53854
54544
  * Each function reference gets its own isolated instance.
53855
54545
  */
53856
- this._getInstance = functoolsKit.memoize(([run]) => run, (run, interval) => new IntervalFnInstance(run, interval));
54546
+ this._getInstance = functoolsKit.memoize(([run]) => run, (run, interval, key) => new IntervalFnInstance(run, interval, key));
53857
54547
  /**
53858
54548
  * Memoized factory to get or create an `IntervalFileInstance` for an async function.
53859
54549
  * Each function reference gets its own isolated persistent instance.
53860
54550
  */
53861
- this._getFileInstance = functoolsKit.memoize(([run]) => run, (run, interval, name) => new IntervalFileInstance(run, interval, name));
54551
+ this._getFileInstance = functoolsKit.memoize(([run]) => run, (run, interval, name, key) => new IntervalFileInstance(run, interval, name, key));
53862
54552
  /**
53863
54553
  * Wrap a signal function with in-memory once-per-interval firing.
53864
54554
  *
@@ -53870,21 +54560,30 @@ class IntervalUtils {
53870
54560
  *
53871
54561
  * @param run - Signal function to wrap
53872
54562
  * @param context.interval - Candle interval that controls the firing boundary
53873
- * @returns Wrapped function with the same signature as `TIntervalFn`, plus a `clear()` method
54563
+ * @param context.key - Optional key generator for argument-based state separation
54564
+ * @returns Wrapped function with the same signature as `F`, plus a `clear()` method
53874
54565
  *
53875
54566
  * @example
53876
54567
  * ```typescript
54568
+ * // Without extra args
53877
54569
  * const fireOnce = Interval.fn(mySignalFn, { interval: "15m" });
54570
+ * await fireOnce("BTCUSDT"); // → T or null (fn called)
54571
+ * await fireOnce("BTCUSDT"); // → null (same interval, skipped)
53878
54572
  *
53879
- * await fireOnce("BTCUSDT", when); // signal or null (fn called)
53880
- * await fireOnce("BTCUSDT", when); // → null (same interval, skipped)
54573
+ * // With extra args and key
54574
+ * const fireOnce = Interval.fn(mySignalFn, {
54575
+ * interval: "15m",
54576
+ * key: ([symbol, period]) => `${symbol}_${period}`,
54577
+ * });
54578
+ * await fireOnce("BTCUSDT", 14); // → T or null
54579
+ * await fireOnce("BTCUSDT", 28); // → T or null (separate state)
53881
54580
  * ```
53882
54581
  */
53883
54582
  this.fn = (run, context) => {
53884
54583
  backtest.loggerService.info(INTERVAL_METHOD_NAME_FN, { context });
53885
- const wrappedFn = (symbol, _when) => {
53886
- const instance = this._getInstance(run, context.interval);
53887
- return instance.run(symbol);
54584
+ const wrappedFn = (...args) => {
54585
+ const instance = this._getInstance(run, context.interval, context.key);
54586
+ return instance.run(...args);
53888
54587
  };
53889
54588
  wrappedFn.clear = () => {
53890
54589
  backtest.loggerService.info(INTERVAL_METHOD_NAME_FN_CLEAR);
@@ -53898,6 +54597,14 @@ class IntervalUtils {
53898
54597
  }
53899
54598
  this._getInstance.get(run)?.clear();
53900
54599
  };
54600
+ wrappedFn.gc = () => {
54601
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_FN_GC);
54602
+ if (!ExecutionContextService.hasContext()) {
54603
+ backtest.loggerService.warn(`${INTERVAL_METHOD_NAME_FN_GC} called without execution context, skipping`);
54604
+ return;
54605
+ }
54606
+ return this._getInstance.get(run)?.gc();
54607
+ };
53901
54608
  return wrappedFn;
53902
54609
  };
53903
54610
  /**
@@ -53910,27 +54617,32 @@ class IntervalUtils {
53910
54617
  * The `run` function reference is used as the memoization key for the underlying
53911
54618
  * `IntervalFileInstance`, so each unique function reference gets its own isolated instance.
53912
54619
  *
53913
- * @template T - Async function type to wrap
54620
+ * @template F - Concrete async function type
53914
54621
  * @param run - Async signal function to wrap with persistent once-per-interval firing
53915
54622
  * @param context.interval - Candle interval that controls the firing boundary
53916
54623
  * @param context.name - Human-readable bucket name; becomes the directory prefix
53917
- * @returns Wrapped function with the same signature as `T`, plus an async `clear()` method
53918
- * that deletes persisted records from disk and disposes the memoized instance
54624
+ * @param context.key - Optional entity key generator. Receives `[symbol, alignMs, ...rest]`.
54625
+ * Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
54626
+ * @returns Wrapped function with the same signature as `F`, plus an async `clear()` method
53919
54627
  *
53920
54628
  * @example
53921
54629
  * ```typescript
53922
54630
  * const fetchSignal = async (symbol: string, period: number) => { ... };
53923
- * const fireOnce = Interval.file(fetchSignal, { interval: "1h", name: "fetchSignal" });
53924
- * await fireOnce.clear(); // delete disk records so the function fires again next call
54631
+ * const fireOnce = Interval.file(fetchSignal, {
54632
+ * interval: "1h",
54633
+ * name: "fetchSignal",
54634
+ * key: ([symbol, alignMs, period]) => `${symbol}_${alignMs}_${period}`,
54635
+ * });
54636
+ * await fireOnce("BTCUSDT", 14);
53925
54637
  * ```
53926
54638
  */
53927
54639
  this.file = (run, context) => {
53928
54640
  backtest.loggerService.info(INTERVAL_METHOD_NAME_FILE, { context });
53929
54641
  {
53930
- this._getFileInstance(run, context.interval, context.name);
54642
+ this._getFileInstance(run, context.interval, context.name, context.key);
53931
54643
  }
53932
54644
  const wrappedFn = (...args) => {
53933
- const instance = this._getFileInstance(run, context.interval, context.name);
54645
+ const instance = this._getFileInstance(run, context.interval, context.name, context.key);
53934
54646
  return instance.run(...args);
53935
54647
  };
53936
54648
  wrappedFn.clear = async () => {
@@ -53957,10 +54669,10 @@ class IntervalUtils {
53957
54669
  this.dispose = (run) => {
53958
54670
  backtest.loggerService.info(INTERVAL_METHOD_NAME_DISPOSE, { run });
53959
54671
  this._getInstance.clear(run);
54672
+ this._getFileInstance.clear(run);
53960
54673
  };
53961
54674
  /**
53962
- * Clears all memoized `IntervalFnInstance` and `IntervalFileInstance` objects and
53963
- * resets the `IntervalFileInstance` index counter.
54675
+ * Clears all memoized `IntervalFnInstance` and `IntervalFileInstance` objects.
53964
54676
  * Call this when `process.cwd()` changes between strategy iterations
53965
54677
  * so new instances are created with the updated base path.
53966
54678
  */
@@ -53968,7 +54680,15 @@ class IntervalUtils {
53968
54680
  backtest.loggerService.info(INTERVAL_METHOD_NAME_CLEAR);
53969
54681
  this._getInstance.clear();
53970
54682
  this._getFileInstance.clear();
53971
- IntervalFileInstance.clearCounter();
54683
+ };
54684
+ /**
54685
+ * Resets the IntervalFileInstance index counter to zero.
54686
+ * This is useful when process.cwd() changes between strategy iterations to ensure
54687
+ * that new IntervalFileInstance objects start with index 0 and do not collide with old instances.
54688
+ */
54689
+ this.resetCounter = () => {
54690
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_RESET_COUNTER);
54691
+ IntervalFileInstance.resetCounter();
53972
54692
  };
53973
54693
  }
53974
54694
  }
@@ -55212,9 +55932,13 @@ exports.getPositionEffectivePrice = getPositionEffectivePrice;
55212
55932
  exports.getPositionEntries = getPositionEntries;
55213
55933
  exports.getPositionEntryOverlap = getPositionEntryOverlap;
55214
55934
  exports.getPositionEstimateMinutes = getPositionEstimateMinutes;
55935
+ exports.getPositionHighestMaxDrawdownPnlCost = getPositionHighestMaxDrawdownPnlCost;
55936
+ exports.getPositionHighestMaxDrawdownPnlPercentage = getPositionHighestMaxDrawdownPnlPercentage;
55215
55937
  exports.getPositionHighestPnlCost = getPositionHighestPnlCost;
55216
55938
  exports.getPositionHighestPnlPercentage = getPositionHighestPnlPercentage;
55217
55939
  exports.getPositionHighestProfitBreakeven = getPositionHighestProfitBreakeven;
55940
+ exports.getPositionHighestProfitDistancePnlCost = getPositionHighestProfitDistancePnlCost;
55941
+ exports.getPositionHighestProfitDistancePnlPercentage = getPositionHighestProfitDistancePnlPercentage;
55218
55942
  exports.getPositionHighestProfitMinutes = getPositionHighestProfitMinutes;
55219
55943
  exports.getPositionHighestProfitPrice = getPositionHighestProfitPrice;
55220
55944
  exports.getPositionHighestProfitTimestamp = getPositionHighestProfitTimestamp;