backtest-kit 6.10.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 +212 -103
- package/build/index.mjs +212 -103
- package/package.json +1 -1
- package/types.d.ts +117 -32
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);
|
|
@@ -8138,6 +8103,7 @@ class ClientStrategy {
|
|
|
8138
8103
|
totalPartials: cancelledSignal._partial?.length ?? 0,
|
|
8139
8104
|
originalPriceOpen: cancelledSignal.priceOpen,
|
|
8140
8105
|
pnl: toProfitLossDto(cancelledSignal, currentPrice),
|
|
8106
|
+
note: cancelledSignal.note,
|
|
8141
8107
|
});
|
|
8142
8108
|
// Call onCancel callback
|
|
8143
8109
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
@@ -8191,6 +8157,7 @@ class ClientStrategy {
|
|
|
8191
8157
|
totalPartials: closedSignal._partial?.length ?? 0,
|
|
8192
8158
|
originalPriceOpen: closedSignal.priceOpen,
|
|
8193
8159
|
pnl: toProfitLossDto(closedSignal, currentPrice),
|
|
8160
|
+
note: closedSignal.note,
|
|
8194
8161
|
});
|
|
8195
8162
|
// Call onClose callback
|
|
8196
8163
|
await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
@@ -8272,6 +8239,7 @@ class ClientStrategy {
|
|
|
8272
8239
|
totalPartials: activatedSignal._partial?.length ?? 0,
|
|
8273
8240
|
originalPriceOpen: activatedSignal.priceOpen,
|
|
8274
8241
|
pnl: toProfitLossDto(activatedSignal, currentPrice),
|
|
8242
|
+
note: activatedSignal.note,
|
|
8275
8243
|
});
|
|
8276
8244
|
return await RETURN_IDLE_FN(this, currentPrice);
|
|
8277
8245
|
}
|
|
@@ -8302,6 +8270,7 @@ class ClientStrategy {
|
|
|
8302
8270
|
pendingAt: publicSignalForCommit.pendingAt,
|
|
8303
8271
|
totalEntries: publicSignalForCommit.totalEntries,
|
|
8304
8272
|
totalPartials: publicSignalForCommit.totalPartials,
|
|
8273
|
+
note: publicSignalForCommit.note,
|
|
8305
8274
|
});
|
|
8306
8275
|
// Call onOpen callback
|
|
8307
8276
|
await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
@@ -8437,6 +8406,7 @@ class ClientStrategy {
|
|
|
8437
8406
|
totalPartials: cancelledSignal._partial?.length ?? 0,
|
|
8438
8407
|
originalPriceOpen: cancelledSignal.priceOpen,
|
|
8439
8408
|
pnl: toProfitLossDto(cancelledSignal, currentPrice),
|
|
8409
|
+
note: cancelledSignal.note,
|
|
8440
8410
|
});
|
|
8441
8411
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
8442
8412
|
const cancelledResult = {
|
|
@@ -8491,6 +8461,7 @@ class ClientStrategy {
|
|
|
8491
8461
|
totalPartials: closedSignal._partial?.length ?? 0,
|
|
8492
8462
|
originalPriceOpen: closedSignal.priceOpen,
|
|
8493
8463
|
pnl: toProfitLossDto(closedSignal, currentPrice),
|
|
8464
|
+
note: closedSignal.note,
|
|
8494
8465
|
});
|
|
8495
8466
|
await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
8496
8467
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
@@ -10040,6 +10011,8 @@ class MergeRisk {
|
|
|
10040
10011
|
}
|
|
10041
10012
|
}
|
|
10042
10013
|
|
|
10014
|
+
/** Default interval for strategies that do not specify one */
|
|
10015
|
+
const STRATEGY_DEFAULT_INTERVAL = "1m";
|
|
10043
10016
|
/**
|
|
10044
10017
|
* If syncSubject listener or any registered action throws, it means the signal was not properly synchronized
|
|
10045
10018
|
* to the exchange (e.g. limit order failed to fill).
|
|
@@ -10425,7 +10398,7 @@ class StrategyConnectionService {
|
|
|
10425
10398
|
* @returns Configured ClientStrategy instance
|
|
10426
10399
|
*/
|
|
10427
10400
|
this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$t(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
10428
|
-
const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
10401
|
+
const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
10429
10402
|
return new ClientStrategy({
|
|
10430
10403
|
symbol,
|
|
10431
10404
|
interval,
|
|
@@ -16144,8 +16117,8 @@ class StrategySchemaService {
|
|
|
16144
16117
|
if (strategySchema.actions?.some((value) => typeof value !== "string")) {
|
|
16145
16118
|
throw new Error(`strategy schema validation failed: invalid actions for strategyName=${strategySchema.strategyName} actions=[${strategySchema.actions}]`);
|
|
16146
16119
|
}
|
|
16147
|
-
if (typeof strategySchema.interval !== "string") {
|
|
16148
|
-
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}`);
|
|
16149
16122
|
}
|
|
16150
16123
|
if (typeof strategySchema.getSignal !== "function") {
|
|
16151
16124
|
throw new Error(`strategy schema validation failed: missing getSignal for strategyName=${strategySchema.strategyName}`);
|
|
@@ -18091,6 +18064,53 @@ class WalkerCommandService {
|
|
|
18091
18064
|
}
|
|
18092
18065
|
}
|
|
18093
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
|
+
|
|
18094
18114
|
/**
|
|
18095
18115
|
* Column configuration for backtest markdown reports.
|
|
18096
18116
|
*
|
|
@@ -18766,7 +18786,7 @@ const partial_columns = [
|
|
|
18766
18786
|
{
|
|
18767
18787
|
key: "note",
|
|
18768
18788
|
label: "Note",
|
|
18769
|
-
format: (data) => data.note
|
|
18789
|
+
format: (data) => toPlainString(data.note ?? "N/A"),
|
|
18770
18790
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
18771
18791
|
},
|
|
18772
18792
|
{
|
|
@@ -18926,7 +18946,7 @@ const breakeven_columns = [
|
|
|
18926
18946
|
{
|
|
18927
18947
|
key: "note",
|
|
18928
18948
|
label: "Note",
|
|
18929
|
-
format: (data) => data.note
|
|
18949
|
+
format: (data) => toPlainString(data.note ?? "N/A"),
|
|
18930
18950
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
18931
18951
|
},
|
|
18932
18952
|
{
|
|
@@ -51665,6 +51685,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
|
|
|
51665
51685
|
pnlEntries: data.signal.pnl.pnlEntries,
|
|
51666
51686
|
scheduledAt: data.signal.scheduledAt,
|
|
51667
51687
|
currentPrice: data.currentPrice,
|
|
51688
|
+
note: data.signal.note,
|
|
51668
51689
|
createdAt: data.createdAt,
|
|
51669
51690
|
};
|
|
51670
51691
|
}
|
|
@@ -51694,6 +51715,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
|
|
|
51694
51715
|
duration: durationMin,
|
|
51695
51716
|
scheduledAt: data.signal.scheduledAt,
|
|
51696
51717
|
pendingAt: data.signal.pendingAt,
|
|
51718
|
+
note: data.signal.note,
|
|
51697
51719
|
createdAt: data.createdAt,
|
|
51698
51720
|
};
|
|
51699
51721
|
}
|
|
@@ -51730,6 +51752,7 @@ const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
|
|
|
51730
51752
|
pnlPriceClose: data.data.pnl.priceClose,
|
|
51731
51753
|
pnlCost: data.data.pnl.pnlCost,
|
|
51732
51754
|
pnlEntries: data.data.pnl.pnlEntries,
|
|
51755
|
+
note: data.data.note,
|
|
51733
51756
|
scheduledAt: data.data.scheduledAt,
|
|
51734
51757
|
pendingAt: data.data.pendingAt,
|
|
51735
51758
|
createdAt: data.timestamp,
|
|
@@ -51765,6 +51788,7 @@ const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
|
|
|
51765
51788
|
pnlPriceClose: data.data.pnl.priceClose,
|
|
51766
51789
|
pnlCost: data.data.pnl.pnlCost,
|
|
51767
51790
|
pnlEntries: data.data.pnl.pnlEntries,
|
|
51791
|
+
note: data.data.note,
|
|
51768
51792
|
scheduledAt: data.data.scheduledAt,
|
|
51769
51793
|
pendingAt: data.data.pendingAt,
|
|
51770
51794
|
createdAt: data.timestamp,
|
|
@@ -51799,6 +51823,7 @@ const CREATE_BREAKEVEN_NOTIFICATION_FN = (data) => ({
|
|
|
51799
51823
|
pnlPriceClose: data.data.pnl.priceClose,
|
|
51800
51824
|
pnlCost: data.data.pnl.pnlCost,
|
|
51801
51825
|
pnlEntries: data.data.pnl.pnlEntries,
|
|
51826
|
+
note: data.data.note,
|
|
51802
51827
|
scheduledAt: data.data.scheduledAt,
|
|
51803
51828
|
pendingAt: data.data.pendingAt,
|
|
51804
51829
|
createdAt: data.timestamp,
|
|
@@ -51840,6 +51865,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51840
51865
|
pnlEntries: data.pnl.pnlEntries,
|
|
51841
51866
|
scheduledAt: data.scheduledAt,
|
|
51842
51867
|
pendingAt: data.pendingAt,
|
|
51868
|
+
note: data.note,
|
|
51843
51869
|
createdAt: data.timestamp,
|
|
51844
51870
|
};
|
|
51845
51871
|
}
|
|
@@ -51872,6 +51898,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51872
51898
|
pnlEntries: data.pnl.pnlEntries,
|
|
51873
51899
|
scheduledAt: data.scheduledAt,
|
|
51874
51900
|
pendingAt: data.pendingAt,
|
|
51901
|
+
note: data.note,
|
|
51875
51902
|
createdAt: data.timestamp,
|
|
51876
51903
|
};
|
|
51877
51904
|
}
|
|
@@ -51903,6 +51930,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51903
51930
|
pnlEntries: data.pnl.pnlEntries,
|
|
51904
51931
|
scheduledAt: data.scheduledAt,
|
|
51905
51932
|
pendingAt: data.pendingAt,
|
|
51933
|
+
note: data.note,
|
|
51906
51934
|
createdAt: data.timestamp,
|
|
51907
51935
|
};
|
|
51908
51936
|
}
|
|
@@ -51935,6 +51963,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51935
51963
|
pnlEntries: data.pnl.pnlEntries,
|
|
51936
51964
|
scheduledAt: data.scheduledAt,
|
|
51937
51965
|
pendingAt: data.pendingAt,
|
|
51966
|
+
note: data.note,
|
|
51938
51967
|
createdAt: data.timestamp,
|
|
51939
51968
|
};
|
|
51940
51969
|
}
|
|
@@ -51967,6 +51996,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51967
51996
|
pnlEntries: data.pnl.pnlEntries,
|
|
51968
51997
|
scheduledAt: data.scheduledAt,
|
|
51969
51998
|
pendingAt: data.pendingAt,
|
|
51999
|
+
note: data.note,
|
|
51970
52000
|
createdAt: data.timestamp,
|
|
51971
52001
|
};
|
|
51972
52002
|
}
|
|
@@ -51999,6 +52029,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51999
52029
|
pnlEntries: data.pnl.pnlEntries,
|
|
52000
52030
|
scheduledAt: data.scheduledAt,
|
|
52001
52031
|
pendingAt: data.pendingAt,
|
|
52032
|
+
note: data.note,
|
|
52002
52033
|
createdAt: data.timestamp,
|
|
52003
52034
|
};
|
|
52004
52035
|
}
|
|
@@ -52032,6 +52063,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
52032
52063
|
pnlEntries: data.pnl.pnlEntries,
|
|
52033
52064
|
scheduledAt: data.scheduledAt,
|
|
52034
52065
|
pendingAt: data.pendingAt,
|
|
52066
|
+
note: data.note,
|
|
52035
52067
|
createdAt: data.timestamp,
|
|
52036
52068
|
};
|
|
52037
52069
|
}
|
|
@@ -52055,6 +52087,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
52055
52087
|
pnlPriceClose: data.pnl.priceClose,
|
|
52056
52088
|
pnlCost: data.pnl.pnlCost,
|
|
52057
52089
|
pnlEntries: data.pnl.pnlEntries,
|
|
52090
|
+
note: data.note,
|
|
52058
52091
|
createdAt: data.timestamp,
|
|
52059
52092
|
};
|
|
52060
52093
|
}
|
|
@@ -52078,6 +52111,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
52078
52111
|
pnlPriceClose: data.pnl.priceClose,
|
|
52079
52112
|
pnlCost: data.pnl.pnlCost,
|
|
52080
52113
|
pnlEntries: data.pnl.pnlEntries,
|
|
52114
|
+
note: data.note,
|
|
52081
52115
|
createdAt: data.timestamp,
|
|
52082
52116
|
};
|
|
52083
52117
|
}
|
|
@@ -52119,6 +52153,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
|
|
|
52119
52153
|
totalPartials: data.totalPartials,
|
|
52120
52154
|
scheduledAt: data.scheduledAt,
|
|
52121
52155
|
pendingAt: data.pendingAt,
|
|
52156
|
+
note: data.signal.note,
|
|
52122
52157
|
createdAt: data.timestamp,
|
|
52123
52158
|
};
|
|
52124
52159
|
}
|
|
@@ -52151,6 +52186,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
|
|
|
52151
52186
|
scheduledAt: data.scheduledAt,
|
|
52152
52187
|
pendingAt: data.pendingAt,
|
|
52153
52188
|
closeReason: data.closeReason,
|
|
52189
|
+
note: data.signal.note,
|
|
52154
52190
|
createdAt: data.timestamp,
|
|
52155
52191
|
};
|
|
52156
52192
|
}
|
|
@@ -53650,6 +53686,7 @@ const CACHE_METHOD_NAME_FILE = "CacheUtils.file";
|
|
|
53650
53686
|
const CACHE_METHOD_NAME_FILE_CLEAR = "CacheUtils.file.clear";
|
|
53651
53687
|
const CACHE_METHOD_NAME_DISPOSE = "CacheUtils.dispose";
|
|
53652
53688
|
const CACHE_METHOD_NAME_CLEAR = "CacheUtils.clear";
|
|
53689
|
+
const CACHE_METHOD_NAME_RESET_COUNTER = "CacheUtils.resetCounter";
|
|
53653
53690
|
const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
|
|
53654
53691
|
const MS_PER_MINUTE$1 = 60000;
|
|
53655
53692
|
const INTERVAL_MINUTES$1 = {
|
|
@@ -53901,7 +53938,7 @@ class CacheFileInstance {
|
|
|
53901
53938
|
/**
|
|
53902
53939
|
* Clears the index counter.
|
|
53903
53940
|
*/
|
|
53904
|
-
static
|
|
53941
|
+
static resetCounter() {
|
|
53905
53942
|
CacheFileInstance._indexCounter = 0;
|
|
53906
53943
|
}
|
|
53907
53944
|
/**
|
|
@@ -54156,11 +54193,17 @@ class CacheUtils {
|
|
|
54156
54193
|
*/
|
|
54157
54194
|
this.clear = () => {
|
|
54158
54195
|
backtest.loggerService.info(CACHE_METHOD_NAME_CLEAR);
|
|
54159
|
-
|
|
54160
|
-
|
|
54161
|
-
|
|
54162
|
-
|
|
54163
|
-
|
|
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();
|
|
54164
54207
|
};
|
|
54165
54208
|
}
|
|
54166
54209
|
}
|
|
@@ -54184,10 +54227,12 @@ const INTERVAL_METHOD_NAME_RUN = "IntervalFnInstance.run";
|
|
|
54184
54227
|
const INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN = "IntervalFileInstance.run";
|
|
54185
54228
|
const INTERVAL_METHOD_NAME_FN = "IntervalUtils.fn";
|
|
54186
54229
|
const INTERVAL_METHOD_NAME_FN_CLEAR = "IntervalUtils.fn.clear";
|
|
54230
|
+
const INTERVAL_METHOD_NAME_FN_GC = "IntervalUtils.fn.gc";
|
|
54187
54231
|
const INTERVAL_METHOD_NAME_FILE = "IntervalUtils.file";
|
|
54188
54232
|
const INTERVAL_METHOD_NAME_FILE_CLEAR = "IntervalUtils.file.clear";
|
|
54189
54233
|
const INTERVAL_METHOD_NAME_DISPOSE = "IntervalUtils.dispose";
|
|
54190
54234
|
const INTERVAL_METHOD_NAME_CLEAR = "IntervalUtils.clear";
|
|
54235
|
+
const INTERVAL_METHOD_NAME_RESET_COUNTER = "IntervalUtils.resetCounter";
|
|
54191
54236
|
const MS_PER_MINUTE = 60000;
|
|
54192
54237
|
const INTERVAL_MINUTES = {
|
|
54193
54238
|
"1m": 1,
|
|
@@ -54254,6 +54299,8 @@ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, isBacktest) => {
|
|
|
54254
54299
|
*
|
|
54255
54300
|
* State is kept in memory; use `IntervalFileInstance` for persistence across restarts.
|
|
54256
54301
|
*
|
|
54302
|
+
* @template F - Concrete function type
|
|
54303
|
+
*
|
|
54257
54304
|
* @example
|
|
54258
54305
|
* ```typescript
|
|
54259
54306
|
* const instance = new IntervalFnInstance(mySignalFn, "1h");
|
|
@@ -54269,30 +54316,34 @@ class IntervalFnInstance {
|
|
|
54269
54316
|
*
|
|
54270
54317
|
* @param fn - Function to fire once per interval
|
|
54271
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`
|
|
54272
54321
|
*/
|
|
54273
|
-
constructor(fn, interval) {
|
|
54322
|
+
constructor(fn, interval, key = ([symbol]) => symbol) {
|
|
54274
54323
|
this.fn = fn;
|
|
54275
54324
|
this.interval = interval;
|
|
54276
|
-
|
|
54325
|
+
this.key = key;
|
|
54326
|
+
/** Stores the last aligned timestamp per context+symbol+args key. */
|
|
54277
54327
|
this._stateMap = new Map();
|
|
54278
54328
|
/**
|
|
54279
54329
|
* Execute the signal function with once-per-interval enforcement.
|
|
54280
54330
|
*
|
|
54281
54331
|
* Algorithm:
|
|
54282
54332
|
* 1. Align the current execution context `when` to the interval boundary.
|
|
54283
|
-
* 2.
|
|
54284
|
-
* 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
|
|
54285
54336
|
* the signal. If it returns `null`, leave state unchanged so the next call retries.
|
|
54286
54337
|
*
|
|
54287
54338
|
* Requires active method context and execution context.
|
|
54288
54339
|
*
|
|
54289
|
-
* @param
|
|
54340
|
+
* @param args - Arguments forwarded to the wrapped function
|
|
54290
54341
|
* @returns The value returned by `fn` on the first non-null fire, `null` on all subsequent calls
|
|
54291
54342
|
* within the same interval or when `fn` itself returned `null`
|
|
54292
54343
|
* @throws Error if method context, execution context, or interval is missing
|
|
54293
54344
|
*/
|
|
54294
|
-
this.run = async (
|
|
54295
|
-
backtest.loggerService.debug(INTERVAL_METHOD_NAME_RUN, {
|
|
54345
|
+
this.run = async (...args) => {
|
|
54346
|
+
backtest.loggerService.debug(INTERVAL_METHOD_NAME_RUN, { args });
|
|
54296
54347
|
const step = INTERVAL_MINUTES[this.interval];
|
|
54297
54348
|
{
|
|
54298
54349
|
if (!MethodContextService.hasContext()) {
|
|
@@ -54306,15 +54357,16 @@ class IntervalFnInstance {
|
|
|
54306
54357
|
}
|
|
54307
54358
|
}
|
|
54308
54359
|
const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
|
|
54309
|
-
const key = `${contextKey}:${symbol}`;
|
|
54310
54360
|
const currentWhen = backtest.executionContextService.context.when;
|
|
54311
54361
|
const currentAligned = align(currentWhen.getTime(), this.interval);
|
|
54312
|
-
|
|
54362
|
+
const argKey = this.key(args);
|
|
54363
|
+
const stateKey = `${contextKey}:${argKey}`;
|
|
54364
|
+
if (this._stateMap.get(stateKey) === currentAligned) {
|
|
54313
54365
|
return null;
|
|
54314
54366
|
}
|
|
54315
|
-
const result = await this.fn(
|
|
54367
|
+
const result = await this.fn.apply(null, args);
|
|
54316
54368
|
if (result !== null) {
|
|
54317
|
-
this._stateMap.set(
|
|
54369
|
+
this._stateMap.set(stateKey, currentAligned);
|
|
54318
54370
|
}
|
|
54319
54371
|
return result;
|
|
54320
54372
|
};
|
|
@@ -54335,6 +54387,28 @@ class IntervalFnInstance {
|
|
|
54335
54387
|
}
|
|
54336
54388
|
}
|
|
54337
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
|
+
};
|
|
54338
54412
|
}
|
|
54339
54413
|
}
|
|
54340
54414
|
/**
|
|
@@ -54348,7 +54422,7 @@ class IntervalFnInstance {
|
|
|
54348
54422
|
*
|
|
54349
54423
|
* Fired state survives process restarts — unlike `IntervalFnInstance` which is in-memory only.
|
|
54350
54424
|
*
|
|
54351
|
-
* @template
|
|
54425
|
+
* @template F - Concrete async function type
|
|
54352
54426
|
*
|
|
54353
54427
|
* @example
|
|
54354
54428
|
* ```typescript
|
|
@@ -54369,7 +54443,7 @@ class IntervalFileInstance {
|
|
|
54369
54443
|
* Resets the index counter to zero.
|
|
54370
54444
|
* Call this when clearing all instances (e.g. on `IntervalUtils.clear()`).
|
|
54371
54445
|
*/
|
|
54372
|
-
static
|
|
54446
|
+
static resetCounter() {
|
|
54373
54447
|
IntervalFileInstance._indexCounter = 0;
|
|
54374
54448
|
}
|
|
54375
54449
|
/**
|
|
@@ -54378,18 +54452,21 @@ class IntervalFileInstance {
|
|
|
54378
54452
|
* @param fn - Async signal function to fire once per interval
|
|
54379
54453
|
* @param interval - Candle interval that controls the firing boundary
|
|
54380
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}\``
|
|
54381
54457
|
*/
|
|
54382
|
-
constructor(fn, interval, name) {
|
|
54458
|
+
constructor(fn, interval, name, key = ([symbol, alignMs]) => `${symbol}_${alignMs}`) {
|
|
54383
54459
|
this.fn = fn;
|
|
54384
54460
|
this.interval = interval;
|
|
54385
54461
|
this.name = name;
|
|
54462
|
+
this.key = key;
|
|
54386
54463
|
/**
|
|
54387
54464
|
* Execute the async function with persistent once-per-interval enforcement.
|
|
54388
54465
|
*
|
|
54389
54466
|
* Algorithm:
|
|
54390
54467
|
* 1. Build bucket = `${name}_${interval}_${index}` — fixed per instance, used as directory name.
|
|
54391
|
-
* 2. Align execution context `when` to interval boundary → `
|
|
54392
|
-
* 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]`).
|
|
54393
54470
|
* 4. Try to read from `PersistIntervalAdapter` using (bucket, entityKey).
|
|
54394
54471
|
* 5. On hit — return `null` (interval already fired).
|
|
54395
54472
|
* 6. On miss — call `fn`. If non-null, write to disk and return result. If null, skip write and return null.
|
|
@@ -54397,12 +54474,13 @@ class IntervalFileInstance {
|
|
|
54397
54474
|
* Requires active method context and execution context.
|
|
54398
54475
|
*
|
|
54399
54476
|
* @param symbol - Trading pair symbol (e.g. "BTCUSDT")
|
|
54477
|
+
* @param args - Additional arguments forwarded to the wrapped function
|
|
54400
54478
|
* @returns The value on the first non-null fire, `null` if already fired this interval
|
|
54401
54479
|
* or if `fn` itself returned `null`
|
|
54402
54480
|
* @throws Error if method context, execution context, or interval is missing
|
|
54403
54481
|
*/
|
|
54404
|
-
this.run = async (
|
|
54405
|
-
backtest.loggerService.debug(INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN, {
|
|
54482
|
+
this.run = async (...args) => {
|
|
54483
|
+
backtest.loggerService.debug(INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN, { args });
|
|
54406
54484
|
const step = INTERVAL_MINUTES[this.interval];
|
|
54407
54485
|
{
|
|
54408
54486
|
if (!MethodContextService.hasContext()) {
|
|
@@ -54415,15 +54493,16 @@ class IntervalFileInstance {
|
|
|
54415
54493
|
throw new Error(`IntervalFileInstance unknown interval=${this.interval}`);
|
|
54416
54494
|
}
|
|
54417
54495
|
}
|
|
54496
|
+
const [symbol, ...rest] = args;
|
|
54418
54497
|
const { when } = backtest.executionContextService.context;
|
|
54419
|
-
const
|
|
54498
|
+
const alignedMs = align(when.getTime(), this.interval);
|
|
54420
54499
|
const bucket = `${this.name}_${this.interval}_${this.index}`;
|
|
54421
|
-
const entityKey =
|
|
54500
|
+
const entityKey = this.key([symbol, alignedMs, ...rest]);
|
|
54422
54501
|
const cached = await PersistIntervalAdapter.readIntervalData(bucket, entityKey);
|
|
54423
54502
|
if (cached !== null) {
|
|
54424
54503
|
return null;
|
|
54425
54504
|
}
|
|
54426
|
-
const result = await this.fn(
|
|
54505
|
+
const result = await this.fn.call(null, ...args);
|
|
54427
54506
|
if (result !== null) {
|
|
54428
54507
|
await PersistIntervalAdapter.writeIntervalData({ id: entityKey, data: result, removed: false }, bucket, entityKey);
|
|
54429
54508
|
}
|
|
@@ -54464,12 +54543,12 @@ class IntervalUtils {
|
|
|
54464
54543
|
* Memoized factory to get or create an `IntervalFnInstance` for a function.
|
|
54465
54544
|
* Each function reference gets its own isolated instance.
|
|
54466
54545
|
*/
|
|
54467
|
-
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));
|
|
54468
54547
|
/**
|
|
54469
54548
|
* Memoized factory to get or create an `IntervalFileInstance` for an async function.
|
|
54470
54549
|
* Each function reference gets its own isolated persistent instance.
|
|
54471
54550
|
*/
|
|
54472
|
-
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));
|
|
54473
54552
|
/**
|
|
54474
54553
|
* Wrap a signal function with in-memory once-per-interval firing.
|
|
54475
54554
|
*
|
|
@@ -54481,21 +54560,30 @@ class IntervalUtils {
|
|
|
54481
54560
|
*
|
|
54482
54561
|
* @param run - Signal function to wrap
|
|
54483
54562
|
* @param context.interval - Candle interval that controls the firing boundary
|
|
54484
|
-
* @
|
|
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
|
|
54485
54565
|
*
|
|
54486
54566
|
* @example
|
|
54487
54567
|
* ```typescript
|
|
54568
|
+
* // Without extra args
|
|
54488
54569
|
* const fireOnce = Interval.fn(mySignalFn, { interval: "15m" });
|
|
54489
|
-
*
|
|
54490
54570
|
* await fireOnce("BTCUSDT"); // → T or null (fn called)
|
|
54491
54571
|
* await fireOnce("BTCUSDT"); // → null (same interval, skipped)
|
|
54572
|
+
*
|
|
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)
|
|
54492
54580
|
* ```
|
|
54493
54581
|
*/
|
|
54494
54582
|
this.fn = (run, context) => {
|
|
54495
54583
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_FN, { context });
|
|
54496
|
-
const wrappedFn = (
|
|
54497
|
-
const instance = this._getInstance(run, context.interval);
|
|
54498
|
-
return instance.run(
|
|
54584
|
+
const wrappedFn = (...args) => {
|
|
54585
|
+
const instance = this._getInstance(run, context.interval, context.key);
|
|
54586
|
+
return instance.run(...args);
|
|
54499
54587
|
};
|
|
54500
54588
|
wrappedFn.clear = () => {
|
|
54501
54589
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_FN_CLEAR);
|
|
@@ -54509,6 +54597,14 @@ class IntervalUtils {
|
|
|
54509
54597
|
}
|
|
54510
54598
|
this._getInstance.get(run)?.clear();
|
|
54511
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
|
+
};
|
|
54512
54608
|
return wrappedFn;
|
|
54513
54609
|
};
|
|
54514
54610
|
/**
|
|
@@ -54521,28 +54617,33 @@ class IntervalUtils {
|
|
|
54521
54617
|
* The `run` function reference is used as the memoization key for the underlying
|
|
54522
54618
|
* `IntervalFileInstance`, so each unique function reference gets its own isolated instance.
|
|
54523
54619
|
*
|
|
54524
|
-
* @template
|
|
54620
|
+
* @template F - Concrete async function type
|
|
54525
54621
|
* @param run - Async signal function to wrap with persistent once-per-interval firing
|
|
54526
54622
|
* @param context.interval - Candle interval that controls the firing boundary
|
|
54527
54623
|
* @param context.name - Human-readable bucket name; becomes the directory prefix
|
|
54528
|
-
* @
|
|
54529
|
-
*
|
|
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
|
|
54530
54627
|
*
|
|
54531
54628
|
* @example
|
|
54532
54629
|
* ```typescript
|
|
54533
|
-
* const fetchSignal = async (symbol: string,
|
|
54534
|
-
* const fireOnce = Interval.file(fetchSignal, {
|
|
54535
|
-
*
|
|
54630
|
+
* const fetchSignal = async (symbol: string, period: number) => { ... };
|
|
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);
|
|
54536
54637
|
* ```
|
|
54537
54638
|
*/
|
|
54538
54639
|
this.file = (run, context) => {
|
|
54539
54640
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_FILE, { context });
|
|
54540
54641
|
{
|
|
54541
|
-
this._getFileInstance(run, context.interval, context.name);
|
|
54642
|
+
this._getFileInstance(run, context.interval, context.name, context.key);
|
|
54542
54643
|
}
|
|
54543
|
-
const wrappedFn = (
|
|
54544
|
-
const instance = this._getFileInstance(run, context.interval, context.name);
|
|
54545
|
-
return instance.run(
|
|
54644
|
+
const wrappedFn = (...args) => {
|
|
54645
|
+
const instance = this._getFileInstance(run, context.interval, context.name, context.key);
|
|
54646
|
+
return instance.run(...args);
|
|
54546
54647
|
};
|
|
54547
54648
|
wrappedFn.clear = async () => {
|
|
54548
54649
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_FILE_CLEAR);
|
|
@@ -54568,10 +54669,10 @@ class IntervalUtils {
|
|
|
54568
54669
|
this.dispose = (run) => {
|
|
54569
54670
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_DISPOSE, { run });
|
|
54570
54671
|
this._getInstance.clear(run);
|
|
54672
|
+
this._getFileInstance.clear(run);
|
|
54571
54673
|
};
|
|
54572
54674
|
/**
|
|
54573
|
-
* Clears all memoized `IntervalFnInstance` and `IntervalFileInstance` objects
|
|
54574
|
-
* resets the `IntervalFileInstance` index counter.
|
|
54675
|
+
* Clears all memoized `IntervalFnInstance` and `IntervalFileInstance` objects.
|
|
54575
54676
|
* Call this when `process.cwd()` changes between strategy iterations
|
|
54576
54677
|
* so new instances are created with the updated base path.
|
|
54577
54678
|
*/
|
|
@@ -54579,7 +54680,15 @@ class IntervalUtils {
|
|
|
54579
54680
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_CLEAR);
|
|
54580
54681
|
this._getInstance.clear();
|
|
54581
54682
|
this._getFileInstance.clear();
|
|
54582
|
-
|
|
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();
|
|
54583
54692
|
};
|
|
54584
54693
|
}
|
|
54585
54694
|
}
|