backtest-kit 5.5.3 → 5.6.1
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 +1999 -173
- package/build/index.mjs +1987 -173
- package/package.json +1 -1
- package/types.d.ts +1141 -179
package/build/index.cjs
CHANGED
|
@@ -127,6 +127,7 @@ const markdownServices$1 = {
|
|
|
127
127
|
riskMarkdownService: Symbol('riskMarkdownService'),
|
|
128
128
|
strategyMarkdownService: Symbol('strategyMarkdownService'),
|
|
129
129
|
syncMarkdownService: Symbol('syncMarkdownService'),
|
|
130
|
+
highestProfitMarkdownService: Symbol('highestProfitMarkdownService'),
|
|
130
131
|
};
|
|
131
132
|
const reportServices$1 = {
|
|
132
133
|
backtestReportService: Symbol('backtestReportService'),
|
|
@@ -140,6 +141,7 @@ const reportServices$1 = {
|
|
|
140
141
|
riskReportService: Symbol('riskReportService'),
|
|
141
142
|
strategyReportService: Symbol('strategyReportService'),
|
|
142
143
|
syncReportService: Symbol('syncReportService'),
|
|
144
|
+
highestProfitReportService: Symbol('highestProfitReportService'),
|
|
143
145
|
};
|
|
144
146
|
const validationServices$1 = {
|
|
145
147
|
exchangeValidationService: Symbol('exchangeValidationService'),
|
|
@@ -642,6 +644,12 @@ const strategyCommitSubject = new functoolsKit.Subject();
|
|
|
642
644
|
* BacktestLogicPrivateService::*run
|
|
643
645
|
*/
|
|
644
646
|
const backtestScheduleOpenSubject = new functoolsKit.Subject();
|
|
647
|
+
/**
|
|
648
|
+
* Highest profit emitter for real-time profit tracking.
|
|
649
|
+
* Emits updates on the highest profit achieved for an open position.
|
|
650
|
+
* Allows users to track profit milestones and implement custom management logic based on profit levels.
|
|
651
|
+
*/
|
|
652
|
+
const highestProfitSubject = new functoolsKit.Subject();
|
|
645
653
|
|
|
646
654
|
var emitters = /*#__PURE__*/Object.freeze({
|
|
647
655
|
__proto__: null,
|
|
@@ -653,6 +661,7 @@ var emitters = /*#__PURE__*/Object.freeze({
|
|
|
653
661
|
doneWalkerSubject: doneWalkerSubject,
|
|
654
662
|
errorEmitter: errorEmitter,
|
|
655
663
|
exitEmitter: exitEmitter,
|
|
664
|
+
highestProfitSubject: highestProfitSubject,
|
|
656
665
|
partialLossSubject: partialLossSubject,
|
|
657
666
|
partialProfitSubject: partialProfitSubject,
|
|
658
667
|
performanceEmitter: performanceEmitter,
|
|
@@ -4271,6 +4280,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
4271
4280
|
timestamp: currentTime,
|
|
4272
4281
|
_isScheduled: false,
|
|
4273
4282
|
_entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
|
|
4283
|
+
_peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
|
|
4274
4284
|
};
|
|
4275
4285
|
// Валидируем сигнал перед возвратом
|
|
4276
4286
|
VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
|
|
@@ -4295,6 +4305,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
4295
4305
|
timestamp: currentTime,
|
|
4296
4306
|
_isScheduled: true,
|
|
4297
4307
|
_entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
|
|
4308
|
+
_peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
|
|
4298
4309
|
};
|
|
4299
4310
|
// Валидируем сигнал перед возвратом
|
|
4300
4311
|
VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
|
|
@@ -4315,6 +4326,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
4315
4326
|
timestamp: currentTime,
|
|
4316
4327
|
_isScheduled: false,
|
|
4317
4328
|
_entry: [{ price: currentPrice, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
|
|
4329
|
+
_peak: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
|
|
4318
4330
|
};
|
|
4319
4331
|
// Валидируем сигнал перед возвратом
|
|
4320
4332
|
VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
|
|
@@ -5017,6 +5029,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
5017
5029
|
...scheduled,
|
|
5018
5030
|
pendingAt: activationTime,
|
|
5019
5031
|
_isScheduled: false,
|
|
5032
|
+
_peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
|
|
5020
5033
|
};
|
|
5021
5034
|
// Sync open: if external system rejects — cancel scheduled signal instead of opening
|
|
5022
5035
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
|
|
@@ -5653,7 +5666,7 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
5653
5666
|
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
5654
5667
|
return result;
|
|
5655
5668
|
};
|
|
5656
|
-
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
5669
|
+
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice, backtest) => {
|
|
5657
5670
|
let percentTp = 0;
|
|
5658
5671
|
let percentSl = 0;
|
|
5659
5672
|
const currentTime = self.params.execution.context.when.getTime();
|
|
@@ -5674,6 +5687,15 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
5674
5687
|
const tpDistance = effectiveTakeProfit - effectivePriceOpen;
|
|
5675
5688
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
5676
5689
|
percentTp = Math.min(progressPercent, 100);
|
|
5690
|
+
if (currentPrice > signal._peak.price) {
|
|
5691
|
+
const { pnl } = TO_PUBLIC_SIGNAL(signal, currentPrice);
|
|
5692
|
+
signal._peak = { price: currentPrice, timestamp: currentTime, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
|
|
5693
|
+
if (self.params.callbacks?.onWrite) {
|
|
5694
|
+
self.params.callbacks.onWrite(signal.symbol, signal, backtest);
|
|
5695
|
+
}
|
|
5696
|
+
!backtest && await PersistSignalAdapter.writeSignalData(signal, self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
|
|
5697
|
+
await self.params.onHighestProfit(TO_PUBLIC_SIGNAL(signal, currentPrice), currentPrice, currentTime);
|
|
5698
|
+
}
|
|
5677
5699
|
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
|
|
5678
5700
|
}
|
|
5679
5701
|
else if (currentDistance < 0) {
|
|
@@ -5698,6 +5720,15 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
5698
5720
|
const tpDistance = effectivePriceOpen - effectiveTakeProfit;
|
|
5699
5721
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
5700
5722
|
percentTp = Math.min(progressPercent, 100);
|
|
5723
|
+
if (currentPrice < signal._peak.price) {
|
|
5724
|
+
const { pnl } = TO_PUBLIC_SIGNAL(signal, currentPrice);
|
|
5725
|
+
signal._peak = { price: currentPrice, timestamp: currentTime, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
|
|
5726
|
+
if (self.params.callbacks?.onWrite) {
|
|
5727
|
+
self.params.callbacks.onWrite(signal.symbol, signal, backtest);
|
|
5728
|
+
}
|
|
5729
|
+
!backtest && await PersistSignalAdapter.writeSignalData(signal, self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
|
|
5730
|
+
await self.params.onHighestProfit(TO_PUBLIC_SIGNAL(signal, currentPrice), currentPrice, currentTime);
|
|
5731
|
+
}
|
|
5701
5732
|
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
|
|
5702
5733
|
}
|
|
5703
5734
|
if (currentDistance < 0) {
|
|
@@ -5745,7 +5776,7 @@ const RETURN_IDLE_FN = async (self, currentPrice) => {
|
|
|
5745
5776
|
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
5746
5777
|
return result;
|
|
5747
5778
|
};
|
|
5748
|
-
const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePrice, closeTimestamp, reason) => {
|
|
5779
|
+
const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePrice, closeTimestamp, reason, cancelId) => {
|
|
5749
5780
|
self.params.logger.info("ClientStrategy backtest scheduled signal cancelled", {
|
|
5750
5781
|
symbol: self.params.execution.context.symbol,
|
|
5751
5782
|
signalId: scheduled.id,
|
|
@@ -5755,6 +5786,23 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
|
|
|
5755
5786
|
reason,
|
|
5756
5787
|
});
|
|
5757
5788
|
await self.setScheduledSignal(null);
|
|
5789
|
+
if (reason === "user") {
|
|
5790
|
+
await CALL_COMMIT_FN(self, {
|
|
5791
|
+
action: "cancel-scheduled",
|
|
5792
|
+
symbol: self.params.execution.context.symbol,
|
|
5793
|
+
strategyName: self.params.strategyName,
|
|
5794
|
+
exchangeName: self.params.exchangeName,
|
|
5795
|
+
frameName: self.params.frameName,
|
|
5796
|
+
signalId: scheduled.id,
|
|
5797
|
+
backtest: true,
|
|
5798
|
+
cancelId,
|
|
5799
|
+
timestamp: closeTimestamp,
|
|
5800
|
+
totalEntries: scheduled._entry?.length ?? 1,
|
|
5801
|
+
totalPartials: scheduled._partial?.length ?? 0,
|
|
5802
|
+
originalPriceOpen: scheduled.priceOpen,
|
|
5803
|
+
pnl: toProfitLossDto(scheduled, averagePrice),
|
|
5804
|
+
});
|
|
5805
|
+
}
|
|
5758
5806
|
await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
5759
5807
|
const result = {
|
|
5760
5808
|
action: "cancelled",
|
|
@@ -5767,6 +5815,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
|
|
|
5767
5815
|
symbol: self.params.execution.context.symbol,
|
|
5768
5816
|
backtest: self.params.execution.context.backtest,
|
|
5769
5817
|
reason,
|
|
5818
|
+
cancelId,
|
|
5770
5819
|
createdAt: closeTimestamp,
|
|
5771
5820
|
};
|
|
5772
5821
|
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
@@ -5806,6 +5855,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
5806
5855
|
...scheduled,
|
|
5807
5856
|
pendingAt: activationTime,
|
|
5808
5857
|
_isScheduled: false,
|
|
5858
|
+
_peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0 },
|
|
5809
5859
|
};
|
|
5810
5860
|
// Sync open: if external system rejects — cancel scheduled signal instead of opening
|
|
5811
5861
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
|
|
@@ -5955,7 +6005,8 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5955
6005
|
// КРИТИЧНО: Проверяем был ли сигнал отменен пользователем через cancel()
|
|
5956
6006
|
if (self._cancelledSignal) {
|
|
5957
6007
|
// Сигнал был отменен через cancel() в onSchedulePing
|
|
5958
|
-
const
|
|
6008
|
+
const cancelId = self._cancelledSignal.cancelId;
|
|
6009
|
+
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "user", cancelId);
|
|
5959
6010
|
return { outcome: "cancelled", result };
|
|
5960
6011
|
}
|
|
5961
6012
|
// КРИТИЧНО: Проверяем был ли сигнал активирован пользователем через activateScheduled()
|
|
@@ -5985,6 +6036,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5985
6036
|
...activatedSignal,
|
|
5986
6037
|
pendingAt: candle.timestamp,
|
|
5987
6038
|
_isScheduled: false,
|
|
6039
|
+
_peak: { price: activatedSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage: 0, pnlCost: 0 },
|
|
5988
6040
|
};
|
|
5989
6041
|
// Sync open: if external system rejects — cancel scheduled signal instead of opening
|
|
5990
6042
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(candle.timestamp, pendingSignal.priceOpen, pendingSignal, self);
|
|
@@ -6186,6 +6238,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
6186
6238
|
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
6187
6239
|
const tpDistance = effectiveTakeProfit - effectivePriceOpen;
|
|
6188
6240
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
6241
|
+
if (averagePrice > signal._peak.price) {
|
|
6242
|
+
const { pnl } = TO_PUBLIC_SIGNAL(signal, averagePrice);
|
|
6243
|
+
signal._peak = { price: averagePrice, timestamp: currentCandleTimestamp, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
|
|
6244
|
+
if (self.params.callbacks?.onWrite) {
|
|
6245
|
+
self.params.callbacks.onWrite(signal.symbol, signal, true);
|
|
6246
|
+
}
|
|
6247
|
+
await self.params.onHighestProfit(TO_PUBLIC_SIGNAL(signal, averagePrice), averagePrice, currentCandleTimestamp);
|
|
6248
|
+
}
|
|
6189
6249
|
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
|
|
6190
6250
|
}
|
|
6191
6251
|
else if (currentDistance < 0) {
|
|
@@ -6208,6 +6268,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
6208
6268
|
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
6209
6269
|
const tpDistance = effectivePriceOpen - effectiveTakeProfit;
|
|
6210
6270
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
6271
|
+
if (averagePrice < signal._peak.price) {
|
|
6272
|
+
const { pnl } = TO_PUBLIC_SIGNAL(signal, averagePrice);
|
|
6273
|
+
signal._peak = { price: averagePrice, timestamp: currentCandleTimestamp, pnlCost: pnl.pnlCost, pnlPercentage: pnl.pnlPercentage };
|
|
6274
|
+
if (self.params.callbacks?.onWrite) {
|
|
6275
|
+
self.params.callbacks.onWrite(signal.symbol, signal, true);
|
|
6276
|
+
}
|
|
6277
|
+
await self.params.onHighestProfit(TO_PUBLIC_SIGNAL(signal, averagePrice), averagePrice, currentCandleTimestamp);
|
|
6278
|
+
}
|
|
6211
6279
|
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
|
|
6212
6280
|
}
|
|
6213
6281
|
if (currentDistance < 0) {
|
|
@@ -6584,8 +6652,8 @@ class ClientStrategy {
|
|
|
6584
6652
|
* @param symbol - Trading pair symbol
|
|
6585
6653
|
* @returns Promise resolving to effective entry price or null
|
|
6586
6654
|
*/
|
|
6587
|
-
async
|
|
6588
|
-
this.params.logger.debug("ClientStrategy
|
|
6655
|
+
async getPositionEffectivePrice(symbol) {
|
|
6656
|
+
this.params.logger.debug("ClientStrategy getPositionEffectivePrice", { symbol });
|
|
6589
6657
|
if (!this._pendingSignal) {
|
|
6590
6658
|
return null;
|
|
6591
6659
|
}
|
|
@@ -6742,6 +6810,170 @@ class ClientStrategy {
|
|
|
6742
6810
|
}
|
|
6743
6811
|
return entries.map(({ price, cost, timestamp }) => ({ price, cost, timestamp }));
|
|
6744
6812
|
}
|
|
6813
|
+
/**
|
|
6814
|
+
* Returns the original estimated duration for the current pending signal.
|
|
6815
|
+
*
|
|
6816
|
+
* Reflects `minuteEstimatedTime` as set in the signal DTO — the maximum
|
|
6817
|
+
* number of minutes the position is expected to be active before `time_expired`.
|
|
6818
|
+
*
|
|
6819
|
+
* Returns null if no pending signal exists.
|
|
6820
|
+
*
|
|
6821
|
+
* @param symbol - Trading pair symbol
|
|
6822
|
+
* @returns Promise resolving to estimated duration in minutes or null
|
|
6823
|
+
*/
|
|
6824
|
+
async getPositionEstimateMinutes(symbol) {
|
|
6825
|
+
this.params.logger.debug("ClientStrategy getPositionEstimateMinutes", { symbol });
|
|
6826
|
+
if (!this._pendingSignal) {
|
|
6827
|
+
return null;
|
|
6828
|
+
}
|
|
6829
|
+
return this._pendingSignal.minuteEstimatedTime;
|
|
6830
|
+
}
|
|
6831
|
+
/**
|
|
6832
|
+
* Returns the remaining time before the position expires, clamped to zero.
|
|
6833
|
+
*
|
|
6834
|
+
* Computes elapsed minutes since `pendingAt` and subtracts from `minuteEstimatedTime`.
|
|
6835
|
+
* Returns 0 once the estimate is exceeded (never negative).
|
|
6836
|
+
*
|
|
6837
|
+
* Returns null if no pending signal exists.
|
|
6838
|
+
*
|
|
6839
|
+
* @param symbol - Trading pair symbol
|
|
6840
|
+
* @param timestamp - Current Unix timestamp in milliseconds
|
|
6841
|
+
* @returns Promise resolving to remaining minutes (≥ 0) or null
|
|
6842
|
+
*/
|
|
6843
|
+
async getPositionCountdownMinutes(symbol, timestamp) {
|
|
6844
|
+
this.params.logger.debug("ClientStrategy getPositionCountdownMinutes", { symbol });
|
|
6845
|
+
if (!this._pendingSignal) {
|
|
6846
|
+
return null;
|
|
6847
|
+
}
|
|
6848
|
+
const elapsed = Math.floor((timestamp - this._pendingSignal.pendingAt) / 60000);
|
|
6849
|
+
return Math.max(0, this._pendingSignal.minuteEstimatedTime - elapsed);
|
|
6850
|
+
}
|
|
6851
|
+
/**
|
|
6852
|
+
* Returns the best price reached in the profit direction during this position's life.
|
|
6853
|
+
*
|
|
6854
|
+
* Initialized at position open with the entry price and timestamp.
|
|
6855
|
+
* Updated on every tick/candle when VWAP moves beyond the previous record toward TP:
|
|
6856
|
+
* - LONG: tracks the highest price seen above effective entry
|
|
6857
|
+
* - SHORT: tracks the lowest price seen below effective entry
|
|
6858
|
+
*
|
|
6859
|
+
* Returns null if no pending signal exists.
|
|
6860
|
+
* Never returns null when a signal is active — always contains at least the entry price.
|
|
6861
|
+
*
|
|
6862
|
+
* @param symbol - Trading pair symbol
|
|
6863
|
+
* @returns Promise resolving to price or null
|
|
6864
|
+
*/
|
|
6865
|
+
async getPositionHighestProfitPrice(symbol) {
|
|
6866
|
+
this.params.logger.debug("ClientStrategy getPositionHighestProfitPrice", { symbol });
|
|
6867
|
+
if (!this._pendingSignal) {
|
|
6868
|
+
return null;
|
|
6869
|
+
}
|
|
6870
|
+
return this._pendingSignal._peak.price;
|
|
6871
|
+
}
|
|
6872
|
+
/**
|
|
6873
|
+
* Returns the timestamp when the best profit price was recorded during this position's life.
|
|
6874
|
+
*
|
|
6875
|
+
* Returns null if no pending signal exists.
|
|
6876
|
+
*
|
|
6877
|
+
* @param symbol - Trading pair symbol
|
|
6878
|
+
* @returns Promise resolving to timestamp in milliseconds or null
|
|
6879
|
+
*/
|
|
6880
|
+
async getPositionHighestProfitTimestamp(symbol) {
|
|
6881
|
+
this.params.logger.debug("ClientStrategy getPositionHighestProfitTimestamp", { symbol });
|
|
6882
|
+
if (!this._pendingSignal) {
|
|
6883
|
+
return null;
|
|
6884
|
+
}
|
|
6885
|
+
return this._pendingSignal._peak.timestamp;
|
|
6886
|
+
}
|
|
6887
|
+
/**
|
|
6888
|
+
* Returns the PnL percentage at the moment the best profit price was recorded during this position's life.
|
|
6889
|
+
*
|
|
6890
|
+
* Initialized at position open with 0.
|
|
6891
|
+
* Updated on every tick/candle when VWAP moves beyond the previous record toward TP:
|
|
6892
|
+
* - LONG: tracks the PnL percentage at the highest price seen above effective entry
|
|
6893
|
+
* - SHORT: tracks the PnL percentage at the lowest price seen below effective entry
|
|
6894
|
+
*
|
|
6895
|
+
* Returns null if no pending signal exists.
|
|
6896
|
+
* Never returns null when a signal is active — always contains at least 0.
|
|
6897
|
+
*
|
|
6898
|
+
* @param symbol - Trading pair symbol
|
|
6899
|
+
* @returns Promise resolving to PnL percentage or null
|
|
6900
|
+
*/
|
|
6901
|
+
async getPositionHighestPnlPercentage(symbol) {
|
|
6902
|
+
this.params.logger.debug("ClientStrategy getPositionHighestPnlPercentage", { symbol });
|
|
6903
|
+
if (!this._pendingSignal) {
|
|
6904
|
+
return null;
|
|
6905
|
+
}
|
|
6906
|
+
return this._pendingSignal._peak.pnlPercentage;
|
|
6907
|
+
}
|
|
6908
|
+
/**
|
|
6909
|
+
* Returns the PnL cost (in quote currency) at the moment the best profit price was recorded during this position's life.
|
|
6910
|
+
*
|
|
6911
|
+
* Initialized at position open with 0.
|
|
6912
|
+
* Updated on every tick/candle when VWAP moves beyond the previous record toward TP:
|
|
6913
|
+
* - LONG: tracks the PnL cost at the highest price seen above effective entry
|
|
6914
|
+
* - SHORT: tracks the PnL cost at the lowest price seen below effective entry
|
|
6915
|
+
*
|
|
6916
|
+
* Returns null if no pending signal exists.
|
|
6917
|
+
* Never returns null when a signal is active — always contains at least 0.
|
|
6918
|
+
*
|
|
6919
|
+
* @param symbol - Trading pair symbol
|
|
6920
|
+
* @returns Promise resolving to PnL cost or null
|
|
6921
|
+
*/
|
|
6922
|
+
async getPositionHighestPnlCost(symbol) {
|
|
6923
|
+
this.params.logger.debug("ClientStrategy getPositionHighestPnlCost", { symbol });
|
|
6924
|
+
if (!this._pendingSignal) {
|
|
6925
|
+
return null;
|
|
6926
|
+
}
|
|
6927
|
+
return this._pendingSignal._peak.pnlCost;
|
|
6928
|
+
}
|
|
6929
|
+
/**
|
|
6930
|
+
* Returns the number of minutes elapsed since the highest profit price was recorded.
|
|
6931
|
+
*
|
|
6932
|
+
* Measures how long the position has been pulling back from its peak profit level.
|
|
6933
|
+
* Zero when called at the exact moment the peak was set.
|
|
6934
|
+
* Grows continuously as price moves away from the peak without setting a new record.
|
|
6935
|
+
*
|
|
6936
|
+
* Returns null if no pending signal exists.
|
|
6937
|
+
*
|
|
6938
|
+
* @param symbol - Trading pair symbol
|
|
6939
|
+
* @param timestamp - Current Unix timestamp in milliseconds
|
|
6940
|
+
* @returns Promise resolving to drawdown duration in minutes or null
|
|
6941
|
+
*/
|
|
6942
|
+
async getPositionHighestProfitBreakeven(symbol) {
|
|
6943
|
+
this.params.logger.debug("ClientStrategy getPositionHighestProfitBreakeven", { symbol });
|
|
6944
|
+
if (!this._pendingSignal) {
|
|
6945
|
+
return null;
|
|
6946
|
+
}
|
|
6947
|
+
const signal = this._pendingSignal;
|
|
6948
|
+
const effectivePriceOpen = getEffectivePriceOpen(signal);
|
|
6949
|
+
const peakPrice = signal._peak.price;
|
|
6950
|
+
const breakevenThresholdPercent = (GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE + GLOBAL_CONFIG.CC_PERCENT_FEE) * 2 + GLOBAL_CONFIG.CC_BREAKEVEN_THRESHOLD;
|
|
6951
|
+
if (signal.position === "long") {
|
|
6952
|
+
return peakPrice >= effectivePriceOpen * (1 + breakevenThresholdPercent / 100);
|
|
6953
|
+
}
|
|
6954
|
+
else {
|
|
6955
|
+
return peakPrice <= effectivePriceOpen * (1 - breakevenThresholdPercent / 100);
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
6958
|
+
/**
|
|
6959
|
+
* Returns the number of minutes elapsed since the highest profit price was recorded.
|
|
6960
|
+
*
|
|
6961
|
+
* Measures how long the position has been pulling back from its peak.
|
|
6962
|
+
* Zero when called at the exact moment the peak was set.
|
|
6963
|
+
*
|
|
6964
|
+
* Returns null if no pending signal exists.
|
|
6965
|
+
*
|
|
6966
|
+
* @param symbol - Trading pair symbol
|
|
6967
|
+
* @param timestamp - Current Unix timestamp in milliseconds
|
|
6968
|
+
* @returns Promise resolving to drawdown duration in minutes or null
|
|
6969
|
+
*/
|
|
6970
|
+
async getPositionDrawdownMinutes(symbol, timestamp) {
|
|
6971
|
+
this.params.logger.debug("ClientStrategy getPositionDrawdownMinutes", { symbol });
|
|
6972
|
+
if (!this._pendingSignal) {
|
|
6973
|
+
return null;
|
|
6974
|
+
}
|
|
6975
|
+
return Math.floor((timestamp - this._pendingSignal._peak.timestamp) / 60000);
|
|
6976
|
+
}
|
|
6745
6977
|
/**
|
|
6746
6978
|
* Performs a single tick of strategy execution.
|
|
6747
6979
|
*
|
|
@@ -6917,6 +7149,7 @@ class ClientStrategy {
|
|
|
6917
7149
|
...activatedSignal,
|
|
6918
7150
|
pendingAt: currentTime,
|
|
6919
7151
|
_isScheduled: false,
|
|
7152
|
+
_peak: { price: activatedSignal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
|
|
6920
7153
|
};
|
|
6921
7154
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(currentTime, currentPrice, pendingSignal, this);
|
|
6922
7155
|
if (!syncOpenAllowed) {
|
|
@@ -7039,7 +7272,7 @@ class ClientStrategy {
|
|
|
7039
7272
|
if (closedResult) {
|
|
7040
7273
|
return closedResult;
|
|
7041
7274
|
}
|
|
7042
|
-
return await RETURN_PENDING_SIGNAL_ACTIVE_FN(this, this._pendingSignal, averagePrice);
|
|
7275
|
+
return await RETURN_PENDING_SIGNAL_ACTIVE_FN(this, this._pendingSignal, averagePrice, false);
|
|
7043
7276
|
}
|
|
7044
7277
|
/**
|
|
7045
7278
|
* Fast backtests a signal using historical candle data.
|
|
@@ -9026,7 +9259,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
|
|
|
9026
9259
|
* @param backtest - Whether running in backtest mode
|
|
9027
9260
|
* @returns Unique string key for memoization
|
|
9028
9261
|
*/
|
|
9029
|
-
const CREATE_KEY_FN$
|
|
9262
|
+
const CREATE_KEY_FN$p = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
9030
9263
|
const parts = [symbol, strategyName, exchangeName];
|
|
9031
9264
|
if (frameName)
|
|
9032
9265
|
parts.push(frameName);
|
|
@@ -9150,6 +9383,43 @@ const CREATE_COMMIT_FN = (self) => functoolsKit.trycatch(async (event) => {
|
|
|
9150
9383
|
},
|
|
9151
9384
|
defaultValue: null,
|
|
9152
9385
|
});
|
|
9386
|
+
/**
|
|
9387
|
+
* Creates a callback function for emitting highest profit updates to highestProfitSubject.
|
|
9388
|
+
* Called by ClientStrategy when the highest profit for an open position is updated.
|
|
9389
|
+
* Emits HighestProfitContract event to all subscribers with the current price and timestamp.
|
|
9390
|
+
* Used for real-time profit tracking and management logic based on profit levels.
|
|
9391
|
+
*
|
|
9392
|
+
* @param self - Reference to StrategyConnectionService instance
|
|
9393
|
+
* @param strategyName - Name of the strategy
|
|
9394
|
+
* @param exchangeName - Name of the exchange
|
|
9395
|
+
* @param frameName - Name of the frame
|
|
9396
|
+
* @param isBacktest - Flag indicating if the operation is for backtesting
|
|
9397
|
+
* @returns Callback function for highest profit updates
|
|
9398
|
+
*/
|
|
9399
|
+
const CREATE_HIGHEST_PROFIT_FN = (self, strategyName, exchangeName, frameName, isBacktest) => functoolsKit.trycatch(async (signal, currentPrice, timestamp) => {
|
|
9400
|
+
await highestProfitSubject.next({
|
|
9401
|
+
symbol: signal.symbol,
|
|
9402
|
+
signal,
|
|
9403
|
+
currentPrice,
|
|
9404
|
+
timestamp,
|
|
9405
|
+
strategyName,
|
|
9406
|
+
exchangeName,
|
|
9407
|
+
frameName,
|
|
9408
|
+
backtest: isBacktest,
|
|
9409
|
+
});
|
|
9410
|
+
}, {
|
|
9411
|
+
fallback: (error) => {
|
|
9412
|
+
const message = "StrategyConnectionService CREATE_HIGHEST_PROFIT_FN thrown";
|
|
9413
|
+
const payload = {
|
|
9414
|
+
error: functoolsKit.errorData(error),
|
|
9415
|
+
message: functoolsKit.getErrorMessage(error),
|
|
9416
|
+
};
|
|
9417
|
+
bt.loggerService.warn(message, payload);
|
|
9418
|
+
console.warn(message, payload);
|
|
9419
|
+
errorEmitter.next(error);
|
|
9420
|
+
},
|
|
9421
|
+
defaultValue: null,
|
|
9422
|
+
});
|
|
9153
9423
|
/**
|
|
9154
9424
|
* Creates a callback function for emitting dispose events.
|
|
9155
9425
|
*
|
|
@@ -9220,7 +9490,7 @@ class StrategyConnectionService {
|
|
|
9220
9490
|
* @param backtest - Whether running in backtest mode
|
|
9221
9491
|
* @returns Configured ClientStrategy instance
|
|
9222
9492
|
*/
|
|
9223
|
-
this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
9493
|
+
this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$p(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
9224
9494
|
const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
9225
9495
|
return new ClientStrategy({
|
|
9226
9496
|
symbol,
|
|
@@ -9248,6 +9518,7 @@ class StrategyConnectionService {
|
|
|
9248
9518
|
onDispose: CREATE_COMMIT_DISPOSE_FN(this),
|
|
9249
9519
|
onCommit: CREATE_COMMIT_FN(),
|
|
9250
9520
|
onSignalSync: CREATE_SYNC_FN(this, strategyName, exchangeName, frameName, backtest),
|
|
9521
|
+
onHighestProfit: CREATE_HIGHEST_PROFIT_FN(this, strategyName, exchangeName, frameName, backtest),
|
|
9251
9522
|
});
|
|
9252
9523
|
});
|
|
9253
9524
|
/**
|
|
@@ -9321,14 +9592,14 @@ class StrategyConnectionService {
|
|
|
9321
9592
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
9322
9593
|
* @returns Promise resolving to effective entry price or null
|
|
9323
9594
|
*/
|
|
9324
|
-
this.
|
|
9325
|
-
this.loggerService.log("strategyConnectionService
|
|
9595
|
+
this.getPositionEffectivePrice = async (backtest, symbol, context) => {
|
|
9596
|
+
this.loggerService.log("strategyConnectionService getPositionEffectivePrice", {
|
|
9326
9597
|
symbol,
|
|
9327
9598
|
context,
|
|
9328
9599
|
backtest,
|
|
9329
9600
|
});
|
|
9330
9601
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
9331
|
-
return await strategy.
|
|
9602
|
+
return await strategy.getPositionEffectivePrice(symbol);
|
|
9332
9603
|
};
|
|
9333
9604
|
/**
|
|
9334
9605
|
* Returns the number of DCA entries made for the current pending signal.
|
|
@@ -9678,6 +9949,162 @@ class StrategyConnectionService {
|
|
|
9678
9949
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
9679
9950
|
return await strategy.hasPendingSignal(symbol);
|
|
9680
9951
|
};
|
|
9952
|
+
/**
|
|
9953
|
+
* Returns the original estimated duration for the current pending signal.
|
|
9954
|
+
*
|
|
9955
|
+
* Delegates to ClientStrategy.getPositionEstimateMinutes().
|
|
9956
|
+
* Returns null if no pending signal exists.
|
|
9957
|
+
*
|
|
9958
|
+
* @param backtest - Whether running in backtest mode
|
|
9959
|
+
* @param symbol - Trading pair symbol
|
|
9960
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
9961
|
+
* @returns Promise resolving to estimated duration in minutes or null
|
|
9962
|
+
*/
|
|
9963
|
+
this.getPositionEstimateMinutes = async (backtest, symbol, context) => {
|
|
9964
|
+
this.loggerService.log("strategyConnectionService getPositionEstimateMinutes", {
|
|
9965
|
+
symbol,
|
|
9966
|
+
context,
|
|
9967
|
+
});
|
|
9968
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
9969
|
+
return await strategy.getPositionEstimateMinutes(symbol);
|
|
9970
|
+
};
|
|
9971
|
+
/**
|
|
9972
|
+
* Returns the remaining time before the position expires, clamped to zero.
|
|
9973
|
+
*
|
|
9974
|
+
* Resolves current timestamp via timeMetaService and delegates to
|
|
9975
|
+
* ClientStrategy.getPositionCountdownMinutes().
|
|
9976
|
+
* Returns null if no pending signal exists.
|
|
9977
|
+
*
|
|
9978
|
+
* @param backtest - Whether running in backtest mode
|
|
9979
|
+
* @param symbol - Trading pair symbol
|
|
9980
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
9981
|
+
* @returns Promise resolving to remaining minutes (≥ 0) or null
|
|
9982
|
+
*/
|
|
9983
|
+
this.getPositionCountdownMinutes = async (backtest, symbol, context) => {
|
|
9984
|
+
this.loggerService.log("strategyConnectionService getPositionCountdownMinutes", {
|
|
9985
|
+
symbol,
|
|
9986
|
+
context,
|
|
9987
|
+
});
|
|
9988
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
9989
|
+
const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
|
|
9990
|
+
return await strategy.getPositionCountdownMinutes(symbol, timestamp);
|
|
9991
|
+
};
|
|
9992
|
+
/**
|
|
9993
|
+
* Returns the best price reached in the profit direction during this position's life.
|
|
9994
|
+
*
|
|
9995
|
+
* Delegates to ClientStrategy.getPositionHighestProfitPrice().
|
|
9996
|
+
* Returns null if no pending signal exists.
|
|
9997
|
+
*
|
|
9998
|
+
* @param backtest - Whether running in backtest mode
|
|
9999
|
+
* @param symbol - Trading pair symbol
|
|
10000
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
10001
|
+
* @returns Promise resolving to price or null
|
|
10002
|
+
*/
|
|
10003
|
+
this.getPositionHighestProfitPrice = async (backtest, symbol, context) => {
|
|
10004
|
+
this.loggerService.log("strategyConnectionService getPositionHighestProfitPrice", {
|
|
10005
|
+
symbol,
|
|
10006
|
+
context,
|
|
10007
|
+
});
|
|
10008
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
10009
|
+
return await strategy.getPositionHighestProfitPrice(symbol);
|
|
10010
|
+
};
|
|
10011
|
+
/**
|
|
10012
|
+
* Returns the timestamp when the best profit price was recorded during this position's life.
|
|
10013
|
+
*
|
|
10014
|
+
* Delegates to ClientStrategy.getPositionHighestProfitTimestamp().
|
|
10015
|
+
* Returns null if no pending signal exists.
|
|
10016
|
+
*
|
|
10017
|
+
* @param backtest - Whether running in backtest mode
|
|
10018
|
+
* @param symbol - Trading pair symbol
|
|
10019
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
10020
|
+
* @returns Promise resolving to timestamp in milliseconds or null
|
|
10021
|
+
*/
|
|
10022
|
+
this.getPositionHighestProfitTimestamp = async (backtest, symbol, context) => {
|
|
10023
|
+
this.loggerService.log("strategyConnectionService getPositionHighestProfitTimestamp", {
|
|
10024
|
+
symbol,
|
|
10025
|
+
context,
|
|
10026
|
+
});
|
|
10027
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
10028
|
+
return await strategy.getPositionHighestProfitTimestamp(symbol);
|
|
10029
|
+
};
|
|
10030
|
+
/**
|
|
10031
|
+
* Returns the PnL percentage at the moment the best profit price was recorded during this position's life.
|
|
10032
|
+
*
|
|
10033
|
+
* Delegates to ClientStrategy.getPositionHighestPnlPercentage().
|
|
10034
|
+
* Returns null if no pending signal exists.
|
|
10035
|
+
*
|
|
10036
|
+
* @param backtest - Whether running in backtest mode
|
|
10037
|
+
* @param symbol - Trading pair symbol
|
|
10038
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
10039
|
+
* @returns Promise resolving to PnL percentage or null
|
|
10040
|
+
*/
|
|
10041
|
+
this.getPositionHighestPnlPercentage = async (backtest, symbol, context) => {
|
|
10042
|
+
this.loggerService.log("strategyConnectionService getPositionHighestPnlPercentage", {
|
|
10043
|
+
symbol,
|
|
10044
|
+
context,
|
|
10045
|
+
});
|
|
10046
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
10047
|
+
return await strategy.getPositionHighestPnlPercentage(symbol);
|
|
10048
|
+
};
|
|
10049
|
+
/**
|
|
10050
|
+
* Returns the PnL cost (in quote currency) at the moment the best profit price was recorded during this position's life.
|
|
10051
|
+
*
|
|
10052
|
+
* Delegates to ClientStrategy.getPositionHighestPnlCost().
|
|
10053
|
+
* Returns null if no pending signal exists.
|
|
10054
|
+
*
|
|
10055
|
+
* @param backtest - Whether running in backtest mode
|
|
10056
|
+
* @param symbol - Trading pair symbol
|
|
10057
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
10058
|
+
* @returns Promise resolving to PnL cost or null
|
|
10059
|
+
*/
|
|
10060
|
+
this.getPositionHighestPnlCost = async (backtest, symbol, context) => {
|
|
10061
|
+
this.loggerService.log("strategyConnectionService getPositionHighestPnlCost", {
|
|
10062
|
+
symbol,
|
|
10063
|
+
context,
|
|
10064
|
+
});
|
|
10065
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
10066
|
+
return await strategy.getPositionHighestPnlCost(symbol);
|
|
10067
|
+
};
|
|
10068
|
+
/**
|
|
10069
|
+
* Returns whether breakeven was mathematically reachable at the highest profit price.
|
|
10070
|
+
*
|
|
10071
|
+
* Delegates to ClientStrategy.getPositionHighestProfitBreakeven().
|
|
10072
|
+
* Returns null if no pending signal exists.
|
|
10073
|
+
*
|
|
10074
|
+
* @param backtest - Whether running in backtest mode
|
|
10075
|
+
* @param symbol - Trading pair symbol
|
|
10076
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
10077
|
+
* @returns Promise resolving to true if breakeven was reachable at peak, false otherwise, or null
|
|
10078
|
+
*/
|
|
10079
|
+
this.getPositionHighestProfitBreakeven = async (backtest, symbol, context) => {
|
|
10080
|
+
this.loggerService.log("strategyConnectionService getPositionHighestProfitBreakeven", {
|
|
10081
|
+
symbol,
|
|
10082
|
+
context,
|
|
10083
|
+
});
|
|
10084
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
10085
|
+
return await strategy.getPositionHighestProfitBreakeven(symbol);
|
|
10086
|
+
};
|
|
10087
|
+
/**
|
|
10088
|
+
* Returns the number of minutes elapsed since the highest profit price was recorded.
|
|
10089
|
+
*
|
|
10090
|
+
* Resolves current timestamp via timeMetaService and delegates to
|
|
10091
|
+
* ClientStrategy.getPositionDrawdownMinutes().
|
|
10092
|
+
* Returns null if no pending signal exists.
|
|
10093
|
+
*
|
|
10094
|
+
* @param backtest - Whether running in backtest mode
|
|
10095
|
+
* @param symbol - Trading pair symbol
|
|
10096
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
10097
|
+
* @returns Promise resolving to drawdown duration in minutes or null
|
|
10098
|
+
*/
|
|
10099
|
+
this.getPositionDrawdownMinutes = async (backtest, symbol, context) => {
|
|
10100
|
+
this.loggerService.log("strategyConnectionService getPositionDrawdownMinutes", {
|
|
10101
|
+
symbol,
|
|
10102
|
+
context,
|
|
10103
|
+
});
|
|
10104
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
10105
|
+
const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
|
|
10106
|
+
return await strategy.getPositionDrawdownMinutes(symbol, timestamp);
|
|
10107
|
+
};
|
|
9681
10108
|
/**
|
|
9682
10109
|
* Disposes the ClientStrategy instance for the given context.
|
|
9683
10110
|
*
|
|
@@ -9716,7 +10143,7 @@ class StrategyConnectionService {
|
|
|
9716
10143
|
}
|
|
9717
10144
|
return;
|
|
9718
10145
|
}
|
|
9719
|
-
const key = CREATE_KEY_FN$
|
|
10146
|
+
const key = CREATE_KEY_FN$p(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
9720
10147
|
if (!this.getStrategy.has(key)) {
|
|
9721
10148
|
return;
|
|
9722
10149
|
}
|
|
@@ -10889,7 +11316,7 @@ class ClientRisk {
|
|
|
10889
11316
|
* @param backtest - Whether running in backtest mode
|
|
10890
11317
|
* @returns Unique string key for memoization
|
|
10891
11318
|
*/
|
|
10892
|
-
const CREATE_KEY_FN$
|
|
11319
|
+
const CREATE_KEY_FN$o = (riskName, exchangeName, frameName, backtest) => {
|
|
10893
11320
|
const parts = [riskName, exchangeName];
|
|
10894
11321
|
if (frameName)
|
|
10895
11322
|
parts.push(frameName);
|
|
@@ -10988,7 +11415,7 @@ class RiskConnectionService {
|
|
|
10988
11415
|
* @param backtest - True if backtest mode, false if live mode
|
|
10989
11416
|
* @returns Configured ClientRisk instance
|
|
10990
11417
|
*/
|
|
10991
|
-
this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
11418
|
+
this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$o(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
|
|
10992
11419
|
const schema = this.riskSchemaService.get(riskName);
|
|
10993
11420
|
return new ClientRisk({
|
|
10994
11421
|
...schema,
|
|
@@ -11056,7 +11483,7 @@ class RiskConnectionService {
|
|
|
11056
11483
|
payload,
|
|
11057
11484
|
});
|
|
11058
11485
|
if (payload) {
|
|
11059
|
-
const key = CREATE_KEY_FN$
|
|
11486
|
+
const key = CREATE_KEY_FN$o(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
11060
11487
|
this.getRisk.clear(key);
|
|
11061
11488
|
}
|
|
11062
11489
|
else {
|
|
@@ -12523,7 +12950,7 @@ class ClientAction {
|
|
|
12523
12950
|
* @param backtest - Whether running in backtest mode
|
|
12524
12951
|
* @returns Unique string key for memoization
|
|
12525
12952
|
*/
|
|
12526
|
-
const CREATE_KEY_FN$
|
|
12953
|
+
const CREATE_KEY_FN$n = (actionName, strategyName, exchangeName, frameName, backtest) => {
|
|
12527
12954
|
const parts = [actionName, strategyName, exchangeName];
|
|
12528
12955
|
if (frameName)
|
|
12529
12956
|
parts.push(frameName);
|
|
@@ -12574,7 +13001,7 @@ class ActionConnectionService {
|
|
|
12574
13001
|
* @param backtest - True if backtest mode, false if live mode
|
|
12575
13002
|
* @returns Configured ClientAction instance
|
|
12576
13003
|
*/
|
|
12577
|
-
this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
13004
|
+
this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$n(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
|
|
12578
13005
|
const schema = this.actionSchemaService.get(actionName);
|
|
12579
13006
|
return new ClientAction({
|
|
12580
13007
|
...schema,
|
|
@@ -12784,7 +13211,7 @@ class ActionConnectionService {
|
|
|
12784
13211
|
await Promise.all(actions.map(async (action) => await action.dispose()));
|
|
12785
13212
|
return;
|
|
12786
13213
|
}
|
|
12787
|
-
const key = CREATE_KEY_FN$
|
|
13214
|
+
const key = CREATE_KEY_FN$n(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
12788
13215
|
if (!this.getAction.has(key)) {
|
|
12789
13216
|
return;
|
|
12790
13217
|
}
|
|
@@ -12802,7 +13229,7 @@ const METHOD_NAME_VALIDATE$2 = "exchangeCoreService validate";
|
|
|
12802
13229
|
* @param exchangeName - Exchange name
|
|
12803
13230
|
* @returns Unique string key for memoization
|
|
12804
13231
|
*/
|
|
12805
|
-
const CREATE_KEY_FN$
|
|
13232
|
+
const CREATE_KEY_FN$m = (exchangeName) => {
|
|
12806
13233
|
return exchangeName;
|
|
12807
13234
|
};
|
|
12808
13235
|
/**
|
|
@@ -12826,7 +13253,7 @@ class ExchangeCoreService {
|
|
|
12826
13253
|
* @param exchangeName - Name of the exchange to validate
|
|
12827
13254
|
* @returns Promise that resolves when validation is complete
|
|
12828
13255
|
*/
|
|
12829
|
-
this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$
|
|
13256
|
+
this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$m(exchangeName), async (exchangeName) => {
|
|
12830
13257
|
this.loggerService.log(METHOD_NAME_VALIDATE$2, {
|
|
12831
13258
|
exchangeName,
|
|
12832
13259
|
});
|
|
@@ -13078,7 +13505,7 @@ const METHOD_NAME_VALIDATE$1 = "strategyCoreService validate";
|
|
|
13078
13505
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
13079
13506
|
* @returns Unique string key for memoization
|
|
13080
13507
|
*/
|
|
13081
|
-
const CREATE_KEY_FN$
|
|
13508
|
+
const CREATE_KEY_FN$l = (context) => {
|
|
13082
13509
|
const parts = [context.strategyName, context.exchangeName];
|
|
13083
13510
|
if (context.frameName)
|
|
13084
13511
|
parts.push(context.frameName);
|
|
@@ -13110,7 +13537,7 @@ class StrategyCoreService {
|
|
|
13110
13537
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
13111
13538
|
* @returns Promise that resolves when validation is complete
|
|
13112
13539
|
*/
|
|
13113
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
13540
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$l(context), async (context) => {
|
|
13114
13541
|
this.loggerService.log(METHOD_NAME_VALIDATE$1, {
|
|
13115
13542
|
context,
|
|
13116
13543
|
});
|
|
@@ -13174,13 +13601,13 @@ class StrategyCoreService {
|
|
|
13174
13601
|
await this.validate(context);
|
|
13175
13602
|
return await this.strategyConnectionService.getTotalCostClosed(backtest, symbol, context);
|
|
13176
13603
|
};
|
|
13177
|
-
this.
|
|
13178
|
-
this.loggerService.log("strategyCoreService
|
|
13604
|
+
this.getPositionEffectivePrice = async (backtest, symbol, context) => {
|
|
13605
|
+
this.loggerService.log("strategyCoreService getPositionEffectivePrice", {
|
|
13179
13606
|
symbol,
|
|
13180
13607
|
context,
|
|
13181
13608
|
});
|
|
13182
13609
|
await this.validate(context);
|
|
13183
|
-
return await this.strategyConnectionService.
|
|
13610
|
+
return await this.strategyConnectionService.getPositionEffectivePrice(backtest, symbol, context);
|
|
13184
13611
|
};
|
|
13185
13612
|
this.getPositionInvestedCount = async (backtest, symbol, context) => {
|
|
13186
13613
|
this.loggerService.log("strategyCoreService getPositionInvestedCount", {
|
|
@@ -13873,6 +14300,143 @@ class StrategyCoreService {
|
|
|
13873
14300
|
await this.validate(context);
|
|
13874
14301
|
return await this.strategyConnectionService.hasPendingSignal(backtest, symbol, context);
|
|
13875
14302
|
};
|
|
14303
|
+
/**
|
|
14304
|
+
* Returns the original estimated duration for the current pending signal.
|
|
14305
|
+
*
|
|
14306
|
+
* Validates strategy existence and delegates to connection service.
|
|
14307
|
+
* Returns null if no pending signal exists.
|
|
14308
|
+
*
|
|
14309
|
+
* @param backtest - Whether running in backtest mode
|
|
14310
|
+
* @param symbol - Trading pair symbol
|
|
14311
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14312
|
+
* @returns Promise resolving to estimated duration in minutes or null
|
|
14313
|
+
*/
|
|
14314
|
+
this.getPositionEstimateMinutes = async (backtest, symbol, context) => {
|
|
14315
|
+
this.loggerService.log("strategyCoreService getPositionEstimateMinutes", {
|
|
14316
|
+
symbol,
|
|
14317
|
+
context,
|
|
14318
|
+
});
|
|
14319
|
+
await this.validate(context);
|
|
14320
|
+
return await this.strategyConnectionService.getPositionEstimateMinutes(backtest, symbol, context);
|
|
14321
|
+
};
|
|
14322
|
+
/**
|
|
14323
|
+
* Returns the remaining time before the position expires, clamped to zero.
|
|
14324
|
+
*
|
|
14325
|
+
* Validates strategy existence and delegates to connection service.
|
|
14326
|
+
* Returns null if no pending signal exists.
|
|
14327
|
+
*
|
|
14328
|
+
* @param backtest - Whether running in backtest mode
|
|
14329
|
+
* @param symbol - Trading pair symbol
|
|
14330
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14331
|
+
* @returns Promise resolving to remaining minutes (≥ 0) or null
|
|
14332
|
+
*/
|
|
14333
|
+
this.getPositionCountdownMinutes = async (backtest, symbol, context) => {
|
|
14334
|
+
this.loggerService.log("strategyCoreService getPositionCountdownMinutes", {
|
|
14335
|
+
symbol,
|
|
14336
|
+
context,
|
|
14337
|
+
});
|
|
14338
|
+
await this.validate(context);
|
|
14339
|
+
return await this.strategyConnectionService.getPositionCountdownMinutes(backtest, symbol, context);
|
|
14340
|
+
};
|
|
14341
|
+
/**
|
|
14342
|
+
* Returns the best price reached in the profit direction during this position's life.
|
|
14343
|
+
*
|
|
14344
|
+
* @param backtest - Whether running in backtest mode
|
|
14345
|
+
* @param symbol - Trading pair symbol
|
|
14346
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14347
|
+
* @returns Promise resolving to price or null
|
|
14348
|
+
*/
|
|
14349
|
+
this.getPositionHighestProfitPrice = async (backtest, symbol, context) => {
|
|
14350
|
+
this.loggerService.log("strategyCoreService getPositionHighestProfitPrice", {
|
|
14351
|
+
symbol,
|
|
14352
|
+
context,
|
|
14353
|
+
});
|
|
14354
|
+
await this.validate(context);
|
|
14355
|
+
return await this.strategyConnectionService.getPositionHighestProfitPrice(backtest, symbol, context);
|
|
14356
|
+
};
|
|
14357
|
+
/**
|
|
14358
|
+
* Returns the timestamp when the best profit price was recorded during this position's life.
|
|
14359
|
+
*
|
|
14360
|
+
* @param backtest - Whether running in backtest mode
|
|
14361
|
+
* @param symbol - Trading pair symbol
|
|
14362
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14363
|
+
* @returns Promise resolving to timestamp in milliseconds or null
|
|
14364
|
+
*/
|
|
14365
|
+
this.getPositionHighestProfitTimestamp = async (backtest, symbol, context) => {
|
|
14366
|
+
this.loggerService.log("strategyCoreService getPositionHighestProfitTimestamp", {
|
|
14367
|
+
symbol,
|
|
14368
|
+
context,
|
|
14369
|
+
});
|
|
14370
|
+
await this.validate(context);
|
|
14371
|
+
return await this.strategyConnectionService.getPositionHighestProfitTimestamp(backtest, symbol, context);
|
|
14372
|
+
};
|
|
14373
|
+
/**
|
|
14374
|
+
* Returns the PnL percentage at the moment the best profit price was recorded during this position's life.
|
|
14375
|
+
*
|
|
14376
|
+
* @param backtest - Whether running in backtest mode
|
|
14377
|
+
* @param symbol - Trading pair symbol
|
|
14378
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14379
|
+
* @returns Promise resolving to PnL percentage or null
|
|
14380
|
+
*/
|
|
14381
|
+
this.getPositionHighestPnlPercentage = async (backtest, symbol, context) => {
|
|
14382
|
+
this.loggerService.log("strategyCoreService getPositionHighestPnlPercentage", {
|
|
14383
|
+
symbol,
|
|
14384
|
+
context,
|
|
14385
|
+
});
|
|
14386
|
+
await this.validate(context);
|
|
14387
|
+
return await this.strategyConnectionService.getPositionHighestPnlPercentage(backtest, symbol, context);
|
|
14388
|
+
};
|
|
14389
|
+
/**
|
|
14390
|
+
* Returns the PnL cost (in quote currency) at the moment the best profit price was recorded during this position's life.
|
|
14391
|
+
*
|
|
14392
|
+
* @param backtest - Whether running in backtest mode
|
|
14393
|
+
* @param symbol - Trading pair symbol
|
|
14394
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14395
|
+
* @returns Promise resolving to PnL cost or null
|
|
14396
|
+
*/
|
|
14397
|
+
this.getPositionHighestPnlCost = async (backtest, symbol, context) => {
|
|
14398
|
+
this.loggerService.log("strategyCoreService getPositionHighestPnlCost", {
|
|
14399
|
+
symbol,
|
|
14400
|
+
context,
|
|
14401
|
+
});
|
|
14402
|
+
await this.validate(context);
|
|
14403
|
+
return await this.strategyConnectionService.getPositionHighestPnlCost(backtest, symbol, context);
|
|
14404
|
+
};
|
|
14405
|
+
/**
|
|
14406
|
+
* Returns whether breakeven was mathematically reachable at the highest profit price.
|
|
14407
|
+
*
|
|
14408
|
+
* @param backtest - Whether running in backtest mode
|
|
14409
|
+
* @param symbol - Trading pair symbol
|
|
14410
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14411
|
+
* @returns Promise resolving to true if breakeven was reachable at peak, false otherwise, or null
|
|
14412
|
+
*/
|
|
14413
|
+
this.getPositionHighestProfitBreakeven = async (backtest, symbol, context) => {
|
|
14414
|
+
this.loggerService.log("strategyCoreService getPositionHighestProfitBreakeven", {
|
|
14415
|
+
symbol,
|
|
14416
|
+
context,
|
|
14417
|
+
});
|
|
14418
|
+
await this.validate(context);
|
|
14419
|
+
return await this.strategyConnectionService.getPositionHighestProfitBreakeven(backtest, symbol, context);
|
|
14420
|
+
};
|
|
14421
|
+
/**
|
|
14422
|
+
* Returns the number of minutes elapsed since the highest profit price was recorded.
|
|
14423
|
+
*
|
|
14424
|
+
* Validates strategy existence and delegates to connection service.
|
|
14425
|
+
* Returns null if no pending signal exists.
|
|
14426
|
+
*
|
|
14427
|
+
* @param backtest - Whether running in backtest mode
|
|
14428
|
+
* @param symbol - Trading pair symbol
|
|
14429
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14430
|
+
* @returns Promise resolving to drawdown duration in minutes or null
|
|
14431
|
+
*/
|
|
14432
|
+
this.getPositionDrawdownMinutes = async (backtest, symbol, context) => {
|
|
14433
|
+
this.loggerService.log("strategyCoreService getPositionDrawdownMinutes", {
|
|
14434
|
+
symbol,
|
|
14435
|
+
context,
|
|
14436
|
+
});
|
|
14437
|
+
await this.validate(context);
|
|
14438
|
+
return await this.strategyConnectionService.getPositionDrawdownMinutes(backtest, symbol, context);
|
|
14439
|
+
};
|
|
13876
14440
|
}
|
|
13877
14441
|
}
|
|
13878
14442
|
|
|
@@ -13945,7 +14509,7 @@ class SizingGlobalService {
|
|
|
13945
14509
|
* @param context - Context with riskName, exchangeName, frameName
|
|
13946
14510
|
* @returns Unique string key for memoization
|
|
13947
14511
|
*/
|
|
13948
|
-
const CREATE_KEY_FN$
|
|
14512
|
+
const CREATE_KEY_FN$k = (context) => {
|
|
13949
14513
|
const parts = [context.riskName, context.exchangeName];
|
|
13950
14514
|
if (context.frameName)
|
|
13951
14515
|
parts.push(context.frameName);
|
|
@@ -13971,7 +14535,7 @@ class RiskGlobalService {
|
|
|
13971
14535
|
* @param payload - Payload with riskName, exchangeName and frameName
|
|
13972
14536
|
* @returns Promise that resolves when validation is complete
|
|
13973
14537
|
*/
|
|
13974
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
14538
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$k(context), async (context) => {
|
|
13975
14539
|
this.loggerService.log("riskGlobalService validate", {
|
|
13976
14540
|
context,
|
|
13977
14541
|
});
|
|
@@ -14049,7 +14613,7 @@ const METHOD_NAME_VALIDATE = "actionCoreService validate";
|
|
|
14049
14613
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14050
14614
|
* @returns Unique string key for memoization
|
|
14051
14615
|
*/
|
|
14052
|
-
const CREATE_KEY_FN$
|
|
14616
|
+
const CREATE_KEY_FN$j = (context) => {
|
|
14053
14617
|
const parts = [context.strategyName, context.exchangeName];
|
|
14054
14618
|
if (context.frameName)
|
|
14055
14619
|
parts.push(context.frameName);
|
|
@@ -14093,7 +14657,7 @@ class ActionCoreService {
|
|
|
14093
14657
|
* @param context - Strategy execution context with strategyName, exchangeName and frameName
|
|
14094
14658
|
* @returns Promise that resolves when all validations complete
|
|
14095
14659
|
*/
|
|
14096
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
14660
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$j(context), async (context) => {
|
|
14097
14661
|
this.loggerService.log(METHOD_NAME_VALIDATE, {
|
|
14098
14662
|
context,
|
|
14099
14663
|
});
|
|
@@ -17904,6 +18468,93 @@ const sync_columns = [
|
|
|
17904
18468
|
},
|
|
17905
18469
|
];
|
|
17906
18470
|
|
|
18471
|
+
/**
|
|
18472
|
+
* Column configuration for highest profit markdown reports.
|
|
18473
|
+
*
|
|
18474
|
+
* Defines the table structure for displaying highest-profit-record events.
|
|
18475
|
+
*
|
|
18476
|
+
* @see HighestProfitMarkdownService
|
|
18477
|
+
* @see ColumnModel
|
|
18478
|
+
* @see HighestProfitEvent
|
|
18479
|
+
*/
|
|
18480
|
+
const highest_profit_columns = [
|
|
18481
|
+
{
|
|
18482
|
+
key: "symbol",
|
|
18483
|
+
label: "Symbol",
|
|
18484
|
+
format: (data) => data.symbol,
|
|
18485
|
+
isVisible: () => true,
|
|
18486
|
+
},
|
|
18487
|
+
{
|
|
18488
|
+
key: "strategyName",
|
|
18489
|
+
label: "Strategy",
|
|
18490
|
+
format: (data) => data.strategyName,
|
|
18491
|
+
isVisible: () => true,
|
|
18492
|
+
},
|
|
18493
|
+
{
|
|
18494
|
+
key: "signalId",
|
|
18495
|
+
label: "Signal ID",
|
|
18496
|
+
format: (data) => data.signalId,
|
|
18497
|
+
isVisible: () => true,
|
|
18498
|
+
},
|
|
18499
|
+
{
|
|
18500
|
+
key: "position",
|
|
18501
|
+
label: "Position",
|
|
18502
|
+
format: (data) => data.position.toUpperCase(),
|
|
18503
|
+
isVisible: () => true,
|
|
18504
|
+
},
|
|
18505
|
+
{
|
|
18506
|
+
key: "pnl",
|
|
18507
|
+
label: "PNL (net)",
|
|
18508
|
+
format: (data) => {
|
|
18509
|
+
const pnlPercentage = data.pnl.pnlPercentage;
|
|
18510
|
+
return `${pnlPercentage > 0 ? "+" : ""}${pnlPercentage.toFixed(2)}%`;
|
|
18511
|
+
},
|
|
18512
|
+
isVisible: () => true,
|
|
18513
|
+
},
|
|
18514
|
+
{
|
|
18515
|
+
key: "pnlCost",
|
|
18516
|
+
label: "PNL (USD)",
|
|
18517
|
+
format: (data) => `${data.pnl.pnlCost > 0 ? "+" : ""}${data.pnl.pnlCost.toFixed(2)} USD`,
|
|
18518
|
+
isVisible: () => true,
|
|
18519
|
+
},
|
|
18520
|
+
{
|
|
18521
|
+
key: "currentPrice",
|
|
18522
|
+
label: "Record Price",
|
|
18523
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
18524
|
+
isVisible: () => true,
|
|
18525
|
+
},
|
|
18526
|
+
{
|
|
18527
|
+
key: "priceOpen",
|
|
18528
|
+
label: "Entry Price",
|
|
18529
|
+
format: (data) => `${data.priceOpen.toFixed(8)} USD`,
|
|
18530
|
+
isVisible: () => true,
|
|
18531
|
+
},
|
|
18532
|
+
{
|
|
18533
|
+
key: "priceTakeProfit",
|
|
18534
|
+
label: "Take Profit",
|
|
18535
|
+
format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
|
|
18536
|
+
isVisible: () => true,
|
|
18537
|
+
},
|
|
18538
|
+
{
|
|
18539
|
+
key: "priceStopLoss",
|
|
18540
|
+
label: "Stop Loss",
|
|
18541
|
+
format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
|
|
18542
|
+
isVisible: () => true,
|
|
18543
|
+
},
|
|
18544
|
+
{
|
|
18545
|
+
key: "timestamp",
|
|
18546
|
+
label: "Timestamp",
|
|
18547
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
18548
|
+
isVisible: () => true,
|
|
18549
|
+
},
|
|
18550
|
+
{
|
|
18551
|
+
key: "mode",
|
|
18552
|
+
label: "Mode",
|
|
18553
|
+
format: (data) => (data.backtest ? "Backtest" : "Live"),
|
|
18554
|
+
isVisible: () => true,
|
|
18555
|
+
},
|
|
18556
|
+
];
|
|
18557
|
+
|
|
17907
18558
|
/**
|
|
17908
18559
|
* Column configuration for walker strategy comparison table in markdown reports.
|
|
17909
18560
|
*
|
|
@@ -18121,6 +18772,8 @@ const COLUMN_CONFIG = {
|
|
|
18121
18772
|
strategy_columns,
|
|
18122
18773
|
/** Columns for signal sync lifecycle events (signal-open, signal-close) */
|
|
18123
18774
|
sync_columns,
|
|
18775
|
+
/** Columns for highest profit milestone tracking events */
|
|
18776
|
+
highest_profit_columns,
|
|
18124
18777
|
/** Walker: PnL summary columns */
|
|
18125
18778
|
walker_pnl_columns,
|
|
18126
18779
|
/** Walker: strategy-level summary columns */
|
|
@@ -18161,6 +18814,7 @@ const WILDCARD_TARGET$1 = {
|
|
|
18161
18814
|
schedule: true,
|
|
18162
18815
|
walker: true,
|
|
18163
18816
|
sync: true,
|
|
18817
|
+
highest_profit: true,
|
|
18164
18818
|
};
|
|
18165
18819
|
/**
|
|
18166
18820
|
* JSONL-based markdown adapter with append-only writes.
|
|
@@ -18388,7 +19042,7 @@ class MarkdownUtils {
|
|
|
18388
19042
|
*
|
|
18389
19043
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
18390
19044
|
*/
|
|
18391
|
-
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, } = WILDCARD_TARGET$1) => {
|
|
19045
|
+
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) => {
|
|
18392
19046
|
bt.loggerService.debug(MARKDOWN_METHOD_NAME_ENABLE, {
|
|
18393
19047
|
backtest: bt$1,
|
|
18394
19048
|
breakeven,
|
|
@@ -18401,6 +19055,7 @@ class MarkdownUtils {
|
|
|
18401
19055
|
schedule,
|
|
18402
19056
|
walker,
|
|
18403
19057
|
sync,
|
|
19058
|
+
highest_profit,
|
|
18404
19059
|
});
|
|
18405
19060
|
const unList = [];
|
|
18406
19061
|
if (bt$1) {
|
|
@@ -18436,6 +19091,9 @@ class MarkdownUtils {
|
|
|
18436
19091
|
if (sync) {
|
|
18437
19092
|
unList.push(bt.syncMarkdownService.subscribe());
|
|
18438
19093
|
}
|
|
19094
|
+
if (highest_profit) {
|
|
19095
|
+
unList.push(bt.highestProfitMarkdownService.subscribe());
|
|
19096
|
+
}
|
|
18439
19097
|
return functoolsKit.compose(...unList.map((un) => () => void un()));
|
|
18440
19098
|
};
|
|
18441
19099
|
/**
|
|
@@ -18475,7 +19133,7 @@ class MarkdownUtils {
|
|
|
18475
19133
|
* Markdown.disable();
|
|
18476
19134
|
* ```
|
|
18477
19135
|
*/
|
|
18478
|
-
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, } = WILDCARD_TARGET$1) => {
|
|
19136
|
+
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) => {
|
|
18479
19137
|
bt.loggerService.debug(MARKDOWN_METHOD_NAME_DISABLE, {
|
|
18480
19138
|
backtest: bt$1,
|
|
18481
19139
|
breakeven,
|
|
@@ -18488,6 +19146,7 @@ class MarkdownUtils {
|
|
|
18488
19146
|
schedule,
|
|
18489
19147
|
walker,
|
|
18490
19148
|
sync,
|
|
19149
|
+
highest_profit,
|
|
18491
19150
|
});
|
|
18492
19151
|
if (bt$1) {
|
|
18493
19152
|
bt.backtestMarkdownService.unsubscribe();
|
|
@@ -18522,6 +19181,9 @@ class MarkdownUtils {
|
|
|
18522
19181
|
if (sync) {
|
|
18523
19182
|
bt.syncMarkdownService.unsubscribe();
|
|
18524
19183
|
}
|
|
19184
|
+
if (highest_profit) {
|
|
19185
|
+
bt.highestProfitMarkdownService.unsubscribe();
|
|
19186
|
+
}
|
|
18525
19187
|
};
|
|
18526
19188
|
}
|
|
18527
19189
|
}
|
|
@@ -18628,7 +19290,7 @@ const Markdown = new MarkdownAdapter();
|
|
|
18628
19290
|
* @param backtest - Whether running in backtest mode
|
|
18629
19291
|
* @returns Unique string key for memoization
|
|
18630
19292
|
*/
|
|
18631
|
-
const CREATE_KEY_FN$
|
|
19293
|
+
const CREATE_KEY_FN$i = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
18632
19294
|
const parts = [symbol, strategyName, exchangeName];
|
|
18633
19295
|
if (frameName)
|
|
18634
19296
|
parts.push(frameName);
|
|
@@ -18645,7 +19307,7 @@ const CREATE_KEY_FN$h = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
18645
19307
|
* @param timestamp - Unix timestamp in milliseconds
|
|
18646
19308
|
* @returns Filename string
|
|
18647
19309
|
*/
|
|
18648
|
-
const CREATE_FILE_NAME_FN$
|
|
19310
|
+
const CREATE_FILE_NAME_FN$b = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
18649
19311
|
const parts = [symbol, strategyName, exchangeName];
|
|
18650
19312
|
if (frameName) {
|
|
18651
19313
|
parts.push(frameName);
|
|
@@ -18674,12 +19336,12 @@ function isUnsafe$3(value) {
|
|
|
18674
19336
|
return false;
|
|
18675
19337
|
}
|
|
18676
19338
|
/** Maximum number of signals to store in backtest reports */
|
|
18677
|
-
const MAX_EVENTS$
|
|
19339
|
+
const MAX_EVENTS$a = 250;
|
|
18678
19340
|
/**
|
|
18679
19341
|
* Storage class for accumulating closed signals per strategy.
|
|
18680
19342
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
18681
19343
|
*/
|
|
18682
|
-
let ReportStorage$
|
|
19344
|
+
let ReportStorage$9 = class ReportStorage {
|
|
18683
19345
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
18684
19346
|
this.symbol = symbol;
|
|
18685
19347
|
this.strategyName = strategyName;
|
|
@@ -18696,7 +19358,7 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
18696
19358
|
addSignal(data) {
|
|
18697
19359
|
this._signalList.unshift(data);
|
|
18698
19360
|
// Trim queue if exceeded MAX_EVENTS
|
|
18699
|
-
if (this._signalList.length > MAX_EVENTS$
|
|
19361
|
+
if (this._signalList.length > MAX_EVENTS$a) {
|
|
18700
19362
|
this._signalList.pop();
|
|
18701
19363
|
}
|
|
18702
19364
|
}
|
|
@@ -18820,7 +19482,7 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
18820
19482
|
async dump(strategyName, path = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
|
|
18821
19483
|
const markdown = await this.getReport(strategyName, columns);
|
|
18822
19484
|
const timestamp = getContextTimestamp();
|
|
18823
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
19485
|
+
const filename = CREATE_FILE_NAME_FN$b(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
18824
19486
|
await Markdown.writeData("backtest", markdown, {
|
|
18825
19487
|
path,
|
|
18826
19488
|
file: filename,
|
|
@@ -18867,7 +19529,7 @@ class BacktestMarkdownService {
|
|
|
18867
19529
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
18868
19530
|
* Each combination gets its own isolated storage instance.
|
|
18869
19531
|
*/
|
|
18870
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
19532
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$i(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$9(symbol, strategyName, exchangeName, frameName));
|
|
18871
19533
|
/**
|
|
18872
19534
|
* Processes tick events and accumulates closed signals.
|
|
18873
19535
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -19024,7 +19686,7 @@ class BacktestMarkdownService {
|
|
|
19024
19686
|
payload,
|
|
19025
19687
|
});
|
|
19026
19688
|
if (payload) {
|
|
19027
|
-
const key = CREATE_KEY_FN$
|
|
19689
|
+
const key = CREATE_KEY_FN$i(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
19028
19690
|
this.getStorage.clear(key);
|
|
19029
19691
|
}
|
|
19030
19692
|
else {
|
|
@@ -19086,7 +19748,7 @@ class BacktestMarkdownService {
|
|
|
19086
19748
|
* @param backtest - Whether running in backtest mode
|
|
19087
19749
|
* @returns Unique string key for memoization
|
|
19088
19750
|
*/
|
|
19089
|
-
const CREATE_KEY_FN$
|
|
19751
|
+
const CREATE_KEY_FN$h = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
19090
19752
|
const parts = [symbol, strategyName, exchangeName];
|
|
19091
19753
|
if (frameName)
|
|
19092
19754
|
parts.push(frameName);
|
|
@@ -19103,7 +19765,7 @@ const CREATE_KEY_FN$g = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
19103
19765
|
* @param timestamp - Unix timestamp in milliseconds
|
|
19104
19766
|
* @returns Filename string
|
|
19105
19767
|
*/
|
|
19106
|
-
const CREATE_FILE_NAME_FN$
|
|
19768
|
+
const CREATE_FILE_NAME_FN$a = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
19107
19769
|
const parts = [symbol, strategyName, exchangeName];
|
|
19108
19770
|
if (frameName) {
|
|
19109
19771
|
parts.push(frameName);
|
|
@@ -19132,12 +19794,12 @@ function isUnsafe$2(value) {
|
|
|
19132
19794
|
return false;
|
|
19133
19795
|
}
|
|
19134
19796
|
/** Maximum number of events to store in live trading reports */
|
|
19135
|
-
const MAX_EVENTS$
|
|
19797
|
+
const MAX_EVENTS$9 = 250;
|
|
19136
19798
|
/**
|
|
19137
19799
|
* Storage class for accumulating all tick events per strategy.
|
|
19138
19800
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
19139
19801
|
*/
|
|
19140
|
-
let ReportStorage$
|
|
19802
|
+
let ReportStorage$8 = class ReportStorage {
|
|
19141
19803
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
19142
19804
|
this.symbol = symbol;
|
|
19143
19805
|
this.strategyName = strategyName;
|
|
@@ -19169,7 +19831,7 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
19169
19831
|
}
|
|
19170
19832
|
{
|
|
19171
19833
|
this._eventList.unshift(newEvent);
|
|
19172
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
19834
|
+
if (this._eventList.length > MAX_EVENTS$9) {
|
|
19173
19835
|
this._eventList.pop();
|
|
19174
19836
|
}
|
|
19175
19837
|
}
|
|
@@ -19199,7 +19861,7 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
19199
19861
|
scheduledAt: data.signal.scheduledAt,
|
|
19200
19862
|
});
|
|
19201
19863
|
// Trim queue if exceeded MAX_EVENTS
|
|
19202
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
19864
|
+
if (this._eventList.length > MAX_EVENTS$9) {
|
|
19203
19865
|
this._eventList.pop();
|
|
19204
19866
|
}
|
|
19205
19867
|
}
|
|
@@ -19243,7 +19905,7 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
19243
19905
|
// If no previous active event found, add new event
|
|
19244
19906
|
this._eventList.unshift(newEvent);
|
|
19245
19907
|
// Trim queue if exceeded MAX_EVENTS
|
|
19246
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
19908
|
+
if (this._eventList.length > MAX_EVENTS$9) {
|
|
19247
19909
|
this._eventList.pop();
|
|
19248
19910
|
}
|
|
19249
19911
|
}
|
|
@@ -19280,7 +19942,7 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
19280
19942
|
};
|
|
19281
19943
|
this._eventList.unshift(newEvent);
|
|
19282
19944
|
// Trim queue if exceeded MAX_EVENTS
|
|
19283
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
19945
|
+
if (this._eventList.length > MAX_EVENTS$9) {
|
|
19284
19946
|
this._eventList.pop();
|
|
19285
19947
|
}
|
|
19286
19948
|
}
|
|
@@ -19308,7 +19970,7 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
19308
19970
|
scheduledAt: data.signal.scheduledAt,
|
|
19309
19971
|
});
|
|
19310
19972
|
// Trim queue if exceeded MAX_EVENTS
|
|
19311
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
19973
|
+
if (this._eventList.length > MAX_EVENTS$9) {
|
|
19312
19974
|
this._eventList.pop();
|
|
19313
19975
|
}
|
|
19314
19976
|
}
|
|
@@ -19351,7 +20013,7 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
19351
20013
|
// If no previous waiting event found, add new event
|
|
19352
20014
|
this._eventList.unshift(newEvent);
|
|
19353
20015
|
// Trim queue if exceeded MAX_EVENTS
|
|
19354
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
20016
|
+
if (this._eventList.length > MAX_EVENTS$9) {
|
|
19355
20017
|
this._eventList.pop();
|
|
19356
20018
|
}
|
|
19357
20019
|
}
|
|
@@ -19380,7 +20042,7 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
19380
20042
|
scheduledAt: data.signal.scheduledAt,
|
|
19381
20043
|
});
|
|
19382
20044
|
// Trim queue if exceeded MAX_EVENTS
|
|
19383
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
20045
|
+
if (this._eventList.length > MAX_EVENTS$9) {
|
|
19384
20046
|
this._eventList.pop();
|
|
19385
20047
|
}
|
|
19386
20048
|
}
|
|
@@ -19519,7 +20181,7 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
19519
20181
|
async dump(strategyName, path = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
|
|
19520
20182
|
const markdown = await this.getReport(strategyName, columns);
|
|
19521
20183
|
const timestamp = getContextTimestamp();
|
|
19522
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
20184
|
+
const filename = CREATE_FILE_NAME_FN$a(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
19523
20185
|
await Markdown.writeData("live", markdown, {
|
|
19524
20186
|
path,
|
|
19525
20187
|
signalId: "",
|
|
@@ -19569,7 +20231,7 @@ class LiveMarkdownService {
|
|
|
19569
20231
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
19570
20232
|
* Each combination gets its own isolated storage instance.
|
|
19571
20233
|
*/
|
|
19572
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
20234
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$h(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$8(symbol, strategyName, exchangeName, frameName));
|
|
19573
20235
|
/**
|
|
19574
20236
|
* Subscribes to live signal emitter to receive tick events.
|
|
19575
20237
|
* Protected against multiple subscriptions.
|
|
@@ -19787,7 +20449,7 @@ class LiveMarkdownService {
|
|
|
19787
20449
|
payload,
|
|
19788
20450
|
});
|
|
19789
20451
|
if (payload) {
|
|
19790
|
-
const key = CREATE_KEY_FN$
|
|
20452
|
+
const key = CREATE_KEY_FN$h(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
19791
20453
|
this.getStorage.clear(key);
|
|
19792
20454
|
}
|
|
19793
20455
|
else {
|
|
@@ -19807,7 +20469,7 @@ class LiveMarkdownService {
|
|
|
19807
20469
|
* @param backtest - Whether running in backtest mode
|
|
19808
20470
|
* @returns Unique string key for memoization
|
|
19809
20471
|
*/
|
|
19810
|
-
const CREATE_KEY_FN$
|
|
20472
|
+
const CREATE_KEY_FN$g = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
19811
20473
|
const parts = [symbol, strategyName, exchangeName];
|
|
19812
20474
|
if (frameName)
|
|
19813
20475
|
parts.push(frameName);
|
|
@@ -19824,7 +20486,7 @@ const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
19824
20486
|
* @param timestamp - Unix timestamp in milliseconds
|
|
19825
20487
|
* @returns Filename string
|
|
19826
20488
|
*/
|
|
19827
|
-
const CREATE_FILE_NAME_FN$
|
|
20489
|
+
const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
19828
20490
|
const parts = [symbol, strategyName, exchangeName];
|
|
19829
20491
|
if (frameName) {
|
|
19830
20492
|
parts.push(frameName);
|
|
@@ -19835,12 +20497,12 @@ const CREATE_FILE_NAME_FN$8 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
19835
20497
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
19836
20498
|
};
|
|
19837
20499
|
/** Maximum number of events to store in schedule reports */
|
|
19838
|
-
const MAX_EVENTS$
|
|
20500
|
+
const MAX_EVENTS$8 = 250;
|
|
19839
20501
|
/**
|
|
19840
20502
|
* Storage class for accumulating scheduled signal events per strategy.
|
|
19841
20503
|
* Maintains a chronological list of scheduled and cancelled events.
|
|
19842
20504
|
*/
|
|
19843
|
-
let ReportStorage$
|
|
20505
|
+
let ReportStorage$7 = class ReportStorage {
|
|
19844
20506
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
19845
20507
|
this.symbol = symbol;
|
|
19846
20508
|
this.strategyName = strategyName;
|
|
@@ -19876,7 +20538,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
19876
20538
|
scheduledAt: data.signal.scheduledAt,
|
|
19877
20539
|
});
|
|
19878
20540
|
// Trim queue if exceeded MAX_EVENTS
|
|
19879
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
20541
|
+
if (this._eventList.length > MAX_EVENTS$8) {
|
|
19880
20542
|
this._eventList.pop();
|
|
19881
20543
|
}
|
|
19882
20544
|
}
|
|
@@ -19912,7 +20574,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
19912
20574
|
};
|
|
19913
20575
|
this._eventList.unshift(newEvent);
|
|
19914
20576
|
// Trim queue if exceeded MAX_EVENTS
|
|
19915
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
20577
|
+
if (this._eventList.length > MAX_EVENTS$8) {
|
|
19916
20578
|
this._eventList.pop();
|
|
19917
20579
|
}
|
|
19918
20580
|
}
|
|
@@ -19950,7 +20612,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
19950
20612
|
};
|
|
19951
20613
|
this._eventList.unshift(newEvent);
|
|
19952
20614
|
// Trim queue if exceeded MAX_EVENTS
|
|
19953
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
20615
|
+
if (this._eventList.length > MAX_EVENTS$8) {
|
|
19954
20616
|
this._eventList.pop();
|
|
19955
20617
|
}
|
|
19956
20618
|
}
|
|
@@ -20057,7 +20719,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
20057
20719
|
async dump(strategyName, path = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
|
|
20058
20720
|
const markdown = await this.getReport(strategyName, columns);
|
|
20059
20721
|
const timestamp = getContextTimestamp();
|
|
20060
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
20722
|
+
const filename = CREATE_FILE_NAME_FN$9(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
20061
20723
|
await Markdown.writeData("schedule", markdown, {
|
|
20062
20724
|
path,
|
|
20063
20725
|
file: filename,
|
|
@@ -20098,7 +20760,7 @@ class ScheduleMarkdownService {
|
|
|
20098
20760
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
20099
20761
|
* Each combination gets its own isolated storage instance.
|
|
20100
20762
|
*/
|
|
20101
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
20763
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$g(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$7(symbol, strategyName, exchangeName, frameName));
|
|
20102
20764
|
/**
|
|
20103
20765
|
* Subscribes to signal emitter to receive scheduled signal events.
|
|
20104
20766
|
* Protected against multiple subscriptions.
|
|
@@ -20301,7 +20963,7 @@ class ScheduleMarkdownService {
|
|
|
20301
20963
|
payload,
|
|
20302
20964
|
});
|
|
20303
20965
|
if (payload) {
|
|
20304
|
-
const key = CREATE_KEY_FN$
|
|
20966
|
+
const key = CREATE_KEY_FN$g(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
20305
20967
|
this.getStorage.clear(key);
|
|
20306
20968
|
}
|
|
20307
20969
|
else {
|
|
@@ -20321,7 +20983,7 @@ class ScheduleMarkdownService {
|
|
|
20321
20983
|
* @param backtest - Whether running in backtest mode
|
|
20322
20984
|
* @returns Unique string key for memoization
|
|
20323
20985
|
*/
|
|
20324
|
-
const CREATE_KEY_FN$
|
|
20986
|
+
const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
20325
20987
|
const parts = [symbol, strategyName, exchangeName];
|
|
20326
20988
|
if (frameName)
|
|
20327
20989
|
parts.push(frameName);
|
|
@@ -20338,7 +21000,7 @@ const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
20338
21000
|
* @param timestamp - Unix timestamp in milliseconds
|
|
20339
21001
|
* @returns Filename string
|
|
20340
21002
|
*/
|
|
20341
|
-
const CREATE_FILE_NAME_FN$
|
|
21003
|
+
const CREATE_FILE_NAME_FN$8 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
20342
21004
|
const parts = [symbol, strategyName, exchangeName];
|
|
20343
21005
|
if (frameName) {
|
|
20344
21006
|
parts.push(frameName);
|
|
@@ -20358,7 +21020,7 @@ function percentile(sortedArray, p) {
|
|
|
20358
21020
|
return sortedArray[Math.max(0, index)];
|
|
20359
21021
|
}
|
|
20360
21022
|
/** Maximum number of performance events to store per strategy */
|
|
20361
|
-
const MAX_EVENTS$
|
|
21023
|
+
const MAX_EVENTS$7 = 10000;
|
|
20362
21024
|
/**
|
|
20363
21025
|
* Storage class for accumulating performance metrics per strategy.
|
|
20364
21026
|
* Maintains a list of all performance events and provides aggregated statistics.
|
|
@@ -20380,7 +21042,7 @@ class PerformanceStorage {
|
|
|
20380
21042
|
addEvent(event) {
|
|
20381
21043
|
this._events.unshift(event);
|
|
20382
21044
|
// Trim queue if exceeded MAX_EVENTS (keep most recent)
|
|
20383
|
-
if (this._events.length > MAX_EVENTS$
|
|
21045
|
+
if (this._events.length > MAX_EVENTS$7) {
|
|
20384
21046
|
this._events.pop();
|
|
20385
21047
|
}
|
|
20386
21048
|
}
|
|
@@ -20497,6 +21159,10 @@ class PerformanceStorage {
|
|
|
20497
21159
|
return [
|
|
20498
21160
|
`# Performance Report: ${strategyName}`,
|
|
20499
21161
|
"",
|
|
21162
|
+
summaryTable,
|
|
21163
|
+
"",
|
|
21164
|
+
"**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type.",
|
|
21165
|
+
"",
|
|
20500
21166
|
`**Total events:** ${stats.totalEvents}`,
|
|
20501
21167
|
`**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
|
|
20502
21168
|
`**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
|
|
@@ -20505,11 +21171,6 @@ class PerformanceStorage {
|
|
|
20505
21171
|
"",
|
|
20506
21172
|
percentages.join("\n"),
|
|
20507
21173
|
"",
|
|
20508
|
-
"## Detailed Metrics",
|
|
20509
|
-
"",
|
|
20510
|
-
summaryTable,
|
|
20511
|
-
"",
|
|
20512
|
-
"**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type."
|
|
20513
21174
|
].join("\n");
|
|
20514
21175
|
}
|
|
20515
21176
|
/**
|
|
@@ -20522,7 +21183,7 @@ class PerformanceStorage {
|
|
|
20522
21183
|
async dump(strategyName, path = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
|
|
20523
21184
|
const markdown = await this.getReport(strategyName, columns);
|
|
20524
21185
|
const timestamp = getContextTimestamp();
|
|
20525
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
21186
|
+
const filename = CREATE_FILE_NAME_FN$8(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
20526
21187
|
await Markdown.writeData("performance", markdown, {
|
|
20527
21188
|
path,
|
|
20528
21189
|
file: filename,
|
|
@@ -20569,7 +21230,7 @@ class PerformanceMarkdownService {
|
|
|
20569
21230
|
* Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
20570
21231
|
* Each combination gets its own isolated storage instance.
|
|
20571
21232
|
*/
|
|
20572
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
21233
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$f(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
|
|
20573
21234
|
/**
|
|
20574
21235
|
* Subscribes to performance emitter to receive performance events.
|
|
20575
21236
|
* Protected against multiple subscriptions.
|
|
@@ -20736,7 +21397,7 @@ class PerformanceMarkdownService {
|
|
|
20736
21397
|
payload,
|
|
20737
21398
|
});
|
|
20738
21399
|
if (payload) {
|
|
20739
|
-
const key = CREATE_KEY_FN$
|
|
21400
|
+
const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
20740
21401
|
this.getStorage.clear(key);
|
|
20741
21402
|
}
|
|
20742
21403
|
else {
|
|
@@ -20750,7 +21411,7 @@ class PerformanceMarkdownService {
|
|
|
20750
21411
|
* Creates a filename for markdown report based on walker name.
|
|
20751
21412
|
* Filename format: "walkerName-timestamp.md"
|
|
20752
21413
|
*/
|
|
20753
|
-
const CREATE_FILE_NAME_FN$
|
|
21414
|
+
const CREATE_FILE_NAME_FN$7 = (walkerName, timestamp) => {
|
|
20754
21415
|
return `${walkerName}-${timestamp}.md`;
|
|
20755
21416
|
};
|
|
20756
21417
|
/**
|
|
@@ -20785,7 +21446,7 @@ function formatMetric(value) {
|
|
|
20785
21446
|
* Storage class for accumulating walker results.
|
|
20786
21447
|
* Maintains a list of all strategy results and provides methods to generate reports.
|
|
20787
21448
|
*/
|
|
20788
|
-
let ReportStorage$
|
|
21449
|
+
let ReportStorage$6 = class ReportStorage {
|
|
20789
21450
|
constructor(walkerName) {
|
|
20790
21451
|
this.walkerName = walkerName;
|
|
20791
21452
|
/** Walker metadata (set from first addResult call) */
|
|
@@ -20973,7 +21634,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
20973
21634
|
async dump(symbol, metric, context, path = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
|
|
20974
21635
|
const markdown = await this.getReport(symbol, metric, context, strategyColumns, pnlColumns);
|
|
20975
21636
|
const timestamp = getContextTimestamp();
|
|
20976
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
21637
|
+
const filename = CREATE_FILE_NAME_FN$7(this.walkerName, timestamp);
|
|
20977
21638
|
await Markdown.writeData("walker", markdown, {
|
|
20978
21639
|
path,
|
|
20979
21640
|
file: filename,
|
|
@@ -21009,7 +21670,7 @@ class WalkerMarkdownService {
|
|
|
21009
21670
|
* Memoized function to get or create ReportStorage for a walker.
|
|
21010
21671
|
* Each walker gets its own isolated storage instance.
|
|
21011
21672
|
*/
|
|
21012
|
-
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$
|
|
21673
|
+
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$6(walkerName));
|
|
21013
21674
|
/**
|
|
21014
21675
|
* Subscribes to walker emitter to receive walker progress events.
|
|
21015
21676
|
* Protected against multiple subscriptions.
|
|
@@ -21206,7 +21867,7 @@ class WalkerMarkdownService {
|
|
|
21206
21867
|
* @param backtest - Whether running in backtest mode
|
|
21207
21868
|
* @returns Unique string key for memoization
|
|
21208
21869
|
*/
|
|
21209
|
-
const CREATE_KEY_FN$
|
|
21870
|
+
const CREATE_KEY_FN$e = (exchangeName, frameName, backtest) => {
|
|
21210
21871
|
const parts = [exchangeName];
|
|
21211
21872
|
if (frameName)
|
|
21212
21873
|
parts.push(frameName);
|
|
@@ -21217,7 +21878,7 @@ const CREATE_KEY_FN$d = (exchangeName, frameName, backtest) => {
|
|
|
21217
21878
|
* Creates a filename for markdown report based on memoization key components.
|
|
21218
21879
|
* Filename format: "strategyName_exchangeName_frameName-timestamp.md"
|
|
21219
21880
|
*/
|
|
21220
|
-
const CREATE_FILE_NAME_FN$
|
|
21881
|
+
const CREATE_FILE_NAME_FN$6 = (strategyName, exchangeName, frameName, timestamp) => {
|
|
21221
21882
|
const parts = [strategyName, exchangeName];
|
|
21222
21883
|
if (frameName) {
|
|
21223
21884
|
parts.push(frameName);
|
|
@@ -21250,7 +21911,7 @@ function isUnsafe(value) {
|
|
|
21250
21911
|
return false;
|
|
21251
21912
|
}
|
|
21252
21913
|
/** Maximum number of signals to store per symbol in heatmap reports */
|
|
21253
|
-
const MAX_EVENTS$
|
|
21914
|
+
const MAX_EVENTS$6 = 250;
|
|
21254
21915
|
/**
|
|
21255
21916
|
* Storage class for accumulating closed signals per strategy and generating heatmap.
|
|
21256
21917
|
* Maintains symbol-level statistics and provides portfolio-wide metrics.
|
|
@@ -21276,7 +21937,7 @@ class HeatmapStorage {
|
|
|
21276
21937
|
const signals = this.symbolData.get(symbol);
|
|
21277
21938
|
signals.unshift(data);
|
|
21278
21939
|
// Trim queue if exceeded MAX_EVENTS per symbol
|
|
21279
|
-
if (signals.length > MAX_EVENTS$
|
|
21940
|
+
if (signals.length > MAX_EVENTS$6) {
|
|
21280
21941
|
signals.pop();
|
|
21281
21942
|
}
|
|
21282
21943
|
}
|
|
@@ -21527,7 +22188,7 @@ class HeatmapStorage {
|
|
|
21527
22188
|
async dump(strategyName, path = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
|
|
21528
22189
|
const markdown = await this.getReport(strategyName, columns);
|
|
21529
22190
|
const timestamp = getContextTimestamp();
|
|
21530
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
22191
|
+
const filename = CREATE_FILE_NAME_FN$6(strategyName, this.exchangeName, this.frameName, timestamp);
|
|
21531
22192
|
await Markdown.writeData("heat", markdown, {
|
|
21532
22193
|
path,
|
|
21533
22194
|
file: filename,
|
|
@@ -21573,7 +22234,7 @@ class HeatMarkdownService {
|
|
|
21573
22234
|
* Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
|
|
21574
22235
|
* Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
|
|
21575
22236
|
*/
|
|
21576
|
-
this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
22237
|
+
this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$e(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
|
|
21577
22238
|
/**
|
|
21578
22239
|
* Subscribes to signal emitter to receive tick events.
|
|
21579
22240
|
* Protected against multiple subscriptions.
|
|
@@ -21768,7 +22429,7 @@ class HeatMarkdownService {
|
|
|
21768
22429
|
payload,
|
|
21769
22430
|
});
|
|
21770
22431
|
if (payload) {
|
|
21771
|
-
const key = CREATE_KEY_FN$
|
|
22432
|
+
const key = CREATE_KEY_FN$e(payload.exchangeName, payload.frameName, payload.backtest);
|
|
21772
22433
|
this.getStorage.clear(key);
|
|
21773
22434
|
}
|
|
21774
22435
|
else {
|
|
@@ -22799,7 +23460,7 @@ class ClientPartial {
|
|
|
22799
23460
|
* @param backtest - Whether running in backtest mode
|
|
22800
23461
|
* @returns Unique string key for memoization
|
|
22801
23462
|
*/
|
|
22802
|
-
const CREATE_KEY_FN$
|
|
23463
|
+
const CREATE_KEY_FN$d = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
|
|
22803
23464
|
/**
|
|
22804
23465
|
* Creates a callback function for emitting profit events to partialProfitSubject.
|
|
22805
23466
|
*
|
|
@@ -22921,7 +23582,7 @@ class PartialConnectionService {
|
|
|
22921
23582
|
* Key format: "signalId:backtest" or "signalId:live"
|
|
22922
23583
|
* Value: ClientPartial instance with logger and event emitters
|
|
22923
23584
|
*/
|
|
22924
|
-
this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$
|
|
23585
|
+
this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$d(signalId, backtest), (signalId, backtest) => {
|
|
22925
23586
|
return new ClientPartial({
|
|
22926
23587
|
signalId,
|
|
22927
23588
|
logger: this.loggerService,
|
|
@@ -23011,7 +23672,7 @@ class PartialConnectionService {
|
|
|
23011
23672
|
const partial = this.getPartial(data.id, backtest);
|
|
23012
23673
|
await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
|
|
23013
23674
|
await partial.clear(symbol, data, priceClose, backtest);
|
|
23014
|
-
const key = CREATE_KEY_FN$
|
|
23675
|
+
const key = CREATE_KEY_FN$d(data.id, backtest);
|
|
23015
23676
|
this.getPartial.clear(key);
|
|
23016
23677
|
};
|
|
23017
23678
|
}
|
|
@@ -23027,7 +23688,7 @@ class PartialConnectionService {
|
|
|
23027
23688
|
* @param backtest - Whether running in backtest mode
|
|
23028
23689
|
* @returns Unique string key for memoization
|
|
23029
23690
|
*/
|
|
23030
|
-
const CREATE_KEY_FN$
|
|
23691
|
+
const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
23031
23692
|
const parts = [symbol, strategyName, exchangeName];
|
|
23032
23693
|
if (frameName)
|
|
23033
23694
|
parts.push(frameName);
|
|
@@ -23038,7 +23699,7 @@ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
23038
23699
|
* Creates a filename for markdown report based on memoization key components.
|
|
23039
23700
|
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
23040
23701
|
*/
|
|
23041
|
-
const CREATE_FILE_NAME_FN$
|
|
23702
|
+
const CREATE_FILE_NAME_FN$5 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
23042
23703
|
const parts = [symbol, strategyName, exchangeName];
|
|
23043
23704
|
if (frameName) {
|
|
23044
23705
|
parts.push(frameName);
|
|
@@ -23049,12 +23710,12 @@ const CREATE_FILE_NAME_FN$4 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
23049
23710
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
23050
23711
|
};
|
|
23051
23712
|
/** Maximum number of events to store in partial reports */
|
|
23052
|
-
const MAX_EVENTS$
|
|
23713
|
+
const MAX_EVENTS$5 = 250;
|
|
23053
23714
|
/**
|
|
23054
23715
|
* Storage class for accumulating partial profit/loss events per symbol-strategy pair.
|
|
23055
23716
|
* Maintains a chronological list of profit and loss level events.
|
|
23056
23717
|
*/
|
|
23057
|
-
let ReportStorage$
|
|
23718
|
+
let ReportStorage$5 = class ReportStorage {
|
|
23058
23719
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
23059
23720
|
this.symbol = symbol;
|
|
23060
23721
|
this.strategyName = strategyName;
|
|
@@ -23097,7 +23758,7 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
23097
23758
|
backtest,
|
|
23098
23759
|
});
|
|
23099
23760
|
// Trim queue if exceeded MAX_EVENTS
|
|
23100
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
23761
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
23101
23762
|
this._eventList.pop();
|
|
23102
23763
|
}
|
|
23103
23764
|
}
|
|
@@ -23135,7 +23796,7 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
23135
23796
|
backtest,
|
|
23136
23797
|
});
|
|
23137
23798
|
// Trim queue if exceeded MAX_EVENTS
|
|
23138
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
23799
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
23139
23800
|
this._eventList.pop();
|
|
23140
23801
|
}
|
|
23141
23802
|
}
|
|
@@ -23211,7 +23872,7 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
23211
23872
|
async dump(symbol, strategyName, path = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
|
|
23212
23873
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
23213
23874
|
const timestamp = getContextTimestamp();
|
|
23214
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
23875
|
+
const filename = CREATE_FILE_NAME_FN$5(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
23215
23876
|
await Markdown.writeData("partial", markdown, {
|
|
23216
23877
|
path,
|
|
23217
23878
|
file: filename,
|
|
@@ -23252,7 +23913,7 @@ class PartialMarkdownService {
|
|
|
23252
23913
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
23253
23914
|
* Each combination gets its own isolated storage instance.
|
|
23254
23915
|
*/
|
|
23255
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
23916
|
+
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));
|
|
23256
23917
|
/**
|
|
23257
23918
|
* Subscribes to partial profit/loss signal emitters to receive events.
|
|
23258
23919
|
* Protected against multiple subscriptions.
|
|
@@ -23462,7 +24123,7 @@ class PartialMarkdownService {
|
|
|
23462
24123
|
payload,
|
|
23463
24124
|
});
|
|
23464
24125
|
if (payload) {
|
|
23465
|
-
const key = CREATE_KEY_FN$
|
|
24126
|
+
const key = CREATE_KEY_FN$c(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
23466
24127
|
this.getStorage.clear(key);
|
|
23467
24128
|
}
|
|
23468
24129
|
else {
|
|
@@ -23478,7 +24139,7 @@ class PartialMarkdownService {
|
|
|
23478
24139
|
* @param context - Context with strategyName, exchangeName, frameName
|
|
23479
24140
|
* @returns Unique string key for memoization
|
|
23480
24141
|
*/
|
|
23481
|
-
const CREATE_KEY_FN$
|
|
24142
|
+
const CREATE_KEY_FN$b = (context) => {
|
|
23482
24143
|
const parts = [context.strategyName, context.exchangeName];
|
|
23483
24144
|
if (context.frameName)
|
|
23484
24145
|
parts.push(context.frameName);
|
|
@@ -23552,7 +24213,7 @@ class PartialGlobalService {
|
|
|
23552
24213
|
* @param context - Context with strategyName, exchangeName and frameName
|
|
23553
24214
|
* @param methodName - Name of the calling method for error tracking
|
|
23554
24215
|
*/
|
|
23555
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
24216
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$b(context), (context, methodName) => {
|
|
23556
24217
|
this.loggerService.log("partialGlobalService validate", {
|
|
23557
24218
|
context,
|
|
23558
24219
|
methodName,
|
|
@@ -24007,7 +24668,7 @@ class ClientBreakeven {
|
|
|
24007
24668
|
* @param backtest - Whether running in backtest mode
|
|
24008
24669
|
* @returns Unique string key for memoization
|
|
24009
24670
|
*/
|
|
24010
|
-
const CREATE_KEY_FN$
|
|
24671
|
+
const CREATE_KEY_FN$a = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
|
|
24011
24672
|
/**
|
|
24012
24673
|
* Creates a callback function for emitting breakeven events to breakevenSubject.
|
|
24013
24674
|
*
|
|
@@ -24093,7 +24754,7 @@ class BreakevenConnectionService {
|
|
|
24093
24754
|
* Key format: "signalId:backtest" or "signalId:live"
|
|
24094
24755
|
* Value: ClientBreakeven instance with logger and event emitter
|
|
24095
24756
|
*/
|
|
24096
|
-
this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$
|
|
24757
|
+
this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$a(signalId, backtest), (signalId, backtest) => {
|
|
24097
24758
|
return new ClientBreakeven({
|
|
24098
24759
|
signalId,
|
|
24099
24760
|
logger: this.loggerService,
|
|
@@ -24154,7 +24815,7 @@ class BreakevenConnectionService {
|
|
|
24154
24815
|
const breakeven = this.getBreakeven(data.id, backtest);
|
|
24155
24816
|
await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
|
|
24156
24817
|
await breakeven.clear(symbol, data, priceClose, backtest);
|
|
24157
|
-
const key = CREATE_KEY_FN$
|
|
24818
|
+
const key = CREATE_KEY_FN$a(data.id, backtest);
|
|
24158
24819
|
this.getBreakeven.clear(key);
|
|
24159
24820
|
};
|
|
24160
24821
|
}
|
|
@@ -24170,7 +24831,7 @@ class BreakevenConnectionService {
|
|
|
24170
24831
|
* @param backtest - Whether running in backtest mode
|
|
24171
24832
|
* @returns Unique string key for memoization
|
|
24172
24833
|
*/
|
|
24173
|
-
const CREATE_KEY_FN$
|
|
24834
|
+
const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
24174
24835
|
const parts = [symbol, strategyName, exchangeName];
|
|
24175
24836
|
if (frameName)
|
|
24176
24837
|
parts.push(frameName);
|
|
@@ -24181,7 +24842,7 @@ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
24181
24842
|
* Creates a filename for markdown report based on memoization key components.
|
|
24182
24843
|
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
24183
24844
|
*/
|
|
24184
|
-
const CREATE_FILE_NAME_FN$
|
|
24845
|
+
const CREATE_FILE_NAME_FN$4 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
24185
24846
|
const parts = [symbol, strategyName, exchangeName];
|
|
24186
24847
|
if (frameName) {
|
|
24187
24848
|
parts.push(frameName);
|
|
@@ -24192,12 +24853,12 @@ const CREATE_FILE_NAME_FN$3 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
24192
24853
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
24193
24854
|
};
|
|
24194
24855
|
/** Maximum number of events to store in breakeven reports */
|
|
24195
|
-
const MAX_EVENTS$
|
|
24856
|
+
const MAX_EVENTS$4 = 250;
|
|
24196
24857
|
/**
|
|
24197
24858
|
* Storage class for accumulating breakeven events per symbol-strategy pair.
|
|
24198
24859
|
* Maintains a chronological list of breakeven events.
|
|
24199
24860
|
*/
|
|
24200
|
-
let ReportStorage$
|
|
24861
|
+
let ReportStorage$4 = class ReportStorage {
|
|
24201
24862
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
24202
24863
|
this.symbol = symbol;
|
|
24203
24864
|
this.strategyName = strategyName;
|
|
@@ -24238,7 +24899,7 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
24238
24899
|
backtest,
|
|
24239
24900
|
});
|
|
24240
24901
|
// Trim queue if exceeded MAX_EVENTS
|
|
24241
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
24902
|
+
if (this._eventList.length > MAX_EVENTS$4) {
|
|
24242
24903
|
this._eventList.pop();
|
|
24243
24904
|
}
|
|
24244
24905
|
}
|
|
@@ -24306,7 +24967,7 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
24306
24967
|
async dump(symbol, strategyName, path = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
|
|
24307
24968
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
24308
24969
|
const timestamp = getContextTimestamp();
|
|
24309
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
24970
|
+
const filename = CREATE_FILE_NAME_FN$4(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
24310
24971
|
await Markdown.writeData("breakeven", markdown, {
|
|
24311
24972
|
path,
|
|
24312
24973
|
file: filename,
|
|
@@ -24347,7 +25008,7 @@ class BreakevenMarkdownService {
|
|
|
24347
25008
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
24348
25009
|
* Each combination gets its own isolated storage instance.
|
|
24349
25010
|
*/
|
|
24350
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
25011
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$4(symbol, strategyName, exchangeName, frameName));
|
|
24351
25012
|
/**
|
|
24352
25013
|
* Subscribes to breakeven signal emitter to receive events.
|
|
24353
25014
|
* Protected against multiple subscriptions.
|
|
@@ -24536,7 +25197,7 @@ class BreakevenMarkdownService {
|
|
|
24536
25197
|
payload,
|
|
24537
25198
|
});
|
|
24538
25199
|
if (payload) {
|
|
24539
|
-
const key = CREATE_KEY_FN$
|
|
25200
|
+
const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
24540
25201
|
this.getStorage.clear(key);
|
|
24541
25202
|
}
|
|
24542
25203
|
else {
|
|
@@ -24552,7 +25213,7 @@ class BreakevenMarkdownService {
|
|
|
24552
25213
|
* @param context - Context with strategyName, exchangeName, frameName
|
|
24553
25214
|
* @returns Unique string key for memoization
|
|
24554
25215
|
*/
|
|
24555
|
-
const CREATE_KEY_FN$
|
|
25216
|
+
const CREATE_KEY_FN$8 = (context) => {
|
|
24556
25217
|
const parts = [context.strategyName, context.exchangeName];
|
|
24557
25218
|
if (context.frameName)
|
|
24558
25219
|
parts.push(context.frameName);
|
|
@@ -24626,7 +25287,7 @@ class BreakevenGlobalService {
|
|
|
24626
25287
|
* @param context - Context with strategyName, exchangeName and frameName
|
|
24627
25288
|
* @param methodName - Name of the calling method for error tracking
|
|
24628
25289
|
*/
|
|
24629
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
25290
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$8(context), (context, methodName) => {
|
|
24630
25291
|
this.loggerService.log("breakevenGlobalService validate", {
|
|
24631
25292
|
context,
|
|
24632
25293
|
methodName,
|
|
@@ -24846,7 +25507,7 @@ class ConfigValidationService {
|
|
|
24846
25507
|
* @param backtest - Whether running in backtest mode
|
|
24847
25508
|
* @returns Unique string key for memoization
|
|
24848
25509
|
*/
|
|
24849
|
-
const CREATE_KEY_FN$
|
|
25510
|
+
const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
24850
25511
|
const parts = [symbol, strategyName, exchangeName];
|
|
24851
25512
|
if (frameName)
|
|
24852
25513
|
parts.push(frameName);
|
|
@@ -24857,7 +25518,7 @@ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
24857
25518
|
* Creates a filename for markdown report based on memoization key components.
|
|
24858
25519
|
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
24859
25520
|
*/
|
|
24860
|
-
const CREATE_FILE_NAME_FN$
|
|
25521
|
+
const CREATE_FILE_NAME_FN$3 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
24861
25522
|
const parts = [symbol, strategyName, exchangeName];
|
|
24862
25523
|
if (frameName) {
|
|
24863
25524
|
parts.push(frameName);
|
|
@@ -24868,12 +25529,12 @@ const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
24868
25529
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
24869
25530
|
};
|
|
24870
25531
|
/** Maximum number of events to store in risk reports */
|
|
24871
|
-
const MAX_EVENTS$
|
|
25532
|
+
const MAX_EVENTS$3 = 250;
|
|
24872
25533
|
/**
|
|
24873
25534
|
* Storage class for accumulating risk rejection events per symbol-strategy pair.
|
|
24874
25535
|
* Maintains a chronological list of rejected signals due to risk limits.
|
|
24875
25536
|
*/
|
|
24876
|
-
let ReportStorage$
|
|
25537
|
+
let ReportStorage$3 = class ReportStorage {
|
|
24877
25538
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
24878
25539
|
this.symbol = symbol;
|
|
24879
25540
|
this.strategyName = strategyName;
|
|
@@ -24890,7 +25551,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
24890
25551
|
addRejectionEvent(event) {
|
|
24891
25552
|
this._eventList.unshift(event);
|
|
24892
25553
|
// Trim queue if exceeded MAX_EVENTS
|
|
24893
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
25554
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
24894
25555
|
this._eventList.pop();
|
|
24895
25556
|
}
|
|
24896
25557
|
}
|
|
@@ -24974,7 +25635,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
24974
25635
|
async dump(symbol, strategyName, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
|
|
24975
25636
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
24976
25637
|
const timestamp = getContextTimestamp();
|
|
24977
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
25638
|
+
const filename = CREATE_FILE_NAME_FN$3(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
24978
25639
|
await Markdown.writeData("risk", markdown, {
|
|
24979
25640
|
path,
|
|
24980
25641
|
file: filename,
|
|
@@ -25015,7 +25676,7 @@ class RiskMarkdownService {
|
|
|
25015
25676
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
25016
25677
|
* Each combination gets its own isolated storage instance.
|
|
25017
25678
|
*/
|
|
25018
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
25679
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$3(symbol, strategyName, exchangeName, frameName));
|
|
25019
25680
|
/**
|
|
25020
25681
|
* Subscribes to risk rejection emitter to receive rejection events.
|
|
25021
25682
|
* Protected against multiple subscriptions.
|
|
@@ -25204,7 +25865,7 @@ class RiskMarkdownService {
|
|
|
25204
25865
|
payload,
|
|
25205
25866
|
});
|
|
25206
25867
|
if (payload) {
|
|
25207
|
-
const key = CREATE_KEY_FN$
|
|
25868
|
+
const key = CREATE_KEY_FN$7(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
25208
25869
|
this.getStorage.clear(key);
|
|
25209
25870
|
}
|
|
25210
25871
|
else {
|
|
@@ -25502,6 +26163,7 @@ const WILDCARD_TARGET = {
|
|
|
25502
26163
|
schedule: true,
|
|
25503
26164
|
walker: true,
|
|
25504
26165
|
sync: true,
|
|
26166
|
+
highest_profit: true,
|
|
25505
26167
|
};
|
|
25506
26168
|
/**
|
|
25507
26169
|
* Utility class for managing report services.
|
|
@@ -25539,7 +26201,7 @@ class ReportUtils {
|
|
|
25539
26201
|
*
|
|
25540
26202
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
25541
26203
|
*/
|
|
25542
|
-
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, } = WILDCARD_TARGET) => {
|
|
26204
|
+
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) => {
|
|
25543
26205
|
bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
|
|
25544
26206
|
backtest: bt$1,
|
|
25545
26207
|
breakeven,
|
|
@@ -25587,6 +26249,9 @@ class ReportUtils {
|
|
|
25587
26249
|
if (sync) {
|
|
25588
26250
|
unList.push(bt.syncReportService.subscribe());
|
|
25589
26251
|
}
|
|
26252
|
+
if (highest_profit) {
|
|
26253
|
+
unList.push(bt.highestProfitReportService.subscribe());
|
|
26254
|
+
}
|
|
25590
26255
|
return functoolsKit.compose(...unList.map((un) => () => void un()));
|
|
25591
26256
|
};
|
|
25592
26257
|
/**
|
|
@@ -25625,7 +26290,7 @@ class ReportUtils {
|
|
|
25625
26290
|
* Report.disable();
|
|
25626
26291
|
* ```
|
|
25627
26292
|
*/
|
|
25628
|
-
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, } = WILDCARD_TARGET) => {
|
|
26293
|
+
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) => {
|
|
25629
26294
|
bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
|
|
25630
26295
|
backtest: bt$1,
|
|
25631
26296
|
breakeven,
|
|
@@ -25672,6 +26337,9 @@ class ReportUtils {
|
|
|
25672
26337
|
if (sync) {
|
|
25673
26338
|
bt.syncReportService.unsubscribe();
|
|
25674
26339
|
}
|
|
26340
|
+
if (highest_profit) {
|
|
26341
|
+
bt.highestProfitReportService.unsubscribe();
|
|
26342
|
+
}
|
|
25675
26343
|
};
|
|
25676
26344
|
}
|
|
25677
26345
|
}
|
|
@@ -27879,6 +28547,60 @@ class SyncReportService {
|
|
|
27879
28547
|
}
|
|
27880
28548
|
}
|
|
27881
28549
|
|
|
28550
|
+
const HIGHEST_PROFIT_REPORT_METHOD_NAME_SUBSCRIBE = "HighestProfitReportService.subscribe";
|
|
28551
|
+
const HIGHEST_PROFIT_REPORT_METHOD_NAME_UNSUBSCRIBE = "HighestProfitReportService.unsubscribe";
|
|
28552
|
+
const HIGHEST_PROFIT_REPORT_METHOD_NAME_TICK = "HighestProfitReportService.tick";
|
|
28553
|
+
/**
|
|
28554
|
+
* Service for logging highest profit events to the JSONL report database.
|
|
28555
|
+
*
|
|
28556
|
+
* Listens to highestProfitSubject and writes each new price record to
|
|
28557
|
+
* Report.writeData() for persistence and analytics.
|
|
28558
|
+
*/
|
|
28559
|
+
class HighestProfitReportService {
|
|
28560
|
+
constructor() {
|
|
28561
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
28562
|
+
this.tick = async (data) => {
|
|
28563
|
+
this.loggerService.log(HIGHEST_PROFIT_REPORT_METHOD_NAME_TICK, { data });
|
|
28564
|
+
await Report.writeData("highest_profit", {
|
|
28565
|
+
timestamp: data.timestamp,
|
|
28566
|
+
symbol: data.symbol,
|
|
28567
|
+
strategyName: data.signal.strategyName,
|
|
28568
|
+
exchangeName: data.exchangeName,
|
|
28569
|
+
frameName: data.frameName,
|
|
28570
|
+
backtest: data.backtest,
|
|
28571
|
+
signalId: data.signal.id,
|
|
28572
|
+
position: data.signal.position,
|
|
28573
|
+
currentPrice: data.currentPrice,
|
|
28574
|
+
priceOpen: data.signal.priceOpen,
|
|
28575
|
+
priceTakeProfit: data.signal.priceTakeProfit,
|
|
28576
|
+
priceStopLoss: data.signal.priceStopLoss,
|
|
28577
|
+
}, {
|
|
28578
|
+
symbol: data.symbol,
|
|
28579
|
+
strategyName: data.signal.strategyName,
|
|
28580
|
+
exchangeName: data.exchangeName,
|
|
28581
|
+
frameName: data.frameName,
|
|
28582
|
+
signalId: data.signal.id,
|
|
28583
|
+
walkerName: "",
|
|
28584
|
+
});
|
|
28585
|
+
};
|
|
28586
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
28587
|
+
this.loggerService.log(HIGHEST_PROFIT_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
28588
|
+
const unsub = highestProfitSubject.subscribe(this.tick);
|
|
28589
|
+
return () => {
|
|
28590
|
+
this.subscribe.clear();
|
|
28591
|
+
unsub();
|
|
28592
|
+
};
|
|
28593
|
+
});
|
|
28594
|
+
this.unsubscribe = async () => {
|
|
28595
|
+
this.loggerService.log(HIGHEST_PROFIT_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
28596
|
+
if (this.subscribe.hasValue()) {
|
|
28597
|
+
const lastSubscription = this.subscribe();
|
|
28598
|
+
lastSubscription();
|
|
28599
|
+
}
|
|
28600
|
+
};
|
|
28601
|
+
}
|
|
28602
|
+
}
|
|
28603
|
+
|
|
27882
28604
|
/**
|
|
27883
28605
|
* Creates a unique key for memoizing ReportStorage instances.
|
|
27884
28606
|
*
|
|
@@ -27892,7 +28614,7 @@ class SyncReportService {
|
|
|
27892
28614
|
* @returns Colon-separated key string for memoization
|
|
27893
28615
|
* @internal
|
|
27894
28616
|
*/
|
|
27895
|
-
const CREATE_KEY_FN$
|
|
28617
|
+
const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
27896
28618
|
const parts = [symbol, strategyName, exchangeName];
|
|
27897
28619
|
if (frameName)
|
|
27898
28620
|
parts.push(frameName);
|
|
@@ -27912,7 +28634,7 @@ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
27912
28634
|
* @returns Underscore-separated filename with .md extension
|
|
27913
28635
|
* @internal
|
|
27914
28636
|
*/
|
|
27915
|
-
const CREATE_FILE_NAME_FN$
|
|
28637
|
+
const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
27916
28638
|
const parts = [symbol, strategyName, exchangeName];
|
|
27917
28639
|
if (frameName) {
|
|
27918
28640
|
parts.push(frameName);
|
|
@@ -27927,7 +28649,7 @@ const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
27927
28649
|
* Older events are discarded when this limit is exceeded.
|
|
27928
28650
|
* @internal
|
|
27929
28651
|
*/
|
|
27930
|
-
const MAX_EVENTS$
|
|
28652
|
+
const MAX_EVENTS$2 = 250;
|
|
27931
28653
|
/**
|
|
27932
28654
|
* In-memory storage for accumulating strategy events per symbol-strategy pair.
|
|
27933
28655
|
*
|
|
@@ -27940,7 +28662,7 @@ const MAX_EVENTS$1 = 250;
|
|
|
27940
28662
|
*
|
|
27941
28663
|
* @internal
|
|
27942
28664
|
*/
|
|
27943
|
-
let ReportStorage$
|
|
28665
|
+
let ReportStorage$2 = class ReportStorage {
|
|
27944
28666
|
/**
|
|
27945
28667
|
* Creates a new ReportStorage instance.
|
|
27946
28668
|
*
|
|
@@ -27966,7 +28688,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
27966
28688
|
*/
|
|
27967
28689
|
addEvent(event) {
|
|
27968
28690
|
this._eventList.unshift(event);
|
|
27969
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
28691
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
27970
28692
|
this._eventList.pop();
|
|
27971
28693
|
}
|
|
27972
28694
|
}
|
|
@@ -28071,7 +28793,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
28071
28793
|
async dump(symbol, strategyName, path = "./dump/strategy", columns = COLUMN_CONFIG.strategy_columns) {
|
|
28072
28794
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
28073
28795
|
const timestamp = getContextTimestamp();
|
|
28074
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
28796
|
+
const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
28075
28797
|
await Markdown.writeData("strategy", markdown, {
|
|
28076
28798
|
path,
|
|
28077
28799
|
file: filename,
|
|
@@ -28140,7 +28862,7 @@ class StrategyMarkdownService {
|
|
|
28140
28862
|
*
|
|
28141
28863
|
* @internal
|
|
28142
28864
|
*/
|
|
28143
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
28865
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName));
|
|
28144
28866
|
/**
|
|
28145
28867
|
* Records a cancel-scheduled event when a scheduled signal is cancelled.
|
|
28146
28868
|
*
|
|
@@ -28708,7 +29430,7 @@ class StrategyMarkdownService {
|
|
|
28708
29430
|
this.clear = async (payload) => {
|
|
28709
29431
|
this.loggerService.log("strategyMarkdownService clear", { payload });
|
|
28710
29432
|
if (payload) {
|
|
28711
|
-
const key = CREATE_KEY_FN$
|
|
29433
|
+
const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
28712
29434
|
this.getStorage.clear(key);
|
|
28713
29435
|
}
|
|
28714
29436
|
else {
|
|
@@ -28816,7 +29538,7 @@ class StrategyMarkdownService {
|
|
|
28816
29538
|
* Creates a unique key for memoizing ReportStorage instances.
|
|
28817
29539
|
* Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
|
|
28818
29540
|
*/
|
|
28819
|
-
const CREATE_KEY_FN$
|
|
29541
|
+
const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
28820
29542
|
const parts = [symbol, strategyName, exchangeName];
|
|
28821
29543
|
if (frameName)
|
|
28822
29544
|
parts.push(frameName);
|
|
@@ -28827,7 +29549,7 @@ const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
28827
29549
|
* Creates a filename for the markdown report.
|
|
28828
29550
|
* Filename format: "symbol_strategyName_exchangeName[_frameName_backtest|_live]-timestamp.md"
|
|
28829
29551
|
*/
|
|
28830
|
-
const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
29552
|
+
const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
28831
29553
|
const parts = [symbol, strategyName, exchangeName];
|
|
28832
29554
|
if (frameName) {
|
|
28833
29555
|
parts.push(frameName);
|
|
@@ -28838,12 +29560,12 @@ const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, time
|
|
|
28838
29560
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
28839
29561
|
};
|
|
28840
29562
|
/** Maximum number of events to store in sync reports */
|
|
28841
|
-
const MAX_EVENTS = 250;
|
|
29563
|
+
const MAX_EVENTS$1 = 250;
|
|
28842
29564
|
/**
|
|
28843
29565
|
* Storage class for accumulating signal sync events per symbol-strategy-exchange-frame-backtest combination.
|
|
28844
29566
|
* Maintains a chronological list of signal-open and signal-close events.
|
|
28845
29567
|
*/
|
|
28846
|
-
class ReportStorage {
|
|
29568
|
+
let ReportStorage$1 = class ReportStorage {
|
|
28847
29569
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
28848
29570
|
this.symbol = symbol;
|
|
28849
29571
|
this.strategyName = strategyName;
|
|
@@ -28853,7 +29575,7 @@ class ReportStorage {
|
|
|
28853
29575
|
}
|
|
28854
29576
|
addEvent(event) {
|
|
28855
29577
|
this._eventList.unshift(event);
|
|
28856
|
-
if (this._eventList.length > MAX_EVENTS) {
|
|
29578
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
28857
29579
|
this._eventList.pop();
|
|
28858
29580
|
}
|
|
28859
29581
|
}
|
|
@@ -28914,7 +29636,7 @@ class ReportStorage {
|
|
|
28914
29636
|
async dump(symbol, strategyName, path = "./dump/sync", columns = COLUMN_CONFIG.sync_columns) {
|
|
28915
29637
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
28916
29638
|
const timestamp = getContextTimestamp();
|
|
28917
|
-
const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
29639
|
+
const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
28918
29640
|
await Markdown.writeData("sync", markdown, {
|
|
28919
29641
|
path,
|
|
28920
29642
|
file: filename,
|
|
@@ -28925,7 +29647,7 @@ class ReportStorage {
|
|
|
28925
29647
|
frameName: this.frameName,
|
|
28926
29648
|
});
|
|
28927
29649
|
}
|
|
28928
|
-
}
|
|
29650
|
+
};
|
|
28929
29651
|
/**
|
|
28930
29652
|
* Service for generating and saving signal sync markdown reports.
|
|
28931
29653
|
*
|
|
@@ -28948,7 +29670,7 @@ class ReportStorage {
|
|
|
28948
29670
|
class SyncMarkdownService {
|
|
28949
29671
|
constructor() {
|
|
28950
29672
|
this.loggerService = inject(TYPES.loggerService);
|
|
28951
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
29673
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
|
|
28952
29674
|
this.subscribe = functoolsKit.singleshot(() => {
|
|
28953
29675
|
this.loggerService.log("syncMarkdownService init");
|
|
28954
29676
|
const unsubscribe = syncSubject.subscribe(this.tick);
|
|
@@ -29022,6 +29744,190 @@ class SyncMarkdownService {
|
|
|
29022
29744
|
};
|
|
29023
29745
|
this.clear = async (payload) => {
|
|
29024
29746
|
this.loggerService.log("syncMarkdownService clear", { payload });
|
|
29747
|
+
if (payload) {
|
|
29748
|
+
const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
29749
|
+
this.getStorage.clear(key);
|
|
29750
|
+
}
|
|
29751
|
+
else {
|
|
29752
|
+
this.getStorage.clear();
|
|
29753
|
+
}
|
|
29754
|
+
};
|
|
29755
|
+
}
|
|
29756
|
+
}
|
|
29757
|
+
|
|
29758
|
+
/**
|
|
29759
|
+
* Creates a unique memoization key for a symbol-strategy-exchange-frame-backtest combination.
|
|
29760
|
+
*/
|
|
29761
|
+
const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
29762
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
29763
|
+
if (frameName)
|
|
29764
|
+
parts.push(frameName);
|
|
29765
|
+
parts.push(backtest ? "backtest" : "live");
|
|
29766
|
+
return parts.join(":");
|
|
29767
|
+
};
|
|
29768
|
+
/**
|
|
29769
|
+
* Creates a filename for the markdown report.
|
|
29770
|
+
*/
|
|
29771
|
+
const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
29772
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
29773
|
+
if (frameName) {
|
|
29774
|
+
parts.push(frameName);
|
|
29775
|
+
parts.push("backtest");
|
|
29776
|
+
}
|
|
29777
|
+
else
|
|
29778
|
+
parts.push("live");
|
|
29779
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
29780
|
+
};
|
|
29781
|
+
/** Maximum number of events to store per combination */
|
|
29782
|
+
const MAX_EVENTS = 250;
|
|
29783
|
+
/**
|
|
29784
|
+
* Accumulates highest profit events per symbol-strategy-exchange-frame combination.
|
|
29785
|
+
*/
|
|
29786
|
+
class ReportStorage {
|
|
29787
|
+
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
29788
|
+
this.symbol = symbol;
|
|
29789
|
+
this.strategyName = strategyName;
|
|
29790
|
+
this.exchangeName = exchangeName;
|
|
29791
|
+
this.frameName = frameName;
|
|
29792
|
+
this._eventList = [];
|
|
29793
|
+
}
|
|
29794
|
+
/**
|
|
29795
|
+
* Adds a highest profit event to the storage.
|
|
29796
|
+
*/
|
|
29797
|
+
addEvent(data, currentPrice, backtest, timestamp) {
|
|
29798
|
+
this._eventList.unshift({
|
|
29799
|
+
timestamp,
|
|
29800
|
+
symbol: data.symbol,
|
|
29801
|
+
strategyName: data.strategyName,
|
|
29802
|
+
signalId: data.id,
|
|
29803
|
+
position: data.position,
|
|
29804
|
+
pnl: data.pnl,
|
|
29805
|
+
currentPrice,
|
|
29806
|
+
priceOpen: data.priceOpen,
|
|
29807
|
+
priceTakeProfit: data.priceTakeProfit,
|
|
29808
|
+
priceStopLoss: data.priceStopLoss,
|
|
29809
|
+
backtest,
|
|
29810
|
+
});
|
|
29811
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
29812
|
+
this._eventList.pop();
|
|
29813
|
+
}
|
|
29814
|
+
}
|
|
29815
|
+
/**
|
|
29816
|
+
* Returns aggregated statistics from accumulated events.
|
|
29817
|
+
*/
|
|
29818
|
+
async getData() {
|
|
29819
|
+
return {
|
|
29820
|
+
eventList: this._eventList,
|
|
29821
|
+
totalEvents: this._eventList.length,
|
|
29822
|
+
};
|
|
29823
|
+
}
|
|
29824
|
+
/**
|
|
29825
|
+
* Generates a markdown report table for this storage.
|
|
29826
|
+
*/
|
|
29827
|
+
async getReport(symbol, strategyName, columns = COLUMN_CONFIG.highest_profit_columns) {
|
|
29828
|
+
const stats = await this.getData();
|
|
29829
|
+
if (stats.totalEvents === 0) {
|
|
29830
|
+
return [
|
|
29831
|
+
`# Highest Profit Report: ${symbol}:${strategyName}`,
|
|
29832
|
+
"",
|
|
29833
|
+
"No highest profit events recorded yet.",
|
|
29834
|
+
].join("\n");
|
|
29835
|
+
}
|
|
29836
|
+
const visibleColumns = [];
|
|
29837
|
+
for (const col of columns) {
|
|
29838
|
+
if (await col.isVisible()) {
|
|
29839
|
+
visibleColumns.push(col);
|
|
29840
|
+
}
|
|
29841
|
+
}
|
|
29842
|
+
const header = visibleColumns.map((col) => col.label);
|
|
29843
|
+
const separator = visibleColumns.map(() => "---");
|
|
29844
|
+
const rows = await Promise.all(this._eventList.map(async (event, index) => Promise.all(visibleColumns.map((col) => col.format(event, index)))));
|
|
29845
|
+
const tableData = [header, separator, ...rows];
|
|
29846
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
29847
|
+
return [
|
|
29848
|
+
`# Highest Profit Report: ${symbol}:${strategyName}`,
|
|
29849
|
+
"",
|
|
29850
|
+
table,
|
|
29851
|
+
"",
|
|
29852
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
29853
|
+
].join("\n");
|
|
29854
|
+
}
|
|
29855
|
+
/**
|
|
29856
|
+
* Saves the report to disk.
|
|
29857
|
+
*/
|
|
29858
|
+
async dump(symbol, strategyName, path = "./dump/highest_profit", columns = COLUMN_CONFIG.highest_profit_columns) {
|
|
29859
|
+
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
29860
|
+
const timestamp = getContextTimestamp();
|
|
29861
|
+
const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
29862
|
+
await Markdown.writeData("highest_profit", markdown, {
|
|
29863
|
+
path,
|
|
29864
|
+
file: filename,
|
|
29865
|
+
symbol: this.symbol,
|
|
29866
|
+
signalId: "",
|
|
29867
|
+
strategyName: this.strategyName,
|
|
29868
|
+
exchangeName: this.exchangeName,
|
|
29869
|
+
frameName: this.frameName,
|
|
29870
|
+
});
|
|
29871
|
+
}
|
|
29872
|
+
}
|
|
29873
|
+
/**
|
|
29874
|
+
* Service for generating and saving highest profit markdown reports.
|
|
29875
|
+
*
|
|
29876
|
+
* Listens to highestProfitSubject and accumulates events per
|
|
29877
|
+
* symbol-strategy-exchange-frame combination. Provides getData(),
|
|
29878
|
+
* getReport(), and dump() methods matching the Partial pattern.
|
|
29879
|
+
*/
|
|
29880
|
+
class HighestProfitMarkdownService {
|
|
29881
|
+
constructor() {
|
|
29882
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
29883
|
+
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));
|
|
29884
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
29885
|
+
this.loggerService.log("highestProfitMarkdownService init");
|
|
29886
|
+
const unsub = highestProfitSubject.subscribe(this.tick);
|
|
29887
|
+
return () => {
|
|
29888
|
+
this.subscribe.clear();
|
|
29889
|
+
this.clear();
|
|
29890
|
+
unsub();
|
|
29891
|
+
};
|
|
29892
|
+
});
|
|
29893
|
+
this.unsubscribe = async () => {
|
|
29894
|
+
this.loggerService.log("highestProfitMarkdownService unsubscribe");
|
|
29895
|
+
if (this.subscribe.hasValue()) {
|
|
29896
|
+
const lastSubscription = this.subscribe();
|
|
29897
|
+
lastSubscription();
|
|
29898
|
+
}
|
|
29899
|
+
};
|
|
29900
|
+
this.tick = async (data) => {
|
|
29901
|
+
this.loggerService.log("highestProfitMarkdownService tick", { data });
|
|
29902
|
+
const storage = this.getStorage(data.symbol, data.signal.strategyName, data.exchangeName, data.frameName, data.backtest);
|
|
29903
|
+
storage.addEvent(data.signal, data.currentPrice, data.backtest, data.timestamp);
|
|
29904
|
+
};
|
|
29905
|
+
this.getData = async (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
29906
|
+
this.loggerService.log("highestProfitMarkdownService getData", { symbol, strategyName, exchangeName, frameName, backtest });
|
|
29907
|
+
if (!this.subscribe.hasValue()) {
|
|
29908
|
+
throw new Error("HighestProfitMarkdownService not initialized. Call subscribe() before getting data.");
|
|
29909
|
+
}
|
|
29910
|
+
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
29911
|
+
return storage.getData();
|
|
29912
|
+
};
|
|
29913
|
+
this.getReport = async (symbol, strategyName, exchangeName, frameName, backtest, columns = COLUMN_CONFIG.highest_profit_columns) => {
|
|
29914
|
+
this.loggerService.log("highestProfitMarkdownService getReport", { symbol, strategyName, exchangeName, frameName, backtest });
|
|
29915
|
+
if (!this.subscribe.hasValue()) {
|
|
29916
|
+
throw new Error("HighestProfitMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
29917
|
+
}
|
|
29918
|
+
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
29919
|
+
return storage.getReport(symbol, strategyName, columns);
|
|
29920
|
+
};
|
|
29921
|
+
this.dump = async (symbol, strategyName, exchangeName, frameName, backtest, path = "./dump/highest_profit", columns = COLUMN_CONFIG.highest_profit_columns) => {
|
|
29922
|
+
this.loggerService.log("highestProfitMarkdownService dump", { symbol, strategyName, exchangeName, frameName, backtest, path });
|
|
29923
|
+
if (!this.subscribe.hasValue()) {
|
|
29924
|
+
throw new Error("HighestProfitMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
29925
|
+
}
|
|
29926
|
+
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
29927
|
+
await storage.dump(symbol, strategyName, path, columns);
|
|
29928
|
+
};
|
|
29929
|
+
this.clear = async (payload) => {
|
|
29930
|
+
this.loggerService.log("highestProfitMarkdownService clear", { payload });
|
|
29025
29931
|
if (payload) {
|
|
29026
29932
|
const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
29027
29933
|
this.getStorage.clear(key);
|
|
@@ -29371,6 +30277,7 @@ class TimeMetaService {
|
|
|
29371
30277
|
provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
|
|
29372
30278
|
provide(TYPES.strategyMarkdownService, () => new StrategyMarkdownService());
|
|
29373
30279
|
provide(TYPES.syncMarkdownService, () => new SyncMarkdownService());
|
|
30280
|
+
provide(TYPES.highestProfitMarkdownService, () => new HighestProfitMarkdownService());
|
|
29374
30281
|
}
|
|
29375
30282
|
{
|
|
29376
30283
|
provide(TYPES.backtestReportService, () => new BacktestReportService());
|
|
@@ -29384,6 +30291,7 @@ class TimeMetaService {
|
|
|
29384
30291
|
provide(TYPES.riskReportService, () => new RiskReportService());
|
|
29385
30292
|
provide(TYPES.strategyReportService, () => new StrategyReportService());
|
|
29386
30293
|
provide(TYPES.syncReportService, () => new SyncReportService());
|
|
30294
|
+
provide(TYPES.highestProfitReportService, () => new HighestProfitReportService());
|
|
29387
30295
|
}
|
|
29388
30296
|
{
|
|
29389
30297
|
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
@@ -29466,6 +30374,7 @@ const markdownServices = {
|
|
|
29466
30374
|
riskMarkdownService: inject(TYPES.riskMarkdownService),
|
|
29467
30375
|
strategyMarkdownService: inject(TYPES.strategyMarkdownService),
|
|
29468
30376
|
syncMarkdownService: inject(TYPES.syncMarkdownService),
|
|
30377
|
+
highestProfitMarkdownService: inject(TYPES.highestProfitMarkdownService),
|
|
29469
30378
|
};
|
|
29470
30379
|
const reportServices = {
|
|
29471
30380
|
backtestReportService: inject(TYPES.backtestReportService),
|
|
@@ -29479,6 +30388,7 @@ const reportServices = {
|
|
|
29479
30388
|
riskReportService: inject(TYPES.riskReportService),
|
|
29480
30389
|
strategyReportService: inject(TYPES.strategyReportService),
|
|
29481
30390
|
syncReportService: inject(TYPES.syncReportService),
|
|
30391
|
+
highestProfitReportService: inject(TYPES.highestProfitReportService),
|
|
29482
30392
|
};
|
|
29483
30393
|
const validationServices = {
|
|
29484
30394
|
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
@@ -31250,7 +32160,7 @@ const investedCostToPercent = (dollarAmount, investedCost) => {
|
|
|
31250
32160
|
*
|
|
31251
32161
|
* @param newStopLossPrice - Desired absolute stop-loss price
|
|
31252
32162
|
* @param originalStopLossPrice - Original stop-loss price from the pending signal
|
|
31253
|
-
* @param effectivePriceOpen - Effective entry price (from `
|
|
32163
|
+
* @param effectivePriceOpen - Effective entry price (from `getPositionEffectivePrice`)
|
|
31254
32164
|
* @returns percentShift to pass to `commitTrailingStop`
|
|
31255
32165
|
*
|
|
31256
32166
|
* @example
|
|
@@ -31272,7 +32182,7 @@ const slPriceToPercentShift = (newStopLossPrice, originalStopLossPrice, effectiv
|
|
|
31272
32182
|
*
|
|
31273
32183
|
* @param newTakeProfitPrice - Desired absolute take-profit price
|
|
31274
32184
|
* @param originalTakeProfitPrice - Original take-profit price from the pending signal
|
|
31275
|
-
* @param effectivePriceOpen - Effective entry price (from `
|
|
32185
|
+
* @param effectivePriceOpen - Effective entry price (from `getPositionEffectivePrice`)
|
|
31276
32186
|
* @returns percentShift to pass to `commitTrailingTake`
|
|
31277
32187
|
*
|
|
31278
32188
|
* @example
|
|
@@ -31297,7 +32207,7 @@ const tpPriceToPercentShift = (newTakeProfitPrice, originalTakeProfitPrice, effe
|
|
|
31297
32207
|
*
|
|
31298
32208
|
* @param percentShift - Value returned by `slPriceToPercentShift` (or passed to `commitTrailingStop`)
|
|
31299
32209
|
* @param originalStopLossPrice - Original stop-loss price from the pending signal
|
|
31300
|
-
* @param effectivePriceOpen - Effective entry price (from `
|
|
32210
|
+
* @param effectivePriceOpen - Effective entry price (from `getPositionEffectivePrice`)
|
|
31301
32211
|
* @param position - Position direction: "long" or "short"
|
|
31302
32212
|
* @returns Absolute stop-loss price corresponding to the given percentShift
|
|
31303
32213
|
*
|
|
@@ -31324,7 +32234,7 @@ const slPercentShiftToPrice = (percentShift, originalStopLossPrice, effectivePri
|
|
|
31324
32234
|
*
|
|
31325
32235
|
* @param percentShift - Value returned by `tpPriceToPercentShift` (or passed to `commitTrailingTake`)
|
|
31326
32236
|
* @param originalTakeProfitPrice - Original take-profit price from the pending signal
|
|
31327
|
-
* @param effectivePriceOpen - Effective entry price (from `
|
|
32237
|
+
* @param effectivePriceOpen - Effective entry price (from `getPositionEffectivePrice`)
|
|
31328
32238
|
* @param position - Position direction: "long" or "short"
|
|
31329
32239
|
* @returns Absolute take-profit price corresponding to the given percentShift
|
|
31330
32240
|
*
|
|
@@ -31363,7 +32273,7 @@ const percentToCloseCost = (percentToClose, investedCost) => {
|
|
|
31363
32273
|
* Breakeven moves the SL to the effective entry price (effectivePriceOpen).
|
|
31364
32274
|
* The value is the same regardless of position direction.
|
|
31365
32275
|
*
|
|
31366
|
-
* @param effectivePriceOpen - Effective entry price (from `
|
|
32276
|
+
* @param effectivePriceOpen - Effective entry price (from `getPositionEffectivePrice`)
|
|
31367
32277
|
* @returns New stop-loss price equal to the effective entry price
|
|
31368
32278
|
*
|
|
31369
32279
|
* @example
|
|
@@ -32285,13 +33195,22 @@ const GET_TOTAL_COST_CLOSED_METHOD_NAME = "strategy.getTotalCostClosed";
|
|
|
32285
33195
|
const GET_PENDING_SIGNAL_METHOD_NAME = "strategy.getPendingSignal";
|
|
32286
33196
|
const GET_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.getScheduledSignal";
|
|
32287
33197
|
const GET_BREAKEVEN_METHOD_NAME = "strategy.getBreakeven";
|
|
32288
|
-
const GET_POSITION_AVERAGE_PRICE_METHOD_NAME = "strategy.
|
|
33198
|
+
const GET_POSITION_AVERAGE_PRICE_METHOD_NAME = "strategy.getPositionEffectivePrice";
|
|
32289
33199
|
const GET_POSITION_INVESTED_COUNT_METHOD_NAME = "strategy.getPositionInvestedCount";
|
|
32290
33200
|
const GET_POSITION_INVESTED_COST_METHOD_NAME = "strategy.getPositionInvestedCost";
|
|
32291
33201
|
const GET_POSITION_PNL_PERCENT_METHOD_NAME = "strategy.getPositionPnlPercent";
|
|
32292
33202
|
const GET_POSITION_PNL_COST_METHOD_NAME = "strategy.getPositionPnlCost";
|
|
32293
33203
|
const GET_POSITION_LEVELS_METHOD_NAME = "strategy.getPositionLevels";
|
|
32294
33204
|
const GET_POSITION_PARTIALS_METHOD_NAME = "strategy.getPositionPartials";
|
|
33205
|
+
const GET_POSITION_ENTRIES_METHOD_NAME = "strategy.getPositionEntries";
|
|
33206
|
+
const GET_POSITION_ESTIMATE_MINUTES_METHOD_NAME = "strategy.getPositionEstimateMinutes";
|
|
33207
|
+
const GET_POSITION_COUNTDOWN_MINUTES_METHOD_NAME = "strategy.getPositionCountdownMinutes";
|
|
33208
|
+
const GET_POSITION_HIGHEST_PROFIT_PRICE_METHOD_NAME = "strategy.getPositionHighestProfitPrice";
|
|
33209
|
+
const GET_POSITION_HIGHEST_PROFIT_TIMESTAMP_METHOD_NAME = "strategy.getPositionHighestProfitTimestamp";
|
|
33210
|
+
const GET_POSITION_HIGHEST_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionHighestPnlPercentage";
|
|
33211
|
+
const GET_POSITION_HIGHEST_PNL_COST_METHOD_NAME = "strategy.getPositionHighestPnlCost";
|
|
33212
|
+
const GET_POSITION_HIGHEST_PROFIT_BREAKEVEN_METHOD_NAME = "strategy.getPositionHighestProfitBreakeven";
|
|
33213
|
+
const GET_POSITION_DRAWDOWN_MINUTES_METHOD_NAME = "strategy.getPositionDrawdownMinutes";
|
|
32295
33214
|
const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
|
|
32296
33215
|
const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
|
|
32297
33216
|
/**
|
|
@@ -32558,7 +33477,7 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
|
|
|
32558
33477
|
if (!signal) {
|
|
32559
33478
|
return false;
|
|
32560
33479
|
}
|
|
32561
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
33480
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
32562
33481
|
if (effectivePriceOpen === null) {
|
|
32563
33482
|
return false;
|
|
32564
33483
|
}
|
|
@@ -32638,7 +33557,7 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
|
|
|
32638
33557
|
if (!signal) {
|
|
32639
33558
|
return false;
|
|
32640
33559
|
}
|
|
32641
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
33560
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
32642
33561
|
if (effectivePriceOpen === null) {
|
|
32643
33562
|
return false;
|
|
32644
33563
|
}
|
|
@@ -32688,7 +33607,7 @@ async function commitTrailingStopCost(symbol, newStopLossPrice) {
|
|
|
32688
33607
|
if (!signal) {
|
|
32689
33608
|
return false;
|
|
32690
33609
|
}
|
|
32691
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
33610
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
32692
33611
|
if (effectivePriceOpen === null) {
|
|
32693
33612
|
return false;
|
|
32694
33613
|
}
|
|
@@ -32739,7 +33658,7 @@ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
|
|
|
32739
33658
|
if (!signal) {
|
|
32740
33659
|
return false;
|
|
32741
33660
|
}
|
|
32742
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
33661
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
32743
33662
|
if (effectivePriceOpen === null) {
|
|
32744
33663
|
return false;
|
|
32745
33664
|
}
|
|
@@ -32801,7 +33720,7 @@ async function commitBreakeven(symbol) {
|
|
|
32801
33720
|
if (!signal) {
|
|
32802
33721
|
return false;
|
|
32803
33722
|
}
|
|
32804
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
33723
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
32805
33724
|
if (effectivePriceOpen === null) {
|
|
32806
33725
|
return false;
|
|
32807
33726
|
}
|
|
@@ -33097,26 +34016,26 @@ async function getBreakeven(symbol, currentPrice) {
|
|
|
33097
34016
|
*
|
|
33098
34017
|
* @example
|
|
33099
34018
|
* ```typescript
|
|
33100
|
-
* import {
|
|
34019
|
+
* import { getPositionEffectivePrice } from "backtest-kit";
|
|
33101
34020
|
*
|
|
33102
|
-
* const avgPrice = await
|
|
34021
|
+
* const avgPrice = await getPositionEffectivePrice("BTCUSDT");
|
|
33103
34022
|
* // No DCA: avgPrice === priceOpen
|
|
33104
34023
|
* // After DCA at lower price: avgPrice < priceOpen
|
|
33105
34024
|
* ```
|
|
33106
34025
|
*/
|
|
33107
|
-
async function
|
|
34026
|
+
async function getPositionEffectivePrice(symbol) {
|
|
33108
34027
|
bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, {
|
|
33109
34028
|
symbol,
|
|
33110
34029
|
});
|
|
33111
34030
|
if (!ExecutionContextService.hasContext()) {
|
|
33112
|
-
throw new Error("
|
|
34031
|
+
throw new Error("getPositionEffectivePrice requires an execution context");
|
|
33113
34032
|
}
|
|
33114
34033
|
if (!MethodContextService.hasContext()) {
|
|
33115
|
-
throw new Error("
|
|
34034
|
+
throw new Error("getPositionEffectivePrice requires a method context");
|
|
33116
34035
|
}
|
|
33117
34036
|
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
33118
34037
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
33119
|
-
return await bt.strategyCoreService.
|
|
34038
|
+
return await bt.strategyCoreService.getPositionEffectivePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
33120
34039
|
}
|
|
33121
34040
|
/**
|
|
33122
34041
|
* Returns the number of DCA entries made for the current pending signal.
|
|
@@ -33471,6 +34390,284 @@ async function getPositionPartials(symbol) {
|
|
|
33471
34390
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
33472
34391
|
return await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
33473
34392
|
}
|
|
34393
|
+
/**
|
|
34394
|
+
* Returns the list of DCA entry prices and costs for the current pending signal.
|
|
34395
|
+
*
|
|
34396
|
+
* Each element represents a single position entry — the initial open or a subsequent
|
|
34397
|
+
* DCA entry added via commitAverageBuy.
|
|
34398
|
+
*
|
|
34399
|
+
* Returns null if no pending signal exists.
|
|
34400
|
+
* Returns a single-element array if no DCA entries were made.
|
|
34401
|
+
*
|
|
34402
|
+
* Each entry contains:
|
|
34403
|
+
* - `price` — execution price of this entry
|
|
34404
|
+
* - `cost` — dollar cost allocated to this entry (e.g. 100 for $100)
|
|
34405
|
+
*
|
|
34406
|
+
* @param symbol - Trading pair symbol
|
|
34407
|
+
* @returns Promise resolving to array of entry records or null
|
|
34408
|
+
*
|
|
34409
|
+
* @example
|
|
34410
|
+
* ```typescript
|
|
34411
|
+
* import { getPositionEntries } from "backtest-kit";
|
|
34412
|
+
*
|
|
34413
|
+
* const entries = await getPositionEntries("BTCUSDT");
|
|
34414
|
+
* // No DCA: [{ price: 43000, cost: 100 }]
|
|
34415
|
+
* // One DCA: [{ price: 43000, cost: 100 }, { price: 42000, cost: 100 }]
|
|
34416
|
+
* ```
|
|
34417
|
+
*/
|
|
34418
|
+
async function getPositionEntries(symbol) {
|
|
34419
|
+
bt.loggerService.info(GET_POSITION_ENTRIES_METHOD_NAME, { symbol });
|
|
34420
|
+
if (!ExecutionContextService.hasContext()) {
|
|
34421
|
+
throw new Error("getPositionEntries requires an execution context");
|
|
34422
|
+
}
|
|
34423
|
+
if (!MethodContextService.hasContext()) {
|
|
34424
|
+
throw new Error("getPositionEntries requires a method context");
|
|
34425
|
+
}
|
|
34426
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
34427
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
34428
|
+
return await bt.strategyCoreService.getPositionEntries(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
34429
|
+
}
|
|
34430
|
+
/**
|
|
34431
|
+
* Returns the original estimated duration for the current pending signal.
|
|
34432
|
+
*
|
|
34433
|
+
* Reflects `minuteEstimatedTime` as set in the signal DTO — the maximum
|
|
34434
|
+
* number of minutes the position is expected to be active before `time_expired`.
|
|
34435
|
+
*
|
|
34436
|
+
* Returns null if no pending signal exists.
|
|
34437
|
+
*
|
|
34438
|
+
* @param symbol - Trading pair symbol
|
|
34439
|
+
* @returns Promise resolving to estimated duration in minutes or null
|
|
34440
|
+
*
|
|
34441
|
+
* @example
|
|
34442
|
+
* ```typescript
|
|
34443
|
+
* import { getPositionEstimateMinutes } from "backtest-kit";
|
|
34444
|
+
*
|
|
34445
|
+
* const estimate = await getPositionEstimateMinutes("BTCUSDT");
|
|
34446
|
+
* // e.g. 120 (2 hours)
|
|
34447
|
+
* ```
|
|
34448
|
+
*/
|
|
34449
|
+
async function getPositionEstimateMinutes(symbol) {
|
|
34450
|
+
bt.loggerService.info(GET_POSITION_ESTIMATE_MINUTES_METHOD_NAME, { symbol });
|
|
34451
|
+
if (!ExecutionContextService.hasContext()) {
|
|
34452
|
+
throw new Error("getPositionEstimateMinutes requires an execution context");
|
|
34453
|
+
}
|
|
34454
|
+
if (!MethodContextService.hasContext()) {
|
|
34455
|
+
throw new Error("getPositionEstimateMinutes requires a method context");
|
|
34456
|
+
}
|
|
34457
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
34458
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
34459
|
+
return await bt.strategyCoreService.getPositionEstimateMinutes(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
34460
|
+
}
|
|
34461
|
+
/**
|
|
34462
|
+
* Returns the remaining time before the position expires, clamped to zero.
|
|
34463
|
+
*
|
|
34464
|
+
* Computes elapsed minutes since `pendingAt` and subtracts from `minuteEstimatedTime`.
|
|
34465
|
+
* Returns 0 once the estimate is exceeded (never negative).
|
|
34466
|
+
*
|
|
34467
|
+
* Returns null if no pending signal exists.
|
|
34468
|
+
*
|
|
34469
|
+
* @param symbol - Trading pair symbol
|
|
34470
|
+
* @returns Promise resolving to remaining minutes (≥ 0) or null
|
|
34471
|
+
*
|
|
34472
|
+
* @example
|
|
34473
|
+
* ```typescript
|
|
34474
|
+
* import { getPositionCountdownMinutes } from "backtest-kit";
|
|
34475
|
+
*
|
|
34476
|
+
* const remaining = await getPositionCountdownMinutes("BTCUSDT");
|
|
34477
|
+
* // e.g. 45 (45 minutes left)
|
|
34478
|
+
* // 0 when expired
|
|
34479
|
+
* ```
|
|
34480
|
+
*/
|
|
34481
|
+
async function getPositionCountdownMinutes(symbol) {
|
|
34482
|
+
bt.loggerService.info(GET_POSITION_COUNTDOWN_MINUTES_METHOD_NAME, { symbol });
|
|
34483
|
+
if (!ExecutionContextService.hasContext()) {
|
|
34484
|
+
throw new Error("getPositionCountdownMinutes requires an execution context");
|
|
34485
|
+
}
|
|
34486
|
+
if (!MethodContextService.hasContext()) {
|
|
34487
|
+
throw new Error("getPositionCountdownMinutes requires a method context");
|
|
34488
|
+
}
|
|
34489
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
34490
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
34491
|
+
return await bt.strategyCoreService.getPositionCountdownMinutes(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
34492
|
+
}
|
|
34493
|
+
/**
|
|
34494
|
+
* Returns the best price reached in the profit direction during this position's life.
|
|
34495
|
+
*
|
|
34496
|
+
* Initialized at position open with the entry price and timestamp.
|
|
34497
|
+
* Updated on every tick/candle when VWAP moves beyond the previous record toward TP:
|
|
34498
|
+
* - LONG: tracks the highest price seen above effective entry
|
|
34499
|
+
* - SHORT: tracks the lowest price seen below effective entry
|
|
34500
|
+
*
|
|
34501
|
+
* Returns null if no pending signal exists.
|
|
34502
|
+
* Never returns null when a signal is active — always contains at least the entry price.
|
|
34503
|
+
*
|
|
34504
|
+
* @param symbol - Trading pair symbol
|
|
34505
|
+
* @returns Promise resolving to `{ price, timestamp }` record or null
|
|
34506
|
+
*
|
|
34507
|
+
* @example
|
|
34508
|
+
* ```typescript
|
|
34509
|
+
* import { getPositionHighestProfitPrice } from "backtest-kit";
|
|
34510
|
+
*
|
|
34511
|
+
* const peak = await getPositionHighestProfitPrice("BTCUSDT");
|
|
34512
|
+
* // e.g. { price: 44500, timestamp: 1700000000000 }
|
|
34513
|
+
* ```
|
|
34514
|
+
*/
|
|
34515
|
+
async function getPositionHighestProfitPrice(symbol) {
|
|
34516
|
+
bt.loggerService.info(GET_POSITION_HIGHEST_PROFIT_PRICE_METHOD_NAME, { symbol });
|
|
34517
|
+
if (!ExecutionContextService.hasContext()) {
|
|
34518
|
+
throw new Error("getPositionHighestProfitPrice requires an execution context");
|
|
34519
|
+
}
|
|
34520
|
+
if (!MethodContextService.hasContext()) {
|
|
34521
|
+
throw new Error("getPositionHighestProfitPrice requires a method context");
|
|
34522
|
+
}
|
|
34523
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
34524
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
34525
|
+
return await bt.strategyCoreService.getPositionHighestProfitPrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
34526
|
+
}
|
|
34527
|
+
/**
|
|
34528
|
+
* Returns the timestamp when the best profit price was recorded during this position's life.
|
|
34529
|
+
*
|
|
34530
|
+
* Returns null if no pending signal exists.
|
|
34531
|
+
*
|
|
34532
|
+
* @param symbol - Trading pair symbol
|
|
34533
|
+
* @returns Promise resolving to timestamp in milliseconds or null
|
|
34534
|
+
*
|
|
34535
|
+
* @example
|
|
34536
|
+
* ```typescript
|
|
34537
|
+
* import { getPositionHighestProfitTimestamp } from "backtest-kit";
|
|
34538
|
+
*
|
|
34539
|
+
* const ts = await getPositionHighestProfitTimestamp("BTCUSDT");
|
|
34540
|
+
* // e.g. 1700000000000
|
|
34541
|
+
* ```
|
|
34542
|
+
*/
|
|
34543
|
+
async function getPositionHighestProfitTimestamp(symbol) {
|
|
34544
|
+
bt.loggerService.info(GET_POSITION_HIGHEST_PROFIT_TIMESTAMP_METHOD_NAME, { symbol });
|
|
34545
|
+
if (!ExecutionContextService.hasContext()) {
|
|
34546
|
+
throw new Error("getPositionHighestProfitTimestamp requires an execution context");
|
|
34547
|
+
}
|
|
34548
|
+
if (!MethodContextService.hasContext()) {
|
|
34549
|
+
throw new Error("getPositionHighestProfitTimestamp requires a method context");
|
|
34550
|
+
}
|
|
34551
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
34552
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
34553
|
+
return await bt.strategyCoreService.getPositionHighestProfitTimestamp(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
34554
|
+
}
|
|
34555
|
+
/**
|
|
34556
|
+
* Returns the PnL percentage at the moment the best profit price was recorded during this position's life.
|
|
34557
|
+
*
|
|
34558
|
+
* Returns null if no pending signal exists.
|
|
34559
|
+
*
|
|
34560
|
+
* @param symbol - Trading pair symbol
|
|
34561
|
+
* @returns Promise resolving to PnL percentage or null
|
|
34562
|
+
*
|
|
34563
|
+
* @example
|
|
34564
|
+
* ```typescript
|
|
34565
|
+
* import { getPositionHighestPnlPercentage } from "backtest-kit";
|
|
34566
|
+
*
|
|
34567
|
+
* const pnl = await getPositionHighestPnlPercentage("BTCUSDT");
|
|
34568
|
+
* // e.g. 3.5
|
|
34569
|
+
* ```
|
|
34570
|
+
*/
|
|
34571
|
+
async function getPositionHighestPnlPercentage(symbol) {
|
|
34572
|
+
bt.loggerService.info(GET_POSITION_HIGHEST_PNL_PERCENTAGE_METHOD_NAME, { symbol });
|
|
34573
|
+
if (!ExecutionContextService.hasContext()) {
|
|
34574
|
+
throw new Error("getPositionHighestPnlPercentage requires an execution context");
|
|
34575
|
+
}
|
|
34576
|
+
if (!MethodContextService.hasContext()) {
|
|
34577
|
+
throw new Error("getPositionHighestPnlPercentage requires a method context");
|
|
34578
|
+
}
|
|
34579
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
34580
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
34581
|
+
return await bt.strategyCoreService.getPositionHighestPnlPercentage(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
34582
|
+
}
|
|
34583
|
+
/**
|
|
34584
|
+
* Returns the PnL cost (in quote currency) at the moment the best profit price was recorded during this position's life.
|
|
34585
|
+
*
|
|
34586
|
+
* Returns null if no pending signal exists.
|
|
34587
|
+
*
|
|
34588
|
+
* @param symbol - Trading pair symbol
|
|
34589
|
+
* @returns Promise resolving to PnL cost or null
|
|
34590
|
+
*
|
|
34591
|
+
* @example
|
|
34592
|
+
* ```typescript
|
|
34593
|
+
* import { getPositionHighestPnlCost } from "backtest-kit";
|
|
34594
|
+
*
|
|
34595
|
+
* const pnlCost = await getPositionHighestPnlCost("BTCUSDT");
|
|
34596
|
+
* // e.g. 35.5
|
|
34597
|
+
* ```
|
|
34598
|
+
*/
|
|
34599
|
+
async function getPositionHighestPnlCost(symbol) {
|
|
34600
|
+
bt.loggerService.info(GET_POSITION_HIGHEST_PNL_COST_METHOD_NAME, { symbol });
|
|
34601
|
+
if (!ExecutionContextService.hasContext()) {
|
|
34602
|
+
throw new Error("getPositionHighestPnlCost requires an execution context");
|
|
34603
|
+
}
|
|
34604
|
+
if (!MethodContextService.hasContext()) {
|
|
34605
|
+
throw new Error("getPositionHighestPnlCost requires a method context");
|
|
34606
|
+
}
|
|
34607
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
34608
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
34609
|
+
return await bt.strategyCoreService.getPositionHighestPnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
34610
|
+
}
|
|
34611
|
+
/**
|
|
34612
|
+
* Returns whether breakeven was mathematically reachable at the highest profit price.
|
|
34613
|
+
*
|
|
34614
|
+
* Returns null if no pending signal exists.
|
|
34615
|
+
*
|
|
34616
|
+
* @param symbol - Trading pair symbol
|
|
34617
|
+
* @returns Promise resolving to true if breakeven was reachable at peak, false otherwise, or null
|
|
34618
|
+
*
|
|
34619
|
+
* @example
|
|
34620
|
+
* ```typescript
|
|
34621
|
+
* import { getPositionHighestProfitBreakeven } from "backtest-kit";
|
|
34622
|
+
*
|
|
34623
|
+
* const couldBreakeven = await getPositionHighestProfitBreakeven("BTCUSDT");
|
|
34624
|
+
* // e.g. true (price reached the breakeven threshold at its peak)
|
|
34625
|
+
* ```
|
|
34626
|
+
*/
|
|
34627
|
+
async function getPositionHighestProfitBreakeven(symbol) {
|
|
34628
|
+
bt.loggerService.info(GET_POSITION_HIGHEST_PROFIT_BREAKEVEN_METHOD_NAME, { symbol });
|
|
34629
|
+
if (!ExecutionContextService.hasContext()) {
|
|
34630
|
+
throw new Error("getPositionHighestProfitBreakeven requires an execution context");
|
|
34631
|
+
}
|
|
34632
|
+
if (!MethodContextService.hasContext()) {
|
|
34633
|
+
throw new Error("getPositionHighestProfitBreakeven requires a method context");
|
|
34634
|
+
}
|
|
34635
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
34636
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
34637
|
+
return await bt.strategyCoreService.getPositionHighestProfitBreakeven(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
34638
|
+
}
|
|
34639
|
+
/**
|
|
34640
|
+
* Returns the number of minutes elapsed since the highest profit price was recorded.
|
|
34641
|
+
*
|
|
34642
|
+
* Measures how long the position has been pulling back from its peak profit level.
|
|
34643
|
+
* Zero when called at the exact moment the peak was set.
|
|
34644
|
+
* Grows continuously as price moves away from the peak without setting a new record.
|
|
34645
|
+
*
|
|
34646
|
+
* Returns null if no pending signal exists.
|
|
34647
|
+
*
|
|
34648
|
+
* @param symbol - Trading pair symbol
|
|
34649
|
+
* @returns Promise resolving to drawdown duration in minutes or null
|
|
34650
|
+
*
|
|
34651
|
+
* @example
|
|
34652
|
+
* ```typescript
|
|
34653
|
+
* import { getPositionDrawdownMinutes } from "backtest-kit";
|
|
34654
|
+
*
|
|
34655
|
+
* const drawdown = await getPositionDrawdownMinutes("BTCUSDT");
|
|
34656
|
+
* // e.g. 30 (30 minutes since the highest profit price)
|
|
34657
|
+
* ```
|
|
34658
|
+
*/
|
|
34659
|
+
async function getPositionDrawdownMinutes(symbol) {
|
|
34660
|
+
bt.loggerService.info(GET_POSITION_DRAWDOWN_MINUTES_METHOD_NAME, { symbol });
|
|
34661
|
+
if (!ExecutionContextService.hasContext()) {
|
|
34662
|
+
throw new Error("getPositionDrawdownMinutes requires an execution context");
|
|
34663
|
+
}
|
|
34664
|
+
if (!MethodContextService.hasContext()) {
|
|
34665
|
+
throw new Error("getPositionDrawdownMinutes requires a method context");
|
|
34666
|
+
}
|
|
34667
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
34668
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
34669
|
+
return await bt.strategyCoreService.getPositionDrawdownMinutes(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
34670
|
+
}
|
|
33474
34671
|
/**
|
|
33475
34672
|
* Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
|
|
33476
34673
|
* Use this to prevent duplicate DCA entries at the same price area.
|
|
@@ -33648,6 +34845,8 @@ const LISTEN_STRATEGY_COMMIT_METHOD_NAME = "event.listenStrategyCommit";
|
|
|
33648
34845
|
const LISTEN_STRATEGY_COMMIT_ONCE_METHOD_NAME = "event.listenStrategyCommitOnce";
|
|
33649
34846
|
const LISTEN_SYNC_METHOD_NAME = "event.listenSync";
|
|
33650
34847
|
const LISTEN_SYNC_ONCE_METHOD_NAME = "event.listenSyncOnce";
|
|
34848
|
+
const LISTEN_HIGHEST_PROFIT_METHOD_NAME = "event.listenHighestProfit";
|
|
34849
|
+
const LISTEN_HIGHEST_PROFIT_ONCE_METHOD_NAME = "event.listenHighestProfitOnce";
|
|
33651
34850
|
/**
|
|
33652
34851
|
* Subscribes to all signal events with queued async processing.
|
|
33653
34852
|
*
|
|
@@ -34774,6 +35973,33 @@ function listenSyncOnce(filterFn, fn) {
|
|
|
34774
35973
|
bt.loggerService.log(LISTEN_SYNC_ONCE_METHOD_NAME);
|
|
34775
35974
|
return syncSubject.filter(filterFn).once(fn);
|
|
34776
35975
|
}
|
|
35976
|
+
/**
|
|
35977
|
+
* Subscribes to highest profit events with queued async processing.
|
|
35978
|
+
* Emits when a signal reaches a new highest profit level during its lifecycle.
|
|
35979
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
35980
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
35981
|
+
* Useful for tracking profit milestones and implementing dynamic management logic.
|
|
35982
|
+
*
|
|
35983
|
+
* @param fn - Callback function to handle highest profit events
|
|
35984
|
+
* @return Unsubscribe function to stop listening to events
|
|
35985
|
+
*/
|
|
35986
|
+
function listenHighestProfit(fn) {
|
|
35987
|
+
bt.loggerService.log(LISTEN_HIGHEST_PROFIT_METHOD_NAME);
|
|
35988
|
+
return highestProfitSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
35989
|
+
}
|
|
35990
|
+
/**
|
|
35991
|
+
* Subscribes to filtered highest profit events with one-time execution.
|
|
35992
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
35993
|
+
* and automatically unsubscribes. Useful for waiting for specific profit conditions.
|
|
35994
|
+
*
|
|
35995
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
35996
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
35997
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
35998
|
+
*/
|
|
35999
|
+
function listenHighestProfitOnce(filterFn, fn) {
|
|
36000
|
+
bt.loggerService.log(LISTEN_HIGHEST_PROFIT_ONCE_METHOD_NAME);
|
|
36001
|
+
return highestProfitSubject.filter(filterFn).once(fn);
|
|
36002
|
+
}
|
|
34777
36003
|
|
|
34778
36004
|
const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
|
|
34779
36005
|
const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
|
|
@@ -34787,7 +36013,7 @@ const BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "BacktestUtils.getTotalPer
|
|
|
34787
36013
|
const BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED = "BacktestUtils.getTotalCostClosed";
|
|
34788
36014
|
const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
|
|
34789
36015
|
const BACKTEST_METHOD_NAME_GET_BREAKEVEN = "BacktestUtils.getBreakeven";
|
|
34790
|
-
const BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "BacktestUtils.
|
|
36016
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "BacktestUtils.getPositionEffectivePrice";
|
|
34791
36017
|
const BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT = "BacktestUtils.getPositionInvestedCount";
|
|
34792
36018
|
const BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST = "BacktestUtils.getPositionInvestedCost";
|
|
34793
36019
|
const BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT = "BacktestUtils.getPositionPnlPercent";
|
|
@@ -34795,6 +36021,14 @@ const BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST = "BacktestUtils.getPositionPnl
|
|
|
34795
36021
|
const BACKTEST_METHOD_NAME_GET_POSITION_LEVELS = "BacktestUtils.getPositionLevels";
|
|
34796
36022
|
const BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS = "BacktestUtils.getPositionPartials";
|
|
34797
36023
|
const BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES = "BacktestUtils.getPositionEntries";
|
|
36024
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES = "BacktestUtils.getPositionEstimateMinutes";
|
|
36025
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES = "BacktestUtils.getPositionCountdownMinutes";
|
|
36026
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE = "BacktestUtils.getPositionHighestProfitPrice";
|
|
36027
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP = "BacktestUtils.getPositionHighestProfitTimestamp";
|
|
36028
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE = "BacktestUtils.getPositionHighestPnlPercentage";
|
|
36029
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST = "BacktestUtils.getPositionHighestPnlCost";
|
|
36030
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN = "BacktestUtils.getPositionHighestProfitBreakeven";
|
|
36031
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES = "BacktestUtils.getPositionDrawdownMinutes";
|
|
34798
36032
|
const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
|
|
34799
36033
|
const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
|
|
34800
36034
|
const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
|
|
@@ -35367,7 +36601,7 @@ class BacktestUtils {
|
|
|
35367
36601
|
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
35368
36602
|
* @returns Effective entry price, or null if no active position
|
|
35369
36603
|
*/
|
|
35370
|
-
this.
|
|
36604
|
+
this.getPositionEffectivePrice = async (symbol, context) => {
|
|
35371
36605
|
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, {
|
|
35372
36606
|
symbol,
|
|
35373
36607
|
context,
|
|
@@ -35383,7 +36617,7 @@ class BacktestUtils {
|
|
|
35383
36617
|
actions &&
|
|
35384
36618
|
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
35385
36619
|
}
|
|
35386
|
-
return await bt.strategyCoreService.
|
|
36620
|
+
return await bt.strategyCoreService.getPositionEffectivePrice(true, symbol, context);
|
|
35387
36621
|
};
|
|
35388
36622
|
/**
|
|
35389
36623
|
* Returns the total number of base-asset units currently held in the position.
|
|
@@ -35601,6 +36835,230 @@ class BacktestUtils {
|
|
|
35601
36835
|
}
|
|
35602
36836
|
return await bt.strategyCoreService.getPositionEntries(true, symbol, context);
|
|
35603
36837
|
};
|
|
36838
|
+
/**
|
|
36839
|
+
* Returns the original estimated duration for the current pending signal.
|
|
36840
|
+
*
|
|
36841
|
+
* Reflects `minuteEstimatedTime` as set in the signal DTO — the maximum
|
|
36842
|
+
* number of minutes the position is expected to be active before `time_expired`.
|
|
36843
|
+
*
|
|
36844
|
+
* Returns null if no pending signal exists.
|
|
36845
|
+
*
|
|
36846
|
+
* @param symbol - Trading pair symbol
|
|
36847
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
36848
|
+
* @returns Estimated duration in minutes, or null if no active position
|
|
36849
|
+
*/
|
|
36850
|
+
this.getPositionEstimateMinutes = async (symbol, context) => {
|
|
36851
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES, {
|
|
36852
|
+
symbol,
|
|
36853
|
+
context,
|
|
36854
|
+
});
|
|
36855
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES);
|
|
36856
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES);
|
|
36857
|
+
{
|
|
36858
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
36859
|
+
riskName &&
|
|
36860
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES);
|
|
36861
|
+
riskList &&
|
|
36862
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES));
|
|
36863
|
+
actions &&
|
|
36864
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES));
|
|
36865
|
+
}
|
|
36866
|
+
return await bt.strategyCoreService.getPositionEstimateMinutes(true, symbol, context);
|
|
36867
|
+
};
|
|
36868
|
+
/**
|
|
36869
|
+
* Returns the remaining time before the position expires, clamped to zero.
|
|
36870
|
+
*
|
|
36871
|
+
* Computes elapsed minutes since `pendingAt` and subtracts from `minuteEstimatedTime`.
|
|
36872
|
+
* Returns 0 once the estimate is exceeded (never negative).
|
|
36873
|
+
*
|
|
36874
|
+
* Returns null if no pending signal exists.
|
|
36875
|
+
*
|
|
36876
|
+
* @param symbol - Trading pair symbol
|
|
36877
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
36878
|
+
* @returns Remaining minutes (≥ 0), or null if no active position
|
|
36879
|
+
*/
|
|
36880
|
+
this.getPositionCountdownMinutes = async (symbol, context) => {
|
|
36881
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES, {
|
|
36882
|
+
symbol,
|
|
36883
|
+
context,
|
|
36884
|
+
});
|
|
36885
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES);
|
|
36886
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES);
|
|
36887
|
+
{
|
|
36888
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
36889
|
+
riskName &&
|
|
36890
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES);
|
|
36891
|
+
riskList &&
|
|
36892
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES));
|
|
36893
|
+
actions &&
|
|
36894
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES));
|
|
36895
|
+
}
|
|
36896
|
+
return await bt.strategyCoreService.getPositionCountdownMinutes(true, symbol, context);
|
|
36897
|
+
};
|
|
36898
|
+
/**
|
|
36899
|
+
* Returns the best price reached in the profit direction during this position's life.
|
|
36900
|
+
*
|
|
36901
|
+
* Returns null if no pending signal exists.
|
|
36902
|
+
*
|
|
36903
|
+
* @param symbol - Trading pair symbol
|
|
36904
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
36905
|
+
* @returns price or null if no active position
|
|
36906
|
+
*/
|
|
36907
|
+
this.getPositionHighestProfitPrice = async (symbol, context) => {
|
|
36908
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE, {
|
|
36909
|
+
symbol,
|
|
36910
|
+
context,
|
|
36911
|
+
});
|
|
36912
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE);
|
|
36913
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE);
|
|
36914
|
+
{
|
|
36915
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
36916
|
+
riskName &&
|
|
36917
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE);
|
|
36918
|
+
riskList &&
|
|
36919
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE));
|
|
36920
|
+
actions &&
|
|
36921
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE));
|
|
36922
|
+
}
|
|
36923
|
+
return await bt.strategyCoreService.getPositionHighestProfitPrice(true, symbol, context);
|
|
36924
|
+
};
|
|
36925
|
+
/**
|
|
36926
|
+
* Returns the timestamp when the best profit price was recorded during this position's life.
|
|
36927
|
+
*
|
|
36928
|
+
* Returns null if no pending signal exists.
|
|
36929
|
+
*
|
|
36930
|
+
* @param symbol - Trading pair symbol
|
|
36931
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
36932
|
+
* @returns timestamp in milliseconds or null if no active position
|
|
36933
|
+
*/
|
|
36934
|
+
this.getPositionHighestProfitTimestamp = async (symbol, context) => {
|
|
36935
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP, {
|
|
36936
|
+
symbol,
|
|
36937
|
+
context,
|
|
36938
|
+
});
|
|
36939
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP);
|
|
36940
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP);
|
|
36941
|
+
{
|
|
36942
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
36943
|
+
riskName &&
|
|
36944
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP);
|
|
36945
|
+
riskList &&
|
|
36946
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP));
|
|
36947
|
+
actions &&
|
|
36948
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP));
|
|
36949
|
+
}
|
|
36950
|
+
return await bt.strategyCoreService.getPositionHighestProfitTimestamp(true, symbol, context);
|
|
36951
|
+
};
|
|
36952
|
+
/**
|
|
36953
|
+
* Returns the PnL percentage at the moment the best profit price was recorded during this position's life.
|
|
36954
|
+
*
|
|
36955
|
+
* Returns null if no pending signal exists.
|
|
36956
|
+
*
|
|
36957
|
+
* @param symbol - Trading pair symbol
|
|
36958
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
36959
|
+
* @returns PnL percentage or null if no active position
|
|
36960
|
+
*/
|
|
36961
|
+
this.getPositionHighestPnlPercentage = async (symbol, context) => {
|
|
36962
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE, {
|
|
36963
|
+
symbol,
|
|
36964
|
+
context,
|
|
36965
|
+
});
|
|
36966
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE);
|
|
36967
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE);
|
|
36968
|
+
{
|
|
36969
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
36970
|
+
riskName &&
|
|
36971
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE);
|
|
36972
|
+
riskList &&
|
|
36973
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE));
|
|
36974
|
+
actions &&
|
|
36975
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE));
|
|
36976
|
+
}
|
|
36977
|
+
return await bt.strategyCoreService.getPositionHighestPnlPercentage(true, symbol, context);
|
|
36978
|
+
};
|
|
36979
|
+
/**
|
|
36980
|
+
* Returns the PnL cost (in quote currency) at the moment the best profit price was recorded during this position's life.
|
|
36981
|
+
*
|
|
36982
|
+
* Returns null if no pending signal exists.
|
|
36983
|
+
*
|
|
36984
|
+
* @param symbol - Trading pair symbol
|
|
36985
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
36986
|
+
* @returns PnL cost or null if no active position
|
|
36987
|
+
*/
|
|
36988
|
+
this.getPositionHighestPnlCost = async (symbol, context) => {
|
|
36989
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST, {
|
|
36990
|
+
symbol,
|
|
36991
|
+
context,
|
|
36992
|
+
});
|
|
36993
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST);
|
|
36994
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST);
|
|
36995
|
+
{
|
|
36996
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
36997
|
+
riskName &&
|
|
36998
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST);
|
|
36999
|
+
riskList &&
|
|
37000
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST));
|
|
37001
|
+
actions &&
|
|
37002
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST));
|
|
37003
|
+
}
|
|
37004
|
+
return await bt.strategyCoreService.getPositionHighestPnlCost(true, symbol, context);
|
|
37005
|
+
};
|
|
37006
|
+
/**
|
|
37007
|
+
* Returns whether breakeven was mathematically reachable at the highest profit price.
|
|
37008
|
+
*
|
|
37009
|
+
* @param symbol - Trading pair symbol
|
|
37010
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
37011
|
+
* @returns true if breakeven was reachable at peak, false otherwise, or null if no active position
|
|
37012
|
+
*/
|
|
37013
|
+
this.getPositionHighestProfitBreakeven = async (symbol, context) => {
|
|
37014
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN, {
|
|
37015
|
+
symbol,
|
|
37016
|
+
context,
|
|
37017
|
+
});
|
|
37018
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN);
|
|
37019
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN);
|
|
37020
|
+
{
|
|
37021
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
37022
|
+
riskName &&
|
|
37023
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN);
|
|
37024
|
+
riskList &&
|
|
37025
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN));
|
|
37026
|
+
actions &&
|
|
37027
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN));
|
|
37028
|
+
}
|
|
37029
|
+
return await bt.strategyCoreService.getPositionHighestProfitBreakeven(true, symbol, context);
|
|
37030
|
+
};
|
|
37031
|
+
/**
|
|
37032
|
+
* Returns the number of minutes elapsed since the highest profit price was recorded.
|
|
37033
|
+
*
|
|
37034
|
+
* Measures how long the position has been pulling back from its peak profit level.
|
|
37035
|
+
* Zero when called at the exact moment the peak was set.
|
|
37036
|
+
* Grows continuously as price moves away from the peak without setting a new record.
|
|
37037
|
+
*
|
|
37038
|
+
* Returns null if no pending signal exists.
|
|
37039
|
+
*
|
|
37040
|
+
* @param symbol - Trading pair symbol
|
|
37041
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
37042
|
+
* @returns Drawdown duration in minutes, or null if no active position
|
|
37043
|
+
*/
|
|
37044
|
+
this.getPositionDrawdownMinutes = async (symbol, context) => {
|
|
37045
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES, {
|
|
37046
|
+
symbol,
|
|
37047
|
+
context,
|
|
37048
|
+
});
|
|
37049
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES);
|
|
37050
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES);
|
|
37051
|
+
{
|
|
37052
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
37053
|
+
riskName &&
|
|
37054
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES);
|
|
37055
|
+
riskList &&
|
|
37056
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES));
|
|
37057
|
+
actions &&
|
|
37058
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES));
|
|
37059
|
+
}
|
|
37060
|
+
return await bt.strategyCoreService.getPositionDrawdownMinutes(true, symbol, context);
|
|
37061
|
+
};
|
|
35604
37062
|
/**
|
|
35605
37063
|
* Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
|
|
35606
37064
|
* Use this to prevent duplicate DCA entries at the same price area.
|
|
@@ -36162,7 +37620,7 @@ class BacktestUtils {
|
|
|
36162
37620
|
if (!signal) {
|
|
36163
37621
|
return false;
|
|
36164
37622
|
}
|
|
36165
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
37623
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(true, symbol, context);
|
|
36166
37624
|
if (effectivePriceOpen === null) {
|
|
36167
37625
|
return false;
|
|
36168
37626
|
}
|
|
@@ -36247,7 +37705,7 @@ class BacktestUtils {
|
|
|
36247
37705
|
if (!signal) {
|
|
36248
37706
|
return false;
|
|
36249
37707
|
}
|
|
36250
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
37708
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(true, symbol, context);
|
|
36251
37709
|
if (effectivePriceOpen === null) {
|
|
36252
37710
|
return false;
|
|
36253
37711
|
}
|
|
@@ -36300,7 +37758,7 @@ class BacktestUtils {
|
|
|
36300
37758
|
if (!signal) {
|
|
36301
37759
|
return false;
|
|
36302
37760
|
}
|
|
36303
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
37761
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(true, symbol, context);
|
|
36304
37762
|
if (effectivePriceOpen === null) {
|
|
36305
37763
|
return false;
|
|
36306
37764
|
}
|
|
@@ -36354,7 +37812,7 @@ class BacktestUtils {
|
|
|
36354
37812
|
if (!signal) {
|
|
36355
37813
|
return false;
|
|
36356
37814
|
}
|
|
36357
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
37815
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(true, symbol, context);
|
|
36358
37816
|
if (effectivePriceOpen === null) {
|
|
36359
37817
|
return false;
|
|
36360
37818
|
}
|
|
@@ -36416,7 +37874,7 @@ class BacktestUtils {
|
|
|
36416
37874
|
if (!signal) {
|
|
36417
37875
|
return false;
|
|
36418
37876
|
}
|
|
36419
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
37877
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(true, symbol, context);
|
|
36420
37878
|
if (effectivePriceOpen === null) {
|
|
36421
37879
|
return false;
|
|
36422
37880
|
}
|
|
@@ -36704,7 +38162,7 @@ const LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "LiveUtils.getTotalPercentClos
|
|
|
36704
38162
|
const LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED = "LiveUtils.getTotalCostClosed";
|
|
36705
38163
|
const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
|
|
36706
38164
|
const LIVE_METHOD_NAME_GET_BREAKEVEN = "LiveUtils.getBreakeven";
|
|
36707
|
-
const LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "LiveUtils.
|
|
38165
|
+
const LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "LiveUtils.getPositionEffectivePrice";
|
|
36708
38166
|
const LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT = "LiveUtils.getPositionInvestedCount";
|
|
36709
38167
|
const LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST = "LiveUtils.getPositionInvestedCost";
|
|
36710
38168
|
const LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT = "LiveUtils.getPositionPnlPercent";
|
|
@@ -36712,6 +38170,14 @@ const LIVE_METHOD_NAME_GET_POSITION_PNL_COST = "LiveUtils.getPositionPnlCost";
|
|
|
36712
38170
|
const LIVE_METHOD_NAME_GET_POSITION_LEVELS = "LiveUtils.getPositionLevels";
|
|
36713
38171
|
const LIVE_METHOD_NAME_GET_POSITION_PARTIALS = "LiveUtils.getPositionPartials";
|
|
36714
38172
|
const LIVE_METHOD_NAME_GET_POSITION_ENTRIES = "LiveUtils.getPositionEntries";
|
|
38173
|
+
const LIVE_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES = "LiveUtils.getPositionEstimateMinutes";
|
|
38174
|
+
const LIVE_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES = "LiveUtils.getPositionCountdownMinutes";
|
|
38175
|
+
const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE = "LiveUtils.getPositionHighestProfitPrice";
|
|
38176
|
+
const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP = "LiveUtils.getPositionHighestProfitTimestamp";
|
|
38177
|
+
const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE = "LiveUtils.getPositionHighestPnlPercentage";
|
|
38178
|
+
const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST = "LiveUtils.getPositionHighestPnlCost";
|
|
38179
|
+
const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN = "LiveUtils.getPositionHighestProfitBreakeven";
|
|
38180
|
+
const LIVE_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES = "LiveUtils.getPositionDrawdownMinutes";
|
|
36715
38181
|
const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
|
|
36716
38182
|
const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
|
|
36717
38183
|
const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
|
|
@@ -37315,7 +38781,7 @@ class LiveUtils {
|
|
|
37315
38781
|
* @param context - Execution context with strategyName and exchangeName
|
|
37316
38782
|
* @returns Effective entry price, or null if no active position
|
|
37317
38783
|
*/
|
|
37318
|
-
this.
|
|
38784
|
+
this.getPositionEffectivePrice = async (symbol, context) => {
|
|
37319
38785
|
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, {
|
|
37320
38786
|
symbol,
|
|
37321
38787
|
context,
|
|
@@ -37331,7 +38797,7 @@ class LiveUtils {
|
|
|
37331
38797
|
actions &&
|
|
37332
38798
|
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
37333
38799
|
}
|
|
37334
|
-
return await bt.strategyCoreService.
|
|
38800
|
+
return await bt.strategyCoreService.getPositionEffectivePrice(false, symbol, {
|
|
37335
38801
|
strategyName: context.strategyName,
|
|
37336
38802
|
exchangeName: context.exchangeName,
|
|
37337
38803
|
frameName: "",
|
|
@@ -37581,6 +39047,262 @@ class LiveUtils {
|
|
|
37581
39047
|
frameName: "",
|
|
37582
39048
|
});
|
|
37583
39049
|
};
|
|
39050
|
+
/**
|
|
39051
|
+
* Returns the original estimated duration for the current pending signal.
|
|
39052
|
+
*
|
|
39053
|
+
* Reflects `minuteEstimatedTime` as set in the signal DTO — the maximum
|
|
39054
|
+
* number of minutes the position is expected to be active before `time_expired`.
|
|
39055
|
+
*
|
|
39056
|
+
* Returns null if no pending signal exists.
|
|
39057
|
+
*
|
|
39058
|
+
* @param symbol - Trading pair symbol
|
|
39059
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
39060
|
+
* @returns Estimated duration in minutes, or null if no active position
|
|
39061
|
+
*/
|
|
39062
|
+
this.getPositionEstimateMinutes = async (symbol, context) => {
|
|
39063
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES, {
|
|
39064
|
+
symbol,
|
|
39065
|
+
context,
|
|
39066
|
+
});
|
|
39067
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES);
|
|
39068
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES);
|
|
39069
|
+
{
|
|
39070
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
39071
|
+
riskName &&
|
|
39072
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES);
|
|
39073
|
+
riskList &&
|
|
39074
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES));
|
|
39075
|
+
actions &&
|
|
39076
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_ESTIMATE_MINUTES));
|
|
39077
|
+
}
|
|
39078
|
+
return await bt.strategyCoreService.getPositionEstimateMinutes(false, symbol, {
|
|
39079
|
+
strategyName: context.strategyName,
|
|
39080
|
+
exchangeName: context.exchangeName,
|
|
39081
|
+
frameName: "",
|
|
39082
|
+
});
|
|
39083
|
+
};
|
|
39084
|
+
/**
|
|
39085
|
+
* Returns the remaining time before the position expires, clamped to zero.
|
|
39086
|
+
*
|
|
39087
|
+
* Computes elapsed minutes since `pendingAt` and subtracts from `minuteEstimatedTime`.
|
|
39088
|
+
* Returns 0 once the estimate is exceeded (never negative).
|
|
39089
|
+
*
|
|
39090
|
+
* Returns null if no pending signal exists.
|
|
39091
|
+
*
|
|
39092
|
+
* @param symbol - Trading pair symbol
|
|
39093
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
39094
|
+
* @returns Remaining minutes (≥ 0), or null if no active position
|
|
39095
|
+
*/
|
|
39096
|
+
this.getPositionCountdownMinutes = async (symbol, context) => {
|
|
39097
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES, {
|
|
39098
|
+
symbol,
|
|
39099
|
+
context,
|
|
39100
|
+
});
|
|
39101
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES);
|
|
39102
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES);
|
|
39103
|
+
{
|
|
39104
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
39105
|
+
riskName &&
|
|
39106
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES);
|
|
39107
|
+
riskList &&
|
|
39108
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES));
|
|
39109
|
+
actions &&
|
|
39110
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_COUNTDOWN_MINUTES));
|
|
39111
|
+
}
|
|
39112
|
+
return await bt.strategyCoreService.getPositionCountdownMinutes(false, symbol, {
|
|
39113
|
+
strategyName: context.strategyName,
|
|
39114
|
+
exchangeName: context.exchangeName,
|
|
39115
|
+
frameName: "",
|
|
39116
|
+
});
|
|
39117
|
+
};
|
|
39118
|
+
/**
|
|
39119
|
+
* Returns the best price reached in the profit direction during this position's life.
|
|
39120
|
+
*
|
|
39121
|
+
* Returns null if no pending signal exists.
|
|
39122
|
+
*
|
|
39123
|
+
* @param symbol - Trading pair symbol
|
|
39124
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
39125
|
+
* @returns price or null if no active position
|
|
39126
|
+
*/
|
|
39127
|
+
this.getPositionHighestProfitPrice = async (symbol, context) => {
|
|
39128
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE, {
|
|
39129
|
+
symbol,
|
|
39130
|
+
context,
|
|
39131
|
+
});
|
|
39132
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE);
|
|
39133
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE);
|
|
39134
|
+
{
|
|
39135
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
39136
|
+
riskName &&
|
|
39137
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE);
|
|
39138
|
+
riskList &&
|
|
39139
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE));
|
|
39140
|
+
actions &&
|
|
39141
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_PRICE));
|
|
39142
|
+
}
|
|
39143
|
+
return await bt.strategyCoreService.getPositionHighestProfitPrice(false, symbol, {
|
|
39144
|
+
strategyName: context.strategyName,
|
|
39145
|
+
exchangeName: context.exchangeName,
|
|
39146
|
+
frameName: "",
|
|
39147
|
+
});
|
|
39148
|
+
};
|
|
39149
|
+
/**
|
|
39150
|
+
* Returns the timestamp when the best profit price was recorded during this position's life.
|
|
39151
|
+
*
|
|
39152
|
+
* Returns null if no pending signal exists.
|
|
39153
|
+
*
|
|
39154
|
+
* @param symbol - Trading pair symbol
|
|
39155
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
39156
|
+
* @returns timestamp in milliseconds or null if no active position
|
|
39157
|
+
*/
|
|
39158
|
+
this.getPositionHighestProfitTimestamp = async (symbol, context) => {
|
|
39159
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP, {
|
|
39160
|
+
symbol,
|
|
39161
|
+
context,
|
|
39162
|
+
});
|
|
39163
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP);
|
|
39164
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP);
|
|
39165
|
+
{
|
|
39166
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
39167
|
+
riskName &&
|
|
39168
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP);
|
|
39169
|
+
riskList &&
|
|
39170
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP));
|
|
39171
|
+
actions &&
|
|
39172
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_TIMESTAMP));
|
|
39173
|
+
}
|
|
39174
|
+
return await bt.strategyCoreService.getPositionHighestProfitTimestamp(false, symbol, {
|
|
39175
|
+
strategyName: context.strategyName,
|
|
39176
|
+
exchangeName: context.exchangeName,
|
|
39177
|
+
frameName: "",
|
|
39178
|
+
});
|
|
39179
|
+
};
|
|
39180
|
+
/**
|
|
39181
|
+
* Returns the PnL percentage at the moment the best profit price was recorded during this position's life.
|
|
39182
|
+
*
|
|
39183
|
+
* Returns null if no pending signal exists.
|
|
39184
|
+
*
|
|
39185
|
+
* @param symbol - Trading pair symbol
|
|
39186
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
39187
|
+
* @returns PnL percentage or null if no active position
|
|
39188
|
+
*/
|
|
39189
|
+
this.getPositionHighestPnlPercentage = async (symbol, context) => {
|
|
39190
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE, {
|
|
39191
|
+
symbol,
|
|
39192
|
+
context,
|
|
39193
|
+
});
|
|
39194
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE);
|
|
39195
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE);
|
|
39196
|
+
{
|
|
39197
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
39198
|
+
riskName &&
|
|
39199
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE);
|
|
39200
|
+
riskList &&
|
|
39201
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE));
|
|
39202
|
+
actions &&
|
|
39203
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_PERCENTAGE));
|
|
39204
|
+
}
|
|
39205
|
+
return await bt.strategyCoreService.getPositionHighestPnlPercentage(false, symbol, {
|
|
39206
|
+
strategyName: context.strategyName,
|
|
39207
|
+
exchangeName: context.exchangeName,
|
|
39208
|
+
frameName: "",
|
|
39209
|
+
});
|
|
39210
|
+
};
|
|
39211
|
+
/**
|
|
39212
|
+
* Returns the PnL cost (in quote currency) at the moment the best profit price was recorded during this position's life.
|
|
39213
|
+
*
|
|
39214
|
+
* Returns null if no pending signal exists.
|
|
39215
|
+
*
|
|
39216
|
+
* @param symbol - Trading pair symbol
|
|
39217
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
39218
|
+
* @returns PnL cost or null if no active position
|
|
39219
|
+
*/
|
|
39220
|
+
this.getPositionHighestPnlCost = async (symbol, context) => {
|
|
39221
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST, {
|
|
39222
|
+
symbol,
|
|
39223
|
+
context,
|
|
39224
|
+
});
|
|
39225
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST);
|
|
39226
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST);
|
|
39227
|
+
{
|
|
39228
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
39229
|
+
riskName &&
|
|
39230
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST);
|
|
39231
|
+
riskList &&
|
|
39232
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST));
|
|
39233
|
+
actions &&
|
|
39234
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PNL_COST));
|
|
39235
|
+
}
|
|
39236
|
+
return await bt.strategyCoreService.getPositionHighestPnlCost(false, symbol, {
|
|
39237
|
+
strategyName: context.strategyName,
|
|
39238
|
+
exchangeName: context.exchangeName,
|
|
39239
|
+
frameName: "",
|
|
39240
|
+
});
|
|
39241
|
+
};
|
|
39242
|
+
/**
|
|
39243
|
+
* Returns whether breakeven was mathematically reachable at the highest profit price.
|
|
39244
|
+
*
|
|
39245
|
+
* @param symbol - Trading pair symbol
|
|
39246
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
39247
|
+
* @returns true if breakeven was reachable at peak, false otherwise, or null if no active position
|
|
39248
|
+
*/
|
|
39249
|
+
this.getPositionHighestProfitBreakeven = async (symbol, context) => {
|
|
39250
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN, {
|
|
39251
|
+
symbol,
|
|
39252
|
+
context,
|
|
39253
|
+
});
|
|
39254
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN);
|
|
39255
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN);
|
|
39256
|
+
{
|
|
39257
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
39258
|
+
riskName &&
|
|
39259
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN);
|
|
39260
|
+
riskList &&
|
|
39261
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN));
|
|
39262
|
+
actions &&
|
|
39263
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_BREAKEVEN));
|
|
39264
|
+
}
|
|
39265
|
+
return await bt.strategyCoreService.getPositionHighestProfitBreakeven(false, symbol, {
|
|
39266
|
+
strategyName: context.strategyName,
|
|
39267
|
+
exchangeName: context.exchangeName,
|
|
39268
|
+
frameName: "",
|
|
39269
|
+
});
|
|
39270
|
+
};
|
|
39271
|
+
/**
|
|
39272
|
+
* Returns the number of minutes elapsed since the highest profit price was recorded.
|
|
39273
|
+
*
|
|
39274
|
+
* Measures how long the position has been pulling back from its peak profit level.
|
|
39275
|
+
* Zero when called at the exact moment the peak was set.
|
|
39276
|
+
* Grows continuously as price moves away from the peak without setting a new record.
|
|
39277
|
+
*
|
|
39278
|
+
* Returns null if no pending signal exists.
|
|
39279
|
+
*
|
|
39280
|
+
* @param symbol - Trading pair symbol
|
|
39281
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
39282
|
+
* @returns Drawdown duration in minutes, or null if no active position
|
|
39283
|
+
*/
|
|
39284
|
+
this.getPositionDrawdownMinutes = async (symbol, context) => {
|
|
39285
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES, {
|
|
39286
|
+
symbol,
|
|
39287
|
+
context,
|
|
39288
|
+
});
|
|
39289
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES);
|
|
39290
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES);
|
|
39291
|
+
{
|
|
39292
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
39293
|
+
riskName &&
|
|
39294
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES);
|
|
39295
|
+
riskList &&
|
|
39296
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES));
|
|
39297
|
+
actions &&
|
|
39298
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_DRAWDOWN_MINUTES));
|
|
39299
|
+
}
|
|
39300
|
+
return await bt.strategyCoreService.getPositionDrawdownMinutes(false, symbol, {
|
|
39301
|
+
strategyName: context.strategyName,
|
|
39302
|
+
exchangeName: context.exchangeName,
|
|
39303
|
+
frameName: "",
|
|
39304
|
+
});
|
|
39305
|
+
};
|
|
37584
39306
|
/**
|
|
37585
39307
|
* Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
|
|
37586
39308
|
* Use this to prevent duplicate DCA entries at the same price area.
|
|
@@ -38235,7 +39957,7 @@ class LiveUtils {
|
|
|
38235
39957
|
if (!signal) {
|
|
38236
39958
|
return false;
|
|
38237
39959
|
}
|
|
38238
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
39960
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(false, symbol, {
|
|
38239
39961
|
strategyName: context.strategyName,
|
|
38240
39962
|
exchangeName: context.exchangeName,
|
|
38241
39963
|
frameName: "",
|
|
@@ -38335,7 +40057,7 @@ class LiveUtils {
|
|
|
38335
40057
|
if (!signal) {
|
|
38336
40058
|
return false;
|
|
38337
40059
|
}
|
|
38338
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
40060
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(false, symbol, {
|
|
38339
40061
|
strategyName: context.strategyName,
|
|
38340
40062
|
exchangeName: context.exchangeName,
|
|
38341
40063
|
frameName: "",
|
|
@@ -38404,7 +40126,7 @@ class LiveUtils {
|
|
|
38404
40126
|
if (!signal) {
|
|
38405
40127
|
return false;
|
|
38406
40128
|
}
|
|
38407
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
40129
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(false, symbol, {
|
|
38408
40130
|
strategyName: context.strategyName,
|
|
38409
40131
|
exchangeName: context.exchangeName,
|
|
38410
40132
|
frameName: "",
|
|
@@ -38474,7 +40196,7 @@ class LiveUtils {
|
|
|
38474
40196
|
if (!signal) {
|
|
38475
40197
|
return false;
|
|
38476
40198
|
}
|
|
38477
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
40199
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(false, symbol, {
|
|
38478
40200
|
strategyName: context.strategyName,
|
|
38479
40201
|
exchangeName: context.exchangeName,
|
|
38480
40202
|
frameName: "",
|
|
@@ -38552,7 +40274,7 @@ class LiveUtils {
|
|
|
38552
40274
|
if (!signal) {
|
|
38553
40275
|
return false;
|
|
38554
40276
|
}
|
|
38555
|
-
const effectivePriceOpen = await bt.strategyCoreService.
|
|
40277
|
+
const effectivePriceOpen = await bt.strategyCoreService.getPositionEffectivePrice(false, symbol, {
|
|
38556
40278
|
strategyName: context.strategyName,
|
|
38557
40279
|
exchangeName: context.exchangeName,
|
|
38558
40280
|
frameName: "",
|
|
@@ -41976,6 +43698,98 @@ class PartialUtils {
|
|
|
41976
43698
|
*/
|
|
41977
43699
|
const Partial = new PartialUtils();
|
|
41978
43700
|
|
|
43701
|
+
const HIGHEST_PROFIT_METHOD_NAME_GET_DATA = "HighestProfitUtils.getData";
|
|
43702
|
+
const HIGHEST_PROFIT_METHOD_NAME_GET_REPORT = "HighestProfitUtils.getReport";
|
|
43703
|
+
const HIGHEST_PROFIT_METHOD_NAME_DUMP = "HighestProfitUtils.dump";
|
|
43704
|
+
/**
|
|
43705
|
+
* Utility class for accessing highest profit reports and statistics.
|
|
43706
|
+
*
|
|
43707
|
+
* Provides static-like methods (via singleton instance) to retrieve data
|
|
43708
|
+
* accumulated by HighestProfitMarkdownService from highestProfitSubject events.
|
|
43709
|
+
*
|
|
43710
|
+
* @example
|
|
43711
|
+
* ```typescript
|
|
43712
|
+
* import { HighestProfit } from "backtest-kit";
|
|
43713
|
+
*
|
|
43714
|
+
* const stats = await HighestProfit.getData("BTCUSDT", { strategyName, exchangeName, frameName });
|
|
43715
|
+
* const report = await HighestProfit.getReport("BTCUSDT", { strategyName, exchangeName, frameName });
|
|
43716
|
+
* await HighestProfit.dump("BTCUSDT", { strategyName, exchangeName, frameName });
|
|
43717
|
+
* ```
|
|
43718
|
+
*/
|
|
43719
|
+
class HighestProfitUtils {
|
|
43720
|
+
constructor() {
|
|
43721
|
+
/**
|
|
43722
|
+
* Retrieves statistical data from accumulated highest profit events.
|
|
43723
|
+
*
|
|
43724
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
43725
|
+
* @param context - Execution context
|
|
43726
|
+
* @param backtest - Whether to query backtest data
|
|
43727
|
+
* @returns Promise resolving to HighestProfitStatisticsModel
|
|
43728
|
+
*/
|
|
43729
|
+
this.getData = async (symbol, context, backtest = false) => {
|
|
43730
|
+
bt.loggerService.info(HIGHEST_PROFIT_METHOD_NAME_GET_DATA, { symbol, strategyName: context.strategyName });
|
|
43731
|
+
bt.strategyValidationService.validate(context.strategyName, HIGHEST_PROFIT_METHOD_NAME_GET_DATA);
|
|
43732
|
+
bt.exchangeValidationService.validate(context.exchangeName, HIGHEST_PROFIT_METHOD_NAME_GET_DATA);
|
|
43733
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, HIGHEST_PROFIT_METHOD_NAME_GET_DATA);
|
|
43734
|
+
{
|
|
43735
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
43736
|
+
riskName && bt.riskValidationService.validate(riskName, HIGHEST_PROFIT_METHOD_NAME_GET_DATA);
|
|
43737
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, HIGHEST_PROFIT_METHOD_NAME_GET_DATA));
|
|
43738
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, HIGHEST_PROFIT_METHOD_NAME_GET_DATA));
|
|
43739
|
+
}
|
|
43740
|
+
return await bt.highestProfitMarkdownService.getData(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
43741
|
+
};
|
|
43742
|
+
/**
|
|
43743
|
+
* Generates a markdown report with all highest profit events for a symbol-strategy pair.
|
|
43744
|
+
*
|
|
43745
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
43746
|
+
* @param context - Execution context
|
|
43747
|
+
* @param backtest - Whether to query backtest data
|
|
43748
|
+
* @param columns - Optional column configuration
|
|
43749
|
+
* @returns Promise resolving to markdown formatted report string
|
|
43750
|
+
*/
|
|
43751
|
+
this.getReport = async (symbol, context, backtest = false, columns) => {
|
|
43752
|
+
bt.loggerService.info(HIGHEST_PROFIT_METHOD_NAME_GET_REPORT, { symbol, strategyName: context.strategyName });
|
|
43753
|
+
bt.strategyValidationService.validate(context.strategyName, HIGHEST_PROFIT_METHOD_NAME_GET_REPORT);
|
|
43754
|
+
bt.exchangeValidationService.validate(context.exchangeName, HIGHEST_PROFIT_METHOD_NAME_GET_REPORT);
|
|
43755
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, HIGHEST_PROFIT_METHOD_NAME_GET_REPORT);
|
|
43756
|
+
{
|
|
43757
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
43758
|
+
riskName && bt.riskValidationService.validate(riskName, HIGHEST_PROFIT_METHOD_NAME_GET_REPORT);
|
|
43759
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, HIGHEST_PROFIT_METHOD_NAME_GET_REPORT));
|
|
43760
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, HIGHEST_PROFIT_METHOD_NAME_GET_REPORT));
|
|
43761
|
+
}
|
|
43762
|
+
return await bt.highestProfitMarkdownService.getReport(symbol, context.strategyName, context.exchangeName, context.frameName, backtest, columns);
|
|
43763
|
+
};
|
|
43764
|
+
/**
|
|
43765
|
+
* Generates and saves a markdown report to file.
|
|
43766
|
+
*
|
|
43767
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
43768
|
+
* @param context - Execution context
|
|
43769
|
+
* @param backtest - Whether to query backtest data
|
|
43770
|
+
* @param path - Output directory path (default: "./dump/highest_profit")
|
|
43771
|
+
* @param columns - Optional column configuration
|
|
43772
|
+
*/
|
|
43773
|
+
this.dump = async (symbol, context, backtest = false, path, columns) => {
|
|
43774
|
+
bt.loggerService.info(HIGHEST_PROFIT_METHOD_NAME_DUMP, { symbol, strategyName: context.strategyName, path });
|
|
43775
|
+
bt.strategyValidationService.validate(context.strategyName, HIGHEST_PROFIT_METHOD_NAME_DUMP);
|
|
43776
|
+
bt.exchangeValidationService.validate(context.exchangeName, HIGHEST_PROFIT_METHOD_NAME_DUMP);
|
|
43777
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, HIGHEST_PROFIT_METHOD_NAME_DUMP);
|
|
43778
|
+
{
|
|
43779
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
43780
|
+
riskName && bt.riskValidationService.validate(riskName, HIGHEST_PROFIT_METHOD_NAME_DUMP);
|
|
43781
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, HIGHEST_PROFIT_METHOD_NAME_DUMP));
|
|
43782
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, HIGHEST_PROFIT_METHOD_NAME_DUMP));
|
|
43783
|
+
}
|
|
43784
|
+
await bt.highestProfitMarkdownService.dump(symbol, context.strategyName, context.exchangeName, context.frameName, backtest, path, columns);
|
|
43785
|
+
};
|
|
43786
|
+
}
|
|
43787
|
+
}
|
|
43788
|
+
/**
|
|
43789
|
+
* Global singleton instance of HighestProfitUtils.
|
|
43790
|
+
*/
|
|
43791
|
+
const HighestProfit = new HighestProfitUtils();
|
|
43792
|
+
|
|
41979
43793
|
/**
|
|
41980
43794
|
* Utility class containing predefined trading constants for take-profit and stop-loss levels.
|
|
41981
43795
|
*
|
|
@@ -46453,6 +48267,7 @@ exports.Constant = Constant;
|
|
|
46453
48267
|
exports.Exchange = Exchange;
|
|
46454
48268
|
exports.ExecutionContextService = ExecutionContextService;
|
|
46455
48269
|
exports.Heat = Heat;
|
|
48270
|
+
exports.HighestProfit = HighestProfit;
|
|
46456
48271
|
exports.Live = Live;
|
|
46457
48272
|
exports.Log = Log;
|
|
46458
48273
|
exports.Markdown = Markdown;
|
|
@@ -46532,8 +48347,17 @@ exports.getMode = getMode;
|
|
|
46532
48347
|
exports.getNextCandles = getNextCandles;
|
|
46533
48348
|
exports.getOrderBook = getOrderBook;
|
|
46534
48349
|
exports.getPendingSignal = getPendingSignal;
|
|
46535
|
-
exports.
|
|
48350
|
+
exports.getPositionCountdownMinutes = getPositionCountdownMinutes;
|
|
48351
|
+
exports.getPositionDrawdownMinutes = getPositionDrawdownMinutes;
|
|
48352
|
+
exports.getPositionEffectivePrice = getPositionEffectivePrice;
|
|
48353
|
+
exports.getPositionEntries = getPositionEntries;
|
|
46536
48354
|
exports.getPositionEntryOverlap = getPositionEntryOverlap;
|
|
48355
|
+
exports.getPositionEstimateMinutes = getPositionEstimateMinutes;
|
|
48356
|
+
exports.getPositionHighestPnlCost = getPositionHighestPnlCost;
|
|
48357
|
+
exports.getPositionHighestPnlPercentage = getPositionHighestPnlPercentage;
|
|
48358
|
+
exports.getPositionHighestProfitBreakeven = getPositionHighestProfitBreakeven;
|
|
48359
|
+
exports.getPositionHighestProfitPrice = getPositionHighestProfitPrice;
|
|
48360
|
+
exports.getPositionHighestProfitTimestamp = getPositionHighestProfitTimestamp;
|
|
46537
48361
|
exports.getPositionInvestedCost = getPositionInvestedCost;
|
|
46538
48362
|
exports.getPositionInvestedCount = getPositionInvestedCount;
|
|
46539
48363
|
exports.getPositionLevels = getPositionLevels;
|
|
@@ -46574,6 +48398,8 @@ exports.listenDoneWalker = listenDoneWalker;
|
|
|
46574
48398
|
exports.listenDoneWalkerOnce = listenDoneWalkerOnce;
|
|
46575
48399
|
exports.listenError = listenError;
|
|
46576
48400
|
exports.listenExit = listenExit;
|
|
48401
|
+
exports.listenHighestProfit = listenHighestProfit;
|
|
48402
|
+
exports.listenHighestProfitOnce = listenHighestProfitOnce;
|
|
46577
48403
|
exports.listenPartialLossAvailable = listenPartialLossAvailable;
|
|
46578
48404
|
exports.listenPartialLossAvailableOnce = listenPartialLossAvailableOnce;
|
|
46579
48405
|
exports.listenPartialProfitAvailable = listenPartialProfitAvailable;
|