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.mjs CHANGED
@@ -108,6 +108,7 @@ const markdownServices$1 = {
108
108
  strategyMarkdownService: Symbol('strategyMarkdownService'),
109
109
  syncMarkdownService: Symbol('syncMarkdownService'),
110
110
  highestProfitMarkdownService: Symbol('highestProfitMarkdownService'),
111
+ maxDrawdownMarkdownService: Symbol('maxDrawdownMarkdownService'),
111
112
  };
112
113
  const reportServices$1 = {
113
114
  backtestReportService: Symbol('backtestReportService'),
@@ -122,6 +123,7 @@ const reportServices$1 = {
122
123
  strategyReportService: Symbol('strategyReportService'),
123
124
  syncReportService: Symbol('syncReportService'),
124
125
  highestProfitReportService: Symbol('highestProfitReportService'),
126
+ maxDrawdownReportService: Symbol('maxDrawdownReportService'),
125
127
  };
126
128
  const validationServices$1 = {
127
129
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -453,6 +455,13 @@ const GLOBAL_CONFIG = {
453
455
  * Default: 250 events
454
456
  */
455
457
  CC_MAX_HIGHEST_PROFIT_MARKDOWN_ROWS: 250,
458
+ /**
459
+ * Maximum number of events to keep in max drawdown markdown report storage.
460
+ * Older events are removed (FIFO) when this limit is exceeded.
461
+ *
462
+ * Default: 250 events
463
+ */
464
+ CC_MAX_MAX_DRAWDOWN_MARKDOWN_ROWS: 250,
456
465
  /**
457
466
  * Maximum number of events to keep in live markdown report storage.
458
467
  * Older events are removed (FIFO) when this limit is exceeded.
@@ -727,6 +736,12 @@ const backtestScheduleOpenSubject = new Subject();
727
736
  * Allows users to track profit milestones and implement custom management logic based on profit levels.
728
737
  */
729
738
  const highestProfitSubject = new Subject();
739
+ /**
740
+ * Max drawdown emitter for real-time risk tracking.
741
+ * Emits updates on the maximum drawdown experienced for an open position.
742
+ * Allows users to track drawdown levels and implement custom risk management logic based on drawdown thresholds.
743
+ */
744
+ const maxDrawdownSubject = new Subject();
730
745
 
731
746
  var emitters = /*#__PURE__*/Object.freeze({
732
747
  __proto__: null,
@@ -739,6 +754,7 @@ var emitters = /*#__PURE__*/Object.freeze({
739
754
  errorEmitter: errorEmitter,
740
755
  exitEmitter: exitEmitter,
741
756
  highestProfitSubject: highestProfitSubject,
757
+ maxDrawdownSubject: maxDrawdownSubject,
742
758
  partialLossSubject: partialLossSubject,
743
759
  partialProfitSubject: partialProfitSubject,
744
760
  performanceEmitter: performanceEmitter,
@@ -4868,6 +4884,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
4868
4884
  _isScheduled: false,
4869
4885
  _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4870
4886
  _peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4887
+ _fall: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4871
4888
  };
4872
4889
  // Валидируем сигнал перед возвратом
4873
4890
  validatePendingSignal(signalRow, currentPrice);
@@ -4893,6 +4910,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
4893
4910
  _isScheduled: true,
4894
4911
  _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4895
4912
  _peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4913
+ _fall: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4896
4914
  };
4897
4915
  // Валидируем сигнал перед возвратом
4898
4916
  validateScheduledSignal(scheduledSignalRow, currentPrice);
@@ -4915,6 +4933,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
4915
4933
  _isScheduled: false,
4916
4934
  _entry: [{ price: currentPrice, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4917
4935
  _peak: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4936
+ _fall: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4918
4937
  };
4919
4938
  // Валидируем сигнал перед возвратом
4920
4939
  validatePendingSignal(signalRow, currentPrice);
@@ -5626,6 +5645,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
5626
5645
  pendingAt: activationTime,
5627
5646
  _isScheduled: false,
5628
5647
  _peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
5648
+ _fall: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
5629
5649
  };
5630
5650
  // Sync open: if external system rejects — cancel scheduled signal instead of opening
5631
5651
  const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
@@ -6300,6 +6320,15 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice, backt
6300
6320
  const slDistance = effectivePriceOpen - effectiveStopLoss;
6301
6321
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
6302
6322
  percentSl = Math.min(progressPercent, 100);
6323
+ if (currentPrice < signal._fall.price) {
6324
+ const { pnl } = TO_PUBLIC_SIGNAL(signal, currentPrice);
6325
+ signal._fall = { price: currentPrice, timestamp: currentTime, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
6326
+ if (self.params.callbacks?.onWrite) {
6327
+ self.params.callbacks.onWrite(signal.symbol, signal, backtest);
6328
+ }
6329
+ !backtest && await PersistSignalAdapter.writeSignalData(signal, self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
6330
+ await self.params.onMaxDrawdown(TO_PUBLIC_SIGNAL(signal, currentPrice), currentPrice, currentTime);
6331
+ }
6303
6332
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
6304
6333
  }
6305
6334
  }
@@ -6333,6 +6362,15 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice, backt
6333
6362
  const slDistance = effectiveStopLoss - effectivePriceOpen;
6334
6363
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
6335
6364
  percentSl = Math.min(progressPercent, 100);
6365
+ if (currentPrice > signal._fall.price) {
6366
+ const { pnl } = TO_PUBLIC_SIGNAL(signal, currentPrice);
6367
+ signal._fall = { price: currentPrice, timestamp: currentTime, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
6368
+ if (self.params.callbacks?.onWrite) {
6369
+ self.params.callbacks.onWrite(signal.symbol, signal, backtest);
6370
+ }
6371
+ !backtest && await PersistSignalAdapter.writeSignalData(signal, self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
6372
+ await self.params.onMaxDrawdown(TO_PUBLIC_SIGNAL(signal, currentPrice), currentPrice, currentTime);
6373
+ }
6336
6374
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
6337
6375
  }
6338
6376
  }
@@ -6453,6 +6491,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
6453
6491
  pendingAt: activationTime,
6454
6492
  _isScheduled: false,
6455
6493
  _peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
6494
+ _fall: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
6456
6495
  };
6457
6496
  // Sync open: if external system rejects — cancel scheduled signal instead of opening
6458
6497
  const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
@@ -6639,6 +6678,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
6639
6678
  pendingAt: candle.timestamp,
6640
6679
  _isScheduled: false,
6641
6680
  _peak: { price: activatedSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage: 0, pnlCost: 0 },
6681
+ _fall: { price: activatedSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage: 0, pnlCost: 0 },
6642
6682
  };
6643
6683
  // Sync open: if external system rejects — cancel scheduled signal instead of opening
6644
6684
  const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(candle.timestamp, pendingSignal.priceOpen, pendingSignal, self);
@@ -6863,6 +6903,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
6863
6903
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
6864
6904
  const slDistance = effectivePriceOpen - effectiveStopLoss;
6865
6905
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
6906
+ if (averagePrice < signal._fall.price) {
6907
+ const { pnl } = TO_PUBLIC_SIGNAL(signal, averagePrice);
6908
+ signal._fall = { price: averagePrice, timestamp: currentCandleTimestamp, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
6909
+ if (self.params.callbacks?.onWrite) {
6910
+ self.params.callbacks.onWrite(signal.symbol, signal, true);
6911
+ }
6912
+ await self.params.onMaxDrawdown(TO_PUBLIC_SIGNAL(signal, averagePrice), averagePrice, currentCandleTimestamp);
6913
+ }
6866
6914
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
6867
6915
  }
6868
6916
  }
@@ -6893,6 +6941,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
6893
6941
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
6894
6942
  const slDistance = effectiveStopLoss - effectivePriceOpen;
6895
6943
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
6944
+ if (averagePrice > signal._fall.price) {
6945
+ const { pnl } = TO_PUBLIC_SIGNAL(signal, averagePrice);
6946
+ signal._fall = { price: averagePrice, timestamp: currentCandleTimestamp, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
6947
+ if (self.params.callbacks?.onWrite) {
6948
+ self.params.callbacks.onWrite(signal.symbol, signal, true);
6949
+ }
6950
+ await self.params.onMaxDrawdown(TO_PUBLIC_SIGNAL(signal, averagePrice), averagePrice, currentCandleTimestamp);
6951
+ }
6896
6952
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
6897
6953
  }
6898
6954
  }
@@ -7614,6 +7670,104 @@ class ClientStrategy {
7614
7670
  }
7615
7671
  return Math.floor((timestamp - this._pendingSignal._peak.timestamp) / 60000);
7616
7672
  }
7673
+ /**
7674
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
7675
+ *
7676
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
7677
+ * pulling back from its peak profit level.
7678
+ *
7679
+ * Returns null if no pending signal exists.
7680
+ *
7681
+ * @param symbol - Trading pair symbol
7682
+ * @param timestamp - Current Unix timestamp in milliseconds
7683
+ * @returns Promise resolving to minutes since last profit peak or null
7684
+ */
7685
+ async getPositionHighestProfitMinutes(symbol, timestamp) {
7686
+ this.params.logger.debug("ClientStrategy getPositionHighestProfitMinutes", { symbol });
7687
+ if (!this._pendingSignal) {
7688
+ return null;
7689
+ }
7690
+ return Math.floor((timestamp - this._pendingSignal._peak.timestamp) / 60000);
7691
+ }
7692
+ /**
7693
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
7694
+ *
7695
+ * Measures how long ago the deepest drawdown point occurred.
7696
+ * Zero when called at the exact moment the trough was set.
7697
+ *
7698
+ * Returns null if no pending signal exists.
7699
+ *
7700
+ * @param symbol - Trading pair symbol
7701
+ * @param timestamp - Current Unix timestamp in milliseconds
7702
+ * @returns Promise resolving to minutes since last drawdown trough or null
7703
+ */
7704
+ async getPositionMaxDrawdownMinutes(symbol, timestamp) {
7705
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownMinutes", { symbol });
7706
+ if (!this._pendingSignal) {
7707
+ return null;
7708
+ }
7709
+ return Math.floor((timestamp - this._pendingSignal._fall.timestamp) / 60000);
7710
+ }
7711
+ /**
7712
+ * Returns the worst price reached in the loss direction during this position's life.
7713
+ *
7714
+ * Returns null if no pending signal exists.
7715
+ *
7716
+ * @param symbol - Trading pair symbol
7717
+ * @returns Promise resolving to price or null
7718
+ */
7719
+ async getPositionMaxDrawdownPrice(symbol) {
7720
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownPrice", { symbol });
7721
+ if (!this._pendingSignal) {
7722
+ return null;
7723
+ }
7724
+ return this._pendingSignal._fall.price;
7725
+ }
7726
+ /**
7727
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
7728
+ *
7729
+ * Returns null if no pending signal exists.
7730
+ *
7731
+ * @param symbol - Trading pair symbol
7732
+ * @returns Promise resolving to timestamp in milliseconds or null
7733
+ */
7734
+ async getPositionMaxDrawdownTimestamp(symbol) {
7735
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownTimestamp", { symbol });
7736
+ if (!this._pendingSignal) {
7737
+ return null;
7738
+ }
7739
+ return this._pendingSignal._fall.timestamp;
7740
+ }
7741
+ /**
7742
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
7743
+ *
7744
+ * Returns null if no pending signal exists.
7745
+ *
7746
+ * @param symbol - Trading pair symbol
7747
+ * @returns Promise resolving to PnL percentage or null
7748
+ */
7749
+ async getPositionMaxDrawdownPnlPercentage(symbol) {
7750
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownPnlPercentage", { symbol });
7751
+ if (!this._pendingSignal) {
7752
+ return null;
7753
+ }
7754
+ return this._pendingSignal._fall.pnlPercentage;
7755
+ }
7756
+ /**
7757
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
7758
+ *
7759
+ * Returns null if no pending signal exists.
7760
+ *
7761
+ * @param symbol - Trading pair symbol
7762
+ * @returns Promise resolving to PnL cost or null
7763
+ */
7764
+ async getPositionMaxDrawdownPnlCost(symbol) {
7765
+ this.params.logger.debug("ClientStrategy getPositionMaxDrawdownPnlCost", { symbol });
7766
+ if (!this._pendingSignal) {
7767
+ return null;
7768
+ }
7769
+ return this._pendingSignal._fall.pnlCost;
7770
+ }
7617
7771
  /**
7618
7772
  * Performs a single tick of strategy execution.
7619
7773
  *
@@ -7790,6 +7944,7 @@ class ClientStrategy {
7790
7944
  pendingAt: currentTime,
7791
7945
  _isScheduled: false,
7792
7946
  _peak: { price: activatedSignal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
7947
+ _fall: { price: activatedSignal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
7793
7948
  };
7794
7949
  const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(currentTime, currentPrice, pendingSignal, this);
7795
7950
  if (!syncOpenAllowed) {
@@ -9906,7 +10061,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
9906
10061
  * @param backtest - Whether running in backtest mode
9907
10062
  * @returns Unique string key for memoization
9908
10063
  */
9909
- const CREATE_KEY_FN$r = (symbol, strategyName, exchangeName, frameName, backtest) => {
10064
+ const CREATE_KEY_FN$s = (symbol, strategyName, exchangeName, frameName, backtest) => {
9910
10065
  const parts = [symbol, strategyName, exchangeName];
9911
10066
  if (frameName)
9912
10067
  parts.push(frameName);
@@ -10067,6 +10222,42 @@ const CREATE_HIGHEST_PROFIT_FN = (self, strategyName, exchangeName, frameName, i
10067
10222
  },
10068
10223
  defaultValue: null,
10069
10224
  });
10225
+ /**
10226
+ * Creates a callback function for emitting max drawdown updates to maxDrawdownSubject.
10227
+ * Called by ClientStrategy when the maximum drawdown for an open position is updated.
10228
+ * Emits MaxDrawdownContract event to all subscribers with the current price and timestamp.
10229
+ * Used for real-time risk tracking and management logic based on drawdown levels.
10230
+ * @param self - Reference to StrategyConnectionService instance
10231
+ * @param strategyName - Name of the strategy
10232
+ * @param exchangeName - Name of the exchange
10233
+ * @param frameName - Name of the frame
10234
+ * @param isBacktest - Flag indicating if the operation is for backtesting
10235
+ * @return Callback function for max drawdown updates
10236
+ */
10237
+ const CREATE_MAX_DRAWDOWN_FN = (self, strategyName, exchangeName, frameName, isBacktest) => trycatch(async (signal, currentPrice, timestamp) => {
10238
+ await maxDrawdownSubject.next({
10239
+ symbol: signal.symbol,
10240
+ signal,
10241
+ currentPrice,
10242
+ timestamp,
10243
+ strategyName,
10244
+ exchangeName,
10245
+ frameName,
10246
+ backtest: isBacktest,
10247
+ });
10248
+ }, {
10249
+ fallback: (error) => {
10250
+ const message = "StrategyConnectionService CREATE_MAX_DRAWDOWN_FN thrown";
10251
+ const payload = {
10252
+ error: errorData(error),
10253
+ message: getErrorMessage(error),
10254
+ };
10255
+ bt.loggerService.warn(message, payload);
10256
+ console.warn(message, payload);
10257
+ errorEmitter.next(error);
10258
+ },
10259
+ defaultValue: null,
10260
+ });
10070
10261
  /**
10071
10262
  * Creates a callback function for emitting dispose events.
10072
10263
  *
@@ -10137,7 +10328,7 @@ class StrategyConnectionService {
10137
10328
  * @param backtest - Whether running in backtest mode
10138
10329
  * @returns Configured ClientStrategy instance
10139
10330
  */
