backtest-kit 5.1.0 → 5.2.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 +102 -67
- package/build/index.mjs +102 -67
- package/package.json +1 -1
- package/types.d.ts +14 -0
package/build/index.cjs
CHANGED
|
@@ -5064,7 +5064,7 @@ const CALL_SCHEDULE_PING_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (s
|
|
|
5064
5064
|
await ExecutionContextService.runInContext(async () => {
|
|
5065
5065
|
const publicSignal = TO_PUBLIC_SIGNAL(scheduled, currentPrice);
|
|
5066
5066
|
// Call system onSchedulePing callback first (emits to pingSubject)
|
|
5067
|
-
await self.params.onSchedulePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, self.params.execution.context.backtest, timestamp);
|
|
5067
|
+
await self.params.onSchedulePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, currentPrice, self.params.execution.context.backtest, timestamp);
|
|
5068
5068
|
// Call user onSchedulePing callback only if signal is still active (not cancelled, not activated)
|
|
5069
5069
|
if (self.params.callbacks?.onSchedulePing) {
|
|
5070
5070
|
await self.params.callbacks.onSchedulePing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
|
|
@@ -5090,7 +5090,7 @@ const CALL_ACTIVE_PING_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (sel
|
|
|
5090
5090
|
await ExecutionContextService.runInContext(async () => {
|
|
5091
5091
|
const publicSignal = TO_PUBLIC_SIGNAL(pending, currentPrice);
|
|
5092
5092
|
// Call system onActivePing callback first (emits to activePingSubject)
|
|
5093
|
-
await self.params.onActivePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, self.params.execution.context.backtest, timestamp);
|
|
5093
|
+
await self.params.onActivePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, currentPrice, self.params.execution.context.backtest, timestamp);
|
|
5094
5094
|
// Call user onActivePing callback only if signal is still active (not closed)
|
|
5095
5095
|
if (self.params.callbacks?.onActivePing) {
|
|
5096
5096
|
await self.params.callbacks.onActivePing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
|
|
@@ -5888,6 +5888,57 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
5888
5888
|
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
5889
5889
|
return result;
|
|
5890
5890
|
};
|
|
5891
|
+
const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, averagePrice, closeTimestamp) => {
|
|
5892
|
+
const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, averagePrice, "closed", closedSignal, self);
|
|
5893
|
+
if (!syncCloseAllowed) {
|
|
5894
|
+
self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
|
|
5895
|
+
symbol: self.params.execution.context.symbol,
|
|
5896
|
+
signalId: closedSignal.id,
|
|
5897
|
+
});
|
|
5898
|
+
self._closedSignal = null;
|
|
5899
|
+
self._pendingSignal = closedSignal;
|
|
5900
|
+
throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
|
|
5901
|
+
`Retry backtest() with new candle data.`);
|
|
5902
|
+
}
|
|
5903
|
+
self._closedSignal = null;
|
|
5904
|
+
await CALL_COMMIT_FN(self, {
|
|
5905
|
+
action: "close-pending",
|
|
5906
|
+
symbol: self.params.execution.context.symbol,
|
|
5907
|
+
strategyName: self.params.strategyName,
|
|
5908
|
+
exchangeName: self.params.exchangeName,
|
|
5909
|
+
frameName: self.params.frameName,
|
|
5910
|
+
signalId: closedSignal.id,
|
|
5911
|
+
backtest: true,
|
|
5912
|
+
closeId: closedSignal.closeId,
|
|
5913
|
+
timestamp: closeTimestamp,
|
|
5914
|
+
totalEntries: closedSignal._entry?.length ?? 1,
|
|
5915
|
+
totalPartials: closedSignal._partial?.length ?? 0,
|
|
5916
|
+
originalPriceOpen: closedSignal.priceOpen,
|
|
5917
|
+
pnl: toProfitLossDto(closedSignal, averagePrice),
|
|
5918
|
+
});
|
|
5919
|
+
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
5920
|
+
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
5921
|
+
await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
5922
|
+
await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
|
|
5923
|
+
const pnl = toProfitLossDto(closedSignal, averagePrice);
|
|
5924
|
+
const result = {
|
|
5925
|
+
action: "closed",
|
|
5926
|
+
signal: TO_PUBLIC_SIGNAL(closedSignal, averagePrice),
|
|
5927
|
+
currentPrice: averagePrice,
|
|
5928
|
+
closeReason: "closed",
|
|
5929
|
+
closeTimestamp,
|
|
5930
|
+
pnl,
|
|
5931
|
+
strategyName: self.params.method.context.strategyName,
|
|
5932
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
5933
|
+
frameName: self.params.method.context.frameName,
|
|
5934
|
+
symbol: self.params.execution.context.symbol,
|
|
5935
|
+
backtest: self.params.execution.context.backtest,
|
|
5936
|
+
closeId: closedSignal.closeId,
|
|
5937
|
+
createdAt: closeTimestamp,
|
|
5938
|
+
};
|
|
5939
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
5940
|
+
return result;
|
|
5941
|
+
};
|
|
5891
5942
|
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
5892
5943
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
5893
5944
|
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
@@ -5905,7 +5956,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5905
5956
|
if (self._cancelledSignal) {
|
|
5906
5957
|
// Сигнал был отменен через cancel() в onSchedulePing
|
|
5907
5958
|
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "user");
|
|
5908
|
-
return {
|
|
5959
|
+
return { outcome: "cancelled", result };
|
|
5909
5960
|
}
|
|
5910
5961
|
// КРИТИЧНО: Проверяем был ли сигнал активирован пользователем через activateScheduled()
|
|
5911
5962
|
// Обрабатываем inline (как в tick()) с риск-проверкой по averagePrice
|
|
@@ -5919,7 +5970,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5919
5970
|
signalId: activatedSignal.id,
|
|
5920
5971
|
});
|
|
5921
5972
|
await self.setScheduledSignal(null);
|
|
5922
|
-
return {
|
|
5973
|
+
return { outcome: "pending" };
|
|
5923
5974
|
}
|
|
5924
5975
|
// Риск-проверка по averagePrice (симметрия с LIVE tick())
|
|
5925
5976
|
if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, averagePrice, candle.timestamp, self.params.execution.context.backtest))) {
|
|
@@ -5928,7 +5979,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5928
5979
|
signalId: activatedSignal.id,
|
|
5929
5980
|
});
|
|
5930
5981
|
await self.setScheduledSignal(null);
|
|
5931
|
-
return {
|
|
5982
|
+
return { outcome: "pending" };
|
|
5932
5983
|
}
|
|
5933
5984
|
const pendingSignal = {
|
|
5934
5985
|
...activatedSignal,
|
|
@@ -5957,7 +6008,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5957
6008
|
originalPriceOpen: activatedSignal.priceOpen,
|
|
5958
6009
|
pnl: toProfitLossDto(activatedSignal, averagePrice),
|
|
5959
6010
|
});
|
|
5960
|
-
return {
|
|
6011
|
+
return { outcome: "pending" };
|
|
5961
6012
|
}
|
|
5962
6013
|
await self.setScheduledSignal(null);
|
|
5963
6014
|
await self.setPendingSignal(pendingSignal);
|
|
@@ -5990,18 +6041,13 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5990
6041
|
});
|
|
5991
6042
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
|
|
5992
6043
|
await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
|
|
5993
|
-
return {
|
|
5994
|
-
activated: true,
|
|
5995
|
-
cancelled: false,
|
|
5996
|
-
activationIndex: i,
|
|
5997
|
-
result: null,
|
|
5998
|
-
};
|
|
6044
|
+
return { outcome: "activated", activationIndex: i };
|
|
5999
6045
|
}
|
|
6000
6046
|
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
6001
6047
|
const elapsedTime = candle.timestamp - scheduled.scheduledAt;
|
|
6002
6048
|
if (elapsedTime >= maxTimeToWait) {
|
|
6003
6049
|
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "timeout");
|
|
6004
|
-
return {
|
|
6050
|
+
return { outcome: "cancelled", result };
|
|
6005
6051
|
}
|
|
6006
6052
|
let shouldActivate = false;
|
|
6007
6053
|
let shouldCancel = false;
|
|
@@ -6039,27 +6085,17 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
6039
6085
|
}
|
|
6040
6086
|
if (shouldCancel) {
|
|
6041
6087
|
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "price_reject");
|
|
6042
|
-
return {
|
|
6088
|
+
return { outcome: "cancelled", result };
|
|
6043
6089
|
}
|
|
6044
6090
|
if (shouldActivate) {
|
|
6045
6091
|
await ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, candle.timestamp);
|
|
6046
|
-
return {
|
|
6047
|
-
activated: true,
|
|
6048
|
-
cancelled: false,
|
|
6049
|
-
activationIndex: i,
|
|
6050
|
-
result: null,
|
|
6051
|
-
};
|
|
6092
|
+
return { outcome: "activated", activationIndex: i };
|
|
6052
6093
|
}
|
|
6053
6094
|
await CALL_SCHEDULE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, candle.timestamp, true, averagePrice);
|
|
6054
6095
|
// Process queued commit events with candle timestamp
|
|
6055
6096
|
await PROCESS_COMMIT_QUEUE_FN(self, averagePrice, candle.timestamp);
|
|
6056
6097
|
}
|
|
6057
|
-
return {
|
|
6058
|
-
activated: false,
|
|
6059
|
-
cancelled: false,
|
|
6060
|
-
activationIndex: -1,
|
|
6061
|
-
result: null,
|
|
6062
|
-
};
|
|
6098
|
+
return { outcome: "pending" };
|
|
6063
6099
|
};
|
|
6064
6100
|
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
6065
6101
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
@@ -6078,6 +6114,10 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
6078
6114
|
const startIndex = Math.max(0, i - (candlesCount - 1));
|
|
6079
6115
|
const recentCandles = candles.slice(startIndex, i + 1);
|
|
6080
6116
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
6117
|
+
// КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
|
|
6118
|
+
if (self._closedSignal) {
|
|
6119
|
+
return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
|
|
6120
|
+
}
|
|
6081
6121
|
await CALL_ACTIVE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentCandleTimestamp, true, averagePrice);
|
|
6082
6122
|
let shouldClose = false;
|
|
6083
6123
|
let closeReason;
|
|
@@ -6182,7 +6222,32 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
6182
6222
|
// Process queued commit events with candle timestamp
|
|
6183
6223
|
await PROCESS_COMMIT_QUEUE_FN(self, averagePrice, currentCandleTimestamp);
|
|
6184
6224
|
}
|
|
6185
|
-
|
|
6225
|
+
// Loop exhausted without closing — check if we have enough data
|
|
6226
|
+
const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
6227
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
6228
|
+
const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
|
|
6229
|
+
const signalTime = signal.pendingAt;
|
|
6230
|
+
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
6231
|
+
const elapsedTime = closeTimestamp - signalTime;
|
|
6232
|
+
if (elapsedTime < maxTimeToWait) {
|
|
6233
|
+
const bufferCandlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
6234
|
+
const requiredCandlesCount = signal.minuteEstimatedTime + bufferCandlesCount + 1;
|
|
6235
|
+
throw new Error(functoolsKit.str.newline(`ClientStrategy backtest: Insufficient candle data for pending signal. ` +
|
|
6236
|
+
`Signal opened at ${new Date(signal.pendingAt).toISOString()}, ` +
|
|
6237
|
+
`last candle at ${new Date(closeTimestamp).toISOString()}. ` +
|
|
6238
|
+
`Elapsed: ${Math.floor(elapsedTime / 60000)}min of ${signal.minuteEstimatedTime}min required. ` +
|
|
6239
|
+
`Provided ${candles.length} candles, but need at least ${requiredCandlesCount} candles. ` +
|
|
6240
|
+
`\nBreakdown: ${signal.minuteEstimatedTime} candles for signal lifetime + ${bufferCandlesCount} buffer candles. ` +
|
|
6241
|
+
`\nBuffer explanation: VWAP calculation requires ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT} candles, ` +
|
|
6242
|
+
`so first ${bufferCandlesCount} candles are skipped to ensure accurate price averaging. ` +
|
|
6243
|
+
`Provide complete candle range: [pendingAt - ${bufferCandlesCount}min, pendingAt + ${signal.minuteEstimatedTime}min].`));
|
|
6244
|
+
}
|
|
6245
|
+
const timeExpiredResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, lastPrice, "time_expired", closeTimestamp);
|
|
6246
|
+
if (!timeExpiredResult) {
|
|
6247
|
+
throw new Error(`ClientStrategy backtest: time_expired close rejected by sync (signalId=${signal.id}). ` +
|
|
6248
|
+
`Retry backtest() with new candle data.`);
|
|
6249
|
+
}
|
|
6250
|
+
return timeExpiredResult;
|
|
6186
6251
|
};
|
|
6187
6252
|
/**
|
|
6188
6253
|
* Client implementation for trading strategy lifecycle management.
|
|
@@ -7129,11 +7194,12 @@ class ClientStrategy {
|
|
|
7129
7194
|
priceOpen: scheduled.priceOpen,
|
|
7130
7195
|
position: scheduled.position,
|
|
7131
7196
|
});
|
|
7132
|
-
const
|
|
7133
|
-
if (
|
|
7134
|
-
return result;
|
|
7197
|
+
const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
|
|
7198
|
+
if (scheduledResult.outcome === "cancelled") {
|
|
7199
|
+
return scheduledResult.result;
|
|
7135
7200
|
}
|
|
7136
|
-
if (activated) {
|
|
7201
|
+
if (scheduledResult.outcome === "activated") {
|
|
7202
|
+
const { activationIndex } = scheduledResult;
|
|
7137
7203
|
// КРИТИЧНО: activationIndex - индекс свечи активации в массиве candles
|
|
7138
7204
|
// BacktestLogicPrivateService включил буфер в начало массива, поэтому перед activationIndex достаточно свечей
|
|
7139
7205
|
// PROCESS_PENDING_SIGNAL_CANDLES_FN пропустит первые bufferCandlesCount свечей для VWAP
|
|
@@ -7205,40 +7271,7 @@ class ClientStrategy {
|
|
|
7205
7271
|
if (candles.length < candlesCount) {
|
|
7206
7272
|
this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
|
|
7207
7273
|
}
|
|
7208
|
-
|
|
7209
|
-
if (closedResult) {
|
|
7210
|
-
return closedResult;
|
|
7211
|
-
}
|
|
7212
|
-
// Signal didn't close during candle processing - check if we have enough data
|
|
7213
|
-
const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
7214
|
-
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
7215
|
-
const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
|
|
7216
|
-
const signalTime = signal.pendingAt;
|
|
7217
|
-
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
7218
|
-
const elapsedTime = closeTimestamp - signalTime;
|
|
7219
|
-
// Check if we actually reached time expiration or just ran out of candles
|
|
7220
|
-
if (elapsedTime < maxTimeToWait) {
|
|
7221
|
-
// EDGE CASE: backtest() called with insufficient candle data
|
|
7222
|
-
const bufferCandlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
7223
|
-
const requiredCandlesCount = signal.minuteEstimatedTime + bufferCandlesCount + 1;
|
|
7224
|
-
throw new Error(functoolsKit.str.newline(`ClientStrategy backtest: Insufficient candle data for pending signal. ` +
|
|
7225
|
-
`Signal opened at ${new Date(signal.pendingAt).toISOString()}, ` +
|
|
7226
|
-
`last candle at ${new Date(closeTimestamp).toISOString()}. ` +
|
|
7227
|
-
`Elapsed: ${Math.floor(elapsedTime / 60000)}min of ${signal.minuteEstimatedTime}min required. ` +
|
|
7228
|
-
`Provided ${candles.length} candles, but need at least ${requiredCandlesCount} candles. ` +
|
|
7229
|
-
`\nBreakdown: ${signal.minuteEstimatedTime} candles for signal lifetime + ${bufferCandlesCount} buffer candles. ` +
|
|
7230
|
-
`\nBuffer explanation: VWAP calculation requires ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT} candles, ` +
|
|
7231
|
-
`so first ${bufferCandlesCount} candles are skipped to ensure accurate price averaging. ` +
|
|
7232
|
-
`Provide complete candle range: [pendingAt - ${bufferCandlesCount}min, pendingAt + ${signal.minuteEstimatedTime}min].`));
|
|
7233
|
-
}
|
|
7234
|
-
// Time actually expired - close with time_expired
|
|
7235
|
-
const timeExpiredResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, signal, lastPrice, "time_expired", closeTimestamp);
|
|
7236
|
-
if (!timeExpiredResult) {
|
|
7237
|
-
// Sync rejected the close — signal remains in _pendingSignal, caller must retry
|
|
7238
|
-
throw new Error(`ClientStrategy backtest: time_expired close rejected by sync (signalId=${signal.id}). ` +
|
|
7239
|
-
`Retry backtest() with new candle data.`);
|
|
7240
|
-
}
|
|
7241
|
-
return timeExpiredResult;
|
|
7274
|
+
return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
|
|
7242
7275
|
}
|
|
7243
7276
|
/**
|
|
7244
7277
|
* Stops the strategy from generating new signals.
|
|
@@ -9009,11 +9042,12 @@ const CREATE_KEY_FN$o = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
9009
9042
|
* @param self - Reference to StrategyConnectionService instance
|
|
9010
9043
|
* @returns Callback function for schedule ping events
|
|
9011
9044
|
*/
|
|
9012
|
-
const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, data, backtest, timestamp) => {
|
|
9045
|
+
const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp) => {
|
|
9013
9046
|
const event = {
|
|
9014
9047
|
symbol,
|
|
9015
9048
|
strategyName,
|
|
9016
9049
|
exchangeName,
|
|
9050
|
+
currentPrice,
|
|
9017
9051
|
data,
|
|
9018
9052
|
backtest,
|
|
9019
9053
|
timestamp,
|
|
@@ -9042,11 +9076,12 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (sy
|
|
|
9042
9076
|
* @param self - Reference to StrategyConnectionService instance
|
|
9043
9077
|
* @returns Callback function for active ping events
|
|
9044
9078
|
*/
|
|
9045
|
-
const CREATE_COMMIT_ACTIVE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, data, backtest, timestamp) => {
|
|
9079
|
+
const CREATE_COMMIT_ACTIVE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp) => {
|
|
9046
9080
|
const event = {
|
|
9047
9081
|
symbol,
|
|
9048
9082
|
strategyName,
|
|
9049
9083
|
exchangeName,
|
|
9084
|
+
currentPrice,
|
|
9050
9085
|
data,
|
|
9051
9086
|
backtest,
|
|
9052
9087
|
timestamp,
|
package/build/index.mjs
CHANGED
|
@@ -5044,7 +5044,7 @@ const CALL_SCHEDULE_PING_CALLBACKS_FN = trycatch(beginTime(async (self, symbol,
|
|
|
5044
5044
|
await ExecutionContextService.runInContext(async () => {
|
|
5045
5045
|
const publicSignal = TO_PUBLIC_SIGNAL(scheduled, currentPrice);
|
|
5046
5046
|
// Call system onSchedulePing callback first (emits to pingSubject)
|
|
5047
|
-
await self.params.onSchedulePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, self.params.execution.context.backtest, timestamp);
|
|
5047
|
+
await self.params.onSchedulePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, currentPrice, self.params.execution.context.backtest, timestamp);
|
|
5048
5048
|
// Call user onSchedulePing callback only if signal is still active (not cancelled, not activated)
|
|
5049
5049
|
if (self.params.callbacks?.onSchedulePing) {
|
|
5050
5050
|
await self.params.callbacks.onSchedulePing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
|
|
@@ -5070,7 +5070,7 @@ const CALL_ACTIVE_PING_CALLBACKS_FN = trycatch(beginTime(async (self, symbol, pe
|
|
|
5070
5070
|
await ExecutionContextService.runInContext(async () => {
|
|
5071
5071
|
const publicSignal = TO_PUBLIC_SIGNAL(pending, currentPrice);
|
|
5072
5072
|
// Call system onActivePing callback first (emits to activePingSubject)
|
|
5073
|
-
await self.params.onActivePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, self.params.execution.context.backtest, timestamp);
|
|
5073
|
+
await self.params.onActivePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, currentPrice, self.params.execution.context.backtest, timestamp);
|
|
5074
5074
|
// Call user onActivePing callback only if signal is still active (not closed)
|
|
5075
5075
|
if (self.params.callbacks?.onActivePing) {
|
|
5076
5076
|
await self.params.callbacks.onActivePing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
|
|
@@ -5868,6 +5868,57 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
5868
5868
|
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
5869
5869
|
return result;
|
|
5870
5870
|
};
|
|
5871
|
+
const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, averagePrice, closeTimestamp) => {
|
|
5872
|
+
const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, averagePrice, "closed", closedSignal, self);
|
|
5873
|
+
if (!syncCloseAllowed) {
|
|
5874
|
+
self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
|
|
5875
|
+
symbol: self.params.execution.context.symbol,
|
|
5876
|
+
signalId: closedSignal.id,
|
|
5877
|
+
});
|
|
5878
|
+
self._closedSignal = null;
|
|
5879
|
+
self._pendingSignal = closedSignal;
|
|
5880
|
+
throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
|
|
5881
|
+
`Retry backtest() with new candle data.`);
|
|
5882
|
+
}
|
|
5883
|
+
self._closedSignal = null;
|
|
5884
|
+
await CALL_COMMIT_FN(self, {
|
|
5885
|
+
action: "close-pending",
|
|
5886
|
+
symbol: self.params.execution.context.symbol,
|
|
5887
|
+
strategyName: self.params.strategyName,
|
|
5888
|
+
exchangeName: self.params.exchangeName,
|
|
5889
|
+
frameName: self.params.frameName,
|
|
5890
|
+
signalId: closedSignal.id,
|
|
5891
|
+
backtest: true,
|
|
5892
|
+
closeId: closedSignal.closeId,
|
|
5893
|
+
timestamp: closeTimestamp,
|
|
5894
|
+
totalEntries: closedSignal._entry?.length ?? 1,
|
|
5895
|
+
totalPartials: closedSignal._partial?.length ?? 0,
|
|
5896
|
+
originalPriceOpen: closedSignal.priceOpen,
|
|
5897
|
+
pnl: toProfitLossDto(closedSignal, averagePrice),
|
|
5898
|
+
});
|
|
5899
|
+
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
5900
|
+
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
5901
|
+
await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
5902
|
+
await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
|
|
5903
|
+
const pnl = toProfitLossDto(closedSignal, averagePrice);
|
|
5904
|
+
const result = {
|
|
5905
|
+
action: "closed",
|
|
5906
|
+
signal: TO_PUBLIC_SIGNAL(closedSignal, averagePrice),
|
|
5907
|
+
currentPrice: averagePrice,
|
|
5908
|
+
closeReason: "closed",
|
|
5909
|
+
closeTimestamp,
|
|
5910
|
+
pnl,
|
|
5911
|
+
strategyName: self.params.method.context.strategyName,
|
|
5912
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
5913
|
+
frameName: self.params.method.context.frameName,
|
|
5914
|
+
symbol: self.params.execution.context.symbol,
|
|
5915
|
+
backtest: self.params.execution.context.backtest,
|
|
5916
|
+
closeId: closedSignal.closeId,
|
|
5917
|
+
createdAt: closeTimestamp,
|
|
5918
|
+
};
|
|
5919
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
5920
|
+
return result;
|
|
5921
|
+
};
|
|
5871
5922
|
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
5872
5923
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
5873
5924
|
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
@@ -5885,7 +5936,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5885
5936
|
if (self._cancelledSignal) {
|
|
5886
5937
|
// Сигнал был отменен через cancel() в onSchedulePing
|
|
5887
5938
|
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "user");
|
|
5888
|
-
return {
|
|
5939
|
+
return { outcome: "cancelled", result };
|
|
5889
5940
|
}
|
|
5890
5941
|
// КРИТИЧНО: Проверяем был ли сигнал активирован пользователем через activateScheduled()
|
|
5891
5942
|
// Обрабатываем inline (как в tick()) с риск-проверкой по averagePrice
|
|
@@ -5899,7 +5950,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5899
5950
|
signalId: activatedSignal.id,
|
|
5900
5951
|
});
|
|
5901
5952
|
await self.setScheduledSignal(null);
|
|
5902
|
-
return {
|
|
5953
|
+
return { outcome: "pending" };
|
|
5903
5954
|
}
|
|
5904
5955
|
// Риск-проверка по averagePrice (симметрия с LIVE tick())
|
|
5905
5956
|
if (await not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, averagePrice, candle.timestamp, self.params.execution.context.backtest))) {
|
|
@@ -5908,7 +5959,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5908
5959
|
signalId: activatedSignal.id,
|
|
5909
5960
|
});
|
|
5910
5961
|
await self.setScheduledSignal(null);
|
|
5911
|
-
return {
|
|
5962
|
+
return { outcome: "pending" };
|
|
5912
5963
|
}
|
|
5913
5964
|
const pendingSignal = {
|
|
5914
5965
|
...activatedSignal,
|
|
@@ -5937,7 +5988,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5937
5988
|
originalPriceOpen: activatedSignal.priceOpen,
|
|
5938
5989
|
pnl: toProfitLossDto(activatedSignal, averagePrice),
|
|
5939
5990
|
});
|
|
5940
|
-
return {
|
|
5991
|
+
return { outcome: "pending" };
|
|
5941
5992
|
}
|
|
5942
5993
|
await self.setScheduledSignal(null);
|
|
5943
5994
|
await self.setPendingSignal(pendingSignal);
|
|
@@ -5970,18 +6021,13 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
5970
6021
|
});
|
|
5971
6022
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
|
|
5972
6023
|
await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
|
|
5973
|
-
return {
|
|
5974
|
-
activated: true,
|
|
5975
|
-
cancelled: false,
|
|
5976
|
-
activationIndex: i,
|
|
5977
|
-
result: null,
|
|
5978
|
-
};
|
|
6024
|
+
return { outcome: "activated", activationIndex: i };
|
|
5979
6025
|
}
|
|
5980
6026
|
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
5981
6027
|
const elapsedTime = candle.timestamp - scheduled.scheduledAt;
|
|
5982
6028
|
if (elapsedTime >= maxTimeToWait) {
|
|
5983
6029
|
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "timeout");
|
|
5984
|
-
return {
|
|
6030
|
+
return { outcome: "cancelled", result };
|
|
5985
6031
|
}
|
|
5986
6032
|
let shouldActivate = false;
|
|
5987
6033
|
let shouldCancel = false;
|
|
@@ -6019,27 +6065,17 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
6019
6065
|
}
|
|
6020
6066
|
if (shouldCancel) {
|
|
6021
6067
|
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "price_reject");
|
|
6022
|
-
return {
|
|
6068
|
+
return { outcome: "cancelled", result };
|
|
6023
6069
|
}
|
|
6024
6070
|
if (shouldActivate) {
|
|
6025
6071
|
await ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, candle.timestamp);
|
|
6026
|
-
return {
|
|
6027
|
-
activated: true,
|
|
6028
|
-
cancelled: false,
|
|
6029
|
-
activationIndex: i,
|
|
6030
|
-
result: null,
|
|
6031
|
-
};
|
|
6072
|
+
return { outcome: "activated", activationIndex: i };
|
|
6032
6073
|
}
|
|
6033
6074
|
await CALL_SCHEDULE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, candle.timestamp, true, averagePrice);
|
|
6034
6075
|
// Process queued commit events with candle timestamp
|
|
6035
6076
|
await PROCESS_COMMIT_QUEUE_FN(self, averagePrice, candle.timestamp);
|
|
6036
6077
|
}
|
|
6037
|
-
return {
|
|
6038
|
-
activated: false,
|
|
6039
|
-
cancelled: false,
|
|
6040
|
-
activationIndex: -1,
|
|
6041
|
-
result: null,
|
|
6042
|
-
};
|
|
6078
|
+
return { outcome: "pending" };
|
|
6043
6079
|
};
|
|
6044
6080
|
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
6045
6081
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
@@ -6058,6 +6094,10 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
6058
6094
|
const startIndex = Math.max(0, i - (candlesCount - 1));
|
|
6059
6095
|
const recentCandles = candles.slice(startIndex, i + 1);
|
|
6060
6096
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
6097
|
+
// КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
|
|
6098
|
+
if (self._closedSignal) {
|
|
6099
|
+
return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
|
|
6100
|
+
}
|
|
6061
6101
|
await CALL_ACTIVE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentCandleTimestamp, true, averagePrice);
|
|
6062
6102
|
let shouldClose = false;
|
|
6063
6103
|
let closeReason;
|
|
@@ -6162,7 +6202,32 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
6162
6202
|
// Process queued commit events with candle timestamp
|
|
6163
6203
|
await PROCESS_COMMIT_QUEUE_FN(self, averagePrice, currentCandleTimestamp);
|
|
6164
6204
|
}
|
|
6165
|
-
|
|
6205
|
+
// Loop exhausted without closing — check if we have enough data
|
|
6206
|
+
const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
6207
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
6208
|
+
const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
|
|
6209
|
+
const signalTime = signal.pendingAt;
|
|
6210
|
+
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
6211
|
+
const elapsedTime = closeTimestamp - signalTime;
|
|
6212
|
+
if (elapsedTime < maxTimeToWait) {
|
|
6213
|
+
const bufferCandlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
6214
|
+
const requiredCandlesCount = signal.minuteEstimatedTime + bufferCandlesCount + 1;
|
|
6215
|
+
throw new Error(str.newline(`ClientStrategy backtest: Insufficient candle data for pending signal. ` +
|
|
6216
|
+
`Signal opened at ${new Date(signal.pendingAt).toISOString()}, ` +
|
|
6217
|
+
`last candle at ${new Date(closeTimestamp).toISOString()}. ` +
|
|
6218
|
+
`Elapsed: ${Math.floor(elapsedTime / 60000)}min of ${signal.minuteEstimatedTime}min required. ` +
|
|
6219
|
+
`Provided ${candles.length} candles, but need at least ${requiredCandlesCount} candles. ` +
|
|
6220
|
+
`\nBreakdown: ${signal.minuteEstimatedTime} candles for signal lifetime + ${bufferCandlesCount} buffer candles. ` +
|
|
6221
|
+
`\nBuffer explanation: VWAP calculation requires ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT} candles, ` +
|
|
6222
|
+
`so first ${bufferCandlesCount} candles are skipped to ensure accurate price averaging. ` +
|
|
6223
|
+
`Provide complete candle range: [pendingAt - ${bufferCandlesCount}min, pendingAt + ${signal.minuteEstimatedTime}min].`));
|
|
6224
|
+
}
|
|
6225
|
+
const timeExpiredResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, lastPrice, "time_expired", closeTimestamp);
|
|
6226
|
+
if (!timeExpiredResult) {
|
|
6227
|
+
throw new Error(`ClientStrategy backtest: time_expired close rejected by sync (signalId=${signal.id}). ` +
|
|
6228
|
+
`Retry backtest() with new candle data.`);
|
|
6229
|
+
}
|
|
6230
|
+
return timeExpiredResult;
|
|
6166
6231
|
};
|
|
6167
6232
|
/**
|
|
6168
6233
|
* Client implementation for trading strategy lifecycle management.
|
|
@@ -7109,11 +7174,12 @@ class ClientStrategy {
|
|
|
7109
7174
|
priceOpen: scheduled.priceOpen,
|
|
7110
7175
|
position: scheduled.position,
|
|
7111
7176
|
});
|
|
7112
|
-
const
|
|
7113
|
-
if (
|
|
7114
|
-
return result;
|
|
7177
|
+
const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
|
|
7178
|
+
if (scheduledResult.outcome === "cancelled") {
|
|
7179
|
+
return scheduledResult.result;
|
|
7115
7180
|
}
|
|
7116
|
-
if (activated) {
|
|
7181
|
+
if (scheduledResult.outcome === "activated") {
|
|
7182
|
+
const { activationIndex } = scheduledResult;
|
|
7117
7183
|
// КРИТИЧНО: activationIndex - индекс свечи активации в массиве candles
|
|
7118
7184
|
// BacktestLogicPrivateService включил буфер в начало массива, поэтому перед activationIndex достаточно свечей
|
|
7119
7185
|
// PROCESS_PENDING_SIGNAL_CANDLES_FN пропустит первые bufferCandlesCount свечей для VWAP
|
|
@@ -7185,40 +7251,7 @@ class ClientStrategy {
|
|
|
7185
7251
|
if (candles.length < candlesCount) {
|
|
7186
7252
|
this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
|
|
7187
7253
|
}
|
|
7188
|
-
|
|
7189
|
-
if (closedResult) {
|
|
7190
|
-
return closedResult;
|
|
7191
|
-
}
|
|
7192
|
-
// Signal didn't close during candle processing - check if we have enough data
|
|
7193
|
-
const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
7194
|
-
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
7195
|
-
const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
|
|
7196
|
-
const signalTime = signal.pendingAt;
|
|
7197
|
-
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
7198
|
-
const elapsedTime = closeTimestamp - signalTime;
|
|
7199
|
-
// Check if we actually reached time expiration or just ran out of candles
|
|
7200
|
-
if (elapsedTime < maxTimeToWait) {
|
|
7201
|
-
// EDGE CASE: backtest() called with insufficient candle data
|
|
7202
|
-
const bufferCandlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
7203
|
-
const requiredCandlesCount = signal.minuteEstimatedTime + bufferCandlesCount + 1;
|
|
7204
|
-
throw new Error(str.newline(`ClientStrategy backtest: Insufficient candle data for pending signal. ` +
|
|
7205
|
-
`Signal opened at ${new Date(signal.pendingAt).toISOString()}, ` +
|
|
7206
|
-
`last candle at ${new Date(closeTimestamp).toISOString()}. ` +
|
|
7207
|
-
`Elapsed: ${Math.floor(elapsedTime / 60000)}min of ${signal.minuteEstimatedTime}min required. ` +
|
|
7208
|
-
`Provided ${candles.length} candles, but need at least ${requiredCandlesCount} candles. ` +
|
|
7209
|
-
`\nBreakdown: ${signal.minuteEstimatedTime} candles for signal lifetime + ${bufferCandlesCount} buffer candles. ` +
|
|
7210
|
-
`\nBuffer explanation: VWAP calculation requires ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT} candles, ` +
|
|
7211
|
-
`so first ${bufferCandlesCount} candles are skipped to ensure accurate price averaging. ` +
|
|
7212
|
-
`Provide complete candle range: [pendingAt - ${bufferCandlesCount}min, pendingAt + ${signal.minuteEstimatedTime}min].`));
|
|
7213
|
-
}
|
|
7214
|
-
// Time actually expired - close with time_expired
|
|
7215
|
-
const timeExpiredResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, signal, lastPrice, "time_expired", closeTimestamp);
|
|
7216
|
-
if (!timeExpiredResult) {
|
|
7217
|
-
// Sync rejected the close — signal remains in _pendingSignal, caller must retry
|
|
7218
|
-
throw new Error(`ClientStrategy backtest: time_expired close rejected by sync (signalId=${signal.id}). ` +
|
|
7219
|
-
`Retry backtest() with new candle data.`);
|
|
7220
|
-
}
|
|
7221
|
-
return timeExpiredResult;
|
|
7254
|
+
return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
|
|
7222
7255
|
}
|
|
7223
7256
|
/**
|
|
7224
7257
|
* Stops the strategy from generating new signals.
|
|
@@ -8989,11 +9022,12 @@ const CREATE_KEY_FN$o = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
8989
9022
|
* @param self - Reference to StrategyConnectionService instance
|
|
8990
9023
|
* @returns Callback function for schedule ping events
|
|
8991
9024
|
*/
|
|
8992
|
-
const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strategyName, exchangeName, data, backtest, timestamp) => {
|
|
9025
|
+
const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp) => {
|
|
8993
9026
|
const event = {
|
|
8994
9027
|
symbol,
|
|
8995
9028
|
strategyName,
|
|
8996
9029
|
exchangeName,
|
|
9030
|
+
currentPrice,
|
|
8997
9031
|
data,
|
|
8998
9032
|
backtest,
|
|
8999
9033
|
timestamp,
|
|
@@ -9022,11 +9056,12 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strateg
|
|
|
9022
9056
|
* @param self - Reference to StrategyConnectionService instance
|
|
9023
9057
|
* @returns Callback function for active ping events
|
|
9024
9058
|
*/
|
|
9025
|
-
const CREATE_COMMIT_ACTIVE_PING_FN = (self) => trycatch(async (symbol, strategyName, exchangeName, data, backtest, timestamp) => {
|
|
9059
|
+
const CREATE_COMMIT_ACTIVE_PING_FN = (self) => trycatch(async (symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp) => {
|
|
9026
9060
|
const event = {
|
|
9027
9061
|
symbol,
|
|
9028
9062
|
strategyName,
|
|
9029
9063
|
exchangeName,
|
|
9064
|
+
currentPrice,
|
|
9030
9065
|
data,
|
|
9031
9066
|
backtest,
|
|
9032
9067
|
timestamp,
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -941,6 +941,13 @@ interface SchedulePingContract {
|
|
|
941
941
|
* Contains all signal information: id, position, priceOpen, priceTakeProfit, priceStopLoss, etc.
|
|
942
942
|
*/
|
|
943
943
|
data: IScheduledSignalRow;
|
|
944
|
+
/**
|
|
945
|
+
* Current market price of the symbol at the time of the ping.
|
|
946
|
+
* Useful for users to implement custom monitoring logic based on price conditions.
|
|
947
|
+
* For example, users can choose to cancel the scheduled signal if the price moves too far from priceOpen.
|
|
948
|
+
* Note: This is the current price at the time of the ping, not necessarily the priceOpen of the signal.
|
|
949
|
+
*/
|
|
950
|
+
currentPrice: number;
|
|
944
951
|
/**
|
|
945
952
|
* Execution mode flag.
|
|
946
953
|
* - true: Event from backtest execution (historical candle data)
|
|
@@ -1015,6 +1022,13 @@ interface ActivePingContract {
|
|
|
1015
1022
|
* Contains all signal information: id, position, priceOpen, priceTakeProfit, priceStopLoss, etc.
|
|
1016
1023
|
*/
|
|
1017
1024
|
data: ISignalRow;
|
|
1025
|
+
/**
|
|
1026
|
+
* Current market price of the symbol at the time of the ping.
|
|
1027
|
+
* Useful for users to implement custom management logic based on price conditions.
|
|
1028
|
+
* For example, users can choose to close the pending signal if the price moves too far from priceOpen.
|
|
1029
|
+
* Note: This is the current price at the time of the ping, not necessarily the priceOpen of the signal.
|
|
1030
|
+
*/
|
|
1031
|
+
currentPrice: number;
|
|
1018
1032
|
/**
|
|
1019
1033
|
* Execution mode flag.
|
|
1020
1034
|
* - true: Event from backtest execution (historical candle data)
|