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