10140
- this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$r(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
10331
+ this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$s(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
10141
10332
  const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
10142
10333
  return new ClientStrategy({
10143
10334
  symbol,
@@ -10166,6 +10357,7 @@ class StrategyConnectionService {
10166
10357
  onCommit: CREATE_COMMIT_FN(),
10167
10358
  onSignalSync: CREATE_SYNC_FN(this, strategyName, exchangeName, frameName, backtest),
10168
10359
  onHighestProfit: CREATE_HIGHEST_PROFIT_FN(this, strategyName, exchangeName, frameName, backtest),
10360
+ onMaxDrawdown: CREATE_MAX_DRAWDOWN_FN(this, strategyName, exchangeName, frameName, backtest),
10169
10361
  });
10170
10362
  });
10171
10363
  /**
@@ -10769,6 +10961,130 @@ class StrategyConnectionService {
10769
10961
  const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
10770
10962
  return await strategy.getPositionDrawdownMinutes(symbol, timestamp);
10771
10963
  };
10964
+ /**
10965
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
10966
+ *
10967
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
10968
+ * pulling back from its peak profit level.
10969
+ *
10970
+ * Resolves current timestamp via timeMetaService and delegates to
10971
+ * ClientStrategy.getPositionHighestProfitMinutes().
10972
+ * Returns null if no pending signal exists.
10973
+ *
10974
+ * @param backtest - Whether running in backtest mode
10975
+ * @param symbol - Trading pair symbol
10976
+ * @param context - Execution context with strategyName, exchangeName, frameName
10977
+ * @returns Promise resolving to minutes since last profit peak or null
10978
+ */
10979
+ this.getPositionHighestProfitMinutes = async (backtest, symbol, context) => {
10980
+ this.loggerService.log("strategyConnectionService getPositionHighestProfitMinutes", {
10981
+ symbol,
10982
+ context,
10983
+ });
10984
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
10985
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
10986
+ return await strategy.getPositionHighestProfitMinutes(symbol, timestamp);
10987
+ };
10988
+ /**
10989
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
10990
+ *
10991
+ * Measures how long ago the deepest drawdown point occurred.
10992
+ * Zero when called at the exact moment the trough was set.
10993
+ *
10994
+ * Resolves current timestamp via timeMetaService and delegates to
10995
+ * ClientStrategy.getPositionMaxDrawdownMinutes().
10996
+ * Returns null if no pending signal exists.
10997
+ *
10998
+ * @param backtest - Whether running in backtest mode
10999
+ * @param symbol - Trading pair symbol
11000
+ * @param context - Execution context with strategyName, exchangeName, frameName
11001
+ * @returns Promise resolving to minutes since last drawdown trough or null
11002
+ */
11003
+ this.getPositionMaxDrawdownMinutes = async (backtest, symbol, context) => {
11004
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownMinutes", {
11005
+ symbol,
11006
+ context,
11007
+ });
11008
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11009
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
11010
+ return await strategy.getPositionMaxDrawdownMinutes(symbol, timestamp);
11011
+ };
11012
+ /**
11013
+ * Returns the worst price reached in the loss direction during this position's life.
11014
+ *
11015
+ * Delegates to ClientStrategy.getPositionMaxDrawdownPrice().
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 price or null
11022
+ */
11023
+ this.getPositionMaxDrawdownPrice = async (backtest, symbol, context) => {
11024
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownPrice", {
11025
+ symbol,
11026
+ context,
11027
+ });
11028
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11029
+ return await strategy.getPositionMaxDrawdownPrice(symbol);
11030
+ };
11031
+ /**
11032
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
11033
+ *
11034
+ * Delegates to ClientStrategy.getPositionMaxDrawdownTimestamp().
11035
+ * Returns null if no pending signal exists.
11036
+ *
11037
+ * @param backtest - Whether running in backtest mode
11038
+ * @param symbol - Trading pair symbol
11039
+ * @param context - Execution context with strategyName, exchangeName, frameName
11040
+ * @returns Promise resolving to timestamp in milliseconds or null
11041
+ */
11042
+ this.getPositionMaxDrawdownTimestamp = async (backtest, symbol, context) => {
11043
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownTimestamp", {
11044
+ symbol,
11045
+ context,
11046
+ });
11047
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11048
+ return await strategy.getPositionMaxDrawdownTimestamp(symbol);
11049
+ };
11050
+ /**
11051
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
11052
+ *
11053
+ * Delegates to ClientStrategy.getPositionMaxDrawdownPnlPercentage().
11054
+ * Returns null if no pending signal exists.
11055
+ *
11056
+ * @param backtest - Whether running in backtest mode
11057
+ * @param symbol - Trading pair symbol
11058
+ * @param context - Execution context with strategyName, exchangeName, frameName
11059
+ * @returns Promise resolving to PnL percentage or null
11060
+ */
11061
+ this.getPositionMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
11062
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownPnlPercentage", {
11063
+ symbol,
11064
+ context,
11065
+ });
11066
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11067
+ return await strategy.getPositionMaxDrawdownPnlPercentage(symbol);
11068
+ };
11069
+ /**
11070
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
11071
+ *
11072
+ * Delegates to ClientStrategy.getPositionMaxDrawdownPnlCost().
11073
+ * Returns null if no pending signal exists.
11074
+ *
11075
+ * @param backtest - Whether running in backtest mode
11076
+ * @param symbol - Trading pair symbol
11077
+ * @param context - Execution context with strategyName, exchangeName, frameName
11078
+ * @returns Promise resolving to PnL cost or null
11079
+ */
11080
+ this.getPositionMaxDrawdownPnlCost = async (backtest, symbol, context) => {
11081
+ this.loggerService.log("strategyConnectionService getPositionMaxDrawdownPnlCost", {
11082
+ symbol,
11083
+ context,
11084
+ });
11085
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11086
+ return await strategy.getPositionMaxDrawdownPnlCost(symbol);
11087
+ };
10772
11088
  /**
10773
11089
  * Disposes the ClientStrategy instance for the given context.
10774
11090
  *
@@ -10807,7 +11123,7 @@ class StrategyConnectionService {
10807
11123
  }
10808
11124
  return;
10809
11125
  }
10810
- const key = CREATE_KEY_FN$r(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
11126
+ const key = CREATE_KEY_FN$s(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
10811
11127
  if (!this.getStrategy.has(key)) {
10812
11128
  return;
10813
11129
  }
@@ -11983,7 +12299,7 @@ class ClientRisk {
11983
12299
  * @param backtest - Whether running in backtest mode
11984
12300
  * @returns Unique string key for memoization
11985
12301
  */
