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