backtest-kit 6.5.2 → 6.7.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.
Files changed (4) hide show
  1. package/build/index.cjs +1752 -111
  2. package/build/index.mjs +1744 -112
  3. package/package.json +1 -1
  4. package/types.d.ts +951 -98
package/build/index.cjs CHANGED
@@ -128,6 +128,7 @@ const markdownServices$1 = {
128
128
  strategyMarkdownService: Symbol('strategyMarkdownService'),
129
129
  syncMarkdownService: Symbol('syncMarkdownService'),
130
130
  highestProfitMarkdownService: Symbol('highestProfitMarkdownService'),
131
+ maxDrawdownMarkdownService: Symbol('maxDrawdownMarkdownService'),
131
132
  };
132
133
  const reportServices$1 = {
133
134
  backtestReportService: Symbol('backtestReportService'),
@@ -142,6 +143,7 @@ const reportServices$1 = {
142
143
  strategyReportService: Symbol('strategyReportService'),
143
144
  syncReportService: Symbol('syncReportService'),
144
145
  highestProfitReportService: Symbol('highestProfitReportService'),
146
+ maxDrawdownReportService: Symbol('maxDrawdownReportService'),
145
147
  };
146
148
  const validationServices$1 = {
147
149
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -473,6 +475,13 @@ const GLOBAL_CONFIG = {
473
475
  * Default: 250 events
474
476
  */
475
477
  CC_MAX_HIGHEST_PROFIT_MARKDOWN_ROWS: 250,
478
+ /**
479
+ * Maximum number of events to keep in max drawdown markdown report storage.
480
+ * Older events are removed (FIFO) when this limit is exceeded.
481
+ *
482
+ * Default: 250 events
483
+ */
484
+ CC_MAX_MAX_DRAWDOWN_MARKDOWN_ROWS: 250,
476
485
  /**
477
486
  * Maximum number of events to keep in live markdown report storage.
478
487
  * Older events are removed (FIFO) when this limit is exceeded.
@@ -747,6 +756,12 @@ const backtestScheduleOpenSubject = new functoolsKit.Subject();
747
756
  * Allows users to track profit milestones and implement custom management logic based on profit levels.
748
757
  */
749
758
  const highestProfitSubject = new functoolsKit.Subject();
759
+ /**
760
+ * Max drawdown emitter for real-time risk tracking.
761
+ * Emits updates on the maximum drawdown experienced for an open position.
762
+ * Allows users to track drawdown levels and implement custom risk management logic based on drawdown thresholds.
763
+ */
764
+ const maxDrawdownSubject = new functoolsKit.Subject();
750
765
 
751
766
  var emitters = /*#__PURE__*/Object.freeze({
752
767
  __proto__: null,
@@ -759,6 +774,7 @@ var emitters = /*#__PURE__*/Object.freeze({
759
774
  errorEmitter: errorEmitter,
760
775
  exitEmitter: exitEmitter,
761
776
  highestProfitSubject: highestProfitSubject,
777
+ maxDrawdownSubject: maxDrawdownSubject,
762
778
  partialLossSubject: partialLossSubject,
763
779
  partialProfitSubject: partialProfitSubject,
764
780
  performanceEmitter: performanceEmitter,
@@ -4888,6 +4904,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4888
4904
  _isScheduled: false,
4889
4905
  _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4890
4906
  _peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4907
+ _fall: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4891
4908
  };
4892
4909
  // Валидируем сигнал перед возвратом
4893
4910
  validatePendingSignal(signalRow, currentPrice);
@@ -4913,6 +4930,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4913
4930
  _isScheduled: true,
4914
4931
  _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4915
4932
  _peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4933
+ _fall: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4916
4934
  };
4917
4935
  // Валидируем сигнал перед возвратом
4918
4936
  validateScheduledSignal(scheduledSignalRow, currentPrice);
@@ -4935,6 +4953,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4935
4953
  _isScheduled: false,
4936
4954
  _entry: [{ price: currentPrice, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4937
4955
  _peak: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4956
+ _fall: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4938
4957
  };
4939
4958
  // Валидируем сигнал перед возвратом
4940
4959
  validatePendingSignal(signalRow, currentPrice);
@@ -5646,6 +5665,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
5646
5665
  pendingAt: activationTime,
5647
5666
  _isScheduled: false,
5648
5667
  _peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
5668
+ _fall: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
5649
5669
  };
5650
5670
  // Sync open: if external system rejects — cancel scheduled signal instead of opening
5651
5671
  const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
@@ -6320,6 +6340,15 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice, backt
6320
6340
  const slDistance = effectivePriceOpen - effectiveStopLoss;
6321
6341
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
6322
6342
  percentSl = Math.min(progressPercent, 100);
6343
+ if (currentPrice < signal._fall.price) {
6344
+ const { pnl } = TO_PUBLIC_SIGNAL(signal, currentPrice);
6345
+ signal._fall = { price: currentPrice, timestamp: currentTime, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
6346
+ if (self.params.callbacks?.onWrite) {
6347
+ self.params.callbacks.onWrite(signal.symbol, signal, backtest);
6348
+ }
6349
+ !backtest && await PersistSignalAdapter.writeSignalData(signal, self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
6350
+ await self.params.onMaxDrawdown(TO_PUBLIC_SIGNAL(signal, currentPrice), currentPrice, currentTime);
6351
+ }
6323
6352
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
6324
6353
  }
6325
6354
  }
@@ -6353,6 +6382,15 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice, backt
6353
6382
  const slDistance = effectiveStopLoss - effectivePriceOpen;
6354
6383
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
6355
6384
  percentSl = Math.min(progressPercent, 100);
6385
+ if (currentPrice > signal._fall.price) {
6386
+ const { pnl } = TO_PUBLIC_SIGNAL(signal, currentPrice);
6387
+ signal._fall = { price: currentPrice, timestamp: currentTime, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
6388
+ if (self.params.callbacks?.onWrite) {
6389
+ self.params.callbacks.onWrite(signal.symbol, signal, backtest);
6390
+ }
6391
+ !backtest && await PersistSignalAdapter.writeSignalData(signal, self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
6392
+ await self.params.onMaxDrawdown(TO_PUBLIC_SIGNAL(signal, currentPrice), currentPrice, currentTime);
6393
+ }
6356
6394
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
6357
6395
  }
6358
6396
  }
@@ -6473,6 +6511,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
6473
6511
  pendingAt: activationTime,
6474
6512
  _isScheduled: false,
6475
6513
  _peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
6514
+ _fall: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
6476
6515
  };
6477
6516
  // Sync open: if external system rejects — cancel scheduled signal instead of opening
6478
6517
  const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
@@ -6659,6 +6698,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
6659
6698
  pendingAt: candle.timestamp,
6660
6699
  _isScheduled: false,
6661
6700
  _peak: { price: activatedSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage: 0, pnlCost: 0 },
6701
+ _fall: { price: activatedSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage: 0, pnlCost: 0 },
6662
6702
  };
6663
6703
  // Sync open: if external system rejects — cancel scheduled signal instead of opening
6664
6704
  const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(candle.timestamp, pendingSignal.priceOpen, pendingSignal, self);
@@ -6883,6 +6923,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
6883
6923
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
6884
6924
  const slDistance = effectivePriceOpen - effectiveStopLoss;
6885
6925
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
6926
+ if (averagePrice < signal._fall.price) {
6927
+ const { pnl } = TO_PUBLIC_SIGNAL(signal, averagePrice);
6928
+ signal._fall = { price: averagePrice, timestamp: currentCandleTimestamp, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
6929
+ if (self.params.callbacks?.onWrite) {
6930
+ self.params.callbacks.onWrite(signal.symbol, signal, true);
6931
+ }
6932
+ await self.params.onMaxDrawdown(TO_PUBLIC_SIGNAL(signal, averagePrice), averagePrice, currentCandleTimestamp);
6933
+ }
6886
6934
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
6887
6935
  }
6888
6936
  }
@@ -6913,6 +6961,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
6913
6961
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
6914
6962
  const slDistance = effectiveStopLoss - effectivePriceOpen;
6915
6963
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
6964
+ if (averagePrice > signal._fall.price) {
6965
+ const { pnl } = TO_PUBLIC_SIGNAL(signal, averagePrice);
6966
+ signal._fall = { price: averagePrice, timestamp: currentCandleTimestamp, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
6967
+ if (self.params.callbacks?.onWrite) {
6968
+ self.params.callbacks.onWrite(signal.symbol, signal, true);
6969
+ }
6970
+ await self.params.onMaxDrawdown(TO_PUBLIC_SIGNAL(signal, averagePrice), averagePrice, currentCandleTimestamp);
6971
+ }
6916
6972
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
6917
6973
  }
6918
6974
  }
@@ -7634,6 +7690,104 @@ class ClientStrategy {
7634
7690
  }
7635
7691
  return Math.floor((timestamp - this._pendingSignal._peak.timestamp) / 60000);
7636
7692
  }
7693
+ /**
7694
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
7695
+ *
7696
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
7697
+ * pulling back from its peak profit level.
7698
+ *
7699
+ * Returns null if no pending signal exists.
7700
+ *
7701
+ * @param symbol - Trading pair symbol
7702
+ * @param timestamp - Current Unix timestamp in milliseconds
7703
+ * @returns Promise resolving to minutes since last profit peak or null
7704
+ */
7705
+ async getPositionHighestProfitMinutes(symbol, timestamp) {
7706
+ this.params.logger.debug("ClientStrategy getPositionHighestProfitMinutes", { symbol });
7707
+ if (!this._pendingSignal) {
7708
+ return null;
7709
+ }
7710
+ return Math.floor((timestamp - this._pendingSignal._peak.timestamp) / 60000);
7711
+ }
7712
+ /**
7713
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
7714
+ *
7715
+ * Measures how long ago the deepest drawdown point occurred.
7716
+ * Zero when called at the exact moment the trough was set.
7717
+ *
7718
+ * Returns null if no pending signal exists.
7719
+ *
7720
+ * @param symbol - Trading pair symbol
7721
+ * @param timestamp - Current Unix timestamp in milliseconds
7722
+ * @returns Promise resolving to minutes since last drawdown trough or null
7723
+ */
7724
+ async getPositionMaxDrawdownMinutes(symbol, timestamp) {
7725
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownMinutes", { symbol });
7726
+ if (!this._pendingSignal) {
7727
+ return null;
7728
+ }
7729
+ return Math.floor((timestamp - this._pendingSignal._fall.timestamp) / 60000);
7730
+ }
7731
+ /**
7732
+ * Returns the worst price reached in the loss direction during this position's life.
7733
+ *
7734
+ * Returns null if no pending signal exists.
7735
+ *
7736
+ * @param symbol - Trading pair symbol
7737
+ * @returns Promise resolving to price or null
7738
+ */
7739
+ async getPositionMaxDrawdownPrice(symbol) {
7740
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownPrice", { symbol });
7741
+ if (!this._pendingSignal) {
7742
+ return null;
7743
+ }
7744
+ return this._pendingSignal._fall.price;
7745
+ }
7746
+ /**
7747
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
7748
+ *
7749
+ * Returns null if no pending signal exists.
7750
+ *
7751
+ * @param symbol - Trading pair symbol
7752
+ * @returns Promise resolving to timestamp in milliseconds or null
7753
+ */
7754
+ async getPositionMaxDrawdownTimestamp(symbol) {
7755
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownTimestamp", { symbol });
7756
+ if (!this._pendingSignal) {
7757
+ return null;
7758
+ }
7759
+ return this._pendingSignal._fall.timestamp;
7760
+ }
7761
+ /**
7762
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
7763
+ *
7764
+ * Returns null if no pending signal exists.
7765
+ *
7766
+ * @param symbol - Trading pair symbol
7767
+ * @returns Promise resolving to PnL percentage or null
7768
+ */
7769
+ async getPositionMaxDrawdownPnlPercentage(symbol) {
7770
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownPnlPercentage", { symbol });
7771
+ if (!this._pendingSignal) {
7772
+ return null;
7773
+ }
7774
+ return this._pendingSignal._fall.pnlPercentage;
7775
+ }
7776
+ /**
7777
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
7778
+ *
7779
+ * Returns null if no pending signal exists.
7780
+ *
7781
+ * @param symbol - Trading pair symbol
7782
+ * @returns Promise resolving to PnL cost or null
7783
+ */
7784
+ async getPositionMaxDrawdownPnlCost(symbol) {
7785
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownPnlCost", { symbol });
7786
+ if (!this._pendingSignal) {
7787
+ return null;
7788
+ }
7789
+ return this._pendingSignal._fall.pnlCost;
7790
+ }
7637
7791
  /**
7638
7792
  * Performs a single tick of strategy execution.
7639
7793
  *
@@ -7810,6 +7964,7 @@ class ClientStrategy {
7810
7964
  pendingAt: currentTime,
7811
7965
  _isScheduled: false,
7812
7966
  _peak: { price: activatedSignal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
7967
+ _fall: { price: activatedSignal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
7813
7968
  };
7814
7969
  const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(currentTime, currentPrice, pendingSignal, this);
7815
7970
  if (!syncOpenAllowed) {
@@ -9926,7 +10081,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
9926
10081
  * @param backtest - Whether running in backtest mode
9927
10082
  * @returns Unique string key for memoization
9928
10083
  */
9929
- const CREATE_KEY_FN$r = (symbol, strategyName, exchangeName, frameName, backtest) => {
10084
+ const CREATE_KEY_FN$s = (symbol, strategyName, exchangeName, frameName, backtest) => {
9930
10085
  const parts = [symbol, strategyName, exchangeName];
9931
10086
  if (frameName)
9932
10087
  parts.push(frameName);
@@ -10087,6 +10242,42 @@ const CREATE_HIGHEST_PROFIT_FN = (self, strategyName, exchangeName, frameName, i
10087
10242
  },
10088
10243
  defaultValue: null,
10089
10244
  });
10245
+ /**
10246
+ * Creates a callback function for emitting max drawdown updates to maxDrawdownSubject.
10247
+ * Called by ClientStrategy when the maximum drawdown for an open position is updated.
10248
+ * Emits MaxDrawdownContract event to all subscribers with the current price and timestamp.
10249
+ * Used for real-time risk tracking and management logic based on drawdown levels.
10250
+ * @param self - Reference to StrategyConnectionService instance
10251
+ * @param strategyName - Name of the strategy
10252
+ * @param exchangeName - Name of the exchange
10253
+ * @param frameName - Name of the frame
10254
+ * @param isBacktest - Flag indicating if the operation is for backtesting
10255
+ * @return Callback function for max drawdown updates
10256
+ */
10257
+ const CREATE_MAX_DRAWDOWN_FN = (self, strategyName, exchangeName, frameName, isBacktest) => functoolsKit.trycatch(async (signal, currentPrice, timestamp) => {
10258
+ await maxDrawdownSubject.next({
10259
+ symbol: signal.symbol,
10260
+ signal,
10261
+ currentPrice,
10262
+ timestamp,
10263
+ strategyName,
10264
+ exchangeName,
10265
+ frameName,
10266
+ backtest: isBacktest,
10267
+ });
10268
+ }, {
10269
+ fallback: (error) => {
10270
+ const message = "StrategyConnectionService CREATE_MAX_DRAWDOWN_FN thrown";
10271
+ const payload = {
10272
+ error: functoolsKit.errorData(error),
10273
+ message: functoolsKit.getErrorMessage(error),
10274
+ };
10275
+ bt.loggerService.warn(message, payload);
10276
+ console.warn(message, payload);
10277
+ errorEmitter.next(error);
10278
+ },
10279
+ defaultValue: null,
10280
+ });
10090
10281
  /**
10091
10282
  * Creates a callback function for emitting dispose events.
10092
10283
  *
@@ -10157,7 +10348,7 @@ class StrategyConnectionService {
10157
10348
  * @param backtest - Whether running in backtest mode
10158
10349
  * @returns Configured ClientStrategy instance
10159
10350
  */
