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.mjs
CHANGED
|
@@ -4127,53 +4127,6 @@ const toProfitLossDto = (signal, priceClose) => {
|
|
|
4127
4127
|
};
|
|
4128
4128
|
};
|
|
4129
4129
|
|
|
4130
|
-
/**
|
|
4131
|
-
* Converts markdown content to plain text with minimal formatting
|
|
4132
|
-
* @param content - Markdown string to convert
|
|
4133
|
-
* @returns Plain text representation
|
|
4134
|
-
*/
|
|
4135
|
-
const toPlainString = (content) => {
|
|
4136
|
-
if (!content) {
|
|
4137
|
-
return "";
|
|
4138
|
-
}
|
|
4139
|
-
let text = content;
|
|
4140
|
-
// Remove code blocks
|
|
4141
|
-
text = text.replace(/```[\s\S]*?```/g, "");
|
|
4142
|
-
text = text.replace(/`([^`]+)`/g, "$1");
|
|
4143
|
-
// Remove images
|
|
4144
|
-
text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
|
|
4145
|
-
// Convert links to text only (keep link text, remove URL)
|
|
4146
|
-
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
4147
|
-
// Remove headers (convert to plain text)
|
|
4148
|
-
text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
|
|
4149
|
-
// Remove bold and italic markers
|
|
4150
|
-
text = text.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
|
|
4151
|
-
text = text.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
4152
|
-
text = text.replace(/\*(.+?)\*/g, "$1");
|
|
4153
|
-
text = text.replace(/___(.+?)___/g, "$1");
|
|
4154
|
-
text = text.replace(/__(.+?)__/g, "$1");
|
|
4155
|
-
text = text.replace(/_(.+?)_/g, "$1");
|
|
4156
|
-
// Remove strikethrough
|
|
4157
|
-
text = text.replace(/~~(.+?)~~/g, "$1");
|
|
4158
|
-
// Convert lists to plain text with bullets
|
|
4159
|
-
text = text.replace(/^\s*[-*+]\s+/gm, "• ");
|
|
4160
|
-
text = text.replace(/^\s*\d+\.\s+/gm, "• ");
|
|
4161
|
-
// Remove blockquotes
|
|
4162
|
-
text = text.replace(/^\s*>\s+/gm, "");
|
|
4163
|
-
// Remove horizontal rules
|
|
4164
|
-
text = text.replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "");
|
|
4165
|
-
// Remove HTML tags
|
|
4166
|
-
text = text.replace(/<[^>]+>/g, "");
|
|
4167
|
-
// Remove excessive whitespace and normalize line breaks
|
|
4168
|
-
text = text.replace(/\n[\s\n]*\n/g, "\n");
|
|
4169
|
-
text = text.replace(/[ \t]+/g, " ");
|
|
4170
|
-
// Remove all newline characters
|
|
4171
|
-
text = text.replace(/\n/g, " ");
|
|
4172
|
-
// Remove excessive spaces after newline removal
|
|
4173
|
-
text = text.replace(/\s+/g, " ");
|
|
4174
|
-
return text.trim();
|
|
4175
|
-
};
|
|
4176
|
-
|
|
4177
4130
|
/**
|
|
4178
4131
|
* Returns the total closed state of a position using costBasisAtClose snapshots.
|
|
4179
4132
|
*
|
|
@@ -4808,6 +4761,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
|
|
|
4808
4761
|
originalPriceOpen: publicSignal.originalPriceOpen,
|
|
4809
4762
|
scheduledAt: publicSignal.scheduledAt,
|
|
4810
4763
|
pendingAt: publicSignal.pendingAt,
|
|
4764
|
+
note: publicSignal.note,
|
|
4811
4765
|
});
|
|
4812
4766
|
continue;
|
|
4813
4767
|
}
|
|
@@ -4835,6 +4789,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
|
|
|
4835
4789
|
originalPriceOpen: publicSignal.originalPriceOpen,
|
|
4836
4790
|
scheduledAt: publicSignal.scheduledAt,
|
|
4837
4791
|
pendingAt: publicSignal.pendingAt,
|
|
4792
|
+
note: publicSignal.note,
|
|
4838
4793
|
});
|
|
4839
4794
|
continue;
|
|
4840
4795
|
}
|
|
@@ -4861,6 +4816,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
|
|
|
4861
4816
|
originalPriceOpen: publicSignal.originalPriceOpen,
|
|
4862
4817
|
scheduledAt: publicSignal.scheduledAt,
|
|
4863
4818
|
pendingAt: publicSignal.pendingAt,
|
|
4819
|
+
note: publicSignal.note,
|
|
4864
4820
|
});
|
|
4865
4821
|
continue;
|
|
4866
4822
|
}
|
|
@@ -4888,6 +4844,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
|
|
|
4888
4844
|
originalPriceOpen: publicSignal.originalPriceOpen,
|
|
4889
4845
|
scheduledAt: publicSignal.scheduledAt,
|
|
4890
4846
|
pendingAt: publicSignal.pendingAt,
|
|
4847
|
+
note: publicSignal.note,
|
|
4891
4848
|
});
|
|
4892
4849
|
continue;
|
|
4893
4850
|
}
|
|
@@ -4915,6 +4872,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
|
|
|
4915
4872
|
originalPriceOpen: publicSignal.originalPriceOpen,
|
|
4916
4873
|
scheduledAt: publicSignal.scheduledAt,
|
|
4917
4874
|
pendingAt: publicSignal.pendingAt,
|
|
4875
|
+
note: publicSignal.note,
|
|
4918
4876
|
});
|
|
4919
4877
|
continue;
|
|
4920
4878
|
}
|
|
@@ -4944,6 +4902,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
|
|
|
4944
4902
|
originalPriceOpen: publicSignal.originalPriceOpen,
|
|
4945
4903
|
scheduledAt: publicSignal.scheduledAt,
|
|
4946
4904
|
pendingAt: publicSignal.pendingAt,
|
|
4905
|
+
note: publicSignal.note,
|
|
4947
4906
|
});
|
|
4948
4907
|
continue;
|
|
4949
4908
|
}
|
|
@@ -5042,7 +5001,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
5042
5001
|
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
5043
5002
|
const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
|
|
5044
5003
|
const signal = await Promise.race([
|
|
5045
|
-
self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
|
|
5004
|
+
self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when, currentPrice),
|
|
5046
5005
|
sleep(timeoutMs).then(() => TIMEOUT_SYMBOL),
|
|
5047
5006
|
]);
|
|
5048
5007
|
if (typeof signal === "symbol") {
|
|
@@ -5072,7 +5031,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
5072
5031
|
cost: signal.cost || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST,
|
|
5073
5032
|
priceOpen: signal.priceOpen, // Используем priceOpen из сигнала
|
|
5074
5033
|
position: signal.position,
|
|
5075
|
-
note:
|
|
5034
|
+
note: signal.note || "",
|
|
5076
5035
|
priceTakeProfit: signal.priceTakeProfit,
|
|
5077
5036
|
priceStopLoss: signal.priceStopLoss,
|
|
5078
5037
|
minuteEstimatedTime: signal.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
|
|
@@ -5098,7 +5057,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
5098
5057
|
cost: signal.cost || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST,
|
|
5099
5058
|
priceOpen: signal.priceOpen,
|
|
5100
5059
|
position: signal.position,
|
|
5101
|
-
note:
|
|
5060
|
+
note: signal.note || "",
|
|
5102
5061
|
priceTakeProfit: signal.priceTakeProfit,
|
|
5103
5062
|
priceStopLoss: signal.priceStopLoss,
|
|
5104
5063
|
minuteEstimatedTime: signal.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
|
|
@@ -5123,7 +5082,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
5123
5082
|
cost: signal.cost || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST,
|
|
5124
5083
|
priceOpen: currentPrice,
|
|
5125
5084
|
...structuredClone(signal),
|
|
5126
|
-
note:
|
|
5085
|
+
note: signal.note || "",
|
|
5127
5086
|
minuteEstimatedTime: signal.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
|
|
5128
5087
|
symbol: self.params.execution.context.symbol,
|
|
5129
5088
|
exchangeName: self.params.method.context.exchangeName,
|
|
@@ -5870,6 +5829,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
5870
5829
|
totalPartials: scheduled._partial?.length ?? 0,
|
|
5871
5830
|
originalPriceOpen: scheduled.priceOpen,
|
|
5872
5831
|
pnl: toProfitLossDto(scheduled, scheduled.priceOpen),
|
|
5832
|
+
note: scheduled.note,
|
|
5873
5833
|
});
|
|
5874
5834
|
return null;
|
|
5875
5835
|
}
|
|
@@ -6638,6 +6598,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
|
|
|
6638
6598
|
totalPartials: scheduled._partial?.length ?? 0,
|
|
6639
6599
|
originalPriceOpen: scheduled.priceOpen,
|
|
6640
6600
|
pnl: toProfitLossDto(scheduled, averagePrice),
|
|
6601
|
+
note: scheduled.note,
|
|
6641
6602
|
});
|
|
6642
6603
|
}
|
|
6643
6604
|
await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
@@ -6716,6 +6677,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
6716
6677
|
totalPartials: scheduled._partial?.length ?? 0,
|
|
6717
6678
|
originalPriceOpen: scheduled.priceOpen,
|
|
6718
6679
|
pnl: toProfitLossDto(scheduled, scheduled.priceOpen),
|
|
6680
|
+
note: scheduled.note,
|
|
6719
6681
|
});
|
|
6720
6682
|
return false;
|
|
6721
6683
|
}
|
|
@@ -6803,6 +6765,7 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
|
|
|
6803
6765
|
totalPartials: closedSignal._partial?.length ?? 0,
|
|
6804
6766
|
originalPriceOpen: closedSignal.priceOpen,
|
|
6805
6767
|
pnl: toProfitLossDto(closedSignal, averagePrice),
|
|
6768
|
+
note: closedSignal.note,
|
|
6806
6769
|
});
|
|
6807
6770
|
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
6808
6771
|
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
@@ -6903,6 +6866,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
|
|
|
6903
6866
|
totalPartials: activatedSignal._partial?.length ?? 0,
|
|
6904
6867
|
originalPriceOpen: activatedSignal.priceOpen,
|
|
6905
6868
|
pnl: toProfitLossDto(activatedSignal, averagePrice),
|
|
6869
|
+
note: activatedSignal.note,
|
|
6906
6870
|
});
|
|
6907
6871
|
return { outcome: "pending" };
|
|
6908
6872
|
}
|
|
@@ -6934,6 +6898,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
|
|
|
6934
6898
|
pendingAt: publicSignalForCommit.pendingAt,
|
|
6935
6899
|
totalEntries: publicSignalForCommit.totalEntries,
|
|
6936
6900
|
totalPartials: publicSignalForCommit.totalPartials,
|
|
6901
|
+
note: publicSignalForCommit.note,
|
|
6937
6902
|
});
|
|
6938
6903
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
|
|
6939
6904
|
await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
|
|
@@ -8118,6 +8083,7 @@ class ClientStrategy {
|
|
|
8118
8083
|
totalPartials: cancelledSignal._partial?.length ?? 0,
|
|
8119
8084
|
originalPriceOpen: cancelledSignal.priceOpen,
|
|
8120
8085
|
pnl: toProfitLossDto(cancelledSignal, currentPrice),
|
|
8086
|
+
note: cancelledSignal.note,
|
|
8121
8087
|
});
|
|
8122
8088
|
// Call onCancel callback
|
|
8123
8089
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
@@ -8171,6 +8137,7 @@ class ClientStrategy {
|
|
|
8171
8137
|
totalPartials: closedSignal._partial?.length ?? 0,
|
|
8172
8138
|
originalPriceOpen: closedSignal.priceOpen,
|
|
8173
8139
|
pnl: toProfitLossDto(closedSignal, currentPrice),
|
|
8140
|
+
note: closedSignal.note,
|
|
8174
8141
|
});
|
|
8175
8142
|
// Call onClose callback
|
|
8176
8143
|
await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
@@ -8252,6 +8219,7 @@ class ClientStrategy {
|
|
|
8252
8219
|
totalPartials: activatedSignal._partial?.length ?? 0,
|
|
8253
8220
|
originalPriceOpen: activatedSignal.priceOpen,
|
|
8254
8221
|
pnl: toProfitLossDto(activatedSignal, currentPrice),
|
|
8222
|
+
note: activatedSignal.note,
|
|
8255
8223
|
});
|
|
8256
8224
|
return await RETURN_IDLE_FN(this, currentPrice);
|
|
8257
8225
|
}
|
|
@@ -8282,6 +8250,7 @@ class ClientStrategy {
|
|
|
8282
8250
|
pendingAt: publicSignalForCommit.pendingAt,
|
|
8283
8251
|
totalEntries: publicSignalForCommit.totalEntries,
|
|
8284
8252
|
totalPartials: publicSignalForCommit.totalPartials,
|
|
8253
|
+
note: publicSignalForCommit.note,
|
|
8285
8254
|
});
|
|
8286
8255
|
// Call onOpen callback
|
|
8287
8256
|
await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
@@ -8417,6 +8386,7 @@ class ClientStrategy {
|
|
|
8417
8386
|
totalPartials: cancelledSignal._partial?.length ?? 0,
|
|
8418
8387
|
originalPriceOpen: cancelledSignal.priceOpen,
|
|
8419
8388
|
pnl: toProfitLossDto(cancelledSignal, currentPrice),
|
|
8389
|
+
note: cancelledSignal.note,
|
|
8420
8390
|
});
|
|
8421
8391
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
8422
8392
|
const cancelledResult = {
|
|
@@ -8471,6 +8441,7 @@ class ClientStrategy {
|
|
|
8471
8441
|
totalPartials: closedSignal._partial?.length ?? 0,
|
|
8472
8442
|
originalPriceOpen: closedSignal.priceOpen,
|
|
8473
8443
|
pnl: toProfitLossDto(closedSignal, currentPrice),
|
|
8444
|
+
note: closedSignal.note,
|
|
8474
8445
|
});
|
|
8475
8446
|
await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
8476
8447
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
@@ -10020,6 +9991,8 @@ class MergeRisk {
|
|
|
10020
9991
|
}
|
|
10021
9992
|
}
|
|
10022
9993
|
|
|
9994
|
+
/** Default interval for strategies that do not specify one */
|
|
9995
|
+
const STRATEGY_DEFAULT_INTERVAL = "1m";
|
|
10023
9996
|
/**
|
|
10024
9997
|
* If syncSubject listener or any registered action throws, it means the signal was not properly synchronized
|
|
10025
9998
|
* to the exchange (e.g. limit order failed to fill).
|
|
@@ -10405,7 +10378,7 @@ class StrategyConnectionService {
|
|
|
10405
10378
|
* @returns Configured ClientStrategy instance
|
|
10406
10379
|
*/
|
|
10407
10380
|
this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$t(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
10408
|
-
const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
10381
|
+
const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
10409
10382
|
return new ClientStrategy({
|
|
10410
10383
|
symbol,
|
|
10411
10384
|
interval,
|
|
@@ -16124,8 +16097,8 @@ class StrategySchemaService {
|
|
|
16124
16097
|
if (strategySchema.actions?.some((value) => typeof value !== "string")) {
|
|
16125
16098
|
throw new Error(`strategy schema validation failed: invalid actions for strategyName=${strategySchema.strategyName} actions=[${strategySchema.actions}]`);
|
|
16126
16099
|
}
|
|
16127
|
-
if (typeof strategySchema.interval !== "string") {
|
|
16128
|
-
throw new Error(`strategy schema validation failed:
|
|
16100
|
+
if (strategySchema.interval && typeof strategySchema.interval !== "string") {
|
|
16101
|
+
throw new Error(`strategy schema validation failed: invalid interval for strategyName=${strategySchema.strategyName}`);
|
|
16129
16102
|
}
|
|
16130
16103
|
if (typeof strategySchema.getSignal !== "function") {
|
|
16131
16104
|
throw new Error(`strategy schema validation failed: missing getSignal for strategyName=${strategySchema.strategyName}`);
|
|
@@ -18071,6 +18044,53 @@ class WalkerCommandService {
|
|
|
18071
18044
|
}
|
|
18072
18045
|
}
|
|
18073
18046
|
|
|
18047
|
+
/**
|
|
18048
|
+
* Converts markdown content to plain text with minimal formatting
|
|
18049
|
+
* @param content - Markdown string to convert
|
|
18050
|
+
* @returns Plain text representation
|
|
18051
|
+
*/
|
|
18052
|
+
const toPlainString = (content) => {
|
|
18053
|
+
if (!content) {
|
|
18054
|
+
return "";
|
|
18055
|
+
}
|
|
18056
|
+
let text = content;
|
|
18057
|
+
// Remove code blocks
|
|
18058
|
+
text = text.replace(/```[\s\S]*?```/g, "");
|
|
18059
|
+
text = text.replace(/`([^`]+)`/g, "$1");
|
|
18060
|
+
// Remove images
|
|
18061
|
+
text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
|
|
18062
|
+
// Convert links to text only (keep link text, remove URL)
|
|
18063
|
+
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
18064
|
+
// Remove headers (convert to plain text)
|
|
18065
|
+
text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
|
|
18066
|
+
// Remove bold and italic markers
|
|
18067
|
+
text = text.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
|
|
18068
|
+
text = text.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
18069
|
+
text = text.replace(/\*(.+?)\*/g, "$1");
|
|
18070
|
+
text = text.replace(/___(.+?)___/g, "$1");
|
|
18071
|
+
text = text.replace(/__(.+?)__/g, "$1");
|
|
18072
|
+
text = text.replace(/_(.+?)_/g, "$1");
|
|
18073
|
+
// Remove strikethrough
|
|
18074
|
+
text = text.replace(/~~(.+?)~~/g, "$1");
|
|
18075
|
+
// Convert lists to plain text with bullets
|
|
18076
|
+
text = text.replace(/^\s*[-*+]\s+/gm, "• ");
|
|
18077
|
+
text = text.replace(/^\s*\d+\.\s+/gm, "• ");
|
|
18078
|
+
// Remove blockquotes
|
|
18079
|
+
text = text.replace(/^\s*>\s+/gm, "");
|
|
18080
|
+
// Remove horizontal rules
|
|
18081
|
+
text = text.replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "");
|
|
18082
|
+
// Remove HTML tags
|
|
18083
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
18084
|
+
// Remove excessive whitespace and normalize line breaks
|
|
18085
|
+
text = text.replace(/\n[\s\n]*\n/g, "\n");
|
|
18086
|
+
text = text.replace(/[ \t]+/g, " ");
|
|
18087
|
+
// Remove all newline characters
|
|
18088
|
+
text = text.replace(/\n/g, " ");
|
|
18089
|
+
// Remove excessive spaces after newline removal
|
|
18090
|
+
text = text.replace(/\s+/g, " ");
|
|
18091
|
+
return text.trim();
|
|
18092
|
+
};
|
|
18093
|
+
|
|
18074
18094
|
/**
|
|
18075
18095
|
* Column configuration for backtest markdown reports.
|
|
18076
18096
|
*
|
|
@@ -18746,7 +18766,7 @@ const partial_columns = [
|
|
|
18746
18766
|
{
|
|
18747
18767
|
key: "note",
|
|
18748
18768
|
label: "Note",
|
|
18749
|
-
format: (data) => data.note
|
|
18769
|
+
format: (data) => toPlainString(data.note ?? "N/A"),
|
|
18750
18770
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
18751
18771
|
},
|
|
18752
18772
|
{
|
|
@@ -18906,7 +18926,7 @@ const breakeven_columns = [
|
|
|
18906
18926
|
{
|
|
18907
18927
|
key: "note",
|
|
18908
18928
|
label: "Note",
|
|
18909
|
-
format: (data) => data.note
|
|
18929
|
+
format: (data) => toPlainString(data.note ?? "N/A"),
|
|
18910
18930
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
18911
18931
|
},
|
|
18912
18932
|
{
|
|
@@ -51645,6 +51665,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
|
|
|
51645
51665
|
pnlEntries: data.signal.pnl.pnlEntries,
|
|
51646
51666
|
scheduledAt: data.signal.scheduledAt,
|
|
51647
51667
|
currentPrice: data.currentPrice,
|
|
51668
|
+
note: data.signal.note,
|
|
51648
51669
|
createdAt: data.createdAt,
|
|
51649
51670
|
};
|
|
51650
51671
|
}
|
|
@@ -51674,6 +51695,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
|
|
|
51674
51695
|
duration: durationMin,
|
|
51675
51696
|
scheduledAt: data.signal.scheduledAt,
|
|
51676
51697
|
pendingAt: data.signal.pendingAt,
|
|
51698
|
+
note: data.signal.note,
|
|
51677
51699
|
createdAt: data.createdAt,
|
|
51678
51700
|
};
|
|
51679
51701
|
}
|
|
@@ -51710,6 +51732,7 @@ const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
|
|
|
51710
51732
|
pnlPriceClose: data.data.pnl.priceClose,
|
|
51711
51733
|
pnlCost: data.data.pnl.pnlCost,
|
|
51712
51734
|
pnlEntries: data.data.pnl.pnlEntries,
|
|
51735
|
+
note: data.data.note,
|
|
51713
51736
|
scheduledAt: data.data.scheduledAt,
|
|
51714
51737
|
pendingAt: data.data.pendingAt,
|
|
51715
51738
|
createdAt: data.timestamp,
|
|
@@ -51745,6 +51768,7 @@ const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
|
|
|
51745
51768
|
pnlPriceClose: data.data.pnl.priceClose,
|
|
51746
51769
|
pnlCost: data.data.pnl.pnlCost,
|
|
51747
51770
|
pnlEntries: data.data.pnl.pnlEntries,
|
|
51771
|
+
note: data.data.note,
|
|
51748
51772
|
scheduledAt: data.data.scheduledAt,
|
|
51749
51773
|
pendingAt: data.data.pendingAt,
|
|
51750
51774
|
createdAt: data.timestamp,
|
|
@@ -51779,6 +51803,7 @@ const CREATE_BREAKEVEN_NOTIFICATION_FN = (data) => ({
|
|
|
51779
51803
|
pnlPriceClose: data.data.pnl.priceClose,
|
|
51780
51804
|
pnlCost: data.data.pnl.pnlCost,
|
|
51781
51805
|
pnlEntries: data.data.pnl.pnlEntries,
|
|
51806
|
+
note: data.data.note,
|
|
51782
51807
|
scheduledAt: data.data.scheduledAt,
|
|
51783
51808
|
pendingAt: data.data.pendingAt,
|
|
51784
51809
|
createdAt: data.timestamp,
|
|
@@ -51820,6 +51845,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51820
51845
|
pnlEntries: data.pnl.pnlEntries,
|
|
51821
51846
|
scheduledAt: data.scheduledAt,
|
|
51822
51847
|
pendingAt: data.pendingAt,
|
|
51848
|
+
note: data.note,
|
|
51823
51849
|
createdAt: data.timestamp,
|
|
51824
51850
|
};
|
|
51825
51851
|
}
|
|
@@ -51852,6 +51878,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51852
51878
|
pnlEntries: data.pnl.pnlEntries,
|
|
51853
51879
|
scheduledAt: data.scheduledAt,
|
|
51854
51880
|
pendingAt: data.pendingAt,
|
|
51881
|
+
note: data.note,
|
|
51855
51882
|
createdAt: data.timestamp,
|
|
51856
51883
|
};
|
|
51857
51884
|
}
|
|
@@ -51883,6 +51910,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51883
51910
|
pnlEntries: data.pnl.pnlEntries,
|
|
51884
51911
|
scheduledAt: data.scheduledAt,
|
|
51885
51912
|
pendingAt: data.pendingAt,
|
|
51913
|
+
note: data.note,
|
|
51886
51914
|
createdAt: data.timestamp,
|
|
51887
51915
|
};
|
|
51888
51916
|
}
|
|
@@ -51915,6 +51943,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51915
51943
|
pnlEntries: data.pnl.pnlEntries,
|
|
51916
51944
|
scheduledAt: data.scheduledAt,
|
|
51917
51945
|
pendingAt: data.pendingAt,
|
|
51946
|
+
note: data.note,
|
|
51918
51947
|
createdAt: data.timestamp,
|
|
51919
51948
|
};
|
|
51920
51949
|
}
|
|
@@ -51947,6 +51976,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51947
51976
|
pnlEntries: data.pnl.pnlEntries,
|
|
51948
51977
|
scheduledAt: data.scheduledAt,
|
|
51949
51978
|
pendingAt: data.pendingAt,
|
|
51979
|
+
note: data.note,
|
|
51950
51980
|
createdAt: data.timestamp,
|
|
51951
51981
|
};
|
|
51952
51982
|
}
|
|
@@ -51979,6 +52009,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
51979
52009
|
pnlEntries: data.pnl.pnlEntries,
|
|
51980
52010
|
scheduledAt: data.scheduledAt,
|
|
51981
52011
|
pendingAt: data.pendingAt,
|
|
52012
|
+
note: data.note,
|
|
51982
52013
|
createdAt: data.timestamp,
|
|
51983
52014
|
};
|
|
51984
52015
|
}
|
|
@@ -52012,6 +52043,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
52012
52043
|
pnlEntries: data.pnl.pnlEntries,
|
|
52013
52044
|
scheduledAt: data.scheduledAt,
|
|
52014
52045
|
pendingAt: data.pendingAt,
|
|
52046
|
+
note: data.note,
|
|
52015
52047
|
createdAt: data.timestamp,
|
|
52016
52048
|
};
|
|
52017
52049
|
}
|
|
@@ -52035,6 +52067,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
52035
52067
|
pnlPriceClose: data.pnl.priceClose,
|
|
52036
52068
|
pnlCost: data.pnl.pnlCost,
|
|
52037
52069
|
pnlEntries: data.pnl.pnlEntries,
|
|
52070
|
+
note: data.note,
|
|
52038
52071
|
createdAt: data.timestamp,
|
|
52039
52072
|
};
|
|
52040
52073
|
}
|
|
@@ -52058,6 +52091,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
|
|
|
52058
52091
|
pnlPriceClose: data.pnl.priceClose,
|
|
52059
52092
|
pnlCost: data.pnl.pnlCost,
|
|
52060
52093
|
pnlEntries: data.pnl.pnlEntries,
|
|
52094
|
+
note: data.note,
|
|
52061
52095
|
createdAt: data.timestamp,
|
|
52062
52096
|
};
|
|
52063
52097
|
}
|
|
@@ -52099,6 +52133,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
|
|
|
52099
52133
|
totalPartials: data.totalPartials,
|
|
52100
52134
|
scheduledAt: data.scheduledAt,
|
|
52101
52135
|
pendingAt: data.pendingAt,
|
|
52136
|
+
note: data.signal.note,
|
|
52102
52137
|
createdAt: data.timestamp,
|
|
52103
52138
|
};
|
|
52104
52139
|
}
|
|
@@ -52131,6 +52166,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
|
|
|
52131
52166
|
scheduledAt: data.scheduledAt,
|
|
52132
52167
|
pendingAt: data.pendingAt,
|
|
52133
52168
|
closeReason: data.closeReason,
|
|
52169
|
+
note: data.signal.note,
|
|
52134
52170
|
createdAt: data.timestamp,
|
|
52135
52171
|
};
|
|
52136
52172
|
}
|
|
@@ -53630,6 +53666,7 @@ const CACHE_METHOD_NAME_FILE = "CacheUtils.file";
|
|
|
53630
53666
|
const CACHE_METHOD_NAME_FILE_CLEAR = "CacheUtils.file.clear";
|
|
53631
53667
|
const CACHE_METHOD_NAME_DISPOSE = "CacheUtils.dispose";
|
|
53632
53668
|
const CACHE_METHOD_NAME_CLEAR = "CacheUtils.clear";
|
|
53669
|
+
const CACHE_METHOD_NAME_RESET_COUNTER = "CacheUtils.resetCounter";
|
|
53633
53670
|
const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
|
|
53634
53671
|
const MS_PER_MINUTE$1 = 60000;
|
|
53635
53672
|
const INTERVAL_MINUTES$1 = {
|
|
@@ -53881,7 +53918,7 @@ class CacheFileInstance {
|
|
|
53881
53918
|
/**
|
|
53882
53919
|
* Clears the index counter.
|
|
53883
53920
|
*/
|
|
53884
|
-
static
|
|
53921
|
+
static resetCounter() {
|
|
53885
53922
|
CacheFileInstance._indexCounter = 0;
|
|
53886
53923
|
}
|
|
53887
53924
|
/**
|
|
@@ -54136,11 +54173,17 @@ class CacheUtils {
|
|
|
54136
54173
|
*/
|
|
54137
54174
|
this.clear = () => {
|
|
54138
54175
|
backtest.loggerService.info(CACHE_METHOD_NAME_CLEAR);
|
|
54139
|
-
|
|
54140
|
-
|
|
54141
|
-
|
|
54142
|
-
|
|
54143
|
-
|
|
54176
|
+
this._getFnInstance.clear();
|
|
54177
|
+
this._getFileInstance.clear();
|
|
54178
|
+
};
|
|
54179
|
+
/**
|
|
54180
|
+
* Resets the CacheFileInstance index counter to zero.
|
|
54181
|
+
* This is useful when process.cwd() changes between strategy iterations to ensure
|
|
54182
|
+
* that new CacheFileInstance objects start with index 0 and do not collide with old instances.
|
|
54183
|
+
*/
|
|
54184
|
+
this.resetCounter = () => {
|
|
54185
|
+
backtest.loggerService.info(CACHE_METHOD_NAME_RESET_COUNTER);
|
|
54186
|
+
CacheFileInstance.resetCounter();
|
|
54144
54187
|
};
|
|
54145
54188
|
}
|
|
54146
54189
|
}
|
|
@@ -54164,10 +54207,12 @@ const INTERVAL_METHOD_NAME_RUN = "IntervalFnInstance.run";
|
|
|
54164
54207
|
const INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN = "IntervalFileInstance.run";
|
|
54165
54208
|
const INTERVAL_METHOD_NAME_FN = "IntervalUtils.fn";
|
|
54166
54209
|
const INTERVAL_METHOD_NAME_FN_CLEAR = "IntervalUtils.fn.clear";
|
|
54210
|
+
const INTERVAL_METHOD_NAME_FN_GC = "IntervalUtils.fn.gc";
|
|
54167
54211
|
const INTERVAL_METHOD_NAME_FILE = "IntervalUtils.file";
|
|
54168
54212
|
const INTERVAL_METHOD_NAME_FILE_CLEAR = "IntervalUtils.file.clear";
|
|
54169
54213
|
const INTERVAL_METHOD_NAME_DISPOSE = "IntervalUtils.dispose";
|
|
54170
54214
|
const INTERVAL_METHOD_NAME_CLEAR = "IntervalUtils.clear";
|
|
54215
|
+
const INTERVAL_METHOD_NAME_RESET_COUNTER = "IntervalUtils.resetCounter";
|
|
54171
54216
|
const MS_PER_MINUTE = 60000;
|
|
54172
54217
|
const INTERVAL_MINUTES = {
|
|
54173
54218
|
"1m": 1,
|
|
@@ -54234,6 +54279,8 @@ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, isBacktest) => {
|
|
|
54234
54279
|
*
|
|
54235
54280
|
* State is kept in memory; use `IntervalFileInstance` for persistence across restarts.
|
|
54236
54281
|
*
|
|
54282
|
+
* @template F - Concrete function type
|
|
54283
|
+
*
|
|
54237
54284
|
* @example
|
|
54238
54285
|
* ```typescript
|
|
54239
54286
|
* const instance = new IntervalFnInstance(mySignalFn, "1h");
|
|
@@ -54249,30 +54296,34 @@ class IntervalFnInstance {
|
|
|
54249
54296
|
*
|
|
54250
54297
|
* @param fn - Function to fire once per interval
|
|
54251
54298
|
* @param interval - Candle interval that controls the firing boundary
|
|
54299
|
+
* @param key - Optional key generator for argument-based state separation.
|
|
54300
|
+
* Default: `([symbol]) => symbol`
|
|
54252
54301
|
*/
|
|
54253
|
-
constructor(fn, interval) {
|
|
54302
|
+
constructor(fn, interval, key = ([symbol]) => symbol) {
|
|
54254
54303
|
this.fn = fn;
|
|
54255
54304
|
this.interval = interval;
|
|
54256
|
-
|
|
54305
|
+
this.key = key;
|
|
54306
|
+
/** Stores the last aligned timestamp per context+symbol+args key. */
|
|
54257
54307
|
this._stateMap = new Map();
|
|
54258
54308
|
/**
|
|
54259
54309
|
* Execute the signal function with once-per-interval enforcement.
|
|
54260
54310
|
*
|
|
54261
54311
|
* Algorithm:
|
|
54262
54312
|
* 1. Align the current execution context `when` to the interval boundary.
|
|
54263
|
-
* 2.
|
|
54264
|
-
* 3.
|
|
54313
|
+
* 2. Build state key from context + key generator result.
|
|
54314
|
+
* 3. If the stored aligned timestamp for this key equals the current one → return `null`.
|
|
54315
|
+
* 4. Otherwise call `fn`. If it returns a non-null signal, record the aligned timestamp and return
|
|
54265
54316
|
* the signal. If it returns `null`, leave state unchanged so the next call retries.
|
|
54266
54317
|
*
|
|
54267
54318
|
* Requires active method context and execution context.
|
|
54268
54319
|
*
|
|
54269
|
-
* @param
|
|
54320
|
+
* @param args - Arguments forwarded to the wrapped function
|
|
54270
54321
|
* @returns The value returned by `fn` on the first non-null fire, `null` on all subsequent calls
|
|
54271
54322
|
* within the same interval or when `fn` itself returned `null`
|
|
54272
54323
|
* @throws Error if method context, execution context, or interval is missing
|
|
54273
54324
|
*/
|
|
54274
|
-
this.run = async (
|
|
54275
|
-
backtest.loggerService.debug(INTERVAL_METHOD_NAME_RUN, {
|
|
54325
|
+
this.run = async (...args) => {
|
|
54326
|
+
backtest.loggerService.debug(INTERVAL_METHOD_NAME_RUN, { args });
|
|
54276
54327
|
const step = INTERVAL_MINUTES[this.interval];
|
|
54277
54328
|
{
|
|
54278
54329
|
if (!MethodContextService.hasContext()) {
|
|
@@ -54286,15 +54337,16 @@ class IntervalFnInstance {
|
|
|
54286
54337
|
}
|
|
54287
54338
|
}
|
|
54288
54339
|
const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
|
|
54289
|
-
const key = `${contextKey}:${symbol}`;
|
|
54290
54340
|
const currentWhen = backtest.executionContextService.context.when;
|
|
54291
54341
|
const currentAligned = align(currentWhen.getTime(), this.interval);
|
|
54292
|
-
|
|
54342
|
+
const argKey = this.key(args);
|
|
54343
|
+
const stateKey = `${contextKey}:${argKey}`;
|
|
54344
|
+
if (this._stateMap.get(stateKey) === currentAligned) {
|
|
54293
54345
|
return null;
|
|
54294
54346
|
}
|
|
54295
|
-
const result = await this.fn(
|
|
54347
|
+
const result = await this.fn.apply(null, args);
|
|
54296
54348
|
if (result !== null) {
|
|
54297
|
-
this._stateMap.set(
|
|
54349
|
+
this._stateMap.set(stateKey, currentAligned);
|
|
54298
54350
|
}
|
|
54299
54351
|
return result;
|
|
54300
54352
|
};
|
|
@@ -54315,6 +54367,28 @@ class IntervalFnInstance {
|
|
|
54315
54367
|
}
|
|
54316
54368
|
}
|
|
54317
54369
|
};
|
|
54370
|
+
/**
|
|
54371
|
+
* Garbage collect expired state entries.
|
|
54372
|
+
*
|
|
54373
|
+
* Removes all entries whose aligned timestamp differs from the current interval boundary.
|
|
54374
|
+
* Call this periodically to free memory from stale state entries.
|
|
54375
|
+
*
|
|
54376
|
+
* Requires active execution context to get current time.
|
|
54377
|
+
*
|
|
54378
|
+
* @returns Number of entries removed
|
|
54379
|
+
*/
|
|
54380
|
+
this.gc = () => {
|
|
54381
|
+
const currentWhen = backtest.executionContextService.context.when;
|
|
54382
|
+
const currentAligned = align(currentWhen.getTime(), this.interval);
|
|
54383
|
+
let removed = 0;
|
|
54384
|
+
for (const [key, storedAligned] of this._stateMap.entries()) {
|
|
54385
|
+
if (storedAligned !== currentAligned) {
|
|
54386
|
+
this._stateMap.delete(key);
|
|
54387
|
+
removed++;
|
|
54388
|
+
}
|
|
54389
|
+
}
|
|
54390
|
+
return removed;
|
|
54391
|
+
};
|
|
54318
54392
|
}
|
|
54319
54393
|
}
|
|
54320
54394
|
/**
|
|
@@ -54328,7 +54402,7 @@ class IntervalFnInstance {
|
|
|
54328
54402
|
*
|
|
54329
54403
|
* Fired state survives process restarts — unlike `IntervalFnInstance` which is in-memory only.
|
|
54330
54404
|
*
|
|
54331
|
-
* @template
|
|
54405
|
+
* @template F - Concrete async function type
|
|
54332
54406
|
*
|
|
54333
54407
|
* @example
|
|
54334
54408
|
* ```typescript
|
|
@@ -54349,7 +54423,7 @@ class IntervalFileInstance {
|
|
|
54349
54423
|
* Resets the index counter to zero.
|
|
54350
54424
|
* Call this when clearing all instances (e.g. on `IntervalUtils.clear()`).
|
|
54351
54425
|
*/
|
|
54352
|
-
static
|
|
54426
|
+
static resetCounter() {
|
|
54353
54427
|
IntervalFileInstance._indexCounter = 0;
|
|
54354
54428
|
}
|
|
54355
54429
|
/**
|
|
@@ -54358,18 +54432,21 @@ class IntervalFileInstance {
|
|
|
54358
54432
|
* @param fn - Async signal function to fire once per interval
|
|
54359
54433
|
* @param interval - Candle interval that controls the firing boundary
|
|
54360
54434
|
* @param name - Human-readable bucket name used as the directory prefix
|
|
54435
|
+
* @param key - Dynamic key generator; receives `[symbol, alignMs, ...rest]`.
|
|
54436
|
+
* Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
|
|
54361
54437
|
*/
|
|
54362
|
-
constructor(fn, interval, name) {
|
|
54438
|
+
constructor(fn, interval, name, key = ([symbol, alignMs]) => `${symbol}_${alignMs}`) {
|
|
54363
54439
|
this.fn = fn;
|
|
54364
54440
|
this.interval = interval;
|
|
54365
54441
|
this.name = name;
|
|
54442
|
+
this.key = key;
|
|
54366
54443
|
/**
|
|
54367
54444
|
* Execute the async function with persistent once-per-interval enforcement.
|
|
54368
54445
|
*
|
|
54369
54446
|
* Algorithm:
|
|
54370
54447
|
* 1. Build bucket = `${name}_${interval}_${index}` — fixed per instance, used as directory name.
|
|
54371
|
-
* 2. Align execution context `when` to interval boundary → `
|
|
54372
|
-
* 3. Build entity key
|
|
54448
|
+
* 2. Align execution context `when` to interval boundary → `alignedMs`.
|
|
54449
|
+
* 3. Build entity key from the key generator (receives `[symbol, alignedMs, ...rest]`).
|
|
54373
54450
|
* 4. Try to read from `PersistIntervalAdapter` using (bucket, entityKey).
|
|
54374
54451
|
* 5. On hit — return `null` (interval already fired).
|
|
54375
54452
|
* 6. On miss — call `fn`. If non-null, write to disk and return result. If null, skip write and return null.
|
|
@@ -54377,12 +54454,13 @@ class IntervalFileInstance {
|
|
|
54377
54454
|
* Requires active method context and execution context.
|
|
54378
54455
|
*
|
|
54379
54456
|
* @param symbol - Trading pair symbol (e.g. "BTCUSDT")
|
|
54457
|
+
* @param args - Additional arguments forwarded to the wrapped function
|
|
54380
54458
|
* @returns The value on the first non-null fire, `null` if already fired this interval
|
|
54381
54459
|
* or if `fn` itself returned `null`
|
|
54382
54460
|
* @throws Error if method context, execution context, or interval is missing
|
|
54383
54461
|
*/
|
|
54384
|
-
this.run = async (
|
|
54385
|
-
backtest.loggerService.debug(INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN, {
|
|
54462
|
+
this.run = async (...args) => {
|
|
54463
|
+
backtest.loggerService.debug(INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN, { args });
|
|
54386
54464
|
const step = INTERVAL_MINUTES[this.interval];
|
|
54387
54465
|
{
|
|
54388
54466
|
if (!MethodContextService.hasContext()) {
|
|
@@ -54395,15 +54473,16 @@ class IntervalFileInstance {
|
|
|
54395
54473
|
throw new Error(`IntervalFileInstance unknown interval=${this.interval}`);
|
|
54396
54474
|
}
|
|
54397
54475
|
}
|
|
54476
|
+
const [symbol, ...rest] = args;
|
|
54398
54477
|
const { when } = backtest.executionContextService.context;
|
|
54399
|
-
const
|
|
54478
|
+
const alignedMs = align(when.getTime(), this.interval);
|
|
54400
54479
|
const bucket = `${this.name}_${this.interval}_${this.index}`;
|
|
54401
|
-
const entityKey =
|
|
54480
|
+
const entityKey = this.key([symbol, alignedMs, ...rest]);
|
|
54402
54481
|
const cached = await PersistIntervalAdapter.readIntervalData(bucket, entityKey);
|
|
54403
54482
|
if (cached !== null) {
|
|
54404
54483
|
return null;
|
|
54405
54484
|
}
|
|
54406
|
-
const result = await this.fn(
|
|
54485
|
+
const result = await this.fn.call(null, ...args);
|
|
54407
54486
|
if (result !== null) {
|
|
54408
54487
|
await PersistIntervalAdapter.writeIntervalData({ id: entityKey, data: result, removed: false }, bucket, entityKey);
|
|
54409
54488
|
}
|
|
@@ -54444,12 +54523,12 @@ class IntervalUtils {
|
|
|
54444
54523
|
* Memoized factory to get or create an `IntervalFnInstance` for a function.
|
|
54445
54524
|
* Each function reference gets its own isolated instance.
|
|
54446
54525
|
*/
|
|
54447
|
-
this._getInstance = memoize(([run]) => run, (run, interval) => new IntervalFnInstance(run, interval));
|
|
54526
|
+
this._getInstance = memoize(([run]) => run, (run, interval, key) => new IntervalFnInstance(run, interval, key));
|
|
54448
54527
|
/**
|
|
54449
54528
|
* Memoized factory to get or create an `IntervalFileInstance` for an async function.
|
|
54450
54529
|
* Each function reference gets its own isolated persistent instance.
|
|
54451
54530
|
*/
|
|
54452
|
-
this._getFileInstance = memoize(([run]) => run, (run, interval, name) => new IntervalFileInstance(run, interval, name));
|
|
54531
|
+
this._getFileInstance = memoize(([run]) => run, (run, interval, name, key) => new IntervalFileInstance(run, interval, name, key));
|
|
54453
54532
|
/**
|
|
54454
54533
|
* Wrap a signal function with in-memory once-per-interval firing.
|
|
54455
54534
|
*
|
|
@@ -54461,21 +54540,30 @@ class IntervalUtils {
|
|
|
54461
54540
|
*
|
|
54462
54541
|
* @param run - Signal function to wrap
|
|
54463
54542
|
* @param context.interval - Candle interval that controls the firing boundary
|
|
54464
|
-
* @
|
|
54543
|
+
* @param context.key - Optional key generator for argument-based state separation
|
|
54544
|
+
* @returns Wrapped function with the same signature as `F`, plus a `clear()` method
|
|
54465
54545
|
*
|
|
54466
54546
|
* @example
|
|
54467
54547
|
* ```typescript
|
|
54548
|
+
* // Without extra args
|
|
54468
54549
|
* const fireOnce = Interval.fn(mySignalFn, { interval: "15m" });
|
|
54469
|
-
*
|
|
54470
54550
|
* await fireOnce("BTCUSDT"); // → T or null (fn called)
|
|
54471
54551
|
* await fireOnce("BTCUSDT"); // → null (same interval, skipped)
|
|
54552
|
+
*
|
|
54553
|
+
* // With extra args and key
|
|
54554
|
+
* const fireOnce = Interval.fn(mySignalFn, {
|
|
54555
|
+
* interval: "15m",
|
|
54556
|
+
* key: ([symbol, period]) => `${symbol}_${period}`,
|
|
54557
|
+
* });
|
|
54558
|
+
* await fireOnce("BTCUSDT", 14); // → T or null
|
|
54559
|
+
* await fireOnce("BTCUSDT", 28); // → T or null (separate state)
|
|
54472
54560
|
* ```
|
|
54473
54561
|
*/
|
|
54474
54562
|
this.fn = (run, context) => {
|
|
54475
54563
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_FN, { context });
|
|
54476
|
-
const wrappedFn = (
|
|
54477
|
-
const instance = this._getInstance(run, context.interval);
|
|
54478
|
-
return instance.run(
|
|
54564
|
+
const wrappedFn = (...args) => {
|
|
54565
|
+
const instance = this._getInstance(run, context.interval, context.key);
|
|
54566
|
+
return instance.run(...args);
|
|
54479
54567
|
};
|
|
54480
54568
|
wrappedFn.clear = () => {
|
|
54481
54569
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_FN_CLEAR);
|
|
@@ -54489,6 +54577,14 @@ class IntervalUtils {
|
|
|
54489
54577
|
}
|
|
54490
54578
|
this._getInstance.get(run)?.clear();
|
|
54491
54579
|
};
|
|
54580
|
+
wrappedFn.gc = () => {
|
|
54581
|
+
backtest.loggerService.info(INTERVAL_METHOD_NAME_FN_GC);
|
|
54582
|
+
if (!ExecutionContextService.hasContext()) {
|
|
54583
|
+
backtest.loggerService.warn(`${INTERVAL_METHOD_NAME_FN_GC} called without execution context, skipping`);
|
|
54584
|
+
return;
|
|
54585
|
+
}
|
|
54586
|
+
return this._getInstance.get(run)?.gc();
|
|
54587
|
+
};
|
|
54492
54588
|
return wrappedFn;
|
|
54493
54589
|
};
|
|
54494
54590
|
/**
|
|
@@ -54501,28 +54597,33 @@ class IntervalUtils {
|
|
|
54501
54597
|
* The `run` function reference is used as the memoization key for the underlying
|
|
54502
54598
|
* `IntervalFileInstance`, so each unique function reference gets its own isolated instance.
|
|
54503
54599
|
*
|
|
54504
|
-
* @template
|
|
54600
|
+
* @template F - Concrete async function type
|
|
54505
54601
|
* @param run - Async signal function to wrap with persistent once-per-interval firing
|
|
54506
54602
|
* @param context.interval - Candle interval that controls the firing boundary
|
|
54507
54603
|
* @param context.name - Human-readable bucket name; becomes the directory prefix
|
|
54508
|
-
* @
|
|
54509
|
-
*
|
|
54604
|
+
* @param context.key - Optional entity key generator. Receives `[symbol, alignMs, ...rest]`.
|
|
54605
|
+
* Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
|
|
54606
|
+
* @returns Wrapped function with the same signature as `F`, plus an async `clear()` method
|
|
54510
54607
|
*
|
|
54511
54608
|
* @example
|
|
54512
54609
|
* ```typescript
|
|
54513
|
-
* const fetchSignal = async (symbol: string,
|
|
54514
|
-
* const fireOnce = Interval.file(fetchSignal, {
|
|
54515
|
-
*
|
|
54610
|
+
* const fetchSignal = async (symbol: string, period: number) => { ... };
|
|
54611
|
+
* const fireOnce = Interval.file(fetchSignal, {
|
|
54612
|
+
* interval: "1h",
|
|
54613
|
+
* name: "fetchSignal",
|
|
54614
|
+
* key: ([symbol, alignMs, period]) => `${symbol}_${alignMs}_${period}`,
|
|
54615
|
+
* });
|
|
54616
|
+
* await fireOnce("BTCUSDT", 14);
|
|
54516
54617
|
* ```
|
|
54517
54618
|
*/
|
|
54518
54619
|
this.file = (run, context) => {
|
|
54519
54620
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_FILE, { context });
|
|
54520
54621
|
{
|
|
54521
|
-
this._getFileInstance(run, context.interval, context.name);
|
|
54622
|
+
this._getFileInstance(run, context.interval, context.name, context.key);
|
|
54522
54623
|
}
|
|
54523
|
-
const wrappedFn = (
|
|
54524
|
-
const instance = this._getFileInstance(run, context.interval, context.name);
|
|
54525
|
-
return instance.run(
|
|
54624
|
+
const wrappedFn = (...args) => {
|
|
54625
|
+
const instance = this._getFileInstance(run, context.interval, context.name, context.key);
|
|
54626
|
+
return instance.run(...args);
|
|
54526
54627
|
};
|
|
54527
54628
|
wrappedFn.clear = async () => {
|
|
54528
54629
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_FILE_CLEAR);
|
|
@@ -54548,10 +54649,10 @@ class IntervalUtils {
|
|
|
54548
54649
|
this.dispose = (run) => {
|
|
54549
54650
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_DISPOSE, { run });
|
|
54550
54651
|
this._getInstance.clear(run);
|
|
54652
|
+
this._getFileInstance.clear(run);
|
|
54551
54653
|
};
|
|
54552
54654
|
/**
|
|
54553
|
-
* Clears all memoized `IntervalFnInstance` and `IntervalFileInstance` objects
|
|
54554
|
-
* resets the `IntervalFileInstance` index counter.
|
|
54655
|
+
* Clears all memoized `IntervalFnInstance` and `IntervalFileInstance` objects.
|
|
54555
54656
|
* Call this when `process.cwd()` changes between strategy iterations
|
|
54556
54657
|
* so new instances are created with the updated base path.
|
|
54557
54658
|
*/
|
|
@@ -54559,7 +54660,15 @@ class IntervalUtils {
|
|
|
54559
54660
|
backtest.loggerService.info(INTERVAL_METHOD_NAME_CLEAR);
|
|
54560
54661
|
this._getInstance.clear();
|
|
54561
54662
|
this._getFileInstance.clear();
|
|
54562
|
-
|
|
54663
|
+
};
|
|
54664
|
+
/**
|
|
54665
|
+
* Resets the IntervalFileInstance index counter to zero.
|
|
54666
|
+
* This is useful when process.cwd() changes between strategy iterations to ensure
|
|
54667
|
+
* that new IntervalFileInstance objects start with index 0 and do not collide with old instances.
|
|
54668
|
+
*/
|
|
54669
|
+
this.resetCounter = () => {
|
|
54670
|
+
backtest.loggerService.info(INTERVAL_METHOD_NAME_RESET_COUNTER);
|
|
54671
|
+
IntervalFileInstance.resetCounter();
|
|
54563
54672
|
};
|
|
54564
54673
|
}
|
|
54565
54674
|
}
|