11986
- const CREATE_KEY_FN$q = (riskName, exchangeName, frameName, backtest) => {
12302
+ const CREATE_KEY_FN$r = (riskName, exchangeName, frameName, backtest) => {
11987
12303
  const parts = [riskName, exchangeName];
11988
12304
  if (frameName)
11989
12305
  parts.push(frameName);
@@ -12082,7 +12398,7 @@ class RiskConnectionService {
12082
12398
  * @param backtest - True if backtest mode, false if live mode
12083
12399
  * @returns Configured ClientRisk instance
12084
12400
  */
12085
- this.getRisk = memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$q(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
12401
+ this.getRisk = memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$r(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
12086
12402
  const schema = this.riskSchemaService.get(riskName);
12087
12403
  return new ClientRisk({
12088
12404
  ...schema,
@@ -12150,7 +12466,7 @@ class RiskConnectionService {
12150
12466
  payload,
12151
12467
  });
12152
12468
  if (payload) {
12153
- const key = CREATE_KEY_FN$q(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
12469
+ const key = CREATE_KEY_FN$r(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
12154
12470
  this.getRisk.clear(key);
12155
12471
  }
12156
12472
  else {
@@ -13657,7 +13973,7 @@ class ClientAction {
13657
13973
  * @param backtest - Whether running in backtest mode
13658
13974
  * @returns Unique string key for memoization
13659
13975
  */
13660
- const CREATE_KEY_FN$p = (actionName, strategyName, exchangeName, frameName, backtest) => {
13976
+ const CREATE_KEY_FN$q = (actionName, strategyName, exchangeName, frameName, backtest) => {
13661
13977
  const parts = [actionName, strategyName, exchangeName];
13662
13978
  if (frameName)
13663
13979
  parts.push(frameName);
@@ -13708,7 +14024,7 @@ class ActionConnectionService {
13708
14024
  * @param backtest - True if backtest mode, false if live mode
13709
14025
  * @returns Configured ClientAction instance
13710
14026
  */
13711
- this.getAction = memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$p(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
14027
+ this.getAction = memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$q(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
13712
14028
  const schema = this.actionSchemaService.get(actionName);
13713
14029
  return new ClientAction({
13714
14030
  ...schema,
@@ -13918,7 +14234,7 @@ class ActionConnectionService {
13918
14234
  await Promise.all(actions.map(async (action) => await action.dispose()));
13919
14235
  return;
13920
14236
  }
13921
- const key = CREATE_KEY_FN$p(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
14237
+ const key = CREATE_KEY_FN$q(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13922
14238
  if (!this.getAction.has(key)) {
13923
14239
  return;
13924
14240
  }
@@ -13936,7 +14252,7 @@ const METHOD_NAME_VALIDATE$2 = "exchangeCoreService validate";
13936
14252
  * @param exchangeName - Exchange name
13937
14253
  * @returns Unique string key for memoization
13938
14254
  */
13939
- const CREATE_KEY_FN$o = (exchangeName) => {
14255
+ const CREATE_KEY_FN$p = (exchangeName) => {
13940
14256
  return exchangeName;
13941
14257
  };
13942
14258
  /**
@@ -13960,7 +14276,7 @@ class ExchangeCoreService {
13960
14276
  * @param exchangeName - Name of the exchange to validate
13961
14277
  * @returns Promise that resolves when validation is complete
13962
14278
  */
13963
- this.validate = memoize(([exchangeName]) => CREATE_KEY_FN$o(exchangeName), async (exchangeName) => {
14279
+ this.validate = memoize(([exchangeName]) => CREATE_KEY_FN$p(exchangeName), async (exchangeName) => {
13964
14280
  this.loggerService.log(METHOD_NAME_VALIDATE$2, {
13965
14281
  exchangeName,
13966
14282
  });
@@ -14212,7 +14528,7 @@ const METHOD_NAME_VALIDATE$1 = "strategyCoreService validate";
14212
14528
  * @param context - Execution context with strategyName, exchangeName, frameName
14213
14529
  * @returns Unique string key for memoization
14214
14530
  */
14215
- const CREATE_KEY_FN$n = (context) => {
14531
+ const CREATE_KEY_FN$o = (context) => {
14216
14532
  const parts = [context.strategyName, context.exchangeName];
14217
14533
  if (context.frameName)
14218
14534
  parts.push(context.frameName);
@@ -14244,7 +14560,7 @@ class StrategyCoreService {
14244
14560
  * @param context - Execution context with strategyName, exchangeName, frameName
14245
14561
  * @returns Promise that resolves when validation is complete
14246
14562
  */
14247
- this.validate = memoize(([context]) => CREATE_KEY_FN$n(context), async (context) => {
14563
+ this.validate = memoize(([context]) => CREATE_KEY_FN$o(context), async (context) => {
14248
14564
  this.loggerService.log(METHOD_NAME_VALIDATE$1, {
14249
14565
  context,
14250
14566
  });
@@ -15288,6 +15604,114 @@ class StrategyCoreService {
15288
15604
  await this.validate(context);
15289
15605
  return await this.strategyConnectionService.getPositionDrawdownMinutes(backtest, symbol, context);
15290
15606
  };
15607
+ /**
15608
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
15609
+ *
15610
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
15611
+ * pulling back from its peak profit level.
15612
+ *
15613
+ * Validates strategy existence and delegates to connection service.
15614
+ * Returns null if no pending signal exists.
15615
+ *
15616
+ * @param backtest - Whether running in backtest mode
15617
+ * @param symbol - Trading pair symbol
15618
+ * @param context - Execution context with strategyName, exchangeName, frameName
15619
+ * @returns Promise resolving to minutes since last profit peak or null
15620
+ */
15621
+ this.getPositionHighestProfitMinutes = async (backtest, symbol, context) => {
15622
+ this.loggerService.log("strategyCoreService getPositionHighestProfitMinutes", {
15623
+ symbol,
15624
+ context,
15625
+ });
15626
+ await this.validate(context);
15627
+ return await this.strategyConnectionService.getPositionHighestProfitMinutes(backtest, symbol, context);
15628
+ };
15629
+ /**
15630
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
15631
+ *
15632
+ * Measures how long ago the deepest drawdown point occurred.
15633
+ * Zero when called at the exact moment the trough was set.
15634
+ *
15635
+ * Validates strategy existence and delegates to connection service.
15636
+ * Returns null if no pending signal exists.
15637
+ *
15638
+ * @param backtest - Whether running in backtest mode
15639
+ * @param symbol - Trading pair symbol
15640
+ * @param context - Execution context with strategyName, exchangeName, frameName
15641
+ * @returns Promise resolving to minutes since last drawdown trough or null
15642
+ */
15643
+ this.getPositionMaxDrawdownMinutes = async (backtest, symbol, context) => {
15644
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownMinutes", {
15645
+ symbol,
15646
+ context,
15647
+ });
15648
+ await this.validate(context);
15649
+ return await this.strategyConnectionService.getPositionMaxDrawdownMinutes(backtest, symbol, context);
15650
+ };
15651
+ /**
15652
+ * Returns the worst price reached in the loss direction during this position's life.
15653
+ *
15654
+ * @param backtest - Whether running in backtest mode
15655
+ * @param symbol - Trading pair symbol
15656
+ * @param context - Execution context with strategyName, exchangeName, frameName
15657
+ * @returns Promise resolving to price or null
15658
+ */
15659
+ this.getPositionMaxDrawdownPrice = async (backtest, symbol, context) => {
15660
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownPrice", {
15661
+ symbol,
15662
+ context,
15663
+ });
15664
+ await this.validate(context);
15665
+ return await this.strategyConnectionService.getPositionMaxDrawdownPrice(backtest, symbol, context);
15666
+ };
15667
+ /**
15668
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
15669
+ *
15670
+ * @param backtest - Whether running in backtest mode
15671
+ * @param symbol - Trading pair symbol
15672
+ * @param context - Execution context with strategyName, exchangeName, frameName
15673
+ * @returns Promise resolving to timestamp in milliseconds or null
15674
+ */
15675
+ this.getPositionMaxDrawdownTimestamp = async (backtest, symbol, context) => {
15676
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownTimestamp", {
15677
+ symbol,
15678
+ context,
15679
+ });
15680
+ await this.validate(context);
15681
+ return await this.strategyConnectionService.getPositionMaxDrawdownTimestamp(backtest, symbol, context);
15682
+ };
15683
+ /**
15684
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
15685
+ *
15686
+ * @param backtest - Whether running in backtest mode
15687
+ * @param symbol - Trading pair symbol
15688
+ * @param context - Execution context with strategyName, exchangeName, frameName
15689
+ * @returns Promise resolving to PnL percentage or null
15690
+ */
15691
+ this.getPositionMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
15692
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownPnlPercentage", {
15693
+ symbol,
15694
+ context,
15695
+ });
15696
+ await this.validate(context);
15697
+ return await this.strategyConnectionService.getPositionMaxDrawdownPnlPercentage(backtest, symbol, context);
15698
+ };
15699
+ /**
15700
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
15701
+ *
15702
+ * @param backtest - Whether running in backtest mode
15703
+ * @param symbol - Trading pair symbol
15704
+ * @param context - Execution context with strategyName, exchangeName, frameName
15705
+ * @returns Promise resolving to PnL cost or null
15706
+ */
15707
+ this.getPositionMaxDrawdownPnlCost = async (backtest, symbol, context) => {
15708
+ this.loggerService.log("strategyCoreService getPositionMaxDrawdownPnlCost", {
15709
+ symbol,
15710
+ context,
15711
+ });
15712
+ await this.validate(context);
15713
+ return await this.strategyConnectionService.getPositionMaxDrawdownPnlCost(backtest, symbol, context);
15714
+ };
15291
15715
  }
15292
15716
  }
15293
15717
 
@@ -15360,7 +15784,7 @@ class SizingGlobalService {
15360
15784
  * @param context - Context with riskName, exchangeName, frameName
15361
15785
  * @returns Unique string key for memoization
15362
15786
  */
15363
- const CREATE_KEY_FN$m = (context) => {
15787
+ const CREATE_KEY_FN$n = (context) => {
15364
15788
  const parts = [context.riskName, context.exchangeName];
15365
15789
  if (context.frameName)
15366
15790
  parts.push(context.frameName);
@@ -15386,7 +15810,7 @@ class RiskGlobalService {
15386
15810
  * @param payload - Payload with riskName, exchangeName and frameName
15387
15811
  * @returns Promise that resolves when validation is complete
15388
15812
  */
15389
- this.validate = memoize(([context]) => CREATE_KEY_FN$m(context), async (context) => {
15813
+ this.validate = memoize(([context]) => CREATE_KEY_FN$n(context), async (context) => {
15390
15814
  this.loggerService.log("riskGlobalService validate", {
15391
15815
  context,
15392
15816
  });
@@ -15464,7 +15888,7 @@ const METHOD_NAME_VALIDATE = "actionCoreService validate";
15464
15888
  * @param context - Execution context with strategyName, exchangeName, frameName
15465
15889
  * @returns Unique string key for memoization
15466
15890
  */
15467
- const CREATE_KEY_FN$l = (context) => {
15891
+ const CREATE_KEY_FN$m = (context) => {
15468
15892
  const parts = [context.strategyName, context.exchangeName];
15469
15893
  if (context.frameName)
15470
15894
  parts.push(context.frameName);
@@ -15508,7 +15932,7 @@ class ActionCoreService {
15508
15932
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
15509
15933
  * @returns Promise that resolves when all validations complete
15510
15934
  */
15511
- this.validate = memoize(([context]) => CREATE_KEY_FN$l(context), async (context) => {
15935
+ this.validate = memoize(([context]) => CREATE_KEY_FN$m(context), async (context) => {
15512
15936
  this.loggerService.log(METHOD_NAME_VALIDATE, {
15513
15937
  context,
15514
15938
  });
@@ -18055,6 +18479,21 @@ const backtest_columns = [
18055
18479
  format: (data) => new Date(data.closeTimestamp).toISOString(),
18056
18480
  isVisible: () => true,
18057
18481
  },
18482
+ {
18483
+ key: "peakPnl",
18484
+ label: "Peak PNL",
18485
+ format: (data) => {
18486
+ const v = data.signal._peak?.pnlPercentage;
18487
+ return `${v > 0 ? "+" : ""}${v.toFixed(2)}%`;
18488
+ },
18489
+ isVisible: () => true,
18490
+ },
18491
+ {
18492
+ key: "fallPnl",
18493
+ label: "Max DD PNL",
18494
+ format: (data) => `${data.signal._fall?.pnlPercentage.toFixed(2)}%`,
18495
+ isVisible: () => true,
18496
+ },
18058
18497
  ];
18059
18498
 
18060
18499
  /**
@@ -18165,6 +18604,18 @@ const heat_columns = [
18165
18604
  format: (data) => data.totalTrades.toString(),
18166
18605
  isVisible: () => true,
18167
18606
  },
18607
+ {
18608
+ key: "avgPeakPnl",
18609
+ label: "Avg Peak PNL",
18610
+ format: (data) => data.avgPeakPnl !== null ? str(data.avgPeakPnl, "%") : "N/A",
18611
+ isVisible: () => true,
18612
+ },
18613
+ {
18614
+ key: "avgFallPnl",
18615
+ label: "Avg DD PNL",
18616
+ format: (data) => data.avgFallPnl !== null ? str(data.avgFallPnl, "%") : "N/A",
18617
+ isVisible: () => true,
18618
+ },
18168
18619
  ];
18169
18620
 
18170
18621
  /**
@@ -18362,6 +18813,22 @@ const live_columns = [
18362
18813
  format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
18363
18814
  isVisible: () => true,
18364
18815
  },
18816
+ {
18817
+ key: "peakPnl",
18818
+ label: "Peak PNL",
18819
+ format: (data) => {
18820
+ if (data.peakPnl === undefined)
18821
+ return "N/A";
18822
+ return `${data.peakPnl > 0 ? "+" : ""}${data.peakPnl.toFixed(2)}%`;
18823
+ },
18824
+ isVisible: () => true,
18825
+ },
18826
+ {
18827
+ key: "fallPnl",
18828
+ label: "Max DD PNL",
18829
+ format: (data) => data.fallPnl !== undefined ? `${data.fallPnl.toFixed(2)}%` : "N/A",
18830
+ isVisible: () => true,
18831
+ },
18365
18832
  ];
18366
18833
 
18367
18834
  /**
@@ -19523,6 +19990,93 @@ const highest_profit_columns = [
19523
19990
  },
19524
19991
  ];
19525
19992
 
19993
+ /**
19994
+ * Column configuration for max drawdown markdown reports.
19995
+ *
19996
+ * Defines the table structure for displaying max-drawdown-record events.
19997
+ *
19998
+ * @see MaxDrawdownMarkdownService
19999
+ * @see ColumnModel
20000
+ * @see MaxDrawdownEvent
20001
+ */
20002
+ const max_drawdown_columns = [
20003
+ {
20004
+ key: "symbol",
20005
+ label: "Symbol",
20006
+ format: (data) => data.symbol,
20007
+ isVisible: () => true,
20008
+ },
20009
+ {
20010
+ key: "strategyName",
20011
+ label: "Strategy",
20012
+ format: (data) => data.strategyName,
20013
+ isVisible: () => true,
20014
+ },
20015
+ {
20016
+ key: "signalId",
20017
+ label: "Signal ID",
20018
+ format: (data) => data.signalId,
20019
+ isVisible: () => true,
20020
+ },
20021
+ {
20022
+ key: "position",
20023
+ label: "Position",
20024
+ format: (data) => data.position.toUpperCase(),
20025
+ isVisible: () => true,
20026
+ },
20027
+ {
20028
+ key: "pnl",
20029
+ label: "PNL (net)",
20030
+ format: (data) => {
20031
+ const pnlPercentage = data.pnl.pnlPercentage;
20032
+ return `${pnlPercentage > 0 ? "+" : ""}${pnlPercentage.toFixed(2)}%`;
20033
+ },
20034
+ isVisible: () => true,
20035
+ },
20036
+ {
20037
+ key: "pnlCost",
20038
+ label: "PNL (USD)",
20039
+ format: (data) => `${data.pnl.pnlCost > 0 ? "+" : ""}${data.pnl.pnlCost.toFixed(2)} USD`,
20040
+ isVisible: () => true,
20041
+ },
20042
+ {
20043
+ key: "currentPrice",
20044
+ label: "DD Price",
20045
+ format: (data) => `${data.currentPrice.toFixed(8)} USD`,
20046
+ isVisible: () => true,
20047
+ },
20048
+ {
20049
+ key: "priceOpen",
20050
+ label: "Entry Price",
20051
+ format: (data) => `${data.priceOpen.toFixed(8)} USD`,
20052
+ isVisible: () => true,
20053
+ },
20054
+ {
20055
+ key: "priceTakeProfit",
20056
+ label: "Take Profit",
20057
+ format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
20058
+ isVisible: () => true,
20059
+ },
20060
+ {
20061
+ key: "priceStopLoss",
20062
+ label: "Stop Loss",
20063
+ format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
20064
+ isVisible: () => true,
20065
+ },
20066
+ {
20067
+ key: "timestamp",
20068
+ label: "Timestamp",
20069
+ format: (data) => new Date(data.timestamp).toISOString(),
20070
+ isVisible: () => true,
20071
+ },
20072
+ {
20073
+ key: "mode",
20074
+ label: "Mode",
20075
+ format: (data) => (data.backtest ? "Backtest" : "Live"),
20076
+ isVisible: () => true,
20077
+ },
20078
+ ];
20079
+
19526
20080
  /**
19527
20081
  * Column configuration for walker strategy comparison table in markdown reports.
19528
20082
  *
@@ -19758,6 +20312,8 @@ const COLUMN_CONFIG = {
19758
20312
  sync_columns,
19759
20313
  /** Columns for highest profit milestone tracking events */
19760
20314
  highest_profit_columns,
20315
+ /** Columns for max drawdown milestone tracking events */
20316
+ max_drawdown_columns,
19761
20317
  /** Walker: PnL summary columns */
19762
20318
  walker_pnl_columns,
19763
20319
  /** Walker: strategy-level summary columns */
@@ -19802,6 +20358,7 @@ const WILDCARD_TARGET$1 = {
19802
20358
  walker: true,
19803
20359
  sync: true,
19804
20360
  highest_profit: true,
20361
+ max_drawdown: true,
19805
20362
  };
19806
20363
  /**
19807
20364
  * JSONL-based markdown adapter with append-only writes.
@@ -20029,7 +20586,7 @@ class MarkdownUtils {
20029
20586
  *
20030
20587
  * @returns Cleanup function that unsubscribes from all enabled services
20031
20588
  */
20032
- 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) => {
20589
+ 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) => {
20033
20590
  bt.loggerService.debug(MARKDOWN_METHOD_NAME_ENABLE, {
20034
20591
  backtest: bt$1,
20035
20592
  breakeven,
@@ -20081,6 +20638,9 @@ class MarkdownUtils {
20081
20638
  if (highest_profit) {
20082
20639
  unList.push(bt.highestProfitMarkdownService.subscribe());
20083
20640
  }
20641
+ if (max_drawdown) {
20642
+ unList.push(bt.maxDrawdownMarkdownService.subscribe());
20643
+ }
20084
20644
  return compose(...unList.map((un) => () => void un()));
20085
20645
  };
20086
20646
  /**
@@ -20120,7 +20680,7 @@ class MarkdownUtils {
20120
20680
  * Markdown.disable();
20121
20681
  * ```
20122
20682
  */
20123
- 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) => {
20683
+ 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) => {
20124
20684
  bt.loggerService.debug(MARKDOWN_METHOD_NAME_DISABLE, {
20125
20685
  backtest: bt$1,
20126
20686
  breakeven,
@@ -20171,6 +20731,9 @@ class MarkdownUtils {
20171
20731
  if (highest_profit) {
20172
20732
  bt.highestProfitMarkdownService.unsubscribe();
20173
20733
  }
20734
+ if (max_drawdown) {
20735
+ bt.maxDrawdownMarkdownService.unsubscribe();
20736
+ }
20174
20737
  };
20175
20738
  }
20176
20739
  }
@@ -20286,7 +20849,7 @@ const Markdown = new MarkdownAdapter();
20286
20849
  * @param backtest - Whether running in backtest mode
20287
20850
  * @returns Unique string key for memoization
20288
20851
  */
20289
- const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest) => {
20852
+ const CREATE_KEY_FN$l = (symbol, strategyName, exchangeName, frameName, backtest) => {
20290
20853
  const parts = [symbol, strategyName, exchangeName];
20291
20854
  if (frameName)
20292
20855
  parts.push(frameName);
@@ -20303,7 +20866,7 @@ const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest
20303
20866
  * @param timestamp - Unix timestamp in milliseconds
20304
20867
  * @returns Filename string
20305
20868
  */
20306
- const CREATE_FILE_NAME_FN$b = (symbol, strategyName, exchangeName, frameName, timestamp) => {
20869
+ const CREATE_FILE_NAME_FN$c = (symbol, strategyName, exchangeName, frameName, timestamp) => {
20307
20870
  const parts = [symbol, strategyName, exchangeName];
20308
20871
  if (frameName) {
20309
20872
  parts.push(frameName);
@@ -20335,7 +20898,7 @@ function isUnsafe$3(value) {
20335
20898
  * Storage class for accumulating closed signals per strategy.
20336
20899
  * Maintains a list of all closed signals and provides methods to generate reports.
20337
20900
  */
20338
- let ReportStorage$9 = class ReportStorage {
20901
+ let ReportStorage$a = class ReportStorage {
20339
20902
  constructor(symbol, strategyName, exchangeName, frameName) {
20340
20903
  this.symbol = symbol;
20341
20904
  this.strategyName = strategyName;
@@ -20377,6 +20940,8 @@ let ReportStorage$9 = class ReportStorage {
20377
20940
  annualizedSharpeRatio: null,
20378
20941
  certaintyRatio: null,
20379
20942
  expectedYearlyReturns: null,
20943
+ avgPeakPnl: null,
20944
+ avgFallPnl: null,
20380
20945
  };
20381
20946
  }
20382
20947
  const totalSignals = this._signalList.length;
@@ -20407,6 +20972,9 @@ let ReportStorage$9 = class ReportStorage {
20407
20972
  const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
20408
20973
  const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
20409
20974
  const expectedYearlyReturns = avgPnl * tradesPerYear;
20975
+ // Calculate average peak and fall PNL across all signals
20976
+ const avgPeakPnl = this._signalList.reduce((sum, s) => sum + (s.signal._peak?.pnlPercentage ?? 0), 0) / totalSignals;
20977
+ const avgFallPnl = this._signalList.reduce((sum, s) => sum + (s.signal._fall?.pnlPercentage ?? 0), 0) / totalSignals;
20410
20978
  return {
20411
20979
  signalList: this._signalList,
20412
20980
  totalSignals,
@@ -20420,6 +20988,8 @@ let ReportStorage$9 = class ReportStorage {
20420
20988
  annualizedSharpeRatio: isUnsafe$3(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
20421
20989
  certaintyRatio: isUnsafe$3(certaintyRatio) ? null : certaintyRatio,
20422
20990
  expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
20991
+ avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
20992
+ avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
20423
20993
  };
20424
20994
  }
20425
20995
  /**
@@ -20464,6 +21034,8 @@ let ReportStorage$9 = class ReportStorage {
20464
21034
  `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
20465
21035
  `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
20466
21036
  `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
21037
+ `**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
21038
+ `**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
20467
21039
  ].join("\n");
20468
21040
  }
20469
21041
  /**
@@ -20476,7 +21048,7 @@ let ReportStorage$9 = class ReportStorage {
20476
21048
  async dump(strategyName, path = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
20477
21049
  const markdown = await this.getReport(strategyName, columns);
20478
21050
  const timestamp = getContextTimestamp();
20479
- const filename = CREATE_FILE_NAME_FN$b(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21051
+ const filename = CREATE_FILE_NAME_FN$c(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
20480
21052
  await Markdown.writeData("backtest", markdown, {
20481
21053
  path,
20482
21054
  file: filename,
@@ -20523,7 +21095,7 @@ class BacktestMarkdownService {
20523
21095
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
20524
21096
  * Each combination gets its own isolated storage instance.
20525
21097
  */
20526
- this.getStorage = 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));
21098
+ this.getStorage = 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));
20527
21099
  /**
20528
21100
  * Processes tick events and accumulates closed signals.
20529
21101
  * Should be called from IStrategyCallbacks.onTick.
@@ -20680,7 +21252,7 @@ class BacktestMarkdownService {
20680
21252
  payload,
20681
21253
  });
20682
21254
  if (payload) {
20683
- const key = CREATE_KEY_FN$k(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21255
+ const key = CREATE_KEY_FN$l(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20684
21256
  this.getStorage.clear(key);
20685
21257
  }
20686
21258
  else {
@@ -20742,7 +21314,7 @@ class BacktestMarkdownService {
20742
21314
  * @param backtest - Whether running in backtest mode
20743
21315
  * @returns Unique string key for memoization
20744
21316
  */
20745
- const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest) => {
21317
+ const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest) => {
20746
21318
  const parts = [symbol, strategyName, exchangeName];
20747
21319
  if (frameName)
20748
21320
  parts.push(frameName);
@@ -20759,7 +21331,7 @@ const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest
20759
21331
  * @param timestamp - Unix timestamp in milliseconds
20760
21332
  * @returns Filename string
20761
21333
  */
20762
- const CREATE_FILE_NAME_FN$a = (symbol, strategyName, exchangeName, frameName, timestamp) => {
21334
+ const CREATE_FILE_NAME_FN$b = (symbol, strategyName, exchangeName, frameName, timestamp) => {
20763
21335
  const parts = [symbol, strategyName, exchangeName];
20764
21336
  if (frameName) {
20765
21337
  parts.push(frameName);
@@ -20791,7 +21363,7 @@ function isUnsafe$2(value) {
20791
21363
  * Storage class for accumulating all tick events per strategy.
20792
21364
  * Maintains a chronological list of all events (idle, opened, active, closed).
20793
21365
  */
20794
- let ReportStorage$8 = class ReportStorage {
21366
+ let ReportStorage$9 = class ReportStorage {
20795
21367
  constructor(symbol, strategyName, exchangeName, frameName) {
20796
21368
  this.symbol = symbol;
20797
21369
  this.strategyName = strategyName;
@@ -20931,6 +21503,8 @@ let ReportStorage$8 = class ReportStorage {
20931
21503
  duration: durationMin,
20932
21504
  pendingAt: data.signal.pendingAt,
20933
21505
  scheduledAt: data.signal.scheduledAt,
21506
+ peakPnl: data.signal._peak?.pnlPercentage,
21507
+ fallPnl: data.signal._fall?.pnlPercentage,
20934
21508
  };
20935
21509
  this._eventList.unshift(newEvent);
20936
21510
  // Trim queue if exceeded GLOBAL_CONFIG.CC_MAX_LIVE_MARKDOWN_ROWS
@@ -21060,6 +21634,8 @@ let ReportStorage$8 = class ReportStorage {
21060
21634
  annualizedSharpeRatio: null,
21061
21635
  certaintyRatio: null,
21062
21636
  expectedYearlyReturns: null,
21637
+ avgPeakPnl: null,
21638
+ avgFallPnl: null,
21063
21639
  };
21064
21640
  }
21065
21641
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
@@ -21103,6 +21679,12 @@ let ReportStorage$8 = class ReportStorage {
21103
21679
  const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
21104
21680
  expectedYearlyReturns = avgPnl * tradesPerYear;
21105
21681
  }
21682
+ const avgPeakPnl = totalClosed > 0
21683
+ ? closedEvents.reduce((sum, e) => sum + (e.peakPnl || 0), 0) / totalClosed
21684
+ : 0;
21685
+ const avgFallPnl = totalClosed > 0
21686
+ ? closedEvents.reduce((sum, e) => sum + (e.fallPnl || 0), 0) / totalClosed
21687
+ : 0;
21106
21688
  return {
21107
21689
  eventList: this._eventList,
21108
21690
  totalEvents: this._eventList.length,
@@ -21117,6 +21699,8 @@ let ReportStorage$8 = class ReportStorage {
21117
21699
  annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
21118
21700
  certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
21119
21701
  expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
21702
+ avgPeakPnl: isUnsafe$2(avgPeakPnl) ? null : avgPeakPnl,
21703
+ avgFallPnl: isUnsafe$2(avgFallPnl) ? null : avgFallPnl,
21120
21704
  };
21121
21705
  }
21122
21706
  /**
@@ -21161,6 +21745,8 @@ let ReportStorage$8 = class ReportStorage {
21161
21745
  `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
21162
21746
  `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
21163
21747
  `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
21748
+ `**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
21749
+ `**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
21164
21750
  ].join("\n");
21165
21751
  }
21166
21752
  /**
@@ -21173,7 +21759,7 @@ let ReportStorage$8 = class ReportStorage {
21173
21759
  async dump(strategyName, path = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
21174
21760
  const markdown = await this.getReport(strategyName, columns);
21175
21761
  const timestamp = getContextTimestamp();
21176
- const filename = CREATE_FILE_NAME_FN$a(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21762
+ const filename = CREATE_FILE_NAME_FN$b(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21177
21763
  await Markdown.writeData("live", markdown, {
21178
21764
  path,
21179
21765
  signalId: "",
@@ -21223,7 +21809,7 @@ class LiveMarkdownService {
21223
21809
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
21224
21810
  * Each combination gets its own isolated storage instance.
21225
21811
  */
21226
- this.getStorage = 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));
21812
+ this.getStorage = 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));
21227
21813
  /**
21228
21814
  * Subscribes to live signal emitter to receive tick events.
21229
21815
  * Protected against multiple subscriptions.
@@ -21441,7 +22027,7 @@ class LiveMarkdownService {
21441
22027
  payload,
21442
22028
  });
21443
22029
  if (payload) {
21444
- const key = CREATE_KEY_FN$j(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22030
+ const key = CREATE_KEY_FN$k(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21445
22031
  this.getStorage.clear(key);
21446
22032
  }
21447
22033
  else {
@@ -21461,7 +22047,7 @@ class LiveMarkdownService {
21461
22047
  * @param backtest - Whether running in backtest mode
21462
22048
  * @returns Unique string key for memoization
21463
22049
  */
21464
- const CREATE_KEY_FN$i = (symbol, strategyName, exchangeName, frameName, backtest) => {
22050
+ const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest) => {
21465
22051
  const parts = [symbol, strategyName, exchangeName];
21466
22052
  if (frameName)
21467
22053
  parts.push(frameName);
@@ -21478,7 +22064,7 @@ const CREATE_KEY_FN$i = (symbol, strategyName, exchangeName, frameName, backtest
21478
22064
  * @param timestamp - Unix timestamp in milliseconds
21479
22065
  * @returns Filename string
21480
22066
  */
21481
- const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
22067
+ const CREATE_FILE_NAME_FN$a = (symbol, strategyName, exchangeName, frameName, timestamp) => {
21482
22068
  const parts = [symbol, strategyName, exchangeName];
21483
22069
  if (frameName) {
21484
22070
  parts.push(frameName);
@@ -21492,7 +22078,7 @@ const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, ti
21492
22078
  * Storage class for accumulating scheduled signal events per strategy.
21493
22079
  * Maintains a chronological list of scheduled and cancelled events.
21494
22080
  */
21495
- let ReportStorage$7 = class ReportStorage {
22081
+ let ReportStorage$8 = class ReportStorage {
21496
22082
  constructor(symbol, strategyName, exchangeName, frameName) {
21497
22083
  this.symbol = symbol;
21498
22084
  this.strategyName = strategyName;
@@ -21709,7 +22295,7 @@ let ReportStorage$7 = class ReportStorage {
21709
22295
  async dump(strategyName, path = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
21710
22296
  const markdown = await this.getReport(strategyName, columns);
21711
22297
  const timestamp = getContextTimestamp();
21712
- const filename = CREATE_FILE_NAME_FN$9(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22298
+ const filename = CREATE_FILE_NAME_FN$a(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21713
22299
  await Markdown.writeData("schedule", markdown, {
21714
22300
  path,
21715
22301
  file: filename,
@@ -21750,7 +22336,7 @@ class ScheduleMarkdownService {
21750
22336
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
21751
22337
  * Each combination gets its own isolated storage instance.
21752
22338
  */
21753
- this.getStorage = 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));
22339
+ this.getStorage = 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));
21754
22340
  /**
21755
22341
  * Subscribes to signal emitter to receive scheduled signal events.
21756
22342
  * Protected against multiple subscriptions.
@@ -21953,7 +22539,7 @@ class ScheduleMarkdownService {
21953
22539
  payload,
21954
22540
  });
21955
22541
  if (payload) {
21956
- const key = CREATE_KEY_FN$i(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22542
+ const key = CREATE_KEY_FN$j(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21957
22543
  this.getStorage.clear(key);
21958
22544
  }
21959
22545
  else {
@@ -21973,7 +22559,7 @@ class ScheduleMarkdownService {
21973
22559
  * @param backtest - Whether running in backtest mode
21974
22560
  * @returns Unique string key for memoization
21975
22561
  */
21976
- const CREATE_KEY_FN$h = (symbol, strategyName, exchangeName, frameName, backtest) => {
22562
+ const CREATE_KEY_FN$i = (symbol, strategyName, exchangeName, frameName, backtest) => {
21977
22563
  const parts = [symbol, strategyName, exchangeName];
21978
22564
  if (frameName)
21979
22565
  parts.push(frameName);
@@ -21990,7 +22576,7 @@ const CREATE_KEY_FN$h = (symbol, strategyName, exchangeName, frameName, backtest
21990
22576
  * @param timestamp - Unix timestamp in milliseconds
21991
22577
  * @returns Filename string
21992
22578
  */
21993
- const CREATE_FILE_NAME_FN$8 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
22579
+ const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
21994
22580
  const parts = [symbol, strategyName, exchangeName];
21995
22581
  if (frameName) {
21996
22582
  parts.push(frameName);
@@ -22171,7 +22757,7 @@ class PerformanceStorage {
22171
22757
  async dump(strategyName, path = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
22172
22758
  const markdown = await this.getReport(strategyName, columns);
22173
22759
  const timestamp = getContextTimestamp();
22174
- const filename = CREATE_FILE_NAME_FN$8(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22760
+ const filename = CREATE_FILE_NAME_FN$9(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22175
22761
  await Markdown.writeData("performance", markdown, {
22176
22762
  path,
22177
22763
  file: filename,
@@ -22218,7 +22804,7 @@ class PerformanceMarkdownService {
22218
22804
  * Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
22219
22805
  * Each combination gets its own isolated storage instance.
22220
22806
  */
22221
- this.getStorage = 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));
22807
+ this.getStorage = 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));
22222
22808
  /**
22223
22809
  * Subscribes to performance emitter to receive performance events.
22224
22810
  * Protected against multiple subscriptions.
@@ -22385,7 +22971,7 @@ class PerformanceMarkdownService {
22385
22971
  payload,
22386
22972
  });
22387
22973
  if (payload) {
22388
- const key = CREATE_KEY_FN$h(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22974
+ const key = CREATE_KEY_FN$i(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22389
22975
  this.getStorage.clear(key);
22390
22976
  }
22391
22977
  else {
@@ -22399,7 +22985,7 @@ class PerformanceMarkdownService {
22399
22985
  * Creates a filename for markdown report based on walker name.
22400
22986
  * Filename format: "walkerName-timestamp.md"
22401
22987
  */
22402
- const CREATE_FILE_NAME_FN$7 = (walkerName, timestamp) => {
22988
+ const CREATE_FILE_NAME_FN$8 = (walkerName, timestamp) => {
22403
22989
  return `${walkerName}-${timestamp}.md`;
22404
22990
  };
22405
22991
  /**
@@ -22434,7 +23020,7 @@ function formatMetric(value) {
22434
23020
  * Storage class for accumulating walker results.
22435
23021
  * Maintains a list of all strategy results and provides methods to generate reports.
22436
23022
  */
22437
- let ReportStorage$6 = class ReportStorage {
23023
+ let ReportStorage$7 = class ReportStorage {
22438
23024
  constructor(walkerName) {
22439
23025
  this.walkerName = walkerName;
22440
23026
  /** Walker metadata (set from first addResult call) */
@@ -22631,7 +23217,7 @@ let ReportStorage$6 = class ReportStorage {
22631
23217
  async dump(symbol, metric, context, path = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
22632
23218
  const markdown = await this.getReport(symbol, metric, context, strategyColumns, pnlColumns);
22633
23219
  const timestamp = getContextTimestamp();
22634
- const filename = CREATE_FILE_NAME_FN$7(this.walkerName, timestamp);
23220
+ const filename = CREATE_FILE_NAME_FN$8(this.walkerName, timestamp);
22635
23221
  await Markdown.writeData("walker", markdown, {
22636
23222
  path,
22637
23223
  file: filename,
@@ -22667,7 +23253,7 @@ class WalkerMarkdownService {
22667
23253
  * Memoized function to get or create ReportStorage for a walker.
22668
23254
  * Each walker gets its own isolated storage instance.
22669
23255
  */
22670
- this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$6(walkerName));
23256
+ this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$7(walkerName));
22671
23257
  /**
22672
23258
  * Subscribes to walker emitter to receive walker progress events.
22673
23259
  * Protected against multiple subscriptions.
@@ -22864,7 +23450,7 @@ class WalkerMarkdownService {
22864
23450
  * @param backtest - Whether running in backtest mode
22865
23451
  * @returns Unique string key for memoization
22866
23452
  */
22867
- const CREATE_KEY_FN$g = (exchangeName, frameName, backtest) => {
23453
+ const CREATE_KEY_FN$h = (exchangeName, frameName, backtest) => {
22868
23454
  const parts = [exchangeName];
22869
23455
  if (frameName)
22870
23456
  parts.push(frameName);
@@ -22875,7 +23461,7 @@ const CREATE_KEY_FN$g = (exchangeName, frameName, backtest) => {
22875
23461
  * Creates a filename for markdown report based on memoization key components.
22876
23462
  * Filename format: "strategyName_exchangeName_frameName-timestamp.md"
22877
23463
  */
22878
- const CREATE_FILE_NAME_FN$6 = (strategyName, exchangeName, frameName, timestamp) => {
23464
+ const CREATE_FILE_NAME_FN$7 = (strategyName, exchangeName, frameName, timestamp) => {
22879
23465
  const parts = [strategyName, exchangeName];
22880
23466
  if (frameName) {
22881
23467
  parts.push(frameName);
@@ -23065,6 +23651,13 @@ class HeatmapStorage {
23065
23651
  const lossRate = 100 - winRate;
23066
23652
  expectancy = (winRate / 100) * avgWin + (lossRate / 100) * avgLoss;
23067
23653
  }
23654
+ // Calculate average peak and fall PNL
23655
+ let avgPeakPnl = null;
23656
+ let avgFallPnl = null;
23657
+ if (signals.length > 0) {
23658
+ avgPeakPnl = signals.reduce((acc, s) => acc + (s.signal._peak?.pnlPercentage ?? 0), 0) / signals.length;
23659
+ avgFallPnl = signals.reduce((acc, s) => acc + (s.signal._fall?.pnlPercentage ?? 0), 0) / signals.length;
23660
+ }
23068
23661
  // Apply safe math checks
23069
23662
  if (isUnsafe(winRate))
23070
23663
  winRate = null;
@@ -23086,6 +23679,10 @@ class HeatmapStorage {
23086
23679
  avgLoss = null;
23087
23680
  if (isUnsafe(expectancy))
23088
23681
  expectancy = null;
23682
+ if (isUnsafe(avgPeakPnl))
23683
+ avgPeakPnl = null;
23684
+ if (isUnsafe(avgFallPnl))
23685
+ avgFallPnl = null;
23089
23686
  return {
23090
23687
  symbol,
23091
23688
  totalPnl,
@@ -23103,6 +23700,8 @@ class HeatmapStorage {
23103
23700
  maxWinStreak,
23104
23701
  maxLossStreak,
23105
23702
  expectancy,
23703
+ avgPeakPnl,
23704
+ avgFallPnl,
23106
23705
  };
23107
23706
  }
23108
23707
  /**
@@ -23152,17 +23751,34 @@ class HeatmapStorage {
23152
23751
  const weightedSum = validSharpes.reduce((acc, s) => acc + s.sharpeRatio * s.totalTrades, 0);
23153
23752
  portfolioSharpeRatio = weightedSum / portfolioTotalTrades;
23154
23753
  }
23754
+ // Calculate portfolio-wide weighted average peak/fall PNL
23755
+ let portfolioAvgPeakPnl = null;
23756
+ let portfolioAvgFallPnl = null;
23757
+ const validPeak = symbols.filter((s) => s.avgPeakPnl !== null);
23758
+ const validFall = symbols.filter((s) => s.avgFallPnl !== null);
23759
+ if (validPeak.length > 0 && portfolioTotalTrades > 0) {
23760
+ portfolioAvgPeakPnl = validPeak.reduce((acc, s) => acc + s.avgPeakPnl * s.totalTrades, 0) / portfolioTotalTrades;
23761
+ }
23762
+ if (validFall.length > 0 && portfolioTotalTrades > 0) {
23763
+ portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) / portfolioTotalTrades;
23764
+ }
23155
23765
  // Apply safe math
23156
23766
  if (isUnsafe(portfolioTotalPnl))
23157
23767
  portfolioTotalPnl = null;
23158
23768
  if (isUnsafe(portfolioSharpeRatio))
23159
23769
  portfolioSharpeRatio = null;
23770
+ if (isUnsafe(portfolioAvgPeakPnl))
23771
+ portfolioAvgPeakPnl = null;
23772
+ if (isUnsafe(portfolioAvgFallPnl))
23773
+ portfolioAvgFallPnl = null;
23160
23774
  return {
23161
23775
  symbols,
23162
23776
  totalSymbols,
23163
23777
  portfolioTotalPnl,
23164
23778
  portfolioSharpeRatio,
23165
23779
  portfolioTotalTrades,
23780
+ portfolioAvgPeakPnl,
23781
+ portfolioAvgFallPnl,
23166
23782
  };
23167
23783
  }
23168
23784
  /**
@@ -23211,7 +23827,7 @@ class HeatmapStorage {
23211
23827
  return [
23212
23828
  `# Portfolio Heatmap: ${strategyName}`,
23213
23829
  "",
23214
- `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
23830
+ `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
23215
23831
  "",
23216
23832
  table
23217
23833
  ].join("\n");
@@ -23235,7 +23851,7 @@ class HeatmapStorage {
23235
23851
  async dump(strategyName, path = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
23236
23852
  const markdown = await this.getReport(strategyName, columns);
23237
23853
  const timestamp = getContextTimestamp();
23238
- const filename = CREATE_FILE_NAME_FN$6(strategyName, this.exchangeName, this.frameName, timestamp);
23854
+ const filename = CREATE_FILE_NAME_FN$7(strategyName, this.exchangeName, this.frameName, timestamp);
23239
23855
  await Markdown.writeData("heat", markdown, {
23240
23856
  path,
23241
23857
  file: filename,
@@ -23281,7 +23897,7 @@ class HeatMarkdownService {
23281
23897
  * Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
23282
23898
  * Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
23283
23899
  */
23284
- this.getStorage = memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$g(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
23900
+ this.getStorage = memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$h(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
23285
23901
  /**
23286
23902
  * Subscribes to signal emitter to receive tick events.
23287
23903
  * Protected against multiple subscriptions.
@@ -23499,7 +24115,7 @@ class HeatMarkdownService {
23499
24115
  payload,
23500
24116
  });
23501
24117
  if (payload) {
23502
- const key = CREATE_KEY_FN$g(payload.exchangeName, payload.frameName, payload.backtest);
24118
+ const key = CREATE_KEY_FN$h(payload.exchangeName, payload.frameName, payload.backtest);
23503
24119
  this.getStorage.clear(key);
23504
24120
  }
23505
24121
  else {
@@ -24530,7 +25146,7 @@ class ClientPartial {
24530
25146
  * @param backtest - Whether running in backtest mode
24531
25147
  * @returns Unique string key for memoization
24532
25148
  */
24533
- const CREATE_KEY_FN$f = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
25149
+ const CREATE_KEY_FN$g = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
24534
25150
  /**
24535
25151
  * Creates a callback function for emitting profit events to partialProfitSubject.
24536
25152
  *
@@ -24652,7 +25268,7 @@ class PartialConnectionService {
24652
25268
  * Key format: "signalId:backtest" or "signalId:live"
24653
25269
  * Value: ClientPartial instance with logger and event emitters
24654
25270
  */
24655
- this.getPartial = memoize(([signalId, backtest]) => CREATE_KEY_FN$f(signalId, backtest), (signalId, backtest) => {
25271
+ this.getPartial = memoize(([signalId, backtest]) => CREATE_KEY_FN$g(signalId, backtest), (signalId, backtest) => {
24656
25272
  return new ClientPartial({
24657
25273
  signalId,
24658
25274
  logger: this.loggerService,
@@ -24742,7 +25358,7 @@ class PartialConnectionService {
24742
25358
  const partial = this.getPartial(data.id, backtest);
24743
25359
  await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
24744
25360
  await partial.clear(symbol, data, priceClose, backtest);
24745
- const key = CREATE_KEY_FN$f(data.id, backtest);
25361
+ const key = CREATE_KEY_FN$g(data.id, backtest);
24746
25362
  this.getPartial.clear(key);
24747
25363
  };
24748
25364
  }
@@ -24758,7 +25374,7 @@ class PartialConnectionService {
24758
25374
  * @param backtest - Whether running in backtest mode
24759
25375
  * @returns Unique string key for memoization
24760
25376
  */
24761
- const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest) => {
25377
+ const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
24762
25378
  const parts = [symbol, strategyName, exchangeName];
24763
25379
  if (frameName)
24764
25380
  parts.push(frameName);
@@ -24769,7 +25385,7 @@ const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest
24769
25385
  * Creates a filename for markdown report based on memoization key components.
24770
25386
  * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
24771
25387
  */
24772
- const CREATE_FILE_NAME_FN$5 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
25388
+ const CREATE_FILE_NAME_FN$6 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
24773
25389
  const parts = [symbol, strategyName, exchangeName];
24774
25390
  if (frameName) {
24775
25391
  parts.push(frameName);
@@ -24783,7 +25399,7 @@ const CREATE_FILE_NAME_FN$5 = (symbol, strategyName, exchangeName, frameName, ti
24783
25399
  * Storage class for accumulating partial profit/loss events per symbol-strategy pair.
24784
25400
  * Maintains a chronological list of profit and loss level events.
24785
25401
  */
24786
- let ReportStorage$5 = class ReportStorage {
25402
+ let ReportStorage$6 = class ReportStorage {
24787
25403
  constructor(symbol, strategyName, exchangeName, frameName) {
24788
25404
  this.symbol = symbol;
24789
25405
  this.strategyName = strategyName;
@@ -24940,7 +25556,7 @@ let ReportStorage$5 = class ReportStorage {
24940
25556
  async dump(symbol, strategyName, path = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
24941
25557
  const markdown = await this.getReport(symbol, strategyName, columns);
24942
25558
  const timestamp = getContextTimestamp();
24943
- const filename = CREATE_FILE_NAME_FN$5(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
25559
+ const filename = CREATE_FILE_NAME_FN$6(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
24944
25560
  await Markdown.writeData("partial", markdown, {
24945
25561
  path,
24946
25562
  file: filename,
@@ -24981,7 +25597,7 @@ class PartialMarkdownService {
24981
25597
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
24982
25598
  * Each combination gets its own isolated storage instance.
24983
25599
  */
24984
- this.getStorage = 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));
25600
+ this.getStorage = 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));
24985
25601
  /**
24986
25602
  * Subscribes to partial profit/loss signal emitters to receive events.
24987
25603
  * Protected against multiple subscriptions.
@@ -25191,7 +25807,7 @@ class PartialMarkdownService {
25191
25807
  payload,
25192
25808
  });
25193
25809
  if (payload) {
25194
- const key = CREATE_KEY_FN$e(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25810
+ const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25195
25811
  this.getStorage.clear(key);
25196
25812
  }
25197
25813
  else {
@@ -25207,7 +25823,7 @@ class PartialMarkdownService {
25207
25823
  * @param context - Context with strategyName, exchangeName, frameName
25208
25824
  * @returns Unique string key for memoization
25209
25825
  */
25210
- const CREATE_KEY_FN$d = (context) => {
25826
+ const CREATE_KEY_FN$e = (context) => {
25211
25827
  const parts = [context.strategyName, context.exchangeName];
25212
25828
  if (context.frameName)
25213
25829
  parts.push(context.frameName);
@@ -25281,7 +25897,7 @@ class PartialGlobalService {
25281
25897
  * @param context - Context with strategyName, exchangeName and frameName
25282
25898
  * @param methodName - Name of the calling method for error tracking
25283
25899
  */
25284
- this.validate = memoize(([context]) => CREATE_KEY_FN$d(context), (context, methodName) => {
25900
+ this.validate = memoize(([context]) => CREATE_KEY_FN$e(context), (context, methodName) => {
25285
25901
  this.loggerService.log("partialGlobalService validate", {
25286
25902
  context,
25287
25903
  methodName,
@@ -25736,7 +26352,7 @@ class ClientBreakeven {
25736
26352
  * @param backtest - Whether running in backtest mode
25737
26353
  * @returns Unique string key for memoization
25738
26354
  */
25739
- const CREATE_KEY_FN$c = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
26355
+ const CREATE_KEY_FN$d = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
25740
26356
  /**
25741
26357
  * Creates a callback function for emitting breakeven events to breakevenSubject.
25742
26358
  *
@@ -25822,7 +26438,7 @@ class BreakevenConnectionService {
25822
26438
  * Key format: "signalId:backtest" or "signalId:live"
25823
26439
  * Value: ClientBreakeven instance with logger and event emitter
25824
26440
  */
25825
- this.getBreakeven = memoize(([signalId, backtest]) => CREATE_KEY_FN$c(signalId, backtest), (signalId, backtest) => {
26441
+ this.getBreakeven = memoize(([signalId, backtest]) => CREATE_KEY_FN$d(signalId, backtest), (signalId, backtest) => {
25826
26442
  return new ClientBreakeven({
25827
26443
  signalId,
25828
26444
  logger: this.loggerService,
@@ -25883,7 +26499,7 @@ class BreakevenConnectionService {
25883
26499
  const breakeven = this.getBreakeven(data.id, backtest);
25884
26500
  await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
25885
26501
  await breakeven.clear(symbol, data, priceClose, backtest);
25886
- const key = CREATE_KEY_FN$c(data.id, backtest);
26502
+ const key = CREATE_KEY_FN$d(data.id, backtest);
25887
26503
  this.getBreakeven.clear(key);
25888
26504
  };
25889
26505
  }
@@ -25899,7 +26515,7 @@ class BreakevenConnectionService {
25899
26515
  * @param backtest - Whether running in backtest mode
25900
26516
  * @returns Unique string key for memoization
25901
26517
  */
25902
- const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
26518
+ const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest) => {
25903
26519
  const parts = [symbol, strategyName, exchangeName];
25904
26520
  if (frameName)
25905
26521
  parts.push(frameName);
@@ -25910,7 +26526,7 @@ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest
25910
26526
  * Creates a filename for markdown report based on memoization key components.
25911
26527
  * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
25912
26528
  */
25913
- const CREATE_FILE_NAME_FN$4 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
26529
+ const CREATE_FILE_NAME_FN$5 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
25914
26530
  const parts = [symbol, strategyName, exchangeName];
25915
26531
  if (frameName) {
25916
26532
  parts.push(frameName);
@@ -25924,7 +26540,7 @@ const CREATE_FILE_NAME_FN$4 = (symbol, strategyName, exchangeName, frameName, ti
25924
26540
  * Storage class for accumulating breakeven events per symbol-strategy pair.
25925
26541
  * Maintains a chronological list of breakeven events.
25926
26542
  */
25927
- let ReportStorage$4 = class ReportStorage {
26543
+ let ReportStorage$5 = class ReportStorage {
25928
26544
  constructor(symbol, strategyName, exchangeName, frameName) {
25929
26545
  this.symbol = symbol;
25930
26546
  this.strategyName = strategyName;
@@ -26033,7 +26649,7 @@ let ReportStorage$4 = class ReportStorage {
26033
26649
  async dump(symbol, strategyName, path = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
26034
26650
  const markdown = await this.getReport(symbol, strategyName, columns);
26035
26651
  const timestamp = getContextTimestamp();
26036
- const filename = CREATE_FILE_NAME_FN$4(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
26652
+ const filename = CREATE_FILE_NAME_FN$5(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
26037
26653
  await Markdown.writeData("breakeven", markdown, {
26038
26654
  path,
26039
26655
  file: filename,
@@ -26074,7 +26690,7 @@ class BreakevenMarkdownService {
26074
26690
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
26075
26691
  * Each combination gets its own isolated storage instance.
26076
26692
  */
26077
- this.getStorage = 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));
26693
+ this.getStorage = 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));
26078
26694
  /**
26079
26695
  * Subscribes to breakeven signal emitter to receive events.
26080
26696
  * Protected against multiple subscriptions.
@@ -26263,7 +26879,7 @@ class BreakevenMarkdownService {
26263
26879
  payload,
26264
26880
  });
26265
26881
  if (payload) {
26266
- const key = CREATE_KEY_FN$b(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26882
+ const key = CREATE_KEY_FN$c(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26267
26883
  this.getStorage.clear(key);
26268
26884
  }
26269
26885
  else {
@@ -26279,7 +26895,7 @@ class BreakevenMarkdownService {
26279
26895
  * @param context - Context with strategyName, exchangeName, frameName
26280
26896
  * @returns Unique string key for memoization
26281
26897
  */
26282
- const CREATE_KEY_FN$a = (context) => {
26898
+ const CREATE_KEY_FN$b = (context) => {
26283
26899
  const parts = [context.strategyName, context.exchangeName];
26284
26900
  if (context.frameName)
26285
26901
  parts.push(context.frameName);
@@ -26353,7 +26969,7 @@ class BreakevenGlobalService {
26353
26969
  * @param context - Context with strategyName, exchangeName and frameName
26354
26970
  * @param methodName - Name of the calling method for error tracking
26355
26971
  */
26356
- this.validate = memoize(([context]) => CREATE_KEY_FN$a(context), (context, methodName) => {
26972
+ this.validate = memoize(([context]) => CREATE_KEY_FN$b(context), (context, methodName) => {
26357
26973
  this.loggerService.log("breakevenGlobalService validate", {
26358
26974
  context,
26359
26975
  methodName,
@@ -26574,7 +27190,7 @@ class ConfigValidationService {
26574
27190
  * @param backtest - Whether running in backtest mode
26575
27191
  * @returns Unique string key for memoization
26576
27192
  */
26577
- const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
27193
+ const CREATE_KEY_FN$a = (symbol, strategyName, exchangeName, frameName, backtest) => {
26578
27194
  const parts = [symbol, strategyName, exchangeName];
26579
27195
  if (frameName)
26580
27196
  parts.push(frameName);
@@ -26585,7 +27201,7 @@ const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest
26585
27201
  * Creates a filename for markdown report based on memoization key components.
26586
27202
  * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
26587
27203
  */
26588
- const CREATE_FILE_NAME_FN$3 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
27204
+ const CREATE_FILE_NAME_FN$4 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
26589
27205
  const parts = [symbol, strategyName, exchangeName];
26590
27206
  if (frameName) {
26591
27207
  parts.push(frameName);
@@ -26599,7 +27215,7 @@ const CREATE_FILE_NAME_FN$3 = (symbol, strategyName, exchangeName, frameName, ti
26599
27215
  * Storage class for accumulating risk rejection events per symbol-strategy pair.
26600
27216
  * Maintains a chronological list of rejected signals due to risk limits.
26601
27217
  */
26602
- let ReportStorage$3 = class ReportStorage {
27218
+ let ReportStorage$4 = class ReportStorage {
26603
27219
  constructor(symbol, strategyName, exchangeName, frameName) {
26604
27220
  this.symbol = symbol;
26605
27221
  this.strategyName = strategyName;
@@ -26700,7 +27316,7 @@ let ReportStorage$3 = class ReportStorage {
26700
27316
  async dump(symbol, strategyName, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
26701
27317
  const markdown = await this.getReport(symbol, strategyName, columns);
26702
27318
  const timestamp = getContextTimestamp();
26703
- const filename = CREATE_FILE_NAME_FN$3(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
27319
+ const filename = CREATE_FILE_NAME_FN$4(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
26704
27320
  await Markdown.writeData("risk", markdown, {
26705
27321
  path,
26706
27322
  file: filename,
@@ -26741,7 +27357,7 @@ class RiskMarkdownService {
26741
27357
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
26742
27358
  * Each combination gets its own isolated storage instance.
26743
27359
  */
26744
- this.getStorage = 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));
27360
+ this.getStorage = 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));
26745
27361
  /**
26746
27362
  * Subscribes to risk rejection emitter to receive rejection events.
26747
27363
  * Protected against multiple subscriptions.
@@ -26930,7 +27546,7 @@ class RiskMarkdownService {
26930
27546
  payload,
26931
27547
  });
26932
27548
  if (payload) {
26933
- const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
27549
+ const key = CREATE_KEY_FN$a(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26934
27550
  this.getStorage.clear(key);
26935
27551
  }
26936
27552
  else {
@@ -27232,6 +27848,7 @@ const WILDCARD_TARGET = {
27232
27848
  walker: true,
27233
27849
  sync: true,
27234
27850
  highest_profit: true,
27851
+ max_drawdown: true,
27235
27852
  };
27236
27853
  /**
27237
27854
  * Utility class for managing report services.
@@ -27269,7 +27886,7 @@ class ReportUtils {
27269
27886
  *
27270
27887
  * @returns Cleanup function that unsubscribes from all enabled services
27271
27888
  */
27272
- 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) => {
27889
+ 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) => {
27273
27890
  bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
27274
27891
  backtest: bt$1,
27275
27892
  breakeven,
@@ -27320,6 +27937,9 @@ class ReportUtils {
27320
27937
  if (highest_profit) {
27321
27938
  unList.push(bt.highestProfitReportService.subscribe());
27322
27939
  }
27940
+ if (max_drawdown) {
27941
+ unList.push(bt.maxDrawdownReportService.subscribe());
27942
+ }
27323
27943
  return compose(...unList.map((un) => () => void un()));
27324
27944
  };
27325
27945
  /**
@@ -27358,7 +27978,7 @@ class ReportUtils {
27358
27978
  * Report.disable();
27359
27979
  * ```
27360
27980
  */
27361
- 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) => {
27981
+ 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) => {
27362
27982
  bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
27363
27983
  backtest: bt$1,
27364
27984
  breakeven,
@@ -27408,6 +28028,9 @@ class ReportUtils {
27408
28028
  if (highest_profit) {
27409
28029
  bt.highestProfitReportService.unsubscribe();
27410
28030
  }
28031
+ if (max_drawdown) {
28032
+ bt.maxDrawdownReportService.unsubscribe();
28033
+ }
27411
28034
  };
27412
28035
  }
27413
28036
  }
@@ -27619,6 +28242,8 @@ class BacktestReportService {
27619
28242
  pnlPriceClose: data.pnl.priceClose,
27620
28243
  totalPartials: data.signal?.totalPartials,
27621
28244
  cost: data.signal?.cost,
28245
+ peakPnl: data.signal?._peak?.pnlPercentage,
28246
+ fallPnl: data.signal?._fall?.pnlPercentage,
27622
28247
  }, { ...searchOptions, signalId: data.signal?.id });
27623
28248
  }
27624
28249
  else if (data.action === "closed") {
@@ -27651,6 +28276,8 @@ class BacktestReportService {
27651
28276
  closeReason: data.closeReason,
27652
28277
  closeTime: data.closeTimestamp,
27653
28278
  duration: durationMin,
28279
+ peakPnl: data.signal?._peak?.pnlPercentage,
28280
+ fallPnl: data.signal?._fall?.pnlPercentage,
27654
28281
  }, { ...searchOptions, signalId: data.signal?.id });
27655
28282
  }
27656
28283
  };
@@ -27813,6 +28440,8 @@ class LiveReportService {
27813
28440
  pnlPriceClose: data.pnl.priceClose,
27814
28441
  totalPartials: data.signal?.totalPartials,
27815
28442
  cost: data.signal?.cost,
28443
+ peakPnl: data.signal?._peak?.pnlPercentage,
28444
+ fallPnl: data.signal?._fall?.pnlPercentage,
27816
28445
  }, { ...searchOptions, signalId: data.signal?.id });
27817
28446
  }
27818
28447
  else if (data.action === "opened") {
@@ -27863,6 +28492,8 @@ class LiveReportService {
27863
28492
  pnlPriceClose: data.pnl.priceClose,
27864
28493
  totalPartials: data.signal?.totalPartials,
27865
28494
  cost: data.signal?.cost,
28495
+ peakPnl: data.signal?._peak?.pnlPercentage,
28496
+ fallPnl: data.signal?._fall?.pnlPercentage,
27866
28497
  }, { ...searchOptions, signalId: data.signal?.id });
27867
28498
  }
27868
28499
  else if (data.action === "closed") {
@@ -27895,6 +28526,8 @@ class LiveReportService {
27895
28526
  closeReason: data.closeReason,
27896
28527
  duration: durationMin,
27897
28528
  closeTime: data.closeTimestamp,
28529
+ peakPnl: data.signal?._peak?.pnlPercentage,
28530
+ fallPnl: data.signal?._fall?.pnlPercentage,
27898
28531
  }, { ...searchOptions, signalId: data.signal?.id });
27899
28532
  }
27900
28533
  else if (data.action === "cancelled") {
@@ -28486,6 +29119,8 @@ class HeatReportService {
28486
29119
  openTime: data.signal?.pendingAt,
28487
29120
  scheduledAt: data.signal?.scheduledAt,
28488
29121
  closeTime: data.closeTimestamp,
29122
+ peakPnl: data.signal?._peak?.pnlPercentage,
29123
+ fallPnl: data.signal?._fall?.pnlPercentage,
28489
29124
  }, {
28490
29125
  symbol: data.symbol,
28491
29126
  strategyName: data.strategyName,
@@ -29749,7 +30384,7 @@ class HighestProfitReportService {
29749
30384
  * @returns Colon-separated key string for memoization
29750
30385
  * @internal
29751
30386
  */
29752
- const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30387
+ const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
29753
30388
  const parts = [symbol, strategyName, exchangeName];
29754
30389
  if (frameName)
29755
30390
  parts.push(frameName);
@@ -29769,7 +30404,7 @@ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest
29769
30404
  * @returns Underscore-separated filename with .md extension
29770
30405
  * @internal
29771
30406
  */
29772
- const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
30407
+ const CREATE_FILE_NAME_FN$3 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
29773
30408
  const parts = [symbol, strategyName, exchangeName];
29774
30409
  if (frameName) {
29775
30410
  parts.push(frameName);
@@ -29791,7 +30426,7 @@ const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, ti
29791
30426
  *
29792
30427
  * @internal
29793
30428
  */
29794
- let ReportStorage$2 = class ReportStorage {
30429
+ let ReportStorage$3 = class ReportStorage {
29795
30430
  /**
29796
30431
  * Creates a new ReportStorage instance.
29797
30432
  *
@@ -29922,7 +30557,7 @@ let ReportStorage$2 = class ReportStorage {
29922
30557
  async dump(symbol, strategyName, path = "./dump/strategy", columns = COLUMN_CONFIG.strategy_columns) {
29923
30558
  const markdown = await this.getReport(symbol, strategyName, columns);
29924
30559
  const timestamp = getContextTimestamp();
29925
- const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
30560
+ const filename = CREATE_FILE_NAME_FN$3(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
29926
30561
  await Markdown.writeData("strategy", markdown, {
29927
30562
  path,
29928
30563
  file: filename,
@@ -29991,7 +30626,7 @@ class StrategyMarkdownService {
29991
30626
  *
29992
30627
  * @internal
29993
30628
  */
29994
- this.getStorage = 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));
30629
+ this.getStorage = 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));
29995
30630
  /**
29996
30631
  * Records a cancel-scheduled event when a scheduled signal is cancelled.
29997
30632
  *
@@ -30559,7 +31194,7 @@ class StrategyMarkdownService {
30559
31194
  this.clear = async (payload) => {
30560
31195
  this.loggerService.log("strategyMarkdownService clear", { payload });
30561
31196
  if (payload) {
30562
- const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31197
+ const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
30563
31198
  this.getStorage.clear(key);
30564
31199
  }
30565
31200
  else {
@@ -30667,7 +31302,7 @@ class StrategyMarkdownService {
30667
31302
  * Creates a unique key for memoizing ReportStorage instances.
30668
31303
  * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
30669
31304
  */
30670
- const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31305
+ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30671
31306
  const parts = [symbol, strategyName, exchangeName];
30672
31307
  if (frameName)
30673
31308
  parts.push(frameName);
@@ -30678,7 +31313,7 @@ const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest
30678
31313
  * Creates a filename for the markdown report.
30679
31314
  * Filename format: "symbol_strategyName_exchangeName[_frameName_backtest|_live]-timestamp.md"
30680
31315
  */
30681
- const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
31316
+ const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
30682
31317
  const parts = [symbol, strategyName, exchangeName];
30683
31318
  if (frameName) {
30684
31319
  parts.push(frameName);
@@ -30692,7 +31327,7 @@ const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, ti
30692
31327
  * Storage class for accumulating signal sync events per symbol-strategy-exchange-frame-backtest combination.
30693
31328
  * Maintains a chronological list of signal-open and signal-close events.
30694
31329
  */
30695
- let ReportStorage$1 = class ReportStorage {
31330
+ let ReportStorage$2 = class ReportStorage {
30696
31331
  constructor(symbol, strategyName, exchangeName, frameName, backtest) {
30697
31332
  this.symbol = symbol;
30698
31333
  this.strategyName = strategyName;
@@ -30826,7 +31461,7 @@ let ReportStorage$1 = class ReportStorage {
30826
31461
  async dump(symbol, strategyName, path = "./dump/sync", columns = COLUMN_CONFIG.sync_columns) {
30827
31462
  const markdown = await this.getReport(symbol, strategyName, columns);
30828
31463
  const timestamp = getContextTimestamp();
30829
- const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
31464
+ const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
30830
31465
  await Markdown.writeData("sync", markdown, {
30831
31466
  path,
30832
31467
  file: filename,
@@ -30860,7 +31495,7 @@ let ReportStorage$1 = class ReportStorage {
30860
31495
  class SyncMarkdownService {
30861
31496
  constructor() {
30862
31497
  this.loggerService = inject(TYPES.loggerService);
30863
- this.getStorage = 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));
31498
+ this.getStorage = 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));
30864
31499
  /**
30865
31500
  * Subscribes to `syncSubject` to start receiving `SignalSyncContract` events.
30866
31501
  * Protected against multiple subscriptions via `singleshot` — subsequent calls
@@ -31056,7 +31691,7 @@ class SyncMarkdownService {
31056
31691
  this.clear = async (payload) => {
31057
31692
  this.loggerService.log("syncMarkdownService clear", { payload });
31058
31693
  if (payload) {
31059
- const key = CREATE_KEY_FN$7(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31694
+ const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31060
31695
  this.getStorage.clear(key);
31061
31696
  }
31062
31697
  else {
@@ -31069,7 +31704,7 @@ class SyncMarkdownService {
31069
31704
  /**
31070
31705
  * Creates a unique memoization key for a symbol-strategy-exchange-frame-backtest combination.
31071
31706
  */
31072
- const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31707
+ const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31073
31708
  const parts = [symbol, strategyName, exchangeName];
31074
31709
  if (frameName)
31075
31710
  parts.push(frameName);
@@ -31079,7 +31714,7 @@ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest
31079
31714
  /**
31080
31715
  * Creates a filename for the markdown report.
31081
31716
  */
31082
- const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
31717
+ const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
31083
31718
  const parts = [symbol, strategyName, exchangeName];
31084
31719
  if (frameName) {
31085
31720
  parts.push(frameName);
@@ -31092,7 +31727,7 @@ const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, time
31092
31727
  /**
31093
31728
  * Accumulates highest profit events per symbol-strategy-exchange-frame combination.
31094
31729
  */
31095
- class ReportStorage {
31730
+ let ReportStorage$1 = class ReportStorage {
31096
31731
  constructor(symbol, strategyName, exchangeName, frameName) {
31097
31732
  this.symbol = symbol;
31098
31733
  this.strategyName = strategyName;
@@ -31223,7 +31858,7 @@ class ReportStorage {
31223
31858
  async dump(symbol, strategyName, path = "./dump/highest_profit", columns = COLUMN_CONFIG.highest_profit_columns) {
31224
31859
  const markdown = await this.getReport(symbol, strategyName, columns);
31225
31860
  const timestamp = getContextTimestamp();
31226
- const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
31861
+ const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
31227
31862
  await Markdown.writeData("highest_profit", markdown, {
31228
31863
  path,
31229
31864
  file: filename,
@@ -31234,7 +31869,7 @@ class ReportStorage {
31234
31869
  frameName: this.frameName,
31235
31870
  });
31236
31871
  }
31237
- }
31872
+ };
31238
31873
  /**
31239
31874
  * Service for generating and saving highest profit markdown reports.
31240
31875
  *
@@ -31245,7 +31880,7 @@ class ReportStorage {
31245
31880
  class HighestProfitMarkdownService {
31246
31881
  constructor() {
31247
31882
  this.loggerService = inject(TYPES.loggerService);
31248
- this.getStorage = 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));
31883
+ this.getStorage = 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));
31249
31884
  /**
31250
31885
  * Subscribes to `highestProfitSubject` to start receiving `HighestProfitContract`
31251
31886
  * events. Protected against multiple subscriptions via `singleshot` — subsequent
@@ -31411,7 +32046,7 @@ class HighestProfitMarkdownService {
31411
32046
  this.clear = async (payload) => {
31412
32047
  this.loggerService.log("highestProfitMarkdownService clear", { payload });
31413
32048
  if (payload) {
31414
- const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32049
+ const key = CREATE_KEY_FN$7(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31415
32050
  this.getStorage.clear(key);
31416
32051
  }
31417
32052
  else {
@@ -31433,7 +32068,7 @@ const LISTEN_TIMEOUT$1 = 120000;
31433
32068
  * @param backtest - Whether running in backtest mode
31434
32069
  * @returns Unique string key for memoization
31435
32070
  */
31436
- const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
32071
+ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31437
32072
  const parts = [symbol, strategyName, exchangeName];
31438
32073
  if (frameName)
31439
32074
  parts.push(frameName);
@@ -31476,7 +32111,7 @@ class PriceMetaService {
31476
32111
  * Each subject holds the latest currentPrice emitted by the strategy iterator for that key.
31477
32112
  * Instances are cached until clear() is called.
31478
32113
  */
31479
- this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
32114
+ this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
31480
32115
  /**
31481
32116
  * Returns the current market price for the given symbol and context.
31482
32117
  *
@@ -31505,10 +32140,10 @@ class PriceMetaService {
31505
32140
  if (source.data) {
31506
32141
  return source.data;
31507
32142
  }
31508
- 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...`);
32143
+ 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...`);
31509
32144
  const currentPrice = await waitForNext(source, (data) => !!data, LISTEN_TIMEOUT$1);
31510
32145
  if (typeof currentPrice === "symbol") {
31511
- throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$5(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
32146
+ throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$6(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31512
32147
  }
31513
32148
  return currentPrice;
31514
32149
  };
@@ -31550,7 +32185,7 @@ class PriceMetaService {
31550
32185
  this.getSource.clear();
31551
32186
  return;
31552
32187
  }
31553
- const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32188
+ const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31554
32189
  this.getSource.clear(key);
31555
32190
  };
31556
32191
  }
@@ -31568,7 +32203,7 @@ const LISTEN_TIMEOUT = 120000;
31568
32203
  * @param backtest - Whether running in backtest mode
31569
32204
  * @returns Unique string key for memoization
31570
32205
  */
31571
- const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
32206
+ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31572
32207
  const parts = [symbol, strategyName, exchangeName];
31573
32208
  if (frameName)
31574
32209
  parts.push(frameName);
@@ -31611,7 +32246,7 @@ class TimeMetaService {
31611
32246
  * Each subject holds the latest createdAt timestamp emitted by the strategy iterator for that key.
31612
32247
  * Instances are cached until clear() is called.
31613
32248
  */
31614
- this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$4(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
32249
+ this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
31615
32250
  /**
31616
32251
  * Returns the current candle timestamp (in milliseconds) for the given symbol and context.
31617
32252
  *
@@ -31639,10 +32274,10 @@ class TimeMetaService {
31639
32274
  if (source.data) {
31640
32275
  return source.data;
31641
32276
  }
31642
- 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...`);
32277
+ 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...`);
31643
32278
  const timestamp = await waitForNext(source, (data) => !!data, LISTEN_TIMEOUT);
31644
32279
  if (typeof timestamp === "symbol") {
31645
- throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$4(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
32280
+ throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$5(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31646
32281
  }
31647
32282
  return timestamp;
31648
32283
  };
@@ -31684,12 +32319,315 @@ class TimeMetaService {
31684
32319
  this.getSource.clear();
31685
32320
  return;
31686
32321
  }
31687
- const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32322
+ const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31688
32323
  this.getSource.clear(key);
31689
32324
  };
31690
32325
  }
31691
32326
  }
31692
32327
 
32328
+ const MAX_DRAWDOWN_REPORT_METHOD_NAME_SUBSCRIBE = "MaxDrawdownReportService.subscribe";
32329
+ const MAX_DRAWDOWN_REPORT_METHOD_NAME_UNSUBSCRIBE = "MaxDrawdownReportService.unsubscribe";
32330
+ const MAX_DRAWDOWN_REPORT_METHOD_NAME_TICK = "MaxDrawdownReportService.tick";
32331
+ /**
32332
+ * Service for logging max drawdown events to the JSONL report database.
32333
+ *
32334
+ * Listens to maxDrawdownSubject and writes each new drawdown record to
32335
+ * Report.writeData() for persistence and analytics.
32336
+ */
32337
+ class MaxDrawdownReportService {
32338
+ constructor() {
32339
+ this.loggerService = inject(TYPES.loggerService);
32340
+ /**
32341
+ * Handles a single `MaxDrawdownContract` event emitted by `maxDrawdownSubject`.
32342
+ *
32343
+ * Writes a JSONL record to the `"max_drawdown"` report database via
32344
+ * `Report.writeData`, capturing the full signal snapshot at the moment
32345
+ * the new drawdown record was set:
32346
+ * - `timestamp`, `symbol`, `strategyName`, `exchangeName`, `frameName`, `backtest`
32347
+ * - `signalId`, `position`, `currentPrice`
32348
+ * - `priceOpen`, `priceTakeProfit`, `priceStopLoss` (effective values from the signal)
32349
+ *
32350
+ * `strategyName` and signal-level fields are sourced from `data.signal`
32351
+ * rather than the contract root.
32352
+ *
32353
+ * @param data - `MaxDrawdownContract` payload containing `symbol`,
32354
+ * `signal`, `currentPrice`, `backtest`, `timestamp`, `exchangeName`,
32355
+ * `frameName`
32356
+ */
32357
+ this.tick = async (data) => {
32358
+ this.loggerService.log(MAX_DRAWDOWN_REPORT_METHOD_NAME_TICK, { data });
32359
+ await Report.writeData("max_drawdown", {
32360
+ timestamp: data.timestamp,
32361
+ symbol: data.symbol,
32362
+ strategyName: data.signal.strategyName,
32363
+ exchangeName: data.exchangeName,
32364
+ frameName: data.frameName,
32365
+ backtest: data.backtest,
32366
+ signalId: data.signal.id,
32367
+ position: data.signal.position,
32368
+ currentPrice: data.currentPrice,
32369
+ priceOpen: data.signal.priceOpen,
32370
+ priceTakeProfit: data.signal.priceTakeProfit,
32371
+ priceStopLoss: data.signal.priceStopLoss,
32372
+ }, {
32373
+ symbol: data.symbol,
32374
+ strategyName: data.signal.strategyName,
32375
+ exchangeName: data.exchangeName,
32376
+ frameName: data.frameName,
32377
+ signalId: data.signal.id,
32378
+ walkerName: "",
32379
+ });
32380
+ };
32381
+ /**
32382
+ * Subscribes to `maxDrawdownSubject` to start persisting drawdown records.
32383
+ * Protected against multiple subscriptions via `singleshot` — subsequent
32384
+ * calls return the same unsubscribe function without re-subscribing.
32385
+ *
32386
+ * The returned unsubscribe function clears the `singleshot` state and
32387
+ * detaches from `maxDrawdownSubject`.
32388
+ *
32389
+ * @returns Unsubscribe function; calling it tears down the subscription
32390
+ */
32391
+ this.subscribe = singleshot(() => {
32392
+ this.loggerService.log(MAX_DRAWDOWN_REPORT_METHOD_NAME_SUBSCRIBE);
32393
+ const unsub = maxDrawdownSubject.subscribe(this.tick);
32394
+ return () => {
32395
+ this.subscribe.clear();
32396
+ unsub();
32397
+ };
32398
+ });
32399
+ /**
32400
+ * Detaches from `maxDrawdownSubject`, stopping further JSONL writes.
32401
+ *
32402
+ * Calls the unsubscribe closure returned by `subscribe()`.
32403
+ * If `subscribe()` was never called, does nothing.
32404
+ */
32405
+ this.unsubscribe = async () => {
32406
+ this.loggerService.log(MAX_DRAWDOWN_REPORT_METHOD_NAME_UNSUBSCRIBE);
32407
+ if (this.subscribe.hasValue()) {
32408
+ const lastSubscription = this.subscribe();
32409
+ lastSubscription();
32410
+ }
32411
+ };
32412
+ }
32413
+ }
32414
+
32415
+ /**
32416
+ * Creates a unique memoization key for a symbol-strategy-exchange-frame-backtest combination.
32417
+ */
32418
+ const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
32419
+ const parts = [symbol, strategyName, exchangeName];
32420
+ if (frameName)
32421
+ parts.push(frameName);
32422
+ parts.push(backtest ? "backtest" : "live");
32423
+ return parts.join(":");
32424
+ };
32425
+ /**
32426
+ * Creates a filename for the markdown report.
32427
+ */
32428
+ const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
32429
+ const parts = [symbol, strategyName, exchangeName];
32430
+ if (frameName) {
32431
+ parts.push(frameName);
32432
+ parts.push("backtest");
32433
+ }
32434
+ else
32435
+ parts.push("live");
32436
+ return `${parts.join("_")}-${timestamp}.md`;
32437
+ };
32438
+ /**
32439
+ * Accumulates max drawdown events per symbol-strategy-exchange-frame combination.
32440
+ */
32441
+ class ReportStorage {
32442
+ constructor(symbol, strategyName, exchangeName, frameName) {
32443
+ this.symbol = symbol;
32444
+ this.strategyName = strategyName;
32445
+ this.exchangeName = exchangeName;
32446
+ this.frameName = frameName;
32447
+ this._eventList = [];
32448
+ }
32449
+ /**
32450
+ * Constructs a `MaxDrawdownEvent` from the given signal snapshot and
32451
+ * prepends it to the internal queue (most recent first).
32452
+ *
32453
+ * Once the queue exceeds `GLOBAL_CONFIG.CC_MAX_MAX_DRAWDOWN_MARKDOWN_ROWS`
32454
+ * entries, the oldest entry is dropped from the tail.
32455
+ */
32456
+ addEvent(data, currentPrice, backtest, timestamp) {
32457
+ this._eventList.unshift({
32458
+ timestamp,
32459
+ symbol: data.symbol,
32460
+ strategyName: data.strategyName,
32461
+ signalId: data.id,
32462
+ position: data.position,
32463
+ pnl: data.pnl,
32464
+ currentPrice,
32465
+ priceOpen: data.priceOpen,
32466
+ priceTakeProfit: data.priceTakeProfit,
32467
+ priceStopLoss: data.priceStopLoss,
32468
+ backtest,
32469
+ });
32470
+ if (this._eventList.length > GLOBAL_CONFIG.CC_MAX_MAX_DRAWDOWN_MARKDOWN_ROWS) {
32471
+ this._eventList.pop();
32472
+ }
32473
+ }
32474
+ /**
32475
+ * Returns the accumulated event list with a total count.
32476
+ */
32477
+ async getData() {
32478
+ return {
32479
+ eventList: this._eventList,
32480
+ totalEvents: this._eventList.length,
32481
+ };
32482
+ }
32483
+ /**
32484
+ * Renders a markdown max drawdown report for this storage instance.
32485
+ */
32486
+ async getReport(symbol, strategyName, columns = COLUMN_CONFIG.max_drawdown_columns) {
32487
+ const stats = await this.getData();
32488
+ if (stats.totalEvents === 0) {
32489
+ return [
32490
+ `# Max Drawdown Report: ${symbol}:${strategyName}`,
32491
+ "",
32492
+ "No max drawdown events recorded yet.",
32493
+ ].join("\n");
32494
+ }
32495
+ const visibleColumns = [];
32496
+ for (const col of columns) {
32497
+ if (await col.isVisible()) {
32498
+ visibleColumns.push(col);
32499
+ }
32500
+ }
32501
+ const header = visibleColumns.map((col) => col.label);
32502
+ const separator = visibleColumns.map(() => "---");
32503
+ const rows = await Promise.all(this._eventList.map(async (event, index) => Promise.all(visibleColumns.map((col) => col.format(event, index)))));
32504
+ const tableData = [header, separator, ...rows];
32505
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
32506
+ return [
32507
+ `# Max Drawdown Report: ${symbol}:${strategyName}`,
32508
+ "",
32509
+ table,
32510
+ "",
32511
+ `**Total events:** ${stats.totalEvents}`,
32512
+ ].join("\n");
32513
+ }
32514
+ /**
32515
+ * Generates the markdown report and persists it via `Markdown.writeData`.
32516
+ */
32517
+ async dump(symbol, strategyName, path = "./dump/max_drawdown", columns = COLUMN_CONFIG.max_drawdown_columns) {
32518
+ const markdown = await this.getReport(symbol, strategyName, columns);
32519
+ const timestamp = getContextTimestamp();
32520
+ const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
32521
+ await Markdown.writeData("max_drawdown", markdown, {
32522
+ path,
32523
+ file: filename,
32524
+ symbol: this.symbol,
32525
+ signalId: "",
32526
+ strategyName: this.strategyName,
32527
+ exchangeName: this.exchangeName,
32528
+ frameName: this.frameName,
32529
+ });
32530
+ }
32531
+ }
32532
+ /**
32533
+ * Service for generating and saving max drawdown markdown reports.
32534
+ *
32535
+ * Listens to maxDrawdownSubject and accumulates events per
32536
+ * symbol-strategy-exchange-frame combination. Provides getData(),
32537
+ * getReport(), and dump() methods matching the HighestProfit pattern.
32538
+ */
32539
+ class MaxDrawdownMarkdownService {
32540
+ constructor() {
32541
+ this.loggerService = inject(TYPES.loggerService);
32542
+ this.getStorage = 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));
32543
+ /**
32544
+ * Subscribes to `maxDrawdownSubject` to start receiving `MaxDrawdownContract`
32545
+ * events. Protected against multiple subscriptions via `singleshot`.
32546
+ *
32547
+ * @returns Unsubscribe function; calling it tears down the subscription and
32548
+ * clears all accumulated data
32549
+ */
32550
+ this.subscribe = singleshot(() => {
32551
+ this.loggerService.log("maxDrawdownMarkdownService init");
32552
+ const unsub = maxDrawdownSubject.subscribe(this.tick);
32553
+ return () => {
32554
+ this.subscribe.clear();
32555
+ this.clear();
32556
+ unsub();
32557
+ };
32558
+ });
32559
+ /**
32560
+ * Detaches from `maxDrawdownSubject` and clears all accumulated data.
32561
+ *
32562
+ * If `subscribe()` was never called, does nothing.
32563
+ */
32564
+ this.unsubscribe = async () => {
32565
+ this.loggerService.log("maxDrawdownMarkdownService unsubscribe");
32566
+ if (this.subscribe.hasValue()) {
32567
+ const lastSubscription = this.subscribe();
32568
+ lastSubscription();
32569
+ }
32570
+ };
32571
+ /**
32572
+ * Handles a single `MaxDrawdownContract` event emitted by `maxDrawdownSubject`.
32573
+ */
32574
+ this.tick = async (data) => {
32575
+ this.loggerService.log("maxDrawdownMarkdownService tick", { data });
32576
+ const storage = this.getStorage(data.symbol, data.signal.strategyName, data.exchangeName, data.frameName, data.backtest);
32577
+ storage.addEvent(data.signal, data.currentPrice, data.backtest, data.timestamp);
32578
+ };
32579
+ /**
32580
+ * Returns accumulated max drawdown statistics for the given context.
32581
+ */
32582
+ this.getData = async (symbol, strategyName, exchangeName, frameName, backtest) => {
32583
+ this.loggerService.log("maxDrawdownMarkdownService getData", { symbol, strategyName, exchangeName, frameName, backtest });
32584
+ if (!this.subscribe.hasValue()) {
32585
+ throw new Error("MaxDrawdownMarkdownService not initialized. Call subscribe() before getting data.");
32586
+ }
32587
+ const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
32588
+ return storage.getData();
32589
+ };
32590
+ /**
32591
+ * Generates a markdown max drawdown report for the given context.
32592
+ */
32593
+ this.getReport = async (symbol, strategyName, exchangeName, frameName, backtest, columns = COLUMN_CONFIG.max_drawdown_columns) => {
32594
+ this.loggerService.log("maxDrawdownMarkdownService getReport", { symbol, strategyName, exchangeName, frameName, backtest });
32595
+ if (!this.subscribe.hasValue()) {
32596
+ throw new Error("MaxDrawdownMarkdownService not initialized. Call subscribe() before generating reports.");
32597
+ }
32598
+ const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
32599
+ return storage.getReport(symbol, strategyName, columns);
32600
+ };
32601
+ /**
32602
+ * Generates the max drawdown report and writes it to disk.
32603
+ */
32604
+ this.dump = async (symbol, strategyName, exchangeName, frameName, backtest, path = "./dump/max_drawdown", columns = COLUMN_CONFIG.max_drawdown_columns) => {
32605
+ this.loggerService.log("maxDrawdownMarkdownService dump", { symbol, strategyName, exchangeName, frameName, backtest, path });
32606
+ if (!this.subscribe.hasValue()) {
32607
+ throw new Error("MaxDrawdownMarkdownService not initialized. Call subscribe() before dumping reports.");
32608
+ }
32609
+ const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
32610
+ await storage.dump(symbol, strategyName, path, columns);
32611
+ };
32612
+ /**
32613
+ * Evicts memoized `ReportStorage` instances, releasing all accumulated event data.
32614
+ *
32615
+ * - With `payload` — clears only the storage bucket for that combination.
32616
+ * - Without `payload` — clears **all** storage buckets.
32617
+ */
32618
+ this.clear = async (payload) => {
32619
+ this.loggerService.log("maxDrawdownMarkdownService clear", { payload });
32620
+ if (payload) {
32621
+ const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32622
+ this.getStorage.clear(key);
32623
+ }
32624
+ else {
32625
+ this.getStorage.clear();
32626
+ }
32627
+ };
32628
+ }
32629
+ }
32630
+
31693
32631
  {
31694
32632
  provide(TYPES.loggerService, () => new LoggerService());
31695
32633
  }
@@ -31760,6 +32698,7 @@ class TimeMetaService {
31760
32698
  provide(TYPES.strategyMarkdownService, () => new StrategyMarkdownService());
31761
32699
  provide(TYPES.syncMarkdownService, () => new SyncMarkdownService());
31762
32700
  provide(TYPES.highestProfitMarkdownService, () => new HighestProfitMarkdownService());
32701
+ provide(TYPES.maxDrawdownMarkdownService, () => new MaxDrawdownMarkdownService());
31763
32702
  }
31764
32703
  {
31765
32704
  provide(TYPES.backtestReportService, () => new BacktestReportService());
@@ -31774,6 +32713,7 @@ class TimeMetaService {
31774
32713
  provide(TYPES.strategyReportService, () => new StrategyReportService());
31775
32714
  provide(TYPES.syncReportService, () => new SyncReportService());
31776
32715
  provide(TYPES.highestProfitReportService, () => new HighestProfitReportService());
32716
+ provide(TYPES.maxDrawdownReportService, () => new MaxDrawdownReportService());
31777
32717
  }
31778
32718
  {
31779
32719
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -31857,6 +32797,7 @@ const markdownServices = {
31857
32797
  strategyMarkdownService: inject(TYPES.strategyMarkdownService),
31858
32798
  syncMarkdownService: inject(TYPES.syncMarkdownService),
31859
32799
  highestProfitMarkdownService: inject(TYPES.highestProfitMarkdownService),
32800
+ maxDrawdownMarkdownService: inject(TYPES.maxDrawdownMarkdownService),
31860
32801
  };
31861
32802
  const reportServices = {
31862
32803
  backtestReportService: inject(TYPES.backtestReportService),
@@ -31871,6 +32812,7 @@ const reportServices = {
31871
32812
  strategyReportService: inject(TYPES.strategyReportService),
31872
32813
  syncReportService: inject(TYPES.syncReportService),
31873
32814
  highestProfitReportService: inject(TYPES.highestProfitReportService),
32815
+ maxDrawdownReportService: inject(TYPES.maxDrawdownReportService),
31874
32816
  };
31875
32817
  const validationServices = {
31876
32818
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -34798,6 +35740,12 @@ const GET_POSITION_HIGHEST_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionHig
34798
35740
  const GET_POSITION_HIGHEST_PNL_COST_METHOD_NAME = "strategy.getPositionHighestPnlCost";
34799
35741
  const GET_POSITION_HIGHEST_PROFIT_BREAKEVEN_METHOD_NAME = "strategy.getPositionHighestProfitBreakeven";
34800
35742
  const GET_POSITION_DRAWDOWN_MINUTES_METHOD_NAME = "strategy.getPositionDrawdownMinutes";
35743
+ const GET_POSITION_HIGHEST_PROFIT_MINUTES_METHOD_NAME = "strategy.getPositionHighestProfitMinutes";
35744
+ const GET_POSITION_MAX_DRAWDOWN_MINUTES_METHOD_NAME = "strategy.getPositionMaxDrawdownMinutes";
35745
+ const GET_POSITION_MAX_DRAWDOWN_PRICE_METHOD_NAME = "strategy.getPositionMaxDrawdownPrice";
35746
+ const GET_POSITION_MAX_DRAWDOWN_TIMESTAMP_METHOD_NAME = "strategy.getPositionMaxDrawdownTimestamp";
35747
+ const GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlPercentage";
35748
+ const GET_POSITION_MAX_DRAWDOWN_PNL_COST_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlCost";
34801
35749
  const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
34802
35750
  const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
34803
35751
  const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
@@ -36257,6 +37205,181 @@ async function getPositionDrawdownMinutes(symbol) {
36257
37205
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
36258
37206
  return await bt.strategyCoreService.getPositionDrawdownMinutes(isBacktest, symbol, { exchangeName, frameName, strategyName });
36259
37207
  }
37208
+ /**
37209
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
37210
+ *
37211
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
37212
+ * pulling back from its peak profit level.
37213
+ * Zero when called at the exact moment the peak was set.
37214
+ *
37215
+ * Returns null if no pending signal exists.
37216
+ *
37217
+ * @param symbol - Trading pair symbol
37218
+ * @returns Promise resolving to minutes since last profit peak or null
37219
+ *
37220
+ * @example
37221
+ * ```typescript
37222
+ * import { getPositionHighestProfitMinutes } from "backtest-kit";
37223
+ *
37224
+ * const minutes = await getPositionHighestProfitMinutes("BTCUSDT");
37225
+ * // e.g. 30 (30 minutes since the highest profit price)
37226
+ * ```
37227
+ */
37228
+ async function getPositionHighestProfitMinutes(symbol) {
37229
+ bt.loggerService.info(GET_POSITION_HIGHEST_PROFIT_MINUTES_METHOD_NAME, { symbol });
37230
+ if (!ExecutionContextService.hasContext()) {
37231
+ throw new Error("getPositionHighestProfitMinutes requires an execution context");
37232
+ }
37233
+ if (!MethodContextService.hasContext()) {
37234
+ throw new Error("getPositionHighestProfitMinutes requires a method context");
37235
+ }
37236
+ const { backtest: isBacktest } = bt.executionContextService.context;
37237
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37238
+ return await bt.strategyCoreService.getPositionHighestProfitMinutes(isBacktest, symbol, { exchangeName, frameName, strategyName });
37239
+ }
37240
+ /**
37241
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
37242
+ *
37243
+ * Measures how long ago the deepest drawdown point occurred.
37244
+ * Zero when called at the exact moment the trough was set.
37245
+ *
37246
+ * Returns null if no pending signal exists.
37247
+ *
37248
+ * @param symbol - Trading pair symbol
37249
+ * @returns Promise resolving to minutes since last drawdown trough or null
37250
+ *
37251
+ * @example
37252
+ * ```typescript
37253
+ * import { getPositionMaxDrawdownMinutes } from "backtest-kit";
37254
+ *
37255
+ * const minutes = await getPositionMaxDrawdownMinutes("BTCUSDT");
37256
+ * // e.g. 15 (15 minutes since the worst loss price)
37257
+ * ```
37258
+ */
37259
+ async function getPositionMaxDrawdownMinutes(symbol) {
37260
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_MINUTES_METHOD_NAME, { symbol });
37261
+ if (!ExecutionContextService.hasContext()) {
37262
+ throw new Error("getPositionMaxDrawdownMinutes requires an execution context");
37263
+ }
37264
+ if (!MethodContextService.hasContext()) {
37265
+ throw new Error("getPositionMaxDrawdownMinutes requires a method context");
37266
+ }
37267
+ const { backtest: isBacktest } = bt.executionContextService.context;
37268
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37269
+ return await bt.strategyCoreService.getPositionMaxDrawdownMinutes(isBacktest, symbol, { exchangeName, frameName, strategyName });
37270
+ }
37271
+ /**
37272
+ * Returns the worst price reached in the loss direction during this position's life.
37273
+ *
37274
+ * Returns null if no pending signal exists.
37275
+ *
37276
+ * @param symbol - Trading pair symbol
37277
+ * @returns Promise resolving to price or null
37278
+ *
37279
+ * @example
37280
+ * ```typescript
37281
+ * import { getPositionMaxDrawdownPrice } from "backtest-kit";
37282
+ *
37283
+ * const price = await getPositionMaxDrawdownPrice("BTCUSDT");
37284
+ * // e.g. 41000 (lowest price seen for a LONG position)
37285
+ * ```
37286
+ */
37287
+ async function getPositionMaxDrawdownPrice(symbol) {
37288
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_PRICE_METHOD_NAME, { symbol });
37289
+ if (!ExecutionContextService.hasContext()) {
37290
+ throw new Error("getPositionMaxDrawdownPrice requires an execution context");
37291
+ }
37292
+ if (!MethodContextService.hasContext()) {
37293
+ throw new Error("getPositionMaxDrawdownPrice requires a method context");
37294
+ }
37295
+ const { backtest: isBacktest } = bt.executionContextService.context;
37296
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37297
+ return await bt.strategyCoreService.getPositionMaxDrawdownPrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
37298
+ }
37299
+ /**
37300
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
37301
+ *
37302
+ * Returns null if no pending signal exists.
37303
+ *
37304
+ * @param symbol - Trading pair symbol
37305
+ * @returns Promise resolving to timestamp in milliseconds or null
37306
+ *
37307
+ * @example
37308
+ * ```typescript
37309
+ * import { getPositionMaxDrawdownTimestamp } from "backtest-kit";
37310
+ *
37311
+ * const ts = await getPositionMaxDrawdownTimestamp("BTCUSDT");
37312
+ * // e.g. 1700000000000
37313
+ * ```
37314
+ */
37315
+ async function getPositionMaxDrawdownTimestamp(symbol) {
37316
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_TIMESTAMP_METHOD_NAME, { symbol });
37317
+ if (!ExecutionContextService.hasContext()) {
37318
+ throw new Error("getPositionMaxDrawdownTimestamp requires an execution context");
37319
+ }
37320
+ if (!MethodContextService.hasContext()) {
37321
+ throw new Error("getPositionMaxDrawdownTimestamp requires a method context");
37322
+ }
37323
+ const { backtest: isBacktest } = bt.executionContextService.context;
37324
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37325
+ return await bt.strategyCoreService.getPositionMaxDrawdownTimestamp(isBacktest, symbol, { exchangeName, frameName, strategyName });
37326
+ }
37327
+ /**
37328
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
37329
+ *
37330
+ * Returns null if no pending signal exists.
37331
+ *
37332
+ * @param symbol - Trading pair symbol
37333
+ * @returns Promise resolving to PnL percentage or null
37334
+ *
37335
+ * @example
37336
+ * ```typescript
37337
+ * import { getPositionMaxDrawdownPnlPercentage } from "backtest-kit";
37338
+ *
37339
+ * const pnl = await getPositionMaxDrawdownPnlPercentage("BTCUSDT");
37340
+ * // e.g. -5.2 (deepest PnL percentage reached)
37341
+ * ```
37342
+ */
37343
+ async function getPositionMaxDrawdownPnlPercentage(symbol) {
37344
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME, { symbol });
37345
+ if (!ExecutionContextService.hasContext()) {
37346
+ throw new Error("getPositionMaxDrawdownPnlPercentage requires an execution context");
37347
+ }
37348
+ if (!MethodContextService.hasContext()) {
37349
+ throw new Error("getPositionMaxDrawdownPnlPercentage requires a method context");
37350
+ }
37351
+ const { backtest: isBacktest } = bt.executionContextService.context;
37352
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37353
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlPercentage(isBacktest, symbol, { exchangeName, frameName, strategyName });
37354
+ }
37355
+ /**
37356
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
37357
+ *
37358
+ * Returns null if no pending signal exists.
37359
+ *
37360
+ * @param symbol - Trading pair symbol
37361
+ * @returns Promise resolving to PnL cost or null
37362
+ *
37363
+ * @example
37364
+ * ```typescript
37365
+ * import { getPositionMaxDrawdownPnlCost } from "backtest-kit";
37366
+ *
37367
+ * const cost = await getPositionMaxDrawdownPnlCost("BTCUSDT");
37368
+ * // e.g. -52 (deepest PnL in quote currency)
37369
+ * ```
37370
+ */
37371
+ async function getPositionMaxDrawdownPnlCost(symbol) {
37372
+ bt.loggerService.info(GET_POSITION_MAX_DRAWDOWN_PNL_COST_METHOD_NAME, { symbol });
37373
+ if (!ExecutionContextService.hasContext()) {
37374
+ throw new Error("getPositionMaxDrawdownPnlCost requires an execution context");
37375
+ }
37376
+ if (!MethodContextService.hasContext()) {
37377
+ throw new Error("getPositionMaxDrawdownPnlCost requires a method context");
37378
+ }
37379
+ const { backtest: isBacktest } = bt.executionContextService.context;
37380
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
37381
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
37382
+ }
36260
37383
  /**
36261
37384
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
36262
37385
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -36498,6 +37621,8 @@ const LISTEN_SYNC_METHOD_NAME = "event.listenSync";
36498
37621
  const LISTEN_SYNC_ONCE_METHOD_NAME = "event.listenSyncOnce";
36499
37622
  const LISTEN_HIGHEST_PROFIT_METHOD_NAME = "event.listenHighestProfit";
36500
37623
  const LISTEN_HIGHEST_PROFIT_ONCE_METHOD_NAME = "event.listenHighestProfitOnce";
37624
+ const LISTEN_MAX_DRAWDOWN_METHOD_NAME = "event.listenMaxDrawdown";
37625
+ const LISTEN_MAX_DRAWDOWN_ONCE_METHOD_NAME = "event.listenMaxDrawdownOnce";
36501
37626
  /**
36502
37627
  * Subscribes to all signal events with queued async processing.
36503
37628
  *
@@ -37848,6 +38973,47 @@ function listenHighestProfitOnce(filterFn, fn) {
37848
38973
  };
37849
38974
  return disposeFn = listenHighestProfit(wrappedFn);
37850
38975
  }
38976
+ /**
38977
+ * Subscribes to max drawdown events with queued async processing.
38978
+ * Emits when a signal reaches a new maximum drawdown level during its lifecycle.
38979
+ * Events are processed sequentially in order received, even if callback is async.
38980
+ * Uses queued wrapper to prevent concurrent execution of the callback.
38981
+ * Useful for tracking drawdown milestones and implementing dynamic risk management logic.
38982
+ * @param fn - Callback function to handle max drawdown events
38983
+ * @return Unsubscribe function to stop listening to events
38984
+ */
38985
+ function listenMaxDrawdown(fn) {
38986
+ bt.loggerService.log(LISTEN_MAX_DRAWDOWN_METHOD_NAME);
38987
+ const wrappedFn = async (event) => {
38988
+ if (await bt.strategyCoreService.hasPendingSignal(event.backtest, event.symbol, {
38989
+ strategyName: event.strategyName,
38990
+ exchangeName: event.exchangeName,
38991
+ frameName: event.frameName,
38992
+ })) {
38993
+ await fn(event);
38994
+ }
38995
+ };
38996
+ return maxDrawdownSubject.subscribe(queued(wrappedFn));
38997
+ }
38998
+ /**
38999
+ * Subscribes to filtered max drawdown events with one-time execution.
39000
+ * Listens for events matching the filter predicate, then executes callback once
39001
+ * and automatically unsubscribes. Useful for waiting for specific drawdown conditions.
39002
+ * @param filterFn - Predicate to filter which events trigger the callback
39003
+ * @param fn - Callback function to handle the filtered event (called only once)
39004
+ * @return Unsubscribe function to cancel the listener before it fires
39005
+ */
39006
+ function listenMaxDrawdownOnce(filterFn, fn) {
39007
+ bt.loggerService.log(LISTEN_MAX_DRAWDOWN_ONCE_METHOD_NAME);
39008
+ let disposeFn;
39009
+ const wrappedFn = async (event) => {
39010
+ if (filterFn(event)) {
39011
+ await fn(event);
39012
+ disposeFn && disposeFn();
39013
+ }
39014
+ };
39015
+ return disposeFn = listenMaxDrawdown(wrappedFn);
39016
+ }
37851
39017
 
37852
39018
  const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
37853
39019
  const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
@@ -37877,6 +39043,12 @@ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE = "BacktestUtils.
37877
39043
  const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST = "BacktestUtils.getPositionHighestPnlCost";
37878
39044
  const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN = "BacktestUtils.getPositionHighestProfitBreakeven";
37879
39045
  const BACKTEST_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES = "BacktestUtils.getPositionDrawdownMinutes";
39046
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES = "BacktestUtils.getPositionHighestProfitMinutes";
39047
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES = "BacktestUtils.getPositionMaxDrawdownMinutes";
39048
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "BacktestUtils.getPositionMaxDrawdownPrice";
39049
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "BacktestUtils.getPositionMaxDrawdownTimestamp";
39050
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "BacktestUtils.getPositionMaxDrawdownPnlPercentage";
39051
+ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "BacktestUtils.getPositionMaxDrawdownPnlCost";
37880
39052
  const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
37881
39053
  const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
37882
39054
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
@@ -38977,6 +40149,175 @@ class BacktestUtils {
38977
40149
  }
38978
40150
  return await bt.strategyCoreService.getPositionDrawdownMinutes(true, symbol, context);
38979
40151
  };
40152
+ /**
40153
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
40154
+ *
40155
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
40156
+ * pulling back from its peak profit level.
40157
+ * Zero when called at the exact moment the peak was set.
40158
+ *
40159
+ * Returns null if no pending signal exists.
40160
+ *
40161
+ * @param symbol - Trading pair symbol
40162
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40163
+ * @returns Minutes since last profit peak, or null if no active position
40164
+ */
40165
+ this.getPositionHighestProfitMinutes = async (symbol, context) => {
40166
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES, {
40167
+ symbol,
40168
+ context,
40169
+ });
40170
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
40171
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
40172
+ {
40173
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40174
+ riskName &&
40175
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
40176
+ riskList &&
40177
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES));
40178
+ actions &&
40179
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES));
40180
+ }
40181
+ return await bt.strategyCoreService.getPositionHighestProfitMinutes(true, symbol, context);
40182
+ };
40183
+ /**
40184
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
40185
+ *
40186
+ * Measures how long ago the deepest drawdown point occurred.
40187
+ * Zero when called at the exact moment the trough was set.
40188
+ *
40189
+ * Returns null if no pending signal exists.
40190
+ *
40191
+ * @param symbol - Trading pair symbol
40192
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40193
+ * @returns Minutes since last drawdown trough, or null if no active position
40194
+ */
40195
+ this.getPositionMaxDrawdownMinutes = async (symbol, context) => {
40196
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES, {
40197
+ symbol,
40198
+ context,
40199
+ });
40200
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
40201
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
40202
+ {
40203
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40204
+ riskName &&
40205
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
40206
+ riskList &&
40207
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES));
40208
+ actions &&
40209
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES));
40210
+ }
40211
+ return await bt.strategyCoreService.getPositionMaxDrawdownMinutes(true, symbol, context);
40212
+ };
40213
+ /**
40214
+ * Returns the worst price reached in the loss direction during this position's life.
40215
+ *
40216
+ * Returns null if no pending signal exists.
40217
+ *
40218
+ * @param symbol - Trading pair symbol
40219
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40220
+ * @returns price or null if no active position
40221
+ */
40222
+ this.getPositionMaxDrawdownPrice = async (symbol, context) => {
40223
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE, {
40224
+ symbol,
40225
+ context,
40226
+ });
40227
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
40228
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
40229
+ {
40230
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40231
+ riskName &&
40232
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
40233
+ riskList &&
40234
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE));
40235
+ actions &&
40236
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE));
40237
+ }
40238
+ return await bt.strategyCoreService.getPositionMaxDrawdownPrice(true, symbol, context);
40239
+ };
40240
+ /**
40241
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
40242
+ *
40243
+ * Returns null if no pending signal exists.
40244
+ *
40245
+ * @param symbol - Trading pair symbol
40246
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40247
+ * @returns timestamp in milliseconds or null if no active position
40248
+ */
40249
+ this.getPositionMaxDrawdownTimestamp = async (symbol, context) => {
40250
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP, {
40251
+ symbol,
40252
+ context,
40253
+ });
40254
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
40255
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
40256
+ {
40257
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40258
+ riskName &&
40259
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
40260
+ riskList &&
40261
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP));
40262
+ actions &&
40263
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP));
40264
+ }
40265
+ return await bt.strategyCoreService.getPositionMaxDrawdownTimestamp(true, symbol, context);
40266
+ };
40267
+ /**
40268
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
40269
+ *
40270
+ * Returns null if no pending signal exists.
40271
+ *
40272
+ * @param symbol - Trading pair symbol
40273
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40274
+ * @returns PnL percentage or null if no active position
40275
+ */
40276
+ this.getPositionMaxDrawdownPnlPercentage = async (symbol, context) => {
40277
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE, {
40278
+ symbol,
40279
+ context,
40280
+ });
40281
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
40282
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
40283
+ {
40284
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40285
+ riskName &&
40286
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
40287
+ riskList &&
40288
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE));
40289
+ actions &&
40290
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE));
40291
+ }
40292
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlPercentage(true, symbol, context);
40293
+ };
40294
+ /**
40295
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
40296
+ *
40297
+ * Returns null if no pending signal exists.
40298
+ *
40299
+ * @param symbol - Trading pair symbol
40300
+ * @param context - Execution context with strategyName, exchangeName, and frameName
40301
+ * @returns PnL cost or null if no active position
40302
+ */
40303
+ this.getPositionMaxDrawdownPnlCost = async (symbol, context) => {
40304
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST, {
40305
+ symbol,
40306
+ context,
40307
+ });
40308
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
40309
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
40310
+ {
40311
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
40312
+ riskName &&
40313
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
40314
+ riskList &&
40315
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST));
40316
+ actions &&
40317
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST));
40318
+ }
40319
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlCost(true, symbol, context);
40320
+ };
38980
40321
  /**
38981
40322
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
38982
40323
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -40096,6 +41437,12 @@ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE = "LiveUtils.getPosit
40096
41437
  const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST = "LiveUtils.getPositionHighestPnlCost";
40097
41438
  const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN = "LiveUtils.getPositionHighestProfitBreakeven";
40098
41439
  const LIVE_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES = "LiveUtils.getPositionDrawdownMinutes";
41440
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES = "LiveUtils.getPositionHighestProfitMinutes";
41441
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES = "LiveUtils.getPositionMaxDrawdownMinutes";
41442
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "LiveUtils.getPositionMaxDrawdownPrice";
41443
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "LiveUtils.getPositionMaxDrawdownTimestamp";
41444
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "LiveUtils.getPositionMaxDrawdownPnlPercentage";
41445
+ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "LiveUtils.getPositionMaxDrawdownPnlCost";
40099
41446
  const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
40100
41447
  const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
40101
41448
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
@@ -41299,6 +42646,199 @@ class LiveUtils {
41299
42646
  frameName: "",
41300
42647
  });
41301
42648
  };
42649
+ /**
42650
+ * Returns the number of minutes elapsed since the highest profit price was recorded.
42651
+ *
42652
+ * Alias for getPositionDrawdownMinutes — measures how long the position has been
42653
+ * pulling back from its peak profit level.
42654
+ * Zero when called at the exact moment the peak was set.
42655
+ *
42656
+ * Returns null if no pending signal exists.
42657
+ *
42658
+ * @param symbol - Trading pair symbol
42659
+ * @param context - Execution context with strategyName and exchangeName
42660
+ * @returns Minutes since last profit peak, or null if no active position
42661
+ */
42662
+ this.getPositionHighestProfitMinutes = async (symbol, context) => {
42663
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES, {
42664
+ symbol,
42665
+ context,
42666
+ });
42667
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
42668
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
42669
+ {
42670
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42671
+ riskName &&
42672
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES);
42673
+ riskList &&
42674
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES));
42675
+ actions &&
42676
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_MINUTES));
42677
+ }
42678
+ return await bt.strategyCoreService.getPositionHighestProfitMinutes(false, symbol, {
42679
+ strategyName: context.strategyName,
42680
+ exchangeName: context.exchangeName,
42681
+ frameName: "",
42682
+ });
42683
+ };
42684
+ /**
42685
+ * Returns the number of minutes elapsed since the worst loss price was recorded.
42686
+ *
42687
+ * Measures how long ago the deepest drawdown point occurred.
42688
+ * Zero when called at the exact moment the trough was set.
42689
+ *
42690
+ * Returns null if no pending signal exists.
42691
+ *
42692
+ * @param symbol - Trading pair symbol
42693
+ * @param context - Execution context with strategyName and exchangeName
42694
+ * @returns Minutes since last drawdown trough, or null if no active position
42695
+ */
42696
+ this.getPositionMaxDrawdownMinutes = async (symbol, context) => {
42697
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES, {
42698
+ symbol,
42699
+ context,
42700
+ });
42701
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
42702
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
42703
+ {
42704
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42705
+ riskName &&
42706
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES);
42707
+ riskList &&
42708
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES));
42709
+ actions &&
42710
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_MINUTES));
42711
+ }
42712
+ return await bt.strategyCoreService.getPositionMaxDrawdownMinutes(false, symbol, {
42713
+ strategyName: context.strategyName,
42714
+ exchangeName: context.exchangeName,
42715
+ frameName: "",
42716
+ });
42717
+ };
42718
+ /**
42719
+ * Returns the worst price reached in the loss direction during this position's life.
42720
+ *
42721
+ * Returns null if no pending signal exists.
42722
+ *
42723
+ * @param symbol - Trading pair symbol
42724
+ * @param context - Execution context with strategyName and exchangeName
42725
+ * @returns price or null if no active position
42726
+ */
42727
+ this.getPositionMaxDrawdownPrice = async (symbol, context) => {
42728
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE, {
42729
+ symbol,
42730
+ context,
42731
+ });
42732
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
42733
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
42734
+ {
42735
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42736
+ riskName &&
42737
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE);
42738
+ riskList &&
42739
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE));
42740
+ actions &&
42741
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE));
42742
+ }
42743
+ return await bt.strategyCoreService.getPositionMaxDrawdownPrice(false, symbol, {
42744
+ strategyName: context.strategyName,
42745
+ exchangeName: context.exchangeName,
42746
+ frameName: "",
42747
+ });
42748
+ };
42749
+ /**
42750
+ * Returns the timestamp when the worst loss price was recorded during this position's life.
42751
+ *
42752
+ * Returns null if no pending signal exists.
42753
+ *
42754
+ * @param symbol - Trading pair symbol
42755
+ * @param context - Execution context with strategyName and exchangeName
42756
+ * @returns timestamp in milliseconds or null if no active position
42757
+ */
42758
+ this.getPositionMaxDrawdownTimestamp = async (symbol, context) => {
42759
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP, {
42760
+ symbol,
42761
+ context,
42762
+ });
42763
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
42764
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
42765
+ {
42766
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42767
+ riskName &&
42768
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP);
42769
+ riskList &&
42770
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP));
42771
+ actions &&
42772
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP));
42773
+ }
42774
+ return await bt.strategyCoreService.getPositionMaxDrawdownTimestamp(false, symbol, {
42775
+ strategyName: context.strategyName,
42776
+ exchangeName: context.exchangeName,
42777
+ frameName: "",
42778
+ });
42779
+ };
42780
+ /**
42781
+ * Returns the PnL percentage at the moment the worst loss price was recorded during this position's life.
42782
+ *
42783
+ * Returns null if no pending signal exists.
42784
+ *
42785
+ * @param symbol - Trading pair symbol
42786
+ * @param context - Execution context with strategyName and exchangeName
42787
+ * @returns PnL percentage or null if no active position
42788
+ */
42789
+ this.getPositionMaxDrawdownPnlPercentage = async (symbol, context) => {
42790
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE, {
42791
+ symbol,
42792
+ context,
42793
+ });
42794
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
42795
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
42796
+ {
42797
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42798
+ riskName &&
42799
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE);
42800
+ riskList &&
42801
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE));
42802
+ actions &&
42803
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE));
42804
+ }
42805
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlPercentage(false, symbol, {
42806
+ strategyName: context.strategyName,
42807
+ exchangeName: context.exchangeName,
42808
+ frameName: "",
42809
+ });
42810
+ };
42811
+ /**
42812
+ * Returns the PnL cost (in quote currency) at the moment the worst loss price was recorded during this position's life.
42813
+ *
42814
+ * Returns null if no pending signal exists.
42815
+ *
42816
+ * @param symbol - Trading pair symbol
42817
+ * @param context - Execution context with strategyName and exchangeName
42818
+ * @returns PnL cost or null if no active position
42819
+ */
42820
+ this.getPositionMaxDrawdownPnlCost = async (symbol, context) => {
42821
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST, {
42822
+ symbol,
42823
+ context,
42824
+ });
42825
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
42826
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
42827
+ {
42828
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
42829
+ riskName &&
42830
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST);
42831
+ riskList &&
42832
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST));
42833
+ actions &&
42834
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST));
42835
+ }
42836
+ return await bt.strategyCoreService.getPositionMaxDrawdownPnlCost(false, symbol, {
42837
+ strategyName: context.strategyName,
42838
+ exchangeName: context.exchangeName,
42839
+ frameName: "",
42840
+ });
42841
+ };
41302
42842
  /**
41303
42843
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
41304
42844
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -47736,6 +49276,98 @@ class HighestProfitUtils {
47736
49276
  */
47737
49277
  const HighestProfit = new HighestProfitUtils();
47738
49278
 
49279
+ const MAX_DRAWDOWN_METHOD_NAME_GET_DATA = "MaxDrawdownUtils.getData";
49280
+ const MAX_DRAWDOWN_METHOD_NAME_GET_REPORT = "MaxDrawdownUtils.getReport";
49281
+ const MAX_DRAWDOWN_METHOD_NAME_DUMP = "MaxDrawdownUtils.dump";
49282
+ /**
49283
+ * Utility class for accessing max drawdown reports and statistics.
49284
+ *
49285
+ * Provides static-like methods (via singleton instance) to retrieve data
49286
+ * accumulated by MaxDrawdownMarkdownService from maxDrawdownSubject events.
49287
+ *
49288
+ * @example
49289
+ * ```typescript
49290
+ * import { MaxDrawdown } from "backtest-kit";
49291
+ *
49292
+ * const stats = await MaxDrawdown.getData("BTCUSDT", { strategyName, exchangeName, frameName });
49293
+ * const report = await MaxDrawdown.getReport("BTCUSDT", { strategyName, exchangeName, frameName });
49294
+ * await MaxDrawdown.dump("BTCUSDT", { strategyName, exchangeName, frameName });
49295
+ * ```
49296
+ */
49297
+ class MaxDrawdownUtils {
49298
+ constructor() {
49299
+ /**
49300
+ * Retrieves statistical data from accumulated max drawdown events.
49301
+ *
49302
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
49303
+ * @param context - Execution context
49304
+ * @param backtest - Whether to query backtest data
49305
+ * @returns Promise resolving to MaxDrawdownStatisticsModel
49306
+ */
49307
+ this.getData = async (symbol, context, backtest = false) => {
49308
+ bt.loggerService.info(MAX_DRAWDOWN_METHOD_NAME_GET_DATA, { symbol, strategyName: context.strategyName });
49309
+ bt.strategyValidationService.validate(context.strategyName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA);
49310
+ bt.exchangeValidationService.validate(context.exchangeName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA);
49311
+ context.frameName && bt.frameValidationService.validate(context.frameName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA);
49312
+ {
49313
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
49314
+ riskName && bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA);
49315
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA));
49316
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, MAX_DRAWDOWN_METHOD_NAME_GET_DATA));
49317
+ }
49318
+ return await bt.maxDrawdownMarkdownService.getData(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
49319
+ };
49320
+ /**
49321
+ * Generates a markdown report with all max drawdown events for a symbol-strategy pair.
49322
+ *
49323
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
49324
+ * @param context - Execution context
49325
+ * @param backtest - Whether to query backtest data
49326
+ * @param columns - Optional column configuration
49327
+ * @returns Promise resolving to markdown formatted report string
49328
+ */
49329
+ this.getReport = async (symbol, context, backtest = false, columns) => {
49330
+ bt.loggerService.info(MAX_DRAWDOWN_METHOD_NAME_GET_REPORT, { symbol, strategyName: context.strategyName });
49331
+ bt.strategyValidationService.validate(context.strategyName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT);
49332
+ bt.exchangeValidationService.validate(context.exchangeName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT);
49333
+ context.frameName && bt.frameValidationService.validate(context.frameName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT);
49334
+ {
49335
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
49336
+ riskName && bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT);
49337
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT));
49338
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, MAX_DRAWDOWN_METHOD_NAME_GET_REPORT));
49339
+ }
49340
+ return await bt.maxDrawdownMarkdownService.getReport(symbol, context.strategyName, context.exchangeName, context.frameName, backtest, columns);
49341
+ };
49342
+ /**
49343
+ * Generates and saves a markdown report to file.
49344
+ *
49345
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
49346
+ * @param context - Execution context
49347
+ * @param backtest - Whether to query backtest data
49348
+ * @param path - Output directory path (default: "./dump/max_drawdown")
49349
+ * @param columns - Optional column configuration
49350
+ */
49351
+ this.dump = async (symbol, context, backtest = false, path, columns) => {
49352
+ bt.loggerService.info(MAX_DRAWDOWN_METHOD_NAME_DUMP, { symbol, strategyName: context.strategyName, path });
49353
+ bt.strategyValidationService.validate(context.strategyName, MAX_DRAWDOWN_METHOD_NAME_DUMP);
49354
+ bt.exchangeValidationService.validate(context.exchangeName, MAX_DRAWDOWN_METHOD_NAME_DUMP);
49355
+ context.frameName && bt.frameValidationService.validate(context.frameName, MAX_DRAWDOWN_METHOD_NAME_DUMP);
49356
+ {
49357
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
49358
+ riskName && bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_DUMP);
49359
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, MAX_DRAWDOWN_METHOD_NAME_DUMP));
49360
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, MAX_DRAWDOWN_METHOD_NAME_DUMP));
49361
+ }
49362
+ await bt.maxDrawdownMarkdownService.dump(symbol, context.strategyName, context.exchangeName, context.frameName, backtest, path, columns);
49363
+ };
49364
+ }
49365
+ }
49366
+ /**
49367
+ * Global singleton instance of MaxDrawdownUtils.
49368
+ */
49369
+ const MaxDrawdown = new MaxDrawdownUtils();
49370
+
47739
49371
  /**
47740
49372
  * Utility class containing predefined trading constants for take-profit and stop-loss levels.
47741
49373
  *
@@ -52600,4 +54232,4 @@ const validateSignal = (signal, currentPrice) => {
52600
54232
  return !errors.length;
52601
54233
  };
52602
54234
 
52603
- export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, Memory, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistMemoryAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Sync, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, warmCandles, writeMemory };
54235
+ export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MaxDrawdown, Memory, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistMemoryAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Sync, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, warmCandles, writeMemory };