10160
- this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$r(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
10351
+ this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$s(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
10161
10352
  const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
10162
10353
  return new ClientStrategy({
10163
10354
  symbol,
@@ -10186,6 +10377,7 @@ class StrategyConnectionService {
10186
10377
  onCommit: CREATE_COMMIT_FN(),
10187
10378
  onSignalSync: CREATE_SYNC_FN(this, strategyName, exchangeName, frameName, backtest),
10188
10379
  onHighestProfit: CREATE_HIGHEST_PROFIT_FN(this, strategyName, exchangeName, frameName, backtest),
10380
+ onMaxDrawdown: CREATE_MAX_DRAWDOWN_FN(this, strategyName, exchangeName, frameName, backtest),
10189
10381
  });
10190
10382
  });
10191
10383
  /**
@@ -10789,6 +10981,130 @@ class StrategyConnectionService {
10789
10981
  const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
10790
10982
  return await strategy.getPositionDrawdownMinutes(symbol, timestamp);
10791
10983
  };
10984
+ /**
10985
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
10986
+ *
10987
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
10988
+ * pulling back from its peak profit level.
10989
+ *
10990
+ * Resolves current timestamp via timeMetaService and delegates to
10991
+ * ClientStrategy.getPositionHighestProfitMinutes().
10992
+ * Returns null if no pending signal exists.
10993
+ *
10994
+ * @param backtest - Whether running in backtest mode
10995
+ * @param symbol - Trading pair symbol
10996
+ * @param context - Execution context with strategyName, exchangeName, frameName
10997
+ * @returns Promise resolving to minutes since last profit peak or null
10998
+ */
10999
+ this.getPositionHighestProfitMinutes = async (backtest, symbol, context) => {
11000
+ this.loggerService.log("strategyConnectionService getPositionHighestProfitMinutes", {
11001
+ symbol,
11002
+ context,
11003
+ });
11004
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11005
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
11006
+ return await strategy.getPositionHighestProfitMinutes(symbol, timestamp);
11007
+ };
11008
+ /**
11009
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
11010
+ *
11011
+ * Measures how long ago the deepest drawdown point occurred.
11012
+ * Zero when called at the exact moment the trough was set.
11013
+ *
11014
+ * Resolves current timestamp via timeMetaService and delegates to
11015
+ * ClientStrategy.getPositionMaxDrawdownMinutes().
11016
+ * Returns null if no pending signal exists.
11017
+ *
11018
+ * @param backtest - Whether running in backtest mode
11019
+ * @param symbol - Trading pair symbol
11020
+ * @param context - Execution context with strategyName, exchangeName, frameName
11021
+ * @returns Promise resolving to minutes since last drawdown trough or null
11022
+ */
11023
+ this.getPositionMaxDrawdownMinutes = async (backtest, symbol, context) => {
11024
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownMinutes", {
11025
+ symbol,
11026
+ context,
11027
+ });
11028
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11029
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
11030
+ return await strategy.getPositionMaxDrawdownMinutes(symbol, timestamp);
11031
+ };
11032
+ /**
11033
+ * Returns the worst price reached in the loss direction during this position's life.
11034
+ *
11035
+ * Delegates to ClientStrategy.getPositionMaxDrawdownPrice().
11036
+ * Returns null if no pending signal exists.
11037
+ *
11038
+ * @param backtest - Whether running in backtest mode
11039
+ * @param symbol - Trading pair symbol
11040
+ * @param context - Execution context with strategyName, exchangeName, frameName
11041
+ * @returns Promise resolving to price or null
11042
+ */
11043
+ this.getPositionMaxDrawdownPrice = async (backtest, symbol, context) => {
11044
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownPrice", {
11045
+ symbol,
11046
+ context,
11047
+ });
11048
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11049
+ return await strategy.getPositionMaxDrawdownPrice(symbol);
11050
+ };
11051
+ /**
11052
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
11053
+ *
11054
+ * Delegates to ClientStrategy.getPositionMaxDrawdownTimestamp().
11055
+ * Returns null if no pending signal exists.
11056
+ *
11057
+ * @param backtest - Whether running in backtest mode
11058
+ * @param symbol - Trading pair symbol
11059
+ * @param context - Execution context with strategyName, exchangeName, frameName
11060
+ * @returns Promise resolving to timestamp in milliseconds or null
11061
+ */
11062
+ this.getPositionMaxDrawdownTimestamp = async (backtest, symbol, context) => {
11063
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownTimestamp", {
11064
+ symbol,
11065
+ context,
11066
+ });
11067
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11068
+ return await strategy.getPositionMaxDrawdownTimestamp(symbol);
11069
+ };
11070
+ /**
11071
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
11072
+ *
11073
+ * Delegates to ClientStrategy.getPositionMaxDrawdownPnlPercentage().
11074
+ * Returns null if no pending signal exists.
11075
+ *
11076
+ * @param backtest - Whether running in backtest mode
11077
+ * @param symbol - Trading pair symbol
11078
+ * @param context - Execution context with strategyName, exchangeName, frameName
11079
+ * @returns Promise resolving to PnL percentage or null
11080
+ */
11081
+ this.getPositionMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
11082
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownPnlPercentage", {
11083
+ symbol,
11084
+ context,
11085
+ });
11086
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11087
+ return await strategy.getPositionMaxDrawdownPnlPercentage(symbol);
11088
+ };
11089
+ /**
11090
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
11091
+ *
11092
+ * Delegates to ClientStrategy.getPositionMaxDrawdownPnlCost().
11093
+ * Returns null if no pending signal exists.
11094
+ *
11095
+ * @param backtest - Whether running in backtest mode
11096
+ * @param symbol - Trading pair symbol
11097
+ * @param context - Execution context with strategyName, exchangeName, frameName
11098
+ * @returns Promise resolving to PnL cost or null
11099
+ */
11100
+ this.getPositionMaxDrawdownPnlCost = async (backtest, symbol, context) => {
11101
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownPnlCost", {
11102
+ symbol,
11103
+ context,
11104
+ });
11105
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11106
+ return await strategy.getPositionMaxDrawdownPnlCost(symbol);
11107
+ };
10792
11108
  /**
10793
11109
  * Disposes the ClientStrategy instance for the given context.
10794
11110
  *
@@ -10827,7 +11143,7 @@ class StrategyConnectionService {
10827
11143
  }
10828
11144
  return;
10829
11145
  }
10830
- const key = CREATE_KEY_FN$r(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
11146
+ const key = CREATE_KEY_FN$s(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
10831
11147
  if (!this.getStrategy.has(key)) {
10832
11148
  return;
10833
11149
  }
@@ -12003,7 +12319,7 @@ class ClientRisk {
12003
12319
  * @param backtest - Whether running in backtest mode
12004
12320
  * @returns Unique string key for memoization
12005
12321
  */
12006
- const CREATE_KEY_FN$q = (riskName, exchangeName, frameName, backtest) => {
12322
+ const CREATE_KEY_FN$r = (riskName, exchangeName, frameName, backtest) => {
12007
12323
  const parts = [riskName, exchangeName];
12008
12324
  if (frameName)
12009
12325
  parts.push(frameName);
@@ -12102,7 +12418,7 @@ class RiskConnectionService {
12102
12418
  * @param backtest - True if backtest mode, false if live mode
12103
12419
  * @returns Configured ClientRisk instance
12104
12420
  */
12105
- this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$q(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
12421
+ this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$r(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
12106
12422
  const schema = this.riskSchemaService.get(riskName);
12107
12423
  return new ClientRisk({
12108
12424
  ...schema,
@@ -12170,7 +12486,7 @@ class RiskConnectionService {
12170
12486
  payload,
12171
12487
  });
12172
12488
  if (payload) {
12173
- const key = CREATE_KEY_FN$q(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
12489
+ const key = CREATE_KEY_FN$r(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
12174
12490
  this.getRisk.clear(key);
12175
12491
  }
12176
12492
  else {
@@ -13677,7 +13993,7 @@ class ClientAction {
13677
13993
  * @param backtest - Whether running in backtest mode
13678
13994
  * @returns Unique string key for memoization
13679
13995
  */
13680
- const CREATE_KEY_FN$p = (actionName, strategyName, exchangeName, frameName, backtest) => {
13996
+ const CREATE_KEY_FN$q = (actionName, strategyName, exchangeName, frameName, backtest) => {
13681
13997
  const parts = [actionName, strategyName, exchangeName];
13682
13998
  if (frameName)
13683
13999
  parts.push(frameName);
@@ -13728,7 +14044,7 @@ class ActionConnectionService {
13728
14044
  * @param backtest - True if backtest mode, false if live mode
13729
14045
  * @returns Configured ClientAction instance
13730
14046
  */
13731
- this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$p(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
14047
+ this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$q(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
13732
14048
  const schema = this.actionSchemaService.get(actionName);
13733
14049
  return new ClientAction({
13734
14050
  ...schema,
@@ -13938,7 +14254,7 @@ class ActionConnectionService {
13938
14254
  await Promise.all(actions.map(async (action) => await action.dispose()));
13939
14255
  return;
13940
14256
  }
13941
- const key = CREATE_KEY_FN$p(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
14257
+ const key = CREATE_KEY_FN$q(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13942
14258
  if (!this.getAction.has(key)) {
13943
14259
  return;
13944
14260
  }
@@ -13956,7 +14272,7 @@ const METHOD_NAME_VALIDATE$2 = "exchangeCoreService validate";
13956
14272
  * @param exchangeName - Exchange name
13957
14273
  * @returns Unique string key for memoization
13958
14274
  */
13959
- const CREATE_KEY_FN$o = (exchangeName) => {
14275
+ const CREATE_KEY_FN$p = (exchangeName) => {
13960
14276
  return exchangeName;
13961
14277
  };
13962
14278
  /**
@@ -13980,7 +14296,7 @@ class ExchangeCoreService {
13980
14296
  * @param exchangeName - Name of the exchange to validate
13981
14297
  * @returns Promise that resolves when validation is complete
13982
14298
  */
13983
- this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$o(exchangeName), async (exchangeName) => {
14299
+ this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$p(exchangeName), async (exchangeName) => {
13984
14300
  this.loggerService.log(METHOD_NAME_VALIDATE$2, {
13985
14301
  exchangeName,
13986
14302
  });
@@ -14232,7 +14548,7 @@ const METHOD_NAME_VALIDATE$1 = "strategyCoreService validate";
14232
14548
  * @param context - Execution context with strategyName, exchangeName, frameName
14233
14549
  * @returns Unique string key for memoization
14234
14550
  */
14235
- const CREATE_KEY_FN$n = (context) => {
14551
+ const CREATE_KEY_FN$o = (context) => {
14236
14552
  const parts = [context.strategyName, context.exchangeName];
14237
14553
  if (context.frameName)
14238
14554
  parts.push(context.frameName);
@@ -14264,7 +14580,7 @@ class StrategyCoreService {
14264
14580
  * @param context - Execution context with strategyName, exchangeName, frameName
14265
14581
  * @returns Promise that resolves when validation is complete
14266
14582
  */
14267
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$n(context), async (context) => {
14583
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$o(context), async (context) => {
14268
14584
  this.loggerService.log(METHOD_NAME_VALIDATE$1, {
14269
14585
  context,
14270
14586
  });
@@ -15308,6 +15624,114 @@ class StrategyCoreService {
15308
15624
  await this.validate(context);
15309
15625
  return await this.strategyConnectionService.getPositionDrawdownMinutes(backtest, symbol, context);
15310
15626
  };
15627
+ /**
15628
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
15629
+ *
15630
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
15631
+ * pulling back from its peak profit level.
15632
+ *
15633
+ * Validates strategy existence and delegates to connection service.
15634
+ * Returns null if no pending signal exists.
15635
+ *
15636
+ * @param backtest - Whether running in backtest mode
15637
+ * @param symbol - Trading pair symbol
15638
+ * @param context - Execution context with strategyName, exchangeName, frameName
15639
+ * @returns Promise resolving to minutes since last profit peak or null
15640
+ */
15641
+ this.getPositionHighestProfitMinutes = async (backtest, symbol, context) => {
15642
+ this.loggerService.log("strategyCoreService getPositionHighestProfitMinutes", {
15643
+ symbol,
15644
+ context,
15645
+ });
15646
+ await this.validate(context);
15647
+ return await this.strategyConnectionService.getPositionHighestProfitMinutes(backtest, symbol, context);
15648
+ };
15649
+ /**
15650
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
15651
+ *
15652
+ * Measures how long ago the deepest drawdown point occurred.
15653
+ * Zero when called at the exact moment the trough was set.
15654
+ *
15655
+ * Validates strategy existence and delegates to connection service.
15656
+ * Returns null if no pending signal exists.
15657
+ *
15658
+ * @param backtest - Whether running in backtest mode
15659
+ * @param symbol - Trading pair symbol
15660
+ * @param context - Execution context with strategyName, exchangeName, frameName
15661
+ * @returns Promise resolving to minutes since last drawdown trough or null
15662
+ */
15663
+ this.getPositionMaxDrawdownMinutes = async (backtest, symbol, context) => {
15664
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownMinutes", {
15665
+ symbol,
15666
+ context,
15667
+ });
15668
+ await this.validate(context);
15669
+ return await this.strategyConnectionService.getPositionMaxDrawdownMinutes(backtest, symbol, context);
15670
+ };
15671
+ /**
15672
+ * Returns the worst price reached in the loss direction during this position's life.
15673
+ *
15674
+ * @param backtest - Whether running in backtest mode
15675
+ * @param symbol - Trading pair symbol
15676
+ * @param context - Execution context with strategyName, exchangeName, frameName
15677
+ * @returns Promise resolving to price or null
15678
+ */
15679
+ this.getPositionMaxDrawdownPrice = async (backtest, symbol, context) => {
15680
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownPrice", {
15681
+ symbol,
15682
+ context,
15683
+ });
15684
+ await this.validate(context);
15685
+ return await this.strategyConnectionService.getPositionMaxDrawdownPrice(backtest, symbol, context);
15686
+ };
15687
+ /**
15688
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
15689
+ *
15690
+ * @param backtest - Whether running in backtest mode
15691
+ * @param symbol - Trading pair symbol
15692
+ * @param context - Execution context with strategyName, exchangeName, frameName
15693
+ * @returns Promise resolving to timestamp in milliseconds or null
15694
+ */
15695
+ this.getPositionMaxDrawdownTimestamp = async (backtest, symbol, context) => {
15696
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownTimestamp", {
15697
+ symbol,
15698
+ context,
15699
+ });
15700
+ await this.validate(context);
15701
+ return await this.strategyConnectionService.getPositionMaxDrawdownTimestamp(backtest, symbol, context);
15702
+ };
15703
+ /**
15704
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
15705
+ *
15706
+ * @param backtest - Whether running in backtest mode
15707
+ * @param symbol - Trading pair symbol
15708
+ * @param context - Execution context with strategyName, exchangeName, frameName
15709
+ * @returns Promise resolving to PnL percentage or null
15710
+ */
15711
+ this.getPositionMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
15712
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownPnlPercentage", {
15713
+ symbol,
15714
+ context,
15715
+ });
15716
+ await this.validate(context);
15717
+ return await this.strategyConnectionService.getPositionMaxDrawdownPnlPercentage(backtest, symbol, context);
15718
+ };
15719
+ /**
15720
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
15721
+ *
15722
+ * @param backtest - Whether running in backtest mode
15723
+ * @param symbol - Trading pair symbol
15724
+ * @param context - Execution context with strategyName, exchangeName, frameName
15725
+ * @returns Promise resolving to PnL cost or null
15726
+ */
15727
+ this.getPositionMaxDrawdownPnlCost = async (backtest, symbol, context) => {
15728
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownPnlCost", {
15729
+ symbol,
15730
+ context,
15731
+ });
15732
+ await this.validate(context);
15733
+ return await this.strategyConnectionService.getPositionMaxDrawdownPnlCost(backtest, symbol, context);
15734
+ };
15311
15735
  }
15312
15736
  }
15313
15737
 
@@ -15380,7 +15804,7 @@ class SizingGlobalService {
15380
15804
  * @param context - Context with riskName, exchangeName, frameName
15381
15805
  * @returns Unique string key for memoization
15382
15806
  */
15383
- const CREATE_KEY_FN$m = (context) => {
15807
+ const CREATE_KEY_FN$n = (context) => {
15384
15808
  const parts = [context.riskName, context.exchangeName];
15385
15809
  if (context.frameName)
15386
15810
  parts.push(context.frameName);
@@ -15406,7 +15830,7 @@ class RiskGlobalService {
15406
15830
  * @param payload - Payload with riskName, exchangeName and frameName
15407
15831
  * @returns Promise that resolves when validation is complete
15408
15832
  */
15409
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$m(context), async (context) => {
15833
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$n(context), async (context) => {
15410
15834
  this.loggerService.log("riskGlobalService validate", {
15411
15835
  context,
15412
15836
  });
@@ -15484,7 +15908,7 @@ const METHOD_NAME_VALIDATE = "actionCoreService validate";
15484
15908
  * @param context - Execution context with strategyName, exchangeName, frameName
15485
15909
  * @returns Unique string key for memoization
15486
15910
  */
15487
- const CREATE_KEY_FN$l = (context) => {
15911
+ const CREATE_KEY_FN$m = (context) => {
15488
15912
  const parts = [context.strategyName, context.exchangeName];
15489
15913
  if (context.frameName)
15490
15914
  parts.push(context.frameName);
@@ -15528,7 +15952,7 @@ class ActionCoreService {
15528
15952
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
15529
15953
  * @returns Promise that resolves when all validations complete
15530
15954
  */
15531
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$l(context), async (context) => {
15955
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$m(context), async (context) => {
15532
15956
  this.loggerService.log(METHOD_NAME_VALIDATE, {
15533
15957
  context,
15534
15958
  });
@@ -18075,6 +18499,21 @@ const backtest_columns = [
18075
18499
  format: (data) => new Date(data.closeTimestamp).toISOString(),
18076
18500
  isVisible: () => true,
18077
18501
  },
18502
+ {
18503
+ key: "peakPnl",
18504
+ label: "Peak PNL",
18505
+ format: (data) => {
18506
+ const v = data.signal._peak?.pnlPercentage;
18507
+ return `${v > 0 ? "+" : ""}${v.toFixed(2)}%`;
18508
+ },
18509
+ isVisible: () => true,
18510
+ },
18511
+ {
18512
+ key: "fallPnl",
18513
+ label: "Max DD PNL",
18514
+ format: (data) => `${data.signal._fall?.pnlPercentage.toFixed(2)}%`,
18515
+ isVisible: () => true,
18516
+ },
18078
18517
  ];
18079
18518
 
18080
18519
  /**
@@ -18185,6 +18624,18 @@ const heat_columns = [
18185
18624
  format: (data) => data.totalTrades.toString(),
18186
18625
  isVisible: () => true,
18187
18626
  },
18627
+ {
18628
+ key: "avgPeakPnl",
18629
+ label: "Avg Peak PNL",
18630
+ format: (data) => data.avgPeakPnl !== null ? functoolsKit.str(data.avgPeakPnl, "%") : "N/A",
18631
+ isVisible: () => true,
18632
+ },
18633
+ {
18634
+ key: "avgFallPnl",
18635
+ label: "Avg DD PNL",
18636
+ format: (data) => data.avgFallPnl !== null ? functoolsKit.str(data.avgFallPnl, "%") : "N/A",
18637
+ isVisible: () => true,
18638
+ },
18188
18639
  ];
18189
18640
 
18190
18641
  /**
@@ -18382,6 +18833,22 @@ const live_columns = [
18382
18833
  format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
18383
18834
  isVisible: () => true,
18384
18835
  },
18836
+ {
18837
+ key: "peakPnl",
18838
+ label: "Peak PNL",
18839
+ format: (data) => {
18840
+ if (data.peakPnl === undefined)
18841
+ return "N/A";
18842
+ return `${data.peakPnl > 0 ? "+" : ""}${data.peakPnl.toFixed(2)}%`;
18843
+ },
18844
+ isVisible: () => true,
18845
+ },
18846
+ {
18847
+ key: "fallPnl",
18848
+ label: "Max DD PNL",
18849
+ format: (data) => data.fallPnl !== undefined ? `${data.fallPnl.toFixed(2)}%` : "N/A",
18850
+ isVisible: () => true,
18851
+ },
18385
18852
  ];
18386
18853
 
18387
18854
  /**
@@ -19543,6 +20010,93 @@ const highest_profit_columns = [
19543
20010
  },
19544
20011
  ];
19545
20012
 
20013
+ /**
20014
+ * Column configuration for max drawdown markdown reports.
20015
+ *
20016
+ * Defines the table structure for displaying max-drawdown-record events.
20017
+ *
20018
+ * @see MaxDrawdownMarkdownService
20019
+ * @see ColumnModel
20020
+ * @see MaxDrawdownEvent
20021
+ */
20022
+ const max_drawdown_columns = [
20023
+ {
20024
+ key: "symbol",
20025
+ label: "Symbol",
20026
+ format: (data) => data.symbol,
20027
+ isVisible: () => true,
20028
+ },
20029
+ {
20030
+ key: "strategyName",
20031
+ label: "Strategy",
20032
+ format: (data) => data.strategyName,
20033
+ isVisible: () => true,
20034
+ },
20035
+ {
20036
+ key: "signalId",
20037
+ label: "Signal ID",
20038
+ format: (data) => data.signalId,
20039
+ isVisible: () => true,
20040
+ },
20041
+ {
20042
+ key: "position",
20043
+ label: "Position",
20044
+ format: (data) => data.position.toUpperCase(),
20045
+ isVisible: () => true,
20046
+ },
20047
+ {
20048
+ key: "pnl",
20049
+ label: "PNL (net)",
20050
+ format: (data) => {
20051
+ const pnlPercentage = data.pnl.pnlPercentage;
20052
+ return `${pnlPercentage > 0 ? "+" : ""}${pnlPercentage.toFixed(2)}%`;
20053
+ },
20054
+ isVisible: () => true,
20055
+ },
20056
+ {
20057
+ key: "pnlCost",
20058
+ label: "PNL (USD)",
20059
+ format: (data) => `${data.pnl.pnlCost > 0 ? "+" : ""}${data.pnl.pnlCost.toFixed(2)} USD`,
20060
+ isVisible: () => true,
20061
+ },
20062
+ {
20063
+ key: "currentPrice",
20064
+ label: "DD Price",
20065
+ format: (data) => `${data.currentPrice.toFixed(8)} USD`,
20066
+ isVisible: () => true,
20067
+ },
20068
+ {
20069
+ key: "priceOpen",
20070
+ label: "Entry Price",
20071
+ format: (data) => `${data.priceOpen.toFixed(8)} USD`,
20072
+ isVisible: () => true,
20073
+ },
20074
+ {
20075
+ key: "priceTakeProfit",
20076
+ label: "Take Profit",
20077
+ format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
20078
+ isVisible: () => true,
20079
+ },
20080
+ {
20081
+ key: "priceStopLoss",
20082
+ label: "Stop Loss",
20083
+ format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
20084
+ isVisible: () => true,
20085
+ },
20086
+ {
20087
+ key: "timestamp",
20088
+ label: "Timestamp",
20089
+ format: (data) => new Date(data.timestamp).toISOString(),
20090
+ isVisible: () => true,
20091
+ },
20092
+ {
20093
+ key: "mode",
20094
+ label: "Mode",
20095
+ format: (data) => (data.backtest ? "Backtest" : "Live"),
20096
+ isVisible: () => true,
20097
+ },
20098
+ ];
20099
+
19546
20100
  /**
19547
20101
  * Column configuration for walker strategy comparison table in markdown reports.
19548
20102
  *
@@ -19778,6 +20332,8 @@ const COLUMN_CONFIG = {
19778
20332
  sync_columns,
19779
20333
  /** Columns for highest profit milestone tracking events */
19780
20334
  highest_profit_columns,
20335
+ /** Columns for max drawdown milestone tracking events */
20336
+ max_drawdown_columns,
19781
20337
  /** Walker: PnL summary columns */
19782
20338
  walker_pnl_columns,
19783
20339
  /** Walker: strategy-level summary columns */
@@ -19822,6 +20378,7 @@ const WILDCARD_TARGET$1 = {
19822
20378
  walker: true,
19823
20379
  sync: true,
19824
20380
  highest_profit: true,
20381
+ max_drawdown: true,
19825
20382
  };
19826
20383
  /**
19827
20384
  * JSONL-based markdown adapter with append-only writes.
@@ -20049,7 +20606,7 @@ class MarkdownUtils {
20049
20606
  *
20050
20607
  * @returns Cleanup function that unsubscribes from all enabled services
20051
20608
  */
20052
- this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, strategy = false, risk = false, schedule = false, walker = false, sync = false, highest_profit = false, } = WILDCARD_TARGET$1) => {
20609
+ this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, strategy = false, risk = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
20053
20610
  bt.loggerService.debug(MARKDOWN_METHOD_NAME_ENABLE, {
20054
20611
  backtest: bt$1,
20055
20612
  breakeven,
@@ -20101,6 +20658,9 @@ class MarkdownUtils {
20101
20658
  if (highest_profit) {
20102
20659
  unList.push(bt.highestProfitMarkdownService.subscribe());
20103
20660
  }
20661
+ if (max_drawdown) {
20662
+ unList.push(bt.maxDrawdownMarkdownService.subscribe());
20663
+ }
20104
20664
  return functoolsKit.compose(...unList.map((un) => () => void un()));
20105
20665
  };
20106
20666
  /**
@@ -20140,7 +20700,7 @@ class MarkdownUtils {
20140
20700
  * Markdown.disable();
20141
20701
  * ```
20142
20702
  */
20143
- this.disable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, strategy = false, schedule = false, walker = false, sync = false, highest_profit = false, } = WILDCARD_TARGET$1) => {
20703
+ this.disable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, strategy = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
20144
20704
  bt.loggerService.debug(MARKDOWN_METHOD_NAME_DISABLE, {
20145
20705
  backtest: bt$1,
20146
20706
  breakeven,
@@ -20191,6 +20751,9 @@ class MarkdownUtils {
20191
20751
  if (highest_profit) {
20192
20752
  bt.highestProfitMarkdownService.unsubscribe();
20193
20753
  }
20754
+ if (max_drawdown) {
20755
+ bt.maxDrawdownMarkdownService.unsubscribe();
20756
+ }
20194
20757
  };
20195
20758
  }
20196
20759
  }
@@ -20306,7 +20869,7 @@ const Markdown = new MarkdownAdapter();
20306
20869
  * @param backtest - Whether running in backtest mode
20307
20870
  * @returns Unique string key for memoization
20308
20871
  */
20309
- const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest) => {
20872
+ const CREATE_KEY_FN$l = (symbol, strategyName, exchangeName, frameName, backtest) => {
20310
20873
  const parts = [symbol, strategyName, exchangeName];
20311
20874
  if (frameName)
20312
20875
  parts.push(frameName);
@@ -20323,7 +20886,7 @@ const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest
20323
20886
  * @param timestamp - Unix timestamp in milliseconds
20324
20887
  * @returns Filename string
20325
20888
  */
20326
- const CREATE_FILE_NAME_FN$b = (symbol, strategyName, exchangeName, frameName, timestamp) => {
20889
+ const CREATE_FILE_NAME_FN$c = (symbol, strategyName, exchangeName, frameName, timestamp) => {
20327
20890
  const parts = [symbol, strategyName, exchangeName];
20328
20891
  if (frameName) {
20329
20892
  parts.push(frameName);
@@ -20355,7 +20918,7 @@ function isUnsafe$3(value) {
20355
20918
  * Storage class for accumulating closed signals per strategy.
20356
20919
  * Maintains a list of all closed signals and provides methods to generate reports.
20357
20920
  */
20358
- let ReportStorage$9 = class ReportStorage {
20921
+ let ReportStorage$a = class ReportStorage {
20359
20922
  constructor(symbol, strategyName, exchangeName, frameName) {
20360
20923
  this.symbol = symbol;
20361
20924
  this.strategyName = strategyName;
@@ -20397,6 +20960,8 @@ let ReportStorage$9 = class ReportStorage {
20397
20960
  annualizedSharpeRatio: null,
20398
20961
  certaintyRatio: null,
20399
20962
  expectedYearlyReturns: null,
20963
+ avgPeakPnl: null,
20964
+ avgFallPnl: null,
20400
20965
  };
20401
20966
  }
20402
20967
  const totalSignals = this._signalList.length;
@@ -20427,6 +20992,9 @@ let ReportStorage$9 = class ReportStorage {
20427
20992
  const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
20428
20993
  const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
20429
20994
  const expectedYearlyReturns = avgPnl * tradesPerYear;
20995
+ // Calculate average peak and fall PNL across all signals
20996
+ const avgPeakPnl = this._signalList.reduce((sum, s) => sum + (s.signal._peak?.pnlPercentage ?? 0), 0) / totalSignals;
20997
+ const avgFallPnl = this._signalList.reduce((sum, s) => sum + (s.signal._fall?.pnlPercentage ?? 0), 0) / totalSignals;
20430
20998
  return {
20431
20999
  signalList: this._signalList,
20432
21000
  totalSignals,
@@ -20440,6 +21008,8 @@ let ReportStorage$9 = class ReportStorage {
20440
21008
  annualizedSharpeRatio: isUnsafe$3(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
20441
21009
  certaintyRatio: isUnsafe$3(certaintyRatio) ? null : certaintyRatio,
20442
21010
  expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
21011
+ avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
21012
+ avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
20443
21013
  };
20444
21014
  }
20445
21015
  /**
@@ -20484,6 +21054,8 @@ let ReportStorage$9 = class ReportStorage {
20484
21054
  `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
20485
21055
  `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
20486
21056
  `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
21057
+ `**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
21058
+ `**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
20487
21059
  ].join("\n");
20488
21060
  }
20489
21061
  /**
@@ -20496,7 +21068,7 @@ let ReportStorage$9 = class ReportStorage {
20496
21068
  async dump(strategyName, path = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
20497
21069
  const markdown = await this.getReport(strategyName, columns);
20498
21070
  const timestamp = getContextTimestamp();
20499
- const filename = CREATE_FILE_NAME_FN$b(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21071
+ const filename = CREATE_FILE_NAME_FN$c(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
20500
21072
  await Markdown.writeData("backtest", markdown, {
20501
21073
  path,
20502
21074
  file: filename,
@@ -20543,7 +21115,7 @@ class BacktestMarkdownService {
20543
21115
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
20544
21116
  * Each combination gets its own isolated storage instance.
20545
21117
  */
20546
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$k(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$9(symbol, strategyName, exchangeName, frameName));
21118
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$l(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$a(symbol, strategyName, exchangeName, frameName));
20547
21119
  /**
20548
21120
  * Processes tick events and accumulates closed signals.
20549
21121
  * Should be called from IStrategyCallbacks.onTick.
@@ -20700,7 +21272,7 @@ class BacktestMarkdownService {
20700
21272
  payload,
20701
21273
  });
20702
21274
  if (payload) {
20703
- const key = CREATE_KEY_FN$k(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21275
+ const key = CREATE_KEY_FN$l(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20704
21276
  this.getStorage.clear(key);
20705
21277
  }
20706
21278
  else {
@@ -20762,7 +21334,7 @@ class BacktestMarkdownService {
20762
21334
  * @param backtest - Whether running in backtest mode
20763
21335
  * @returns Unique string key for memoization
20764
21336
  */
20765
- const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest) => {
21337
+ const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest) => {
20766
21338
  const parts = [symbol, strategyName, exchangeName];
20767
21339
  if (frameName)
20768
21340
  parts.push(frameName);
@@ -20779,7 +21351,7 @@ const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest
20779
21351
  * @param timestamp - Unix timestamp in milliseconds
20780
21352
  * @returns Filename string
20781
21353
  */
20782
- const CREATE_FILE_NAME_FN$a = (symbol, strategyName, exchangeName, frameName, timestamp) => {
21354
+ const CREATE_FILE_NAME_FN$b = (symbol, strategyName, exchangeName, frameName, timestamp) => {
20783
21355
  const parts = [symbol, strategyName, exchangeName];
20784
21356
  if (frameName) {
20785
21357
  parts.push(frameName);
@@ -20811,7 +21383,7 @@ function isUnsafe$2(value) {
20811
21383
  * Storage class for accumulating all tick events per strategy.
20812
21384
  * Maintains a chronological list of all events (idle, opened, active, closed).
20813
21385
  */
20814
- let ReportStorage$8 = class ReportStorage {
21386
+ let ReportStorage$9 = class ReportStorage {
20815
21387
  constructor(symbol, strategyName, exchangeName, frameName) {
20816
21388
  this.symbol = symbol;
20817
21389
  this.strategyName = strategyName;
@@ -20951,6 +21523,8 @@ let ReportStorage$8 = class ReportStorage {
20951
21523
  duration: durationMin,
20952
21524
  pendingAt: data.signal.pendingAt,
20953
21525
  scheduledAt: data.signal.scheduledAt,
21526
+ peakPnl: data.signal._peak?.pnlPercentage,
21527
+ fallPnl: data.signal._fall?.pnlPercentage,
20954
21528
  };
20955
21529
  this._eventList.unshift(newEvent);
20956
21530
  // Trim queue if exceeded GLOBAL_CONFIG.CC_MAX_LIVE_MARKDOWN_ROWS
@@ -21080,6 +21654,8 @@ let ReportStorage$8 = class ReportStorage {
21080
21654
  annualizedSharpeRatio: null,
21081
21655
  certaintyRatio: null,
21082
21656
  expectedYearlyReturns: null,
21657
+ avgPeakPnl: null,
21658
+ avgFallPnl: null,
21083
21659
  };
21084
21660
  }
21085
21661
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
@@ -21123,6 +21699,12 @@ let ReportStorage$8 = class ReportStorage {
21123
21699
  const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
21124
21700
  expectedYearlyReturns = avgPnl * tradesPerYear;
21125
21701
  }
21702
+ const avgPeakPnl = totalClosed > 0
21703
+ ? closedEvents.reduce((sum, e) => sum + (e.peakPnl || 0), 0) / totalClosed
21704
+ : 0;
21705
+ const avgFallPnl = totalClosed > 0
21706
+ ? closedEvents.reduce((sum, e) => sum + (e.fallPnl || 0), 0) / totalClosed
21707
+ : 0;
21126
21708
  return {
21127
21709
  eventList: this._eventList,
21128
21710
  totalEvents: this._eventList.length,
@@ -21137,6 +21719,8 @@ let ReportStorage$8 = class ReportStorage {
21137
21719
  annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
21138
21720
  certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
21139
21721
  expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
21722
+ avgPeakPnl: isUnsafe$2(avgPeakPnl) ? null : avgPeakPnl,
21723
+ avgFallPnl: isUnsafe$2(avgFallPnl) ? null : avgFallPnl,
21140
21724
  };
21141
21725
  }
21142
21726
  /**
@@ -21181,6 +21765,8 @@ let ReportStorage$8 = class ReportStorage {
21181
21765
  `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
21182
21766
  `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
21183
21767
  `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
21768
+ `**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
21769
+ `**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
21184
21770
  ].join("\n");
21185
21771
  }
21186
21772
  /**
@@ -21193,7 +21779,7 @@ let ReportStorage$8 = class ReportStorage {
21193
21779
  async dump(strategyName, path = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
21194
21780
  const markdown = await this.getReport(strategyName, columns);
21195
21781
  const timestamp = getContextTimestamp();
21196
- const filename = CREATE_FILE_NAME_FN$a(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21782
+ const filename = CREATE_FILE_NAME_FN$b(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21197
21783
  await Markdown.writeData("live", markdown, {
21198
21784
  path,
21199
21785
  signalId: "",
@@ -21243,7 +21829,7 @@ class LiveMarkdownService {
21243
21829
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
21244
21830
  * Each combination gets its own isolated storage instance.
21245
21831
  */
21246
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$j(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$8(symbol, strategyName, exchangeName, frameName));
21832
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$k(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$9(symbol, strategyName, exchangeName, frameName));
21247
21833
  /**
21248
21834
  * Subscribes to live signal emitter to receive tick events.
21249
21835
  * Protected against multiple subscriptions.
@@ -21461,7 +22047,7 @@ class LiveMarkdownService {
21461
22047
  payload,
21462
22048
  });
21463
22049
  if (payload) {
21464
- const key = CREATE_KEY_FN$j(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22050
+ const key = CREATE_KEY_FN$k(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21465
22051
  this.getStorage.clear(key);
21466
22052
  }
21467
22053
  else {
@@ -21481,7 +22067,7 @@ class LiveMarkdownService {
21481
22067
  * @param backtest - Whether running in backtest mode
21482
22068
  * @returns Unique string key for memoization
21483
22069
  */
21484
- const CREATE_KEY_FN$i = (symbol, strategyName, exchangeName, frameName, backtest) => {
22070
+ const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest) => {
21485
22071
  const parts = [symbol, strategyName, exchangeName];
21486
22072
  if (frameName)
21487
22073
  parts.push(frameName);
@@ -21498,7 +22084,7 @@ const CREATE_KEY_FN$i = (symbol, strategyName, exchangeName, frameName, backtest
21498
22084
  * @param timestamp - Unix timestamp in milliseconds
21499
22085
  * @returns Filename string
21500
22086
  */
21501
- const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
22087
+ const CREATE_FILE_NAME_FN$a = (symbol, strategyName, exchangeName, frameName, timestamp) => {
21502
22088
  const parts = [symbol, strategyName, exchangeName];
21503
22089
  if (frameName) {
21504
22090
  parts.push(frameName);
@@ -21512,7 +22098,7 @@ const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, ti
21512
22098
  * Storage class for accumulating scheduled signal events per strategy.
21513
22099
  * Maintains a chronological list of scheduled and cancelled events.
21514
22100
  */
21515
- let ReportStorage$7 = class ReportStorage {
22101
+ let ReportStorage$8 = class ReportStorage {
21516
22102
  constructor(symbol, strategyName, exchangeName, frameName) {
21517
22103
  this.symbol = symbol;
21518
22104
  this.strategyName = strategyName;
@@ -21729,7 +22315,7 @@ let ReportStorage$7 = class ReportStorage {
21729
22315
  async dump(strategyName, path = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
21730
22316
  const markdown = await this.getReport(strategyName, columns);
21731
22317
  const timestamp = getContextTimestamp();
21732
- const filename = CREATE_FILE_NAME_FN$9(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22318
+ const filename = CREATE_FILE_NAME_FN$a(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21733
22319
  await Markdown.writeData("schedule", markdown, {
21734
22320
  path,
21735
22321
  file: filename,
@@ -21770,7 +22356,7 @@ class ScheduleMarkdownService {
21770
22356
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
21771
22357
  * Each combination gets its own isolated storage instance.
21772
22358
  */
21773
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$i(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$7(symbol, strategyName, exchangeName, frameName));
22359
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$j(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$8(symbol, strategyName, exchangeName, frameName));
21774
22360
  /**
21775
22361
  * Subscribes to signal emitter to receive scheduled signal events.
21776
22362
  * Protected against multiple subscriptions.
@@ -21973,7 +22559,7 @@ class ScheduleMarkdownService {
21973
22559
  payload,
21974
22560
  });
21975
22561
  if (payload) {
21976
- const key = CREATE_KEY_FN$i(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22562
+ const key = CREATE_KEY_FN$j(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21977
22563
  this.getStorage.clear(key);
21978
22564
  }
21979
22565
  else {
@@ -21993,7 +22579,7 @@ class ScheduleMarkdownService {
21993
22579
  * @param backtest - Whether running in backtest mode
21994
22580
  * @returns Unique string key for memoization
21995
22581
  */
21996
- const CREATE_KEY_FN$h = (symbol, strategyName, exchangeName, frameName, backtest) => {
22582
+ const CREATE_KEY_FN$i = (symbol, strategyName, exchangeName, frameName, backtest) => {
21997
22583
  const parts = [symbol, strategyName, exchangeName];
21998
22584
  if (frameName)
21999
22585
  parts.push(frameName);
@@ -22010,7 +22596,7 @@ const CREATE_KEY_FN$h = (symbol, strategyName, exchangeName, frameName, backtest
22010
22596
  * @param timestamp - Unix timestamp in milliseconds
22011
22597
  * @returns Filename string
22012
22598
  */
22013
- const CREATE_FILE_NAME_FN$8 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
22599
+ const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
22014
22600
  const parts = [symbol, strategyName, exchangeName];
22015
22601
  if (frameName) {
22016
22602
  parts.push(frameName);
@@ -22191,7 +22777,7 @@ class PerformanceStorage {
22191
22777
  async dump(strategyName, path = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
22192
22778
  const markdown = await this.getReport(strategyName, columns);
22193
22779
  const timestamp = getContextTimestamp();
22194
- const filename = CREATE_FILE_NAME_FN$8(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22780
+ const filename = CREATE_FILE_NAME_FN$9(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22195
22781
  await Markdown.writeData("performance", markdown, {
22196
22782
  path,
22197
22783
  file: filename,
@@ -22238,7 +22824,7 @@ class PerformanceMarkdownService {
22238
22824
  * Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
22239
22825
  * Each combination gets its own isolated storage instance.
22240
22826
  */
22241
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$h(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
22827
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$i(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
22242
22828
  /**
22243
22829
  * Subscribes to performance emitter to receive performance events.
22244
22830
  * Protected against multiple subscriptions.
@@ -22405,7 +22991,7 @@ class PerformanceMarkdownService {
22405
22991
  payload,
22406
22992
  });
22407
22993
  if (payload) {
22408
- const key = CREATE_KEY_FN$h(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22994
+ const key = CREATE_KEY_FN$i(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22409
22995
  this.getStorage.clear(key);
22410
22996
  }
22411
22997
  else {
@@ -22419,7 +23005,7 @@ class PerformanceMarkdownService {
22419
23005
  * Creates a filename for markdown report based on walker name.
22420
23006
  * Filename format: "walkerName-timestamp.md"
22421
23007
  */
22422
- const CREATE_FILE_NAME_FN$7 = (walkerName, timestamp) => {
23008
+ const CREATE_FILE_NAME_FN$8 = (walkerName, timestamp) => {
22423
23009
  return `${walkerName}-${timestamp}.md`;
22424
23010
  };
22425
23011
  /**
@@ -22454,7 +23040,7 @@ function formatMetric(value) {
22454
23040
  * Storage class for accumulating walker results.
22455
23041
  * Maintains a list of all strategy results and provides methods to generate reports.
22456
23042
  */
22457
- let ReportStorage$6 = class ReportStorage {
23043
+ let ReportStorage$7 = class ReportStorage {
22458
23044
  constructor(walkerName) {
22459
23045
  this.walkerName = walkerName;
22460
23046
  /** Walker metadata (set from first addResult call) */
@@ -22651,7 +23237,7 @@ let ReportStorage$6 = class ReportStorage {
22651
23237
  async dump(symbol, metric, context, path = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
22652
23238
  const markdown = await this.getReport(symbol, metric, context, strategyColumns, pnlColumns);
22653
23239
  const timestamp = getContextTimestamp();
22654
- const filename = CREATE_FILE_NAME_FN$7(this.walkerName, timestamp);
23240
+ const filename = CREATE_FILE_NAME_FN$8(this.walkerName, timestamp);
22655
23241
  await Markdown.writeData("walker", markdown, {
22656
23242
  path,
22657
23243
  file: filename,
@@ -22687,7 +23273,7 @@ class WalkerMarkdownService {
22687
23273
  * Memoized function to get or create ReportStorage for a walker.
22688
23274
  * Each walker gets its own isolated storage instance.
22689
23275
  */
22690
- this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$6(walkerName));
23276
+ this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$7(walkerName));
22691
23277
  /**
22692
23278
  * Subscribes to walker emitter to receive walker progress events.
22693
23279
  * Protected against multiple subscriptions.
@@ -22884,7 +23470,7 @@ class WalkerMarkdownService {
22884
23470
  * @param backtest - Whether running in backtest mode
22885
23471
  * @returns Unique string key for memoization
22886
23472
  */
22887
- const CREATE_KEY_FN$g = (exchangeName, frameName, backtest) => {
23473
+ const CREATE_KEY_FN$h = (exchangeName, frameName, backtest) => {
22888
23474
  const parts = [exchangeName];
22889
23475
  if (frameName)
22890
23476
  parts.push(frameName);
@@ -22895,7 +23481,7 @@ const CREATE_KEY_FN$g = (exchangeName, frameName, backtest) => {
22895
23481
  * Creates a filename for markdown report based on memoization key components.
22896
23482
  * Filename format: "strategyName_exchangeName_frameName-timestamp.md"
22897
23483
  */
22898
- const CREATE_FILE_NAME_FN$6 = (strategyName, exchangeName, frameName, timestamp) => {
23484
+ const CREATE_FILE_NAME_FN$7 = (strategyName, exchangeName, frameName, timestamp) => {
22899
23485
  const parts = [strategyName, exchangeName];
22900
23486
  if (frameName) {
22901
23487
  parts.push(frameName);
@@ -23085,6 +23671,13 @@ class HeatmapStorage {
23085
23671
  const lossRate = 100 - winRate;
23086
23672
  expectancy = (winRate / 100) * avgWin + (lossRate / 100) * avgLoss;
23087
23673
  }
23674
+ // Calculate average peak and fall PNL
23675
+ let avgPeakPnl = null;
23676
+ let avgFallPnl = null;
23677
+ if (signals.length > 0) {
23678
+ avgPeakPnl = signals.reduce((acc, s) => acc + (s.signal._peak?.pnlPercentage ?? 0), 0) / signals.length;
23679
+ avgFallPnl = signals.reduce((acc, s) => acc + (s.signal._fall?.pnlPercentage ?? 0), 0) / signals.length;
23680
+ }
23088
23681
  // Apply safe math checks
23089
23682
  if (isUnsafe(winRate))
23090
23683
  winRate = null;
@@ -23106,6 +23699,10 @@ class HeatmapStorage {
23106
23699
  avgLoss = null;
23107
23700
  if (isUnsafe(expectancy))
23108
23701
  expectancy = null;
23702
+ if (isUnsafe(avgPeakPnl))
23703
+ avgPeakPnl = null;
23704
+ if (isUnsafe(avgFallPnl))
23705
+ avgFallPnl = null;
23109
23706
  return {
23110
23707
  symbol,
23111
23708
  totalPnl,
@@ -23123,6 +23720,8 @@ class HeatmapStorage {
23123
23720
  maxWinStreak,
23124
23721
  maxLossStreak,
23125
23722
  expectancy,
23723
+ avgPeakPnl,
23724
+ avgFallPnl,
23126
23725
  };
23127
23726
  }
23128
23727
  /**
@@ -23172,17 +23771,34 @@ class HeatmapStorage {
23172
23771
  const weightedSum = validSharpes.reduce((acc, s) => acc + s.sharpeRatio * s.totalTrades, 0);
23173
23772
  portfolioSharpeRatio = weightedSum / portfolioTotalTrades;
23174
23773
  }
23774
+ // Calculate portfolio-wide weighted average peak/fall PNL
23775
+ let portfolioAvgPeakPnl = null;
23776
+ let portfolioAvgFallPnl = null;
23777
+ const validPeak = symbols.filter((s) => s.avgPeakPnl !== null);
23778
+ const validFall = symbols.filter((s) => s.avgFallPnl !== null);
23779
+ if (validPeak.length > 0 && portfolioTotalTrades > 0) {
23780
+ portfolioAvgPeakPnl = validPeak.reduce((acc, s) => acc + s.avgPeakPnl * s.totalTrades, 0) / portfolioTotalTrades;
23781
+ }
23782
+ if (validFall.length > 0 && portfolioTotalTrades > 0) {
23783
+ portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) / portfolioTotalTrades;
23784
+ }
23175
23785
  // Apply safe math
23176
23786
  if (isUnsafe(portfolioTotalPnl))
23177
23787
  portfolioTotalPnl = null;
23178
23788
  if (isUnsafe(portfolioSharpeRatio))
23179
23789
  portfolioSharpeRatio = null;
23790
+ if (isUnsafe(portfolioAvgPeakPnl))
23791
+ portfolioAvgPeakPnl = null;
23792
+ if (isUnsafe(portfolioAvgFallPnl))
23793
+ portfolioAvgFallPnl = null;
23180
23794
  return {
23181
23795
  symbols,
23182
23796
  totalSymbols,
23183
23797
  portfolioTotalPnl,
23184
23798
  portfolioSharpeRatio,
23185
23799
  portfolioTotalTrades,
23800
+ portfolioAvgPeakPnl,
23801
+ portfolioAvgFallPnl,
23186
23802
  };
23187
23803
  }
23188
23804
  /**
@@ -23231,7 +23847,7 @@ class HeatmapStorage {
23231
23847
  return [
23232
23848
  `# Portfolio Heatmap: ${strategyName}`,
23233
23849
  "",
23234
- `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
23850
+ `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? functoolsKit.str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? functoolsKit.str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
23235
23851
  "",
23236
23852
  table
23237
23853
  ].join("\n");
@@ -23255,7 +23871,7 @@ class HeatmapStorage {
23255
23871
  async dump(strategyName, path = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
23256
23872
  const markdown = await this.getReport(strategyName, columns);
23257
23873
  const timestamp = getContextTimestamp();
23258
- const filename = CREATE_FILE_NAME_FN$6(strategyName, this.exchangeName, this.frameName, timestamp);
23874
+ const filename = CREATE_FILE_NAME_FN$7(strategyName, this.exchangeName, this.frameName, timestamp);
23259
23875
  await Markdown.writeData("heat", markdown, {
23260
23876
  path,
23261
23877
  file: filename,
@@ -23301,7 +23917,7 @@ class HeatMarkdownService {
23301
23917
  * Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
23302
23918
  * Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
23303
23919
  */
23304
- this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$g(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
23920
+ this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$h(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
23305
23921
  /**
23306
23922
  * Subscribes to signal emitter to receive tick events.
23307
23923
  * Protected against multiple subscriptions.
@@ -23519,7 +24135,7 @@ class HeatMarkdownService {
23519
24135
  payload,
23520
24136
  });
23521
24137
  if (payload) {
23522
- const key = CREATE_KEY_FN$g(payload.exchangeName, payload.frameName, payload.backtest);
24138
+ const key = CREATE_KEY_FN$h(payload.exchangeName, payload.frameName, payload.backtest);
23523
24139
  this.getStorage.clear(key);
23524
24140
  }
23525
24141
  else {
@@ -24550,7 +25166,7 @@ class ClientPartial {
24550
25166
  * @param backtest - Whether running in backtest mode
24551
25167
  * @returns Unique string key for memoization
24552
25168
  */
24553
- const CREATE_KEY_FN$f = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
25169
+ const CREATE_KEY_FN$g = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
24554
25170
  /**
24555
25171
  * Creates a callback function for emitting profit events to partialProfitSubject.
24556
25172
  *
@@ -24672,7 +25288,7 @@ class PartialConnectionService {
24672
25288
  * Key format: "signalId:backtest" or "signalId:live"
24673
25289
  * Value: ClientPartial instance with logger and event emitters
24674
25290
  */
24675
- this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$f(signalId, backtest), (signalId, backtest) => {
25291
+ this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$g(signalId, backtest), (signalId, backtest) => {
24676
25292
  return new ClientPartial({
24677
25293
  signalId,
24678
25294
  logger: this.loggerService,
@@ -24762,7 +25378,7 @@ class PartialConnectionService {
24762
25378
  const partial = this.getPartial(data.id, backtest);
24763
25379
  await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
24764
25380
  await partial.clear(symbol, data, priceClose, backtest);
24765
- const key = CREATE_KEY_FN$f(data.id, backtest);
25381
+ const key = CREATE_KEY_FN$g(data.id, backtest);
24766
25382
  this.getPartial.clear(key);
24767
25383
  };
24768
25384
  }
@@ -24778,7 +25394,7 @@ class PartialConnectionService {
24778
25394
  * @param backtest - Whether running in backtest mode
24779
25395
  * @returns Unique string key for memoization
24780
25396
  */
24781
- const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest) => {
25397
+ const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
24782
25398
  const parts = [symbol, strategyName, exchangeName];
24783
25399
  if (frameName)
24784
25400
  parts.push(frameName);
@@ -24789,7 +25405,7 @@ const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest
24789
25405
  * Creates a filename for markdown report based on memoization key components.
24790
25406
  * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
24791
25407
  */
24792
- const CREATE_FILE_NAME_FN$5 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
25408
+ const CREATE_FILE_NAME_FN$6 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
24793
25409
  const parts = [symbol, strategyName, exchangeName];
24794
25410
  if (frameName) {
24795
25411
  parts.push(frameName);
@@ -24803,7 +25419,7 @@ const CREATE_FILE_NAME_FN$5 = (symbol, strategyName, exchangeName, frameName, ti
24803
25419
  * Storage class for accumulating partial profit/loss events per symbol-strategy pair.
24804
25420
  * Maintains a chronological list of profit and loss level events.
24805
25421
  */
24806
- let ReportStorage$5 = class ReportStorage {
25422
+ let ReportStorage$6 = class ReportStorage {
24807
25423
  constructor(symbol, strategyName, exchangeName, frameName) {
24808
25424
  this.symbol = symbol;
24809
25425
  this.strategyName = strategyName;
@@ -24960,7 +25576,7 @@ let ReportStorage$5 = class ReportStorage {
24960
25576
  async dump(symbol, strategyName, path = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
24961
25577
  const markdown = await this.getReport(symbol, strategyName, columns);
24962
25578
  const timestamp = getContextTimestamp();
24963
- const filename = CREATE_FILE_NAME_FN$5(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
25579
+ const filename = CREATE_FILE_NAME_FN$6(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
24964
25580
  await Markdown.writeData("partial", markdown, {
24965
25581
  path,
24966
25582
  file: filename,
@@ -25001,7 +25617,7 @@ class PartialMarkdownService {
25001
25617
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
25002
25618
  * Each combination gets its own isolated storage instance.
25003
25619
  */
25004
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$e(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$5(symbol, strategyName, exchangeName, frameName));
25620
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$f(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
25005
25621
  /**
25006
25622
  * Subscribes to partial profit/loss signal emitters to receive events.
25007
25623
  * Protected against multiple subscriptions.
@@ -25211,7 +25827,7 @@ class PartialMarkdownService {
25211
25827
  payload,
25212
25828
  });
25213
25829
  if (payload) {
25214
- const key = CREATE_KEY_FN$e(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25830
+ const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25215
25831
  this.getStorage.clear(key);
25216
25832
  }
25217
25833
  else {
@@ -25227,7 +25843,7 @@ class PartialMarkdownService {
25227
25843
  * @param context - Context with strategyName, exchangeName, frameName
25228
25844
  * @returns Unique string key for memoization
25229
25845
  */
25230
- const CREATE_KEY_FN$d = (context) => {
25846
+ const CREATE_KEY_FN$e = (context) => {
25231
25847
  const parts = [context.strategyName, context.exchangeName];
25232
25848
  if (context.frameName)
25233
25849
  parts.push(context.frameName);
@@ -25301,7 +25917,7 @@ class PartialGlobalService {
25301
25917
  * @param context - Context with strategyName, exchangeName and frameName
25302
25918
  * @param methodName - Name of the calling method for error tracking
25303
25919
  */
25304
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$d(context), (context, methodName) => {
25920
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$e(context), (context, methodName) => {
25305
25921
  this.loggerService.log("partialGlobalService validate", {
25306
25922
  context,
25307
25923
  methodName,
@@ -25756,7 +26372,7 @@ class ClientBreakeven {
25756
26372
  * @param backtest - Whether running in backtest mode
25757
26373
  * @returns Unique string key for memoization
25758
26374
  */
25759
- const CREATE_KEY_FN$c = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
26375
+ const CREATE_KEY_FN$d = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
25760
26376
  /**
25761
26377
  * Creates a callback function for emitting breakeven events to breakevenSubject.
25762
26378
  *
@@ -25842,7 +26458,7 @@ class BreakevenConnectionService {
25842
26458
  * Key format: "signalId:backtest" or "signalId:live"
25843
26459
  * Value: ClientBreakeven instance with logger and event emitter
25844
26460
  */
25845
- this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$c(signalId, backtest), (signalId, backtest) => {
26461
+ this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$d(signalId, backtest), (signalId, backtest) => {
25846
26462
  return new ClientBreakeven({
25847
26463
  signalId,
25848
26464
  logger: this.loggerService,
@@ -25903,7 +26519,7 @@ class BreakevenConnectionService {
25903
26519
  const breakeven = this.getBreakeven(data.id, backtest);
25904
26520
  await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
25905
26521
  await breakeven.clear(symbol, data, priceClose, backtest);
25906
- const key = CREATE_KEY_FN$c(data.id, backtest);
26522
+ const key = CREATE_KEY_FN$d(data.id, backtest);
25907
26523
  this.getBreakeven.clear(key);
25908
26524
  };
25909
26525
  }
@@ -25919,7 +26535,7 @@ class BreakevenConnectionService {
25919
26535
  * @param backtest - Whether running in backtest mode
25920
26536
  * @returns Unique string key for memoization
25921
26537
  */
25922
- const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
26538
+ const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest) => {
25923
26539
  const parts = [symbol, strategyName, exchangeName];
25924
26540
  if (frameName)
25925
26541
  parts.push(frameName);
@@ -25930,7 +26546,7 @@ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest
25930
26546
  * Creates a filename for markdown report based on memoization key components.
25931
26547
  * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
25932
26548
  */
25933
- const CREATE_FILE_NAME_FN$4 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
26549
+ const CREATE_FILE_NAME_FN$5 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
25934
26550
  const parts = [symbol, strategyName, exchangeName];
25935
26551
  if (frameName) {
25936
26552
  parts.push(frameName);
@@ -25944,7 +26560,7 @@ const CREATE_FILE_NAME_FN$4 = (symbol, strategyName, exchangeName, frameName, ti
25944
26560
  * Storage class for accumulating breakeven events per symbol-strategy pair.
25945
26561
  * Maintains a chronological list of breakeven events.
25946
26562
  */
25947
- let ReportStorage$4 = class ReportStorage {
26563
+ let ReportStorage$5 = class ReportStorage {
25948
26564
  constructor(symbol, strategyName, exchangeName, frameName) {
25949
26565
  this.symbol = symbol;
25950
26566
  this.strategyName = strategyName;
@@ -26053,7 +26669,7 @@ let ReportStorage$4 = class ReportStorage {
26053
26669
  async dump(symbol, strategyName, path = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
26054
26670
  const markdown = await this.getReport(symbol, strategyName, columns);
26055
26671
  const timestamp = getContextTimestamp();
26056
- const filename = CREATE_FILE_NAME_FN$4(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
26672
+ const filename = CREATE_FILE_NAME_FN$5(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
26057
26673
  await Markdown.writeData("breakeven", markdown, {
26058
26674
  path,
26059
26675
  file: filename,
@@ -26094,7 +26710,7 @@ class BreakevenMarkdownService {
26094
26710
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
26095
26711
  * Each combination gets its own isolated storage instance.
26096
26712
  */
26097
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$4(symbol, strategyName, exchangeName, frameName));
26713
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$c(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$5(symbol, strategyName, exchangeName, frameName));
26098
26714
  /**
26099
26715
  * Subscribes to breakeven signal emitter to receive events.
26100
26716
  * Protected against multiple subscriptions.
@@ -26283,7 +26899,7 @@ class BreakevenMarkdownService {
26283
26899
  payload,
26284
26900
  });
26285
26901
  if (payload) {
26286
- const key = CREATE_KEY_FN$b(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26902
+ const key = CREATE_KEY_FN$c(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26287
26903
  this.getStorage.clear(key);
26288
26904
  }
26289
26905
  else {
@@ -26299,7 +26915,7 @@ class BreakevenMarkdownService {
26299
26915
  * @param context - Context with strategyName, exchangeName, frameName
26300
26916
  * @returns Unique string key for memoization
26301
26917
  */
26302
- const CREATE_KEY_FN$a = (context) => {
26918
+ const CREATE_KEY_FN$b = (context) => {
26303
26919
  const parts = [context.strategyName, context.exchangeName];
26304
26920
  if (context.frameName)
26305
26921
  parts.push(context.frameName);
@@ -26373,7 +26989,7 @@ class BreakevenGlobalService {
26373
26989
  * @param context - Context with strategyName, exchangeName and frameName
26374
26990
  * @param methodName - Name of the calling method for error tracking
26375
26991
  */
26376
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$a(context), (context, methodName) => {
26992
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$b(context), (context, methodName) => {
26377
26993
  this.loggerService.log("breakevenGlobalService validate", {
26378
26994
  context,
26379
26995
  methodName,
@@ -26594,7 +27210,7 @@ class ConfigValidationService {
26594
27210
  * @param backtest - Whether running in backtest mode
26595
27211
  * @returns Unique string key for memoization
26596
27212
  */
26597
- const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
27213
+ const CREATE_KEY_FN$a = (symbol, strategyName, exchangeName, frameName, backtest) => {
26598
27214
  const parts = [symbol, strategyName, exchangeName];
26599
27215
  if (frameName)
26600
27216
  parts.push(frameName);
@@ -26605,7 +27221,7 @@ const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest
26605
27221
  * Creates a filename for markdown report based on memoization key components.
26606
27222
  * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
26607
27223
  */
26608
- const CREATE_FILE_NAME_FN$3 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
27224
+ const CREATE_FILE_NAME_FN$4 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
26609
27225
  const parts = [symbol, strategyName, exchangeName];
26610
27226
  if (frameName) {
26611
27227
  parts.push(frameName);
@@ -26619,7 +27235,7 @@ const CREATE_FILE_NAME_FN$3 = (symbol, strategyName, exchangeName, frameName, ti
26619
27235
  * Storage class for accumulating risk rejection events per symbol-strategy pair.
26620
27236
  * Maintains a chronological list of rejected signals due to risk limits.
26621
27237
  */
26622
- let ReportStorage$3 = class ReportStorage {
27238
+ let ReportStorage$4 = class ReportStorage {
26623
27239
  constructor(symbol, strategyName, exchangeName, frameName) {
26624
27240
  this.symbol = symbol;
26625
27241
  this.strategyName = strategyName;
@@ -26720,7 +27336,7 @@ let ReportStorage$3 = class ReportStorage {
26720
27336
  async dump(symbol, strategyName, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
26721
27337
  const markdown = await this.getReport(symbol, strategyName, columns);
26722
27338
  const timestamp = getContextTimestamp();
26723
- const filename = CREATE_FILE_NAME_FN$3(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
27339
+ const filename = CREATE_FILE_NAME_FN$4(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
26724
27340
  await Markdown.writeData("risk", markdown, {
26725
27341
  path,
26726
27342
  file: filename,
@@ -26761,7 +27377,7 @@ class RiskMarkdownService {
26761
27377
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
26762
27378
  * Each combination gets its own isolated storage instance.
26763
27379
  */
26764
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$3(symbol, strategyName, exchangeName, frameName));
27380
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$a(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$4(symbol, strategyName, exchangeName, frameName));
26765
27381
  /**
26766
27382
  * Subscribes to risk rejection emitter to receive rejection events.
26767
27383
  * Protected against multiple subscriptions.
@@ -26950,7 +27566,7 @@ class RiskMarkdownService {
26950
27566
  payload,
26951
27567
  });
26952
27568
  if (payload) {
26953
- const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
27569
+ const key = CREATE_KEY_FN$a(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26954
27570
  this.getStorage.clear(key);
26955
27571
  }
26956
27572
  else {
@@ -27252,6 +27868,7 @@ const WILDCARD_TARGET = {
27252
27868
  walker: true,
27253
27869
  sync: true,
27254
27870
  highest_profit: true,
27871
+ max_drawdown: true,
27255
27872
  };
27256
27873
  /**
27257
27874
  * Utility class for managing report services.
@@ -27289,7 +27906,7 @@ class ReportUtils {
27289
27906
  *
27290
27907
  * @returns Cleanup function that unsubscribes from all enabled services
27291
27908
  */
27292
- this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, } = WILDCARD_TARGET) => {
27909
+ this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET) => {
27293
27910
  bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
27294
27911
  backtest: bt$1,
27295
27912
  breakeven,
@@ -27340,6 +27957,9 @@ class ReportUtils {
27340
27957
  if (highest_profit) {
27341
27958
  unList.push(bt.highestProfitReportService.subscribe());
27342
27959
  }
27960
+ if (max_drawdown) {
27961
+ unList.push(bt.maxDrawdownReportService.subscribe());
27962
+ }
27343
27963
  return functoolsKit.compose(...unList.map((un) => () => void un()));
27344
27964
  };
27345
27965
  /**
@@ -27378,7 +27998,7 @@ class ReportUtils {
27378
27998
  * Report.disable();
27379
27999
  * ```
27380
28000
  */
27381
- this.disable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, } = WILDCARD_TARGET) => {
28001
+ this.disable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET) => {
27382
28002
  bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
27383
28003
  backtest: bt$1,
27384
28004
  breakeven,
@@ -27428,6 +28048,9 @@ class ReportUtils {
27428
28048
  if (highest_profit) {
27429
28049
  bt.highestProfitReportService.unsubscribe();
27430
28050
  }
28051
+ if (max_drawdown) {
28052
+ bt.maxDrawdownReportService.unsubscribe();
28053
+ }
27431
28054
  };
27432
28055
  }
27433
28056
  }
@@ -27639,6 +28262,8 @@ class BacktestReportService {
27639
28262
  pnlPriceClose: data.pnl.priceClose,
27640
28263
  totalPartials: data.signal?.totalPartials,
27641
28264
  cost: data.signal?.cost,
28265
+ peakPnl: data.signal?._peak?.pnlPercentage,
28266
+ fallPnl: data.signal?._fall?.pnlPercentage,
27642
28267
  }, { ...searchOptions, signalId: data.signal?.id });
27643
28268
  }
27644
28269
  else if (data.action === "closed") {
@@ -27671,6 +28296,8 @@ class BacktestReportService {
27671
28296
  closeReason: data.closeReason,
27672
28297
  closeTime: data.closeTimestamp,
27673
28298
  duration: durationMin,
28299
+ peakPnl: data.signal?._peak?.pnlPercentage,
28300
+ fallPnl: data.signal?._fall?.pnlPercentage,
27674
28301
  }, { ...searchOptions, signalId: data.signal?.id });
27675
28302
  }
27676
28303
  };
@@ -27833,6 +28460,8 @@ class LiveReportService {
27833
28460
  pnlPriceClose: data.pnl.priceClose,
27834
28461
  totalPartials: data.signal?.totalPartials,
27835
28462
  cost: data.signal?.cost,
28463
+ peakPnl: data.signal?._peak?.pnlPercentage,
28464
+ fallPnl: data.signal?._fall?.pnlPercentage,
27836
28465
  }, { ...searchOptions, signalId: data.signal?.id });
27837
28466
  }
27838
28467
  else if (data.action === "opened") {
@@ -27883,6 +28512,8 @@ class LiveReportService {
27883
28512
  pnlPriceClose: data.pnl.priceClose,
27884
28513
  totalPartials: data.signal?.totalPartials,
27885
28514
  cost: data.signal?.cost,
28515
+ peakPnl: data.signal?._peak?.pnlPercentage,
28516
+ fallPnl: data.signal?._fall?.pnlPercentage,
27886
28517
  }, { ...searchOptions, signalId: data.signal?.id });
27887
28518
  }
27888
28519
  else if (data.action === "closed") {
@@ -27915,6 +28546,8 @@ class LiveReportService {
27915
28546
  closeReason: data.closeReason,
27916
28547
  duration: durationMin,
27917
28548
  closeTime: data.closeTimestamp,
28549
+ peakPnl: data.signal?._peak?.pnlPercentage,
28550
+ fallPnl: data.signal?._fall?.pnlPercentage,
27918
28551
  }, { ...searchOptions, signalId: data.signal?.id });
27919
28552
  }
27920
28553
  else if (data.action === "cancelled") {
@@ -28506,6 +29139,8 @@ class HeatReportService {
28506
29139
  openTime: data.signal?.pendingAt,
28507
29140
  scheduledAt: data.signal?.scheduledAt,
28508
29141
  closeTime: data.closeTimestamp,
29142
+ peakPnl: data.signal?._peak?.pnlPercentage,
29143
+ fallPnl: data.signal?._fall?.pnlPercentage,
28509
29144
  }, {
28510
29145
  symbol: data.symbol,
28511
29146
  strategyName: data.strategyName,
@@ -29769,7 +30404,7 @@ class HighestProfitReportService {
29769
30404
  * @returns Colon-separated key string for memoization
29770
30405
  * @internal
29771
30406
  */
29772
- const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30407
+ const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
29773
30408
  const parts = [symbol, strategyName, exchangeName];
29774
30409
  if (frameName)
29775
30410
  parts.push(frameName);
@@ -29789,7 +30424,7 @@ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest
29789
30424
  * @returns Underscore-separated filename with .md extension
29790
30425
  * @internal
29791
30426
  */
29792
- const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
30427
+ const CREATE_FILE_NAME_FN$3 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
29793
30428
  const parts = [symbol, strategyName, exchangeName];
29794
30429
  if (frameName) {
29795
30430
  parts.push(frameName);
@@ -29811,7 +30446,7 @@ const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, ti
29811
30446
  *
29812
30447
  * @internal
29813
30448
  */
29814
- let ReportStorage$2 = class ReportStorage {
30449
+ let ReportStorage$3 = class ReportStorage {
29815
30450
  /**
29816
30451
  * Creates a new ReportStorage instance.
29817
30452
  *
@@ -29942,7 +30577,7 @@ let ReportStorage$2 = class ReportStorage {
29942
30577
  async dump(symbol, strategyName, path = "./dump/strategy", columns = COLUMN_CONFIG.strategy_columns) {
29943
30578
  const markdown = await this.getReport(symbol, strategyName, columns);
29944
30579
  const timestamp = getContextTimestamp();
29945
- const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
30580
+ const filename = CREATE_FILE_NAME_FN$3(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
29946
30581
  await Markdown.writeData("strategy", markdown, {
29947
30582
  path,
29948
30583
  file: filename,
@@ -30011,7 +30646,7 @@ class StrategyMarkdownService {
30011
30646
  *
30012
30647
  * @internal
30013
30648
  */
30014
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName));
30649
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$3(symbol, strategyName, exchangeName, frameName));
30015
30650
  /**
30016
30651
  * Records a cancel-scheduled event when a scheduled signal is cancelled.
30017
30652
  *
@@ -30579,7 +31214,7 @@ class StrategyMarkdownService {
30579
31214
  this.clear = async (payload) => {
30580
31215
  this.loggerService.log("strategyMarkdownService clear", { payload });
30581
31216
  if (payload) {
30582
- const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31217
+ const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
30583
31218
  this.getStorage.clear(key);
30584
31219
  }
30585
31220
  else {
@@ -30687,7 +31322,7 @@ class StrategyMarkdownService {
30687
31322
  * Creates a unique key for memoizing ReportStorage instances.
30688
31323
  * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
30689
31324
  */
30690
- const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31325
+ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30691
31326
  const parts = [symbol, strategyName, exchangeName];
30692
31327
  if (frameName)
30693
31328
  parts.push(frameName);
@@ -30698,7 +31333,7 @@ const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest
30698
31333
  * Creates a filename for the markdown report.
30699
31334
  * Filename format: "symbol_strategyName_exchangeName[_frameName_backtest|_live]-timestamp.md"
30700
31335
  */
30701
- const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
31336
+ const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
30702
31337
  const parts = [symbol, strategyName, exchangeName];
30703
31338
  if (frameName) {
30704
31339
  parts.push(frameName);
@@ -30712,7 +31347,7 @@ const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, ti
30712
31347
  * Storage class for accumulating signal sync events per symbol-strategy-exchange-frame-backtest combination.
30713
31348
  * Maintains a chronological list of signal-open and signal-close events.
30714
31349
  */
30715
- let ReportStorage$1 = class ReportStorage {
31350
+ let ReportStorage$2 = class ReportStorage {
30716
31351
  constructor(symbol, strategyName, exchangeName, frameName, backtest) {
30717
31352
  this.symbol = symbol;
30718
31353
  this.strategyName = strategyName;
@@ -30846,7 +31481,7 @@ let ReportStorage$1 = class ReportStorage {
30846
31481
  async dump(symbol, strategyName, path = "./dump/sync", columns = COLUMN_CONFIG.sync_columns) {
30847
31482
  const markdown = await this.getReport(symbol, strategyName, columns);
30848
31483
  const timestamp = getContextTimestamp();
30849
- const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
31484
+ const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
30850
31485
  await Markdown.writeData("sync", markdown, {
30851
31486
  path,
30852
31487
  file: filename,
@@ -30880,7 +31515,7 @@ let ReportStorage$1 = class ReportStorage {
30880
31515
  class SyncMarkdownService {
30881
31516
  constructor() {
30882
31517
  this.loggerService = inject(TYPES.loggerService);
30883
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName, backtest));
31518
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName, backtest));
30884
31519
  /**
30885
31520
  * Subscribes to `syncSubject` to start receiving `SignalSyncContract` events.
30886
31521
  * Protected against multiple subscriptions via `singleshot` — subsequent calls
@@ -31076,7 +31711,7 @@ class SyncMarkdownService {
31076
31711
  this.clear = async (payload) => {
31077
31712
  this.loggerService.log("syncMarkdownService clear", { payload });
31078
31713
  if (payload) {
31079
- const key = CREATE_KEY_FN$7(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31714
+ const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31080
31715
  this.getStorage.clear(key);
31081
31716
  }
31082
31717
  else {
@@ -31089,7 +31724,7 @@ class SyncMarkdownService {
31089
31724
  /**
31090
31725
  * Creates a unique memoization key for a symbol-strategy-exchange-frame-backtest combination.
31091
31726
  */
31092
- const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31727
+ const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31093
31728
  const parts = [symbol, strategyName, exchangeName];
31094
31729
  if (frameName)
31095
31730
  parts.push(frameName);
@@ -31099,7 +31734,7 @@ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest
31099
31734
  /**
31100
31735
  * Creates a filename for the markdown report.
31101
31736
  */
31102
- const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
31737
+ const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
31103
31738
  const parts = [symbol, strategyName, exchangeName];
31104
31739
  if (frameName) {
31105
31740
  parts.push(frameName);
@@ -31112,7 +31747,7 @@ const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, time
31112
31747
  /**
31113
31748
  * Accumulates highest profit events per symbol-strategy-exchange-frame combination.
31114
31749
  */
31115
- class ReportStorage {
31750
+ let ReportStorage$1 = class ReportStorage {
31116
31751
  constructor(symbol, strategyName, exchangeName, frameName) {
31117
31752
  this.symbol = symbol;
31118
31753
  this.strategyName = strategyName;
@@ -31243,7 +31878,7 @@ class ReportStorage {
31243
31878
  async dump(symbol, strategyName, path = "./dump/highest_profit", columns = COLUMN_CONFIG.highest_profit_columns) {
31244
31879
  const markdown = await this.getReport(symbol, strategyName, columns);
31245
31880
  const timestamp = getContextTimestamp();
31246
- const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
31881
+ const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
31247
31882
  await Markdown.writeData("highest_profit", markdown, {
31248
31883
  path,
31249
31884
  file: filename,
@@ -31254,7 +31889,7 @@ class ReportStorage {
31254
31889
  frameName: this.frameName,
31255
31890
  });
31256
31891
  }
31257
- }
31892
+ };
31258
31893
  /**
31259
31894
  * Service for generating and saving highest profit markdown reports.
31260
31895
  *
@@ -31265,7 +31900,7 @@ class ReportStorage {
31265
31900
  class HighestProfitMarkdownService {
31266
31901
  constructor() {
31267
31902
  this.loggerService = inject(TYPES.loggerService);
31268
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
31903
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
31269
31904
  /**
31270
31905
  * Subscribes to `highestProfitSubject` to start receiving `HighestProfitContract`
31271
31906
  * events. Protected against multiple subscriptions via `singleshot` — subsequent
@@ -31431,7 +32066,7 @@ class HighestProfitMarkdownService {
31431
32066
  this.clear = async (payload) => {
31432
32067
  this.loggerService.log("highestProfitMarkdownService clear", { payload });
31433
32068
  if (payload) {
31434
- const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32069
+ const key = CREATE_KEY_FN$7(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31435
32070
  this.getStorage.clear(key);
31436
32071
  }
31437
32072
  else {
@@ -31453,7 +32088,7 @@ const LISTEN_TIMEOUT$1 = 120000;
31453
32088
  * @param backtest - Whether running in backtest mode
31454
32089
  * @returns Unique string key for memoization
31455
32090
  */
31456
- const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
32091
+ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31457
32092
  const parts = [symbol, strategyName, exchangeName];
31458
32093
  if (frameName)
31459
32094
  parts.push(frameName);
@@ -31496,7 +32131,7 @@ class PriceMetaService {
31496
32131
  * Each subject holds the latest currentPrice emitted by the strategy iterator for that key.
31497
32132
  * Instances are cached until clear() is called.
31498
32133
  */
31499
- this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
32134
+ this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
31500
32135
  /**
31501
32136
  * Returns the current market price for the given symbol and context.
31502
32137
  *
@@ -31525,10 +32160,10 @@ class PriceMetaService {
31525
32160
  if (source.data) {
31526
32161
  return source.data;
31527
32162
  }
31528
- console.warn(`PriceMetaService: No currentPrice available for ${CREATE_KEY_FN$5(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
32163
+ console.warn(`PriceMetaService: No currentPrice available for ${CREATE_KEY_FN$6(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
31529
32164
  const currentPrice = await functoolsKit.waitForNext(source, (data) => !!data, LISTEN_TIMEOUT$1);
31530
32165
  if (typeof currentPrice === "symbol") {
31531
- throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$5(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
32166
+ throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$6(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31532
32167
  }
31533
32168
  return currentPrice;
31534
32169
  };
@@ -31570,7 +32205,7 @@ class PriceMetaService {
31570
32205
  this.getSource.clear();
31571
32206
  return;
31572
32207
  }
31573
- const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32208
+ const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31574
32209
  this.getSource.clear(key);
31575
32210
  };
31576
32211
  }
@@ -31588,7 +32223,7 @@ const LISTEN_TIMEOUT = 120000;
31588
32223
  * @param backtest - Whether running in backtest mode
31589
32224
  * @returns Unique string key for memoization
31590
32225
  */
31591
- const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
32226
+ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31592
32227
  const parts = [symbol, strategyName, exchangeName];
31593
32228
  if (frameName)
31594
32229
  parts.push(frameName);
@@ -31631,7 +32266,7 @@ class TimeMetaService {
31631
32266
  * Each subject holds the latest createdAt timestamp emitted by the strategy iterator for that key.
31632
32267
  * Instances are cached until clear() is called.
31633
32268
  */
31634
- this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$4(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
32269
+ this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
31635
32270
  /**
31636
32271
  * Returns the current candle timestamp (in milliseconds) for the given symbol and context.
31637
32272
  *
@@ -31659,10 +32294,10 @@ class TimeMetaService {
31659
32294
  if (source.data) {
31660
32295
  return source.data;
31661
32296
  }
31662
- console.warn(`TimeMetaService: No timestamp available for ${CREATE_KEY_FN$4(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
32297
+ console.warn(`TimeMetaService: No timestamp available for ${CREATE_KEY_FN$5(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
31663
32298
  const timestamp = await functoolsKit.waitForNext(source, (data) => !!data, LISTEN_TIMEOUT);
31664
32299
  if (typeof timestamp === "symbol") {
31665
- throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$4(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
32300
+ throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$5(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31666
32301
  }
31667
32302
  return timestamp;
31668
32303
  };
@@ -31704,12 +32339,315 @@ class TimeMetaService {
31704
32339
  this.getSource.clear();
31705
32340
  return;
31706
32341
  }
31707
- const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32342
+ const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31708
32343
  this.getSource.clear(key);
31709
32344
  };
31710
32345
  }
31711
32346
  }
31712
32347
 
32348
+ const MAX_DRAWDOWN_REPORT_METHOD_NAME_SUBSCRIBE = "MaxDrawdownReportService.subscribe";
32349
+ const MAX_DRAWDOWN_REPORT_METHOD_NAME_UNSUBSCRIBE = "MaxDrawdownReportService.unsubscribe";
32350
+ const MAX_DRAWDOWN_REPORT_METHOD_NAME_TICK = "MaxDrawdownReportService.tick";
32351
+ /**
32352
+ * Service for logging max drawdown events to the JSONL report database.
32353
+ *
32354
+ * Listens to maxDrawdownSubject and writes each new drawdown record to
32355
+ * Report.writeData() for persistence and analytics.
32356
+ */
32357
+ class MaxDrawdownReportService {
32358
+ constructor() {
32359
+ this.loggerService = inject(TYPES.loggerService);
32360
+ /**
32361
+ * Handles a single `MaxDrawdownContract` event emitted by `maxDrawdownSubject`.
32362
+ *
32363
+ * Writes a JSONL record to the `"max_drawdown"` report database via
32364
+ * `Report.writeData`, capturing the full signal snapshot at the moment
32365
+ * the new drawdown record was set:
32366
+ * - `timestamp`, `symbol`, `strategyName`, `exchangeName`, `frameName`, `backtest`
32367
+ * - `signalId`, `position`, `currentPrice`
32368
+ * - `priceOpen`, `priceTakeProfit`, `priceStopLoss` (effective values from the signal)
32369
+ *
32370
+ * `strategyName` and signal-level fields are sourced from `data.signal`
32371
+ * rather than the contract root.
32372
+ *
32373
+ * @param data - `MaxDrawdownContract` payload containing `symbol`,
32374
+ * `signal`, `currentPrice`, `backtest`, `timestamp`, `exchangeName`,
32375
+ * `frameName`
32376
+ */
32377
+ this.tick = async (data) => {
32378
+ this.loggerService.log(MAX_DRAWDOWN_REPORT_METHOD_NAME_TICK, { data });
32379
+ await Report.writeData("max_drawdown", {
32380
+ timestamp: data.timestamp,
32381
+ symbol: data.symbol,
32382
+ strategyName: data.signal.strategyName,
32383
+ exchangeName: data.exchangeName,
32384
+ frameName: data.frameName,
32385
+ backtest: data.backtest,
32386
+ signalId: data.signal.id,
32387
+ position: data.signal.position,
32388
+ currentPrice: data.currentPrice,
32389
+ priceOpen: data.signal.priceOpen,
32390
+ priceTakeProfit: data.signal.priceTakeProfit,
32391
+ priceStopLoss: data.signal.priceStopLoss,
32392
+ }, {
32393
+ symbol: data.symbol,
32394
+ strategyName: data.signal.strategyName,
32395
+ exchangeName: data.exchangeName,
32396
+ frameName: data.frameName,
32397
+ signalId: data.signal.id,
32398
+ walkerName: "",
32399
+ });
32400
+ };
32401
+ /**
32402
+ * Subscribes to `maxDrawdownSubject` to start persisting drawdown records.
32403
+ * Protected against multiple subscriptions via `singleshot` — subsequent
32404
+ * calls return the same unsubscribe function without re-subscribing.
32405
+ *
32406
+ * The returned unsubscribe function clears the `singleshot` state and
32407
+ * detaches from `maxDrawdownSubject`.
32408
+ *
32409
+ * @returns Unsubscribe function; calling it tears down the subscription
32410
+ */
32411
+ this.subscribe = functoolsKit.singleshot(() => {
32412
+ this.loggerService.log(MAX_DRAWDOWN_REPORT_METHOD_NAME_SUBSCRIBE);
32413
+ const unsub = maxDrawdownSubject.subscribe(this.tick);
32414
+ return () => {
32415
+ this.subscribe.clear();
32416
+ unsub();
32417
+ };
32418
+ });
32419
+ /**
32420
+ * Detaches from `maxDrawdownSubject`, stopping further JSONL writes.
32421
+ *
32422
+ * Calls the unsubscribe closure returned by `subscribe()`.
32423
+ * If `subscribe()` was never called, does nothing.
32424
+ */
32425
+ this.unsubscribe = async () => {
32426
+ this.loggerService.log(MAX_DRAWDOWN_REPORT_METHOD_NAME_UNSUBSCRIBE);
32427
+ if (this.subscribe.hasValue()) {
32428
+ const lastSubscription = this.subscribe();
32429
+ lastSubscription();
32430
+ }
32431
+ };
32432
+ }
32433
+ }
32434
+
32435
+ /**
32436
+ * Creates a unique memoization key for a symbol-strategy-exchange-frame-backtest combination.
32437
+ */
32438
+ const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
32439
+ const parts = [symbol, strategyName, exchangeName];
32440
+ if (frameName)
32441
+ parts.push(frameName);
32442
+ parts.push(backtest ? "backtest" : "live");
32443
+ return parts.join(":");
32444
+ };
32445
+ /**
32446
+ * Creates a filename for the markdown report.
32447
+ */
32448
+ const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
32449
+ const parts = [symbol, strategyName, exchangeName];
32450
+ if (frameName) {
32451
+ parts.push(frameName);
32452
+ parts.push("backtest");
32453
+ }
32454
+ else
32455
+ parts.push("live");
32456
+ return `${parts.join("_")}-${timestamp}.md`;
32457
+ };
32458
+ /**
32459
+ * Accumulates max drawdown events per symbol-strategy-exchange-frame combination.
32460
+ */
32461
+ class ReportStorage {
32462
+ constructor(symbol, strategyName, exchangeName, frameName) {
32463
+ this.symbol = symbol;
32464
+ this.strategyName = strategyName;
32465
+ this.exchangeName = exchangeName;
32466
+ this.frameName = frameName;
32467
+ this._eventList = [];
32468
+ }
32469
+ /**
32470
+ * Constructs a `MaxDrawdownEvent` from the given signal snapshot and
32471
+ * prepends it to the internal queue (most recent first).
32472
+ *
32473
+ * Once the queue exceeds `GLOBAL_CONFIG.CC_MAX_MAX_DRAWDOWN_MARKDOWN_ROWS`
32474
+ * entries, the oldest entry is dropped from the tail.
32475
+ */
32476
+ addEvent(data, currentPrice, backtest, timestamp) {
32477
+ this._eventList.unshift({
32478
+ timestamp,
32479
+ symbol: data.symbol,
32480
+ strategyName: data.strategyName,
32481
+ signalId: data.id,
32482
+ position: data.position,
32483
+ pnl: data.pnl,
32484
+ currentPrice,
32485
+ priceOpen: data.priceOpen,
32486
+ priceTakeProfit: data.priceTakeProfit,
32487
+ priceStopLoss: data.priceStopLoss,
32488
+ backtest,
32489
+ });
32490
+ if (this._eventList.length > GLOBAL_CONFIG.CC_MAX_MAX_DRAWDOWN_MARKDOWN_ROWS) {
32491
+ this._eventList.pop();
32492
+ }
32493
+ }
32494
+ /**
32495
+ * Returns the accumulated event list with a total count.
32496
+ */
32497
+ async getData() {
32498
+ return {
32499
+ eventList: this._eventList,
32500
+ totalEvents: this._eventList.length,
32501
+ };
32502
+ }
32503
+ /**
32504
+ * Renders a markdown max drawdown report for this storage instance.
32505
+ */
32506
+ async getReport(symbol, strategyName, columns = COLUMN_CONFIG.max_drawdown_columns) {
32507
+ const stats = await this.getData();
32508
+ if (stats.totalEvents === 0) {
32509
+ return [
32510
+ `# Max Drawdown Report: ${symbol}:${strategyName}`,
32511
+ "",
32512
+ "No max drawdown events recorded yet.",
32513
+ ].join("\n");
32514
+ }
32515
+ const visibleColumns = [];
32516
+ for (const col of columns) {
32517
+ if (await col.isVisible()) {
32518
+ visibleColumns.push(col);
32519
+ }
32520
+ }
32521
+ const header = visibleColumns.map((col) => col.label);
32522
+ const separator = visibleColumns.map(() => "---");
32523
+ const rows = await Promise.all(this._eventList.map(async (event, index) => Promise.all(visibleColumns.map((col) => col.format(event, index)))));
32524
+ const tableData = [header, separator, ...rows];
32525
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
32526
+ return [
32527
+ `# Max Drawdown Report: ${symbol}:${strategyName}`,
32528
+ "",
32529
+ table,
32530
+ "",
32531
+ `**Total events:** ${stats.totalEvents}`,
32532
+ ].join("\n");
32533
+ }
32534
+ /**
32535
+ * Generates the markdown report and persists it via `Markdown.writeData`.
32536
+ */
32537
+ async dump(symbol, strategyName, path = "./dump/max_drawdown", columns = COLUMN_CONFIG.max_drawdown_columns) {
32538
+ const markdown = await this.getReport(symbol, strategyName, columns);
32539
+ const timestamp = getContextTimestamp();
32540
+ const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
32541
+ await Markdown.writeData("max_drawdown", markdown, {
32542
+ path,
32543
+ file: filename,
32544
+ symbol: this.symbol,
32545
+ signalId: "",
32546
+ strategyName: this.strategyName,
32547
+ exchangeName: this.exchangeName,
32548
+ frameName: this.frameName,
32549
+ });
32550
+ }
32551
+ }
32552
+ /**
32553
+ * Service for generating and saving max drawdown markdown reports.
32554
+ *
32555
+ * Listens to maxDrawdownSubject and accumulates events per
32556
+ * symbol-strategy-exchange-frame combination. Provides getData(),
32557
+ * getReport(), and dump() methods matching the HighestProfit pattern.
32558
+ */
32559
+ class MaxDrawdownMarkdownService {
32560
+ constructor() {
32561
+ this.loggerService = inject(TYPES.loggerService);
32562
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$4(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
32563
+ /**
32564
+ * Subscribes to `maxDrawdownSubject` to start receiving `MaxDrawdownContract`
32565
+ * events. Protected against multiple subscriptions via `singleshot`.
32566
+ *
32567
+ * @returns Unsubscribe function; calling it tears down the subscription and
32568
+ * clears all accumulated data
32569
+ */
32570
+ this.subscribe = functoolsKit.singleshot(() => {
32571
+ this.loggerService.log("maxDrawdownMarkdownService init");
32572
+ const unsub = maxDrawdownSubject.subscribe(this.tick);
32573
+ return () => {
32574
+ this.subscribe.clear();
32575
+ this.clear();
32576
+ unsub();
32577
+ };
32578
+ });
32579
+ /**
32580
+ * Detaches from `maxDrawdownSubject` and clears all accumulated data.
32581
+ *
32582
+ * If `subscribe()` was never called, does nothing.
32583
+ */
32584
+ this.unsubscribe = async () => {
32585
+ this.loggerService.log("maxDrawdownMarkdownService unsubscribe");
32586
+ if (this.subscribe.hasValue()) {
32587
+ const lastSubscription = this.subscribe();
32588
+ lastSubscription();
32589
+ }
32590
+ };
32591
+ /**
32592
+ * Handles a single `MaxDrawdownContract` event emitted by `maxDrawdownSubject`.
32593
+ */
32594
+ this.tick = async (data) => {
32595
+ this.loggerService.log("maxDrawdownMarkdownService tick", { data });
32596
+ const storage = this.getStorage(data.symbol, data.signal.strategyName, data.exchangeName, data.frameName, data.backtest);
32597
+ storage.addEvent(data.signal, data.currentPrice, data.backtest, data.timestamp);
32598
+ };
32599
+ /**
32600
+ * Returns accumulated max drawdown statistics for the given context.
32601
+ */
32602
+ this.getData = async (symbol, strategyName, exchangeName, frameName, backtest) => {
32603
+ this.loggerService.log("maxDrawdownMarkdownService getData", { symbol, strategyName, exchangeName, frameName, backtest });
32604
+ if (!this.subscribe.hasValue()) {
32605
+ throw new Error("MaxDrawdownMarkdownService not initialized. Call subscribe() before getting data.");
32606
+ }
32607
+ const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
32608
+ return storage.getData();
32609
+ };
32610
+ /**
32611
+ * Generates a markdown max drawdown report for the given context.
32612
+ */
32613
+ this.getReport = async (symbol, strategyName, exchangeName, frameName, backtest, columns = COLUMN_CONFIG.max_drawdown_columns) => {
32614
+ this.loggerService.log("maxDrawdownMarkdownService getReport", { symbol, strategyName, exchangeName, frameName, backtest });
32615
+ if (!this.subscribe.hasValue()) {
32616
+ throw new Error("MaxDrawdownMarkdownService not initialized. Call subscribe() before generating reports.");
32617
+ }
32618
+ const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
32619
+ return storage.getReport(symbol, strategyName, columns);
32620
+ };
32621
+ /**
32622
+ * Generates the max drawdown report and writes it to disk.
32623
+ */
32624
+ this.dump = async (symbol, strategyName, exchangeName, frameName, backtest, path = "./dump/max_drawdown", columns = COLUMN_CONFIG.max_drawdown_columns) => {
32625
+ this.loggerService.log("maxDrawdownMarkdownService dump", { symbol, strategyName, exchangeName, frameName, backtest, path });
32626
+ if (!this.subscribe.hasValue()) {
32627
+ throw new Error("MaxDrawdownMarkdownService not initialized. Call subscribe() before dumping reports.");
32628
+ }
32629
+ const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
32630
+ await storage.dump(symbol, strategyName, path, columns);
32631
+ };
32632
+ /**
32633
+ * Evicts memoized `ReportStorage` instances, releasing all accumulated event data.
32634
+ *
32635
+ * - With `payload` — clears only the storage bucket for that combination.
32636
+ * - Without `payload` — clears **all** storage buckets.
32637
+ */
32638
+ this.clear = async (payload) => {
32639
+ this.loggerService.log("maxDrawdownMarkdownService clear", { payload });
32640
+ if (payload) {
32641
+ const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32642
+ this.getStorage.clear(key);
32643
+ }
32644
+ else {
32645
+ this.getStorage.clear();
32646
+ }
32647
+ };
32648
+ }
32649
+ }
32650
+
31713
32651
  {
31714
32652
  provide(TYPES.loggerService, () => new LoggerService());
31715
32653
  }
@@ -31780,6 +32718,7 @@ class TimeMetaService {
31780
32718
  provide(TYPES.strategyMarkdownService, () => new StrategyMarkdownService());
31781
32719
  provide(TYPES.syncMarkdownService, () => new SyncMarkdownService());
31782
32720
  provide(TYPES.highestProfitMarkdownService, () => new HighestProfitMarkdownService());
32721
+ provide(TYPES.maxDrawdownMarkdownService, () => new MaxDrawdownMarkdownService());
31783
32722
  }
31784
32723
  {
31785
32724
  provide(TYPES.backtestReportService, () => new BacktestReportService());
@@ -31794,6 +32733,7 @@ class TimeMetaService {
31794
32733
  provide(TYPES.strategyReportService, () => new StrategyReportService());
31795
32734
  provide(TYPES.syncReportService, () => new SyncReportService());
31796
32735
  provide(TYPES.highestProfitReportService, () => new HighestProfitReportService());
32736
+ provide(TYPES.maxDrawdownReportService, () => new MaxDrawdownReportService());
31797
32737
  }
31798
32738
  {
31799
32739
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -31877,6 +32817,7 @@ const markdownServices = {
31877
32817
  strategyMarkdownService: inject(TYPES.strategyMarkdownService),
31878
32818
  syncMarkdownService: inject(TYPES.syncMarkdownService),
31879
32819
  highestProfitMarkdownService: inject(TYPES.highestProfitMarkdownService),
32820
+ maxDrawdownMarkdownService: inject(TYPES.maxDrawdownMarkdownService),
31880
32821
  };
31881
32822
  const reportServices = {
31882
32823
  backtestReportService: inject(TYPES.backtestReportService),
@@ -31891,6 +32832,7 @@ const reportServices = {
31891
32832
  strategyReportService: inject(TYPES.strategyReportService),
31892
32833
  syncReportService: inject(TYPES.syncReportService),
31893
32834
  highestProfitReportService: inject(TYPES.highestProfitReportService),
32835
+ maxDrawdownReportService: inject(TYPES.maxDrawdownReportService),
31894
32836
  };
31895
32837
  const validationServices = {
31896
32838
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -34818,6 +35760,12 @@ const GET_POSITION_HIGHEST_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionHig
34818
35760
  const GET_POSITION_HIGHEST_PNL_COST_METHOD_NAME = "strategy.getPositionHighestPnlCost";
34819
35761
  const GET_POSITION_HIGHEST_PROFIT_BREAKEVEN_METHOD_NAME = "strategy.getPositionHighestProfitBreakeven";
34820
35762
  const GET_POSITION_DRAWDOWN_MINUTES_METHOD_NAME = "strategy.getPositionDrawdownMinutes";
35763
+ const GET_POSITION_HIGHEST_PROFIT_MINUTES_METHOD_NAME = "strategy.getPositionHighestProfitMinutes";
35764
+ const GET_POSITION_MAX_DRAWDOWN_MINUTES_METHOD_NAME = "strategy.getPositionMaxDrawdownMinutes";
35765
+ const GET_POSITION_MAX_DRAWDOWN_PRICE_METHOD_NAME = "strategy.getPositionMaxDrawdownPrice";
35766
+ const GET_POSITION_MAX_DRAWDOWN_TIMESTAMP_METHOD_NAME = "strategy.getPositionMaxDrawdownTimestamp";
35767
+ const GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlPercentage";
35768
+ const GET_POSITION_MAX_DRAWDOWN_PNL_COST_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlCost";
34821
35769
  const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
34822
35770
  const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
34823
35771
  const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
@@ -36277,6 +37225,181 @@ async function getPositionDrawdownMinutes(symbol) {
36277
37225
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
36278
37226
  return await bt.strategyCoreService.getPositionDrawdownMinutes(isBacktest, symbol, { exchangeName, frameName, strategyName });
36279
37227
  }
37228
+ /**
37229
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
37230
+ *
37231
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
37232
+ * pulling back from its peak profit level.
37233
+ * Zero when called at the exact moment the peak was set.
37234
+ *
37235
+ * Returns null if no pending signal exists.
37236
+ *
37237
+ * @param symbol - Trading pair symbol
37238
+ * @returns Promise resolving to minutes since last profit peak or null
37239
+ *
37240
+ * @example
37241
+ * ```typescript
37242
+ * import { getPositionHighestProfitMinutes } from "backtest-kit";
37243
+ *
37244
+ * const minutes = await getPositionHighestProfitMinutes("BTCUSDT");
37245
+ * // e.g. 30 (30 minutes since the highest profit price)
37246
+ * ```
37247
+ */
37248
+ async function getPositionHighestProfitMinutes(symbol) {
37249
+ bt.loggerService.info(GET_POSITION_HIGHEST_PROFIT_MINUTES_METHOD_NAME, { symbol });
37250
+ if (!ExecutionContextService.hasContext()) {
37251
+ throw new Error("getPositionHighestProfitMinutes requires an execution context");
37252
+ }
37253
+ if (!MethodContextService.hasContext()) {
37254
+ throw new Error("getPositionHighestProfitMinutes requires a method context");
37255
+ }
37256
+ const { backtest: isBacktest } = bt.executionContextService.context;
37257
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37258
+ return await bt.strategyCoreService.getPositionHighestProfitMinutes(isBacktest, symbol, { exchangeName, frameName, strategyName });
37259
+ }
37260
+ /**
37261
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
37262
+ *
37263
+ * Measures how long ago the deepest drawdown point occurred.
37264
+ * Zero when called at the exact moment the trough was set.
37265
+ *
37266
+ * Returns null if no pending signal exists.
37267
+ *
37268
+ * @param symbol - Trading pair symbol
37269
+ * @returns Promise resolving to minutes since last drawdown trough or null
37270
+ *
37271
+ * @example
37272
+ * ```typescript
37273
+ * import { getPositionMaxDrawdownMinutes } from "backtest-kit";
37274
+ *
37275
+ * const minutes = await getPositionMaxDrawdownMinutes("BTCUSDT");
37276
+ * // e.g. 15 (15 minutes since the worst loss price)
37277
+ * ```
37278
+ */
37279
+ async function getPositionMaxDrawdownMinutes(symbol) {
37280
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_MINUTES_METHOD_NAME, { symbol });
37281
+ if (!ExecutionContextService.hasContext()) {
37282
+ throw new Error("getPositionMaxDrawdownMinutes requires an execution context");
37283
+ }
37284
+ if (!MethodContextService.hasContext()) {
37285
+ throw new Error("getPositionMaxDrawdownMinutes requires a method context");
37286
+ }
37287
+ const { backtest: isBacktest } = bt.executionContextService.context;
37288
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37289
+ return await bt.strategyCoreService.getPositionMaxDrawdownMinutes(isBacktest, symbol, { exchangeName, frameName, strategyName });
37290
+ }
37291
+ /**
37292
+ * Returns the worst price reached in the loss direction during this position's life.
37293
+ *
37294
+ * Returns null if no pending signal exists.
37295
+ *
37296
+ * @param symbol - Trading pair symbol
37297
+ * @returns Promise resolving to price or null
37298
+ *
37299
+ * @example
37300
+ * ```typescript
37301
+ * import { getPositionMaxDrawdownPrice } from "backtest-kit";
37302
+ *
37303
+ * const price = await getPositionMaxDrawdownPrice("BTCUSDT");
37304
+ * // e.g. 41000 (lowest price seen for a LONG position)
37305
+ * ```
37306
+ */
37307
+ async function getPositionMaxDrawdownPrice(symbol) {
37308
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_PRICE_METHOD_NAME, { symbol });
37309
+ if (!ExecutionContextService.hasContext()) {
37310
+ throw new Error("getPositionMaxDrawdownPrice requires an execution context");
37311
+ }
37312
+ if (!MethodContextService.hasContext()) {
37313
+ throw new Error("getPositionMaxDrawdownPrice requires a method context");
37314
+ }
37315
+ const { backtest: isBacktest } = bt.executionContextService.context;
37316
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37317
+ return await bt.strategyCoreService.getPositionMaxDrawdownPrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
37318
+ }
37319
+ /**
37320
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
37321
+ *
37322
+ * Returns null if no pending signal exists.
37323
+ *
37324
+ * @param symbol - Trading pair symbol
37325
+ * @returns Promise resolving to timestamp in milliseconds or null
37326
+ *
37327
+ * @example
37328
+ * ```typescript
37329
+ * import { getPositionMaxDrawdownTimestamp } from "backtest-kit";
37330
+ *
37331
+ * const ts = await getPositionMaxDrawdownTimestamp("BTCUSDT");
37332
+ * // e.g. 1700000000000
37333
+ * ```
37334
+ */
37335
+ async function getPositionMaxDrawdownTimestamp(symbol) {
37336
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_TIMESTAMP_METHOD_NAME, { symbol });
37337
+ if (!ExecutionContextService.hasContext()) {
37338
+ throw new Error("getPositionMaxDrawdownTimestamp requires an execution context");
37339
+ }
37340
+ if (!MethodContextService.hasContext()) {
37341
+ throw new Error("getPositionMaxDrawdownTimestamp requires a method context");
37342
+ }
37343
+ const { backtest: isBacktest } = bt.executionContextService.context;
37344
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37345
+ return await bt.strategyCoreService.getPositionMaxDrawdownTimestamp(isBacktest, symbol, { exchangeName, frameName, strategyName });
37346
+ }
37347
+ /**
37348
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
37349
+ *
37350
+ * Returns null if no pending signal exists.
37351
+ *
37352
+ * @param symbol - Trading pair symbol
37353
+ * @returns Promise resolving to PnL percentage or null
37354
+ *
37355
+ * @example
37356
+ * ```typescript
37357
+ * import { getPositionMaxDrawdownPnlPercentage } from "backtest-kit";
37358
+ *
37359
+ * const pnl = await getPositionMaxDrawdownPnlPercentage("BTCUSDT");
37360
+ * // e.g. -5.2 (deepest PnL percentage reached)
37361
+ * ```
37362
+ */
37363
+ async function getPositionMaxDrawdownPnlPercentage(symbol) {
37364
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME, { symbol });
37365
+ if (!ExecutionContextService.hasContext()) {
37366
+ throw new Error("getPositionMaxDrawdownPnlPercentage requires an execution context");
37367
+ }
37368
+ if (!MethodContextService.hasContext()) {
37369
+ throw new Error("getPositionMaxDrawdownPnlPercentage requires a method context");
37370
+ }
37371
+ const { backtest: isBacktest } = bt.executionContextService.context;
37372
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37373
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlPercentage(isBacktest, symbol, { exchangeName, frameName, strategyName });
37374
+ }
37375
+ /**
37376
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
37377
+ *
37378
+ * Returns null if no pending signal exists.
37379
+ *
37380
+ * @param symbol - Trading pair symbol
37381
+ * @returns Promise resolving to PnL cost or null
37382
+ *
37383
+ * @example
37384
+ * ```typescript
37385
+ * import { getPositionMaxDrawdownPnlCost } from "backtest-kit";
37386
+ *
37387
+ * const cost = await getPositionMaxDrawdownPnlCost("BTCUSDT");
37388
+ * // e.g. -52 (deepest PnL in quote currency)
37389
+ * ```
37390
+ */
37391
+ async function getPositionMaxDrawdownPnlCost(symbol) {
37392
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_PNL_COST_METHOD_NAME, { symbol });
37393
+ if (!ExecutionContextService.hasContext()) {
37394
+ throw new Error("getPositionMaxDrawdownPnlCost requires an execution context");
37395
+ }
37396
+ if (!MethodContextService.hasContext()) {
37397
+ throw new Error("getPositionMaxDrawdownPnlCost requires a method context");
37398
+ }
37399
+ const { backtest: isBacktest } = bt.executionContextService.context;
37400
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37401
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
37402
+ }
36280
37403
  /**
36281
37404
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
36282
37405
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -36518,6 +37641,8 @@ const LISTEN_SYNC_METHOD_NAME = "event.listenSync";
36518
37641
  const LISTEN_SYNC_ONCE_METHOD_NAME = "event.listenSyncOnce";
36519
37642
  const LISTEN_HIGHEST_PROFIT_METHOD_NAME = "event.listenHighestProfit";
36520
37643
  const LISTEN_HIGHEST_PROFIT_ONCE_METHOD_NAME = "event.listenHighestProfitOnce";
37644
+ const LISTEN_MAX_DRAWDOWN_METHOD_NAME = "event.listenMaxDrawdown";
37645
+ const LISTEN_MAX_DRAWDOWN_ONCE_METHOD_NAME = "event.listenMaxDrawdownOnce";
36521
37646
  /**
36522
37647
  * Subscribes to all signal events with queued async processing.
36523
37648
  *
@@ -37868,6 +38993,47 @@ function listenHighestProfitOnce(filterFn, fn) {
37868
38993
  };
37869
38994
  return disposeFn = listenHighestProfit(wrappedFn);
37870
38995
  }
38996
+ /**
38997
+ * Subscribes to max drawdown events with queued async processing.
38998
+ * Emits when a signal reaches a new maximum drawdown level during its lifecycle.
38999
+ * Events are processed sequentially in order received, even if callback is async.
39000
+ * Uses queued wrapper to prevent concurrent execution of the callback.
39001
+ * Useful for tracking drawdown milestones and implementing dynamic risk management logic.
39002
+ * @param fn - Callback function to handle max drawdown events
39003
+ * @return Unsubscribe function to stop listening to events
39004
+ */
39005
+ function listenMaxDrawdown(fn) {
39006
+ bt.loggerService.log(LISTEN_MAX_DRAWDOWN_METHOD_NAME);
39007
+ const wrappedFn = async (event) => {
39008
+ if (await bt.strategyCoreService.hasPendingSignal(event.backtest, event.symbol, {
39009
+ strategyName: event.strategyName,
39010
+ exchangeName: event.exchangeName,
39011
+ frameName: event.frameName,
39012
+ })) {
39013
+ await fn(event);
39014
+ }
39015
+ };
39016
+ return maxDrawdownSubject.subscribe(functoolsKit.queued(wrappedFn));
39017
+ }
39018
+ /**
39019
+ * Subscribes to filtered max drawdown events with one-time execution.
39020
+ * Listens for events matching the filter predicate, then executes callback once
39021
+ * and automatically unsubscribes. Useful for waiting for specific drawdown conditions.
39022
+ * @param filterFn - Predicate to filter which events trigger the callback
39023
+ * @param fn - Callback function to handle the filtered event (called only once)
39024
+ * @return Unsubscribe function to cancel the listener before it fires
39025
+ */
39026
+ function listenMaxDrawdownOnce(filterFn, fn) {
39027
+ bt.loggerService.log(LISTEN_MAX_DRAWDOWN_ONCE_METHOD_NAME);
39028
+ let disposeFn;
39029
+ const wrappedFn = async (event) => {
39030
+ if (filterFn(event)) {
39031
+ await fn(event);
39032
+ disposeFn && disposeFn();
39033
+ }
39034
+ };
39035
+ return disposeFn = listenMaxDrawdown(wrappedFn);
39036
+ }
37871
39037
 
37872
39038
  const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
37873
39039
  const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
@@ -37897,6 +39063,12 @@ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE = "BacktestUtils.
37897
39063
  const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST = "BacktestUtils.getPositionHighestPnlCost";
37898
39064
  const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN = "BacktestUtils.getPositionHighestProfitBreakeven";
37899
39065
  const BACKTEST_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES = "BacktestUtils.getPositionDrawdownMinutes";
39066
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES = "BacktestUtils.getPositionHighestProfitMinutes";
39067
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES = "BacktestUtils.getPositionMaxDrawdownMinutes";
39068
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "BacktestUtils.getPositionMaxDrawdownPrice";
39069
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "BacktestUtils.getPositionMaxDrawdownTimestamp";
39070
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "BacktestUtils.getPositionMaxDrawdownPnlPercentage";
39071
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "BacktestUtils.getPositionMaxDrawdownPnlCost";
37900
39072
  const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
37901
39073
  const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
37902
39074
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
@@ -38997,6 +40169,175 @@ class BacktestUtils {
38997
40169
  }
38998
40170
  return await bt.strategyCoreService.getPositionDrawdownMinutes(true, symbol, context);
38999
40171
  };
40172
+ /**
40173
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
40174
+ *
40175
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
40176
+ * pulling back from its peak profit level.
40177
+ * Zero when called at the exact moment the peak was set.
40178
+ *
40179
+ * Returns null if no pending signal exists.
40180
+ *
40181
+ * @param symbol - Trading pair symbol
40182
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40183
+ * @returns Minutes since last profit peak, or null if no active position
40184
+ */
40185
+ this.getPositionHighestProfitMinutes = async (symbol, context) => {
40186
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES, {
40187
+ symbol,
40188
+ context,
40189
+ });
40190
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
40191
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
40192
+ {
40193
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40194
+ riskName &&
40195
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
40196
+ riskList &&
40197
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES));
40198
+ actions &&
40199
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES));
40200
+ }
40201
+ return await bt.strategyCoreService.getPositionHighestProfitMinutes(true, symbol, context);
40202
+ };
40203
+ /**
40204
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
40205
+ *
40206
+ * Measures how long ago the deepest drawdown point occurred.
40207
+ * Zero when called at the exact moment the trough was set.
40208
+ *
40209
+ * Returns null if no pending signal exists.
40210
+ *
40211
+ * @param symbol - Trading pair symbol
40212
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40213
+ * @returns Minutes since last drawdown trough, or null if no active position
40214
+ */
40215
+ this.getPositionMaxDrawdownMinutes = async (symbol, context) => {
40216
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES, {
40217
+ symbol,
40218
+ context,
40219
+ });
40220
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
40221
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
40222
+ {
40223
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40224
+ riskName &&
40225
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
40226
+ riskList &&
40227
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES));
40228
+ actions &&
40229
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES));
40230
+ }
40231
+ return await bt.strategyCoreService.getPositionMaxDrawdownMinutes(true, symbol, context);
40232
+ };
40233
+ /**
40234
+ * Returns the worst price reached in the loss direction during this position's life.
40235
+ *
40236
+ * Returns null if no pending signal exists.
40237
+ *
40238
+ * @param symbol - Trading pair symbol
40239
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40240
+ * @returns price or null if no active position
40241
+ */
40242
+ this.getPositionMaxDrawdownPrice = async (symbol, context) => {
40243
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE, {
40244
+ symbol,
40245
+ context,
40246
+ });
40247
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
40248
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
40249
+ {
40250
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40251
+ riskName &&
40252
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
40253
+ riskList &&
40254
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE));
40255
+ actions &&
40256
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE));
40257
+ }
40258
+ return await bt.strategyCoreService.getPositionMaxDrawdownPrice(true, symbol, context);
40259
+ };
40260
+ /**
40261
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
40262
+ *
40263
+ * Returns null if no pending signal exists.
40264
+ *
40265
+ * @param symbol - Trading pair symbol
40266
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40267
+ * @returns timestamp in milliseconds or null if no active position
40268
+ */
40269
+ this.getPositionMaxDrawdownTimestamp = async (symbol, context) => {
40270
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP, {
40271
+ symbol,
40272
+ context,
40273
+ });
40274
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
40275
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
40276
+ {
40277
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40278
+ riskName &&
40279
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
40280
+ riskList &&
40281
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP));
40282
+ actions &&
40283
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP));
40284
+ }
40285
+ return await bt.strategyCoreService.getPositionMaxDrawdownTimestamp(true, symbol, context);
40286
+ };
40287
+ /**
40288
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
40289
+ *
40290
+ * Returns null if no pending signal exists.
40291
+ *
40292
+ * @param symbol - Trading pair symbol
40293
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40294
+ * @returns PnL percentage or null if no active position
40295
+ */
40296
+ this.getPositionMaxDrawdownPnlPercentage = async (symbol, context) => {
40297
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE, {
40298
+ symbol,
40299
+ context,
40300
+ });
40301
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
40302
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
40303
+ {
40304
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40305
+ riskName &&
40306
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
40307
+ riskList &&
40308
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE));
40309
+ actions &&
40310
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE));
40311
+ }
40312
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlPercentage(true, symbol, context);
40313
+ };
40314
+ /**
40315
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
40316
+ *
40317
+ * Returns null if no pending signal exists.
40318
+ *
40319
+ * @param symbol - Trading pair symbol
40320
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40321
+ * @returns PnL cost or null if no active position
40322
+ */
40323
+ this.getPositionMaxDrawdownPnlCost = async (symbol, context) => {
40324
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST, {
40325
+ symbol,
40326
+ context,
40327
+ });
40328
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
40329
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
40330
+ {
40331
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40332
+ riskName &&
40333
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
40334
+ riskList &&
40335
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST));
40336
+ actions &&
40337
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST));
40338
+ }
40339
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlCost(true, symbol, context);
40340
+ };
39000
40341
  /**
39001
40342
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
39002
40343
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -40116,6 +41457,12 @@ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE = "LiveUtils.getPosit
40116
41457
  const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST = "LiveUtils.getPositionHighestPnlCost";
40117
41458
  const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN = "LiveUtils.getPositionHighestProfitBreakeven";
40118
41459
  const LIVE_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES = "LiveUtils.getPositionDrawdownMinutes";
41460
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES = "LiveUtils.getPositionHighestProfitMinutes";
41461
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES = "LiveUtils.getPositionMaxDrawdownMinutes";
41462
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "LiveUtils.getPositionMaxDrawdownPrice";
41463
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "LiveUtils.getPositionMaxDrawdownTimestamp";
41464
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "LiveUtils.getPositionMaxDrawdownPnlPercentage";
41465
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "LiveUtils.getPositionMaxDrawdownPnlCost";
40119
41466
  const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
40120
41467
  const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
40121
41468
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
@@ -41319,6 +42666,199 @@ class LiveUtils {
41319
42666
  frameName: "",
41320
42667
  });
41321
42668
  };
42669
+ /**
42670
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
42671
+ *
42672
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
42673
+ * pulling back from its peak profit level.
42674
+ * Zero when called at the exact moment the peak was set.
42675
+ *
42676
+ * Returns null if no pending signal exists.
42677
+ *
42678
+ * @param symbol - Trading pair symbol
42679
+ * @param context - Execution context with strategyName and exchangeName
42680
+ * @returns Minutes since last profit peak, or null if no active position
42681
+ */
42682
+ this.getPositionHighestProfitMinutes = async (symbol, context) => {
42683
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES, {
42684
+ symbol,
42685
+ context,
42686
+ });
42687
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
42688
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
42689
+ {
42690
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42691
+ riskName &&
42692
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
42693
+ riskList &&
42694
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES));
42695
+ actions &&
42696
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES));
42697
+ }
42698
+ return await bt.strategyCoreService.getPositionHighestProfitMinutes(false, symbol, {
42699
+ strategyName: context.strategyName,
42700
+ exchangeName: context.exchangeName,
42701
+ frameName: "",
42702
+ });
42703
+ };
42704
+ /**
42705
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
42706
+ *
42707
+ * Measures how long ago the deepest drawdown point occurred.
42708
+ * Zero when called at the exact moment the trough was set.
42709
+ *
42710
+ * Returns null if no pending signal exists.
42711
+ *
42712
+ * @param symbol - Trading pair symbol
42713
+ * @param context - Execution context with strategyName and exchangeName
42714
+ * @returns Minutes since last drawdown trough, or null if no active position
42715
+ */
42716
+ this.getPositionMaxDrawdownMinutes = async (symbol, context) => {
42717
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES, {
42718
+ symbol,
42719
+ context,
42720
+ });
42721
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
42722
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
42723
+ {
42724
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42725
+ riskName &&
42726
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
42727
+ riskList &&
42728
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES));
42729
+ actions &&
42730
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES));
42731
+ }
42732
+ return await bt.strategyCoreService.getPositionMaxDrawdownMinutes(false, symbol, {
42733
+ strategyName: context.strategyName,
42734
+ exchangeName: context.exchangeName,
42735
+ frameName: "",
42736
+ });
42737
+ };
42738
+ /**
42739
+ * Returns the worst price reached in the loss direction during this position's life.
42740
+ *
42741
+ * Returns null if no pending signal exists.
42742
+ *
42743
+ * @param symbol - Trading pair symbol
42744
+ * @param context - Execution context with strategyName and exchangeName
42745
+ * @returns price or null if no active position
42746
+ */
42747
+ this.getPositionMaxDrawdownPrice = async (symbol, context) => {
42748
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE, {
42749
+ symbol,
42750
+ context,
42751
+ });
42752
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
42753
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
42754
+ {
42755
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42756
+ riskName &&
42757
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
42758
+ riskList &&
42759
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE));
42760
+ actions &&
42761
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE));
42762
+ }
42763
+ return await bt.strategyCoreService.getPositionMaxDrawdownPrice(false, symbol, {
42764
+ strategyName: context.strategyName,
42765
+ exchangeName: context.exchangeName,
42766
+ frameName: "",
42767
+ });
42768
+ };
42769
+ /**
42770
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
42771
+ *
42772
+ * Returns null if no pending signal exists.
42773
+ *
42774
+ * @param symbol - Trading pair symbol
42775
+ * @param context - Execution context with strategyName and exchangeName
42776
+ * @returns timestamp in milliseconds or null if no active position
42777
+ */
42778
+ this.getPositionMaxDrawdownTimestamp = async (symbol, context) => {
42779
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP, {
42780
+ symbol,
42781
+ context,
42782
+ });
42783
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
42784
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
42785
+ {
42786
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42787
+ riskName &&
42788
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
42789
+ riskList &&
42790
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP));
42791
+ actions &&
42792
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP));
42793
+ }
42794
+ return await bt.strategyCoreService.getPositionMaxDrawdownTimestamp(false, symbol, {
42795
+ strategyName: context.strategyName,
42796
+ exchangeName: context.exchangeName,
42797
+ frameName: "",
42798
+ });
42799
+ };
42800
+ /**
42801
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
42802
+ *
42803
+ * Returns null if no pending signal exists.
42804
+ *
42805
+ * @param symbol - Trading pair symbol
42806
+ * @param context - Execution context with strategyName and exchangeName
42807
+ * @returns PnL percentage or null if no active position
42808
+ */
42809
+ this.getPositionMaxDrawdownPnlPercentage = async (symbol, context) => {
42810
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE, {
42811
+ symbol,
42812
+ context,
42813
+ });
42814
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
42815
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
42816
+ {
42817
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42818
+ riskName &&
42819
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
42820
+ riskList &&
42821
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE));
42822
+ actions &&
42823
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE));
42824
+ }
42825
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlPercentage(false, symbol, {
42826
+ strategyName: context.strategyName,
42827
+ exchangeName: context.exchangeName,
42828
+ frameName: "",
42829
+ });
42830
+ };
42831
+ /**
42832
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
42833
+ *
42834
+ * Returns null if no pending signal exists.
42835
+ *
42836
+ * @param symbol - Trading pair symbol
42837
+ * @param context - Execution context with strategyName and exchangeName
42838
+ * @returns PnL cost or null if no active position
42839
+ */
42840
+ this.getPositionMaxDrawdownPnlCost = async (symbol, context) => {
42841
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST, {
42842
+ symbol,
42843
+ context,
42844
+ });
42845
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
42846
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
42847
+ {
42848
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42849
+ riskName &&
42850
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
42851
+ riskList &&
42852
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST));
42853
+ actions &&
42854
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST));
42855
+ }
42856
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlCost(false, symbol, {
42857
+ strategyName: context.strategyName,
42858
+ exchangeName: context.exchangeName,
42859
+ frameName: "",
42860
+ });
42861
+ };
41322
42862
  /**
41323
42863
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
41324
42864
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -47756,6 +49296,98 @@ class HighestProfitUtils {
47756
49296
  */
47757
49297
  const HighestProfit = new HighestProfitUtils();
47758
49298
 
49299
+ const MAX_DRAWDOWN_METHOD_NAME_GET_DATA = "MaxDrawdownUtils.getData";
49300
+ const MAX_DRAWDOWN_METHOD_NAME_GET_REPORT = "MaxDrawdownUtils.getReport";
49301
+ const MAX_DRAWDOWN_METHOD_NAME_DUMP = "MaxDrawdownUtils.dump";
49302
+ /**
49303
+ * Utility class for accessing max drawdown reports and statistics.
49304
+ *
49305
+ * Provides static-like methods (via singleton instance) to retrieve data
49306
+ * accumulated by MaxDrawdownMarkdownService from maxDrawdownSubject events.
49307
+ *
49308
+ * @example
49309
+ * ```typescript
49310
+ * import { MaxDrawdown } from "backtest-kit";
49311
+ *
49312
+ * const stats = await MaxDrawdown.getData("BTCUSDT", { strategyName, exchangeName, frameName });
49313
+ * const report = await MaxDrawdown.getReport("BTCUSDT", { strategyName, exchangeName, frameName });
49314
+ * await MaxDrawdown.dump("BTCUSDT", { strategyName, exchangeName, frameName });
49315
+ * ```
49316
+ */
49317
+ class MaxDrawdownUtils {
49318
+ constructor() {
49319
+ /**
49320
+ * Retrieves statistical data from accumulated max drawdown events.
49321
+ *
49322
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
49323
+ * @param context - Execution context
49324
+ * @param backtest - Whether to query backtest data
49325
+ * @returns Promise resolving to MaxDrawdownStatisticsModel
49326
+ */
49327
+ this.getData = async (symbol, context, backtest = false) => {
49328
+ bt.loggerService.info(MAX_DRAWDOWN_METHOD_NAME_GET_DATA, { symbol, strategyName: context.strategyName });
49329
+ bt.strategyValidationService.validate(context.strategyName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA);
49330
+ bt.exchangeValidationService.validate(context.exchangeName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA);
49331
+ context.frameName && bt.frameValidationService.validate(context.frameName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA);
49332
+ {
49333
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
49334
+ riskName && bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA);
49335
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA));
49336
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA));
49337
+ }
49338
+ return await bt.maxDrawdownMarkdownService.getData(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
49339
+ };
49340
+ /**
49341
+ * Generates a markdown report with all max drawdown events for a symbol-strategy pair.
49342
+ *
49343
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
49344
+ * @param context - Execution context
49345
+ * @param backtest - Whether to query backtest data
49346
+ * @param columns - Optional column configuration
49347
+ * @returns Promise resolving to markdown formatted report string
49348
+ */
49349
+ this.getReport = async (symbol, context, backtest = false, columns) => {
49350
+ bt.loggerService.info(MAX_DRAWDOWN_METHOD_NAME_GET_REPORT, { symbol, strategyName: context.strategyName });
49351
+ bt.strategyValidationService.validate(context.strategyName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT);
49352
+ bt.exchangeValidationService.validate(context.exchangeName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT);
49353
+ context.frameName && bt.frameValidationService.validate(context.frameName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT);
49354
+ {
49355
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
49356
+ riskName && bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT);
49357
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT));
49358
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT));
49359
+ }
49360
+ return await bt.maxDrawdownMarkdownService.getReport(symbol, context.strategyName, context.exchangeName, context.frameName, backtest, columns);
49361
+ };
49362
+ /**
49363
+ * Generates and saves a markdown report to file.
49364
+ *
49365
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
49366
+ * @param context - Execution context
49367
+ * @param backtest - Whether to query backtest data
49368
+ * @param path - Output directory path (default: "./dump/max_drawdown")
49369
+ * @param columns - Optional column configuration
49370
+ */
49371
+ this.dump = async (symbol, context, backtest = false, path, columns) => {
49372
+ bt.loggerService.info(MAX_DRAWDOWN_METHOD_NAME_DUMP, { symbol, strategyName: context.strategyName, path });
49373
+ bt.strategyValidationService.validate(context.strategyName, MAX_DRAWDOWN_METHOD_NAME_DUMP);
49374
+ bt.exchangeValidationService.validate(context.exchangeName, MAX_DRAWDOWN_METHOD_NAME_DUMP);
49375
+ context.frameName && bt.frameValidationService.validate(context.frameName, MAX_DRAWDOWN_METHOD_NAME_DUMP);
49376
+ {
49377
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
49378
+ riskName && bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_DUMP);
49379
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_DUMP));
49380
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, MAX_DRAWDOWN_METHOD_NAME_DUMP));
49381
+ }
49382
+ await bt.maxDrawdownMarkdownService.dump(symbol, context.strategyName, context.exchangeName, context.frameName, backtest, path, columns);
49383
+ };
49384
+ }
49385
+ }
49386
+ /**
49387
+ * Global singleton instance of MaxDrawdownUtils.
49388
+ */
49389
+ const MaxDrawdown = new MaxDrawdownUtils();
49390
+
47759
49391
  /**
47760
49392
  * Utility class containing predefined trading constants for take-profit and stop-loss levels.
47761
49393
  *
@@ -52637,6 +54269,7 @@ exports.Log = Log;
52637
54269
  exports.Markdown = Markdown;
52638
54270
  exports.MarkdownFileBase = MarkdownFileBase;
52639
54271
  exports.MarkdownFolderBase = MarkdownFolderBase;
54272
+ exports.MaxDrawdown = MaxDrawdown;
52640
54273
  exports.Memory = Memory;
52641
54274
  exports.MethodContextService = MethodContextService;
52642
54275
  exports.Notification = Notification;
@@ -52727,11 +54360,17 @@ exports.getPositionEstimateMinutes = getPositionEstimateMinutes;
52727
54360
  exports.getPositionHighestPnlCost = getPositionHighestPnlCost;
52728
54361
  exports.getPositionHighestPnlPercentage = getPositionHighestPnlPercentage;
52729
54362
  exports.getPositionHighestProfitBreakeven = getPositionHighestProfitBreakeven;
54363
+ exports.getPositionHighestProfitMinutes = getPositionHighestProfitMinutes;
52730
54364
  exports.getPositionHighestProfitPrice = getPositionHighestProfitPrice;
52731
54365
  exports.getPositionHighestProfitTimestamp = getPositionHighestProfitTimestamp;
52732
54366
  exports.getPositionInvestedCost = getPositionInvestedCost;
52733
54367
  exports.getPositionInvestedCount = getPositionInvestedCount;
52734
54368
  exports.getPositionLevels = getPositionLevels;
54369
+ exports.getPositionMaxDrawdownMinutes = getPositionMaxDrawdownMinutes;
54370
+ exports.getPositionMaxDrawdownPnlCost = getPositionMaxDrawdownPnlCost;
54371
+ exports.getPositionMaxDrawdownPnlPercentage = getPositionMaxDrawdownPnlPercentage;
54372
+ exports.getPositionMaxDrawdownPrice = getPositionMaxDrawdownPrice;
54373
+ exports.getPositionMaxDrawdownTimestamp = getPositionMaxDrawdownTimestamp;
52735
54374
  exports.getPositionPartialOverlap = getPositionPartialOverlap;
52736
54375
  exports.getPositionPartials = getPositionPartials;
52737
54376
  exports.getPositionPnlCost = getPositionPnlCost;
@@ -52774,6 +54413,8 @@ exports.listenError = listenError;
52774
54413
  exports.listenExit = listenExit;
52775
54414
  exports.listenHighestProfit = listenHighestProfit;
52776
54415
  exports.listenHighestProfitOnce = listenHighestProfitOnce;
54416
+ exports.listenMaxDrawdown = listenMaxDrawdown;
54417
+ exports.listenMaxDrawdownOnce = listenMaxDrawdownOnce;
52777
54418
  exports.listenPartialLossAvailable = listenPartialLossAvailable;
52778
54419
  exports.listenPartialLossAvailableOnce = listenPartialLossAvailableOnce;
52779
54420
  exports.listenPartialProfitAvailable = listenPartialProfitAvailable;