backtest-kit 5.9.0 → 5.10.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.mjs CHANGED
@@ -6325,7 +6325,7 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
6325
6325
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
6326
6326
  return result;
6327
6327
  };
6328
- const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
6328
+ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, frameEndTime) => {
6329
6329
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
6330
6330
  const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
6331
6331
  const bufferCandlesCount = candlesCount - 1;
@@ -6338,6 +6338,11 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
6338
6338
  }
6339
6339
  const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
6340
6340
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
6341
+ // Если timestamp свечи вышел за frameEndTime — отменяем scheduled сигнал
6342
+ if (candle.timestamp > frameEndTime) {
6343
+ const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "timeout");
6344
+ return { outcome: "cancelled", result };
6345
+ }
6341
6346
  // КРИТИЧНО: Проверяем был ли сигнал отменен пользователем через cancel()
6342
6347
  if (self._cancelledSignal) {
6343
6348
  // Сигнал был отменен через cancel() в onSchedulePing
@@ -6485,7 +6490,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
6485
6490
  }
6486
6491
  return { outcome: "pending" };
6487
6492
  };
6488
- const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6493
+ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEndTime) => {
6489
6494
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
6490
6495
  const bufferCandlesCount = candlesCount - 1;
6491
6496
  // КРИТИЧНО: проверяем TP/SL на КАЖДОЙ свече начиная после буфера
@@ -6502,6 +6507,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6502
6507
  const startIndex = Math.max(0, i - (candlesCount - 1));
6503
6508
  const recentCandles = candles.slice(startIndex, i + 1);
6504
6509
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
6510
+ // Если timestamp свечи вышел за frameEndTime — закрываем pending сигнал по time_expired
6511
+ if (currentCandleTimestamp > frameEndTime) {
6512
+ const result = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, averagePrice, "time_expired", currentCandleTimestamp);
6513
+ if (!result) {
6514
+ throw new Error(`ClientStrategy backtest: frameEndTime time_expired close rejected by sync (signalId=${signal.id}).`);
6515
+ }
6516
+ return result;
6517
+ }
6505
6518
  // КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
6506
6519
  if (self._closedSignal) {
6507
6520
  return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
@@ -7667,7 +7680,7 @@ class ClientStrategy {
7667
7680
  * console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired" | "cancelled"
7668
7681
  * ```
7669
7682
  */
7670
- async backtest(symbol, strategyName, candles) {
7683
+ async backtest(symbol, strategyName, candles, frameEndTime) {
7671
7684
  this.params.logger.debug("ClientStrategy backtest", {
7672
7685
  symbol,
7673
7686
  strategyName,
@@ -7675,6 +7688,7 @@ class ClientStrategy {
7675
7688
  candlesCount: candles.length,
7676
7689
  hasScheduled: !!this._scheduledSignal,
7677
7690
  hasPending: !!this._pendingSignal,
7691
+ frameEndTime,
7678
7692
  });
7679
7693
  if (!this.params.execution.context.backtest) {
7680
7694
  throw new Error("ClientStrategy backtest: running in live context");
@@ -7793,7 +7807,7 @@ class ClientStrategy {
7793
7807
  priceOpen: scheduled.priceOpen,
7794
7808
  position: scheduled.position,
7795
7809
  });
7796
- const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
7810
+ const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles, frameEndTime);
7797
7811
  if (scheduledResult.outcome === "cancelled") {
7798
7812
  return scheduledResult.result;
7799
7813
  }
@@ -7870,7 +7884,7 @@ class ClientStrategy {
7870
7884
  if (candles.length < candlesCount) {
7871
7885
  this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
7872
7886
  }
7873
- return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
7887
+ return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles, frameEndTime);
7874
7888
  }
7875
7889
  /**
7876
7890
  * Stops the strategy from generating new signals.
@@ -10263,17 +10277,18 @@ class StrategyConnectionService {
10263
10277
  * @param candles - Array of historical candle data to backtest
10264
10278
  * @returns Promise resolving to backtest result (signal or idle)
10265
10279
  */
10266
- this.backtest = async (symbol, context, candles) => {
10280
+ this.backtest = async (symbol, context, candles, frameEndTime) => {
10267
10281
  const backtest = this.executionContextService.context.backtest;
10268
10282
  this.loggerService.log("strategyConnectionService backtest", {
10269
10283
  symbol,
10270
10284
  context,
10271
10285
  candleCount: candles.length,
10286
+ frameEndTime,
10272
10287
  backtest,
10273
10288
  });
10274
10289
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
10275
10290
  await strategy.waitForInit();
10276
- const tick = await strategy.backtest(symbol, context.strategyName, candles);
10291
+ const tick = await strategy.backtest(symbol, context.strategyName, candles, frameEndTime);
10277
10292
  {
10278
10293
  await CALL_SIGNAL_EMIT_FN(this, tick, context, backtest, symbol);
10279
10294
  }
@@ -14208,17 +14223,18 @@ class StrategyCoreService {
14208
14223
  * @param context - Execution context with strategyName, exchangeName, frameName
14209
14224
  * @returns Closed signal result with PNL
14210
14225
  */
14211
- this.backtest = async (symbol, candles, when, backtest, context) => {
14226
+ this.backtest = async (symbol, candles, frameEndTime, when, backtest, context) => {
14212
14227
  this.loggerService.log("strategyCoreService backtest", {
14213
14228
  symbol,
14214
14229
  candleCount: candles.length,
14215
14230
  when,
14216
14231
  backtest,
14217
14232
  context,
14233
+ frameEndTime,
14218
14234
  });
14219
14235
  await this.validate(context);
14220
14236
  return await ExecutionContextService.runInContext(async () => {
14221
- return await this.strategyConnectionService.backtest(symbol, context, candles);
14237
+ return await this.strategyConnectionService.backtest(symbol, context, candles, frameEndTime);
14222
14238
  }, {
14223
14239
  symbol,
14224
14240
  when,
@@ -16211,7 +16227,9 @@ const TICK_FN = async (self, symbol, when) => {
16211
16227
  });
16212
16228
  }
16213
16229
  catch (error) {
16214
- console.error(`backtestLogicPrivateService tick failed symbol=${symbol} when=${when.toISOString()} strategyName=${self.methodContextService.context.strategyName} exchangeName=${self.methodContextService.context.exchangeName}`);
16230
+ console.error(`backtestLogicPrivateService tick failed symbol=${symbol} when=${when.toISOString()} strategyName=${self.methodContextService.context.strategyName} exchangeName=${self.methodContextService.context.exchangeName} error=${getErrorMessage(error)}`, {
16231
+ error: errorData(error),
16232
+ });
16215
16233
  self.loggerService.warn("backtestLogicPrivateService tick failed", {
16216
16234
  symbol,
16217
16235
  when: when.toISOString(),
@@ -16237,9 +16255,9 @@ const GET_CANDLES_FN = async (self, symbol, candlesNeeded, bufferStartTime, logM
16237
16255
  return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "GET_CANDLES_FN", message: getErrorMessage(error) };
16238
16256
  }
16239
16257
  };
16240
- const BACKTEST_FN = async (self, symbol, candles, when, context, logMeta) => {
16258
+ const BACKTEST_FN = async (self, symbol, candles, frameEndTime, when, context, logMeta) => {
16241
16259
  try {
16242
- return await self.strategyCoreService.backtest(symbol, candles, when, true, context);
16260
+ return await self.strategyCoreService.backtest(symbol, candles, frameEndTime, when, true, context);
16243
16261
  }
16244
16262
  catch (error) {
16245
16263
  console.error(`backtestLogicPrivateService backtest failed symbol=${symbol} when=${when.toISOString()} strategyName=${context.strategyName} exchangeName=${context.exchangeName}`);
@@ -16252,7 +16270,29 @@ const BACKTEST_FN = async (self, symbol, candles, when, context, logMeta) => {
16252
16270
  return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "BACKTEST_FN", message: getErrorMessage(error) };
16253
16271
  }
16254
16272
  };
16255
- const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialResult, bufferMs, signalId) => {
16273
+ const CLOSE_PENDING_FN = async (self, symbol, context, lastChunkCandles, frameEndTime, when, signalId) => {
16274
+ try {
16275
+ await self.strategyCoreService.closePending(true, symbol, context);
16276
+ }
16277
+ catch (error) {
16278
+ const message = `closePending failed: ${getErrorMessage(error)}`;
16279
+ console.error(`backtestLogicPrivateService CLOSE_PENDING_FN: ${message} symbol=${symbol}`);
16280
+ await errorEmitter.next(error instanceof Error ? error : new Error(message));
16281
+ return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "CLOSE_PENDING_FN", message };
16282
+ }
16283
+ const result = await BACKTEST_FN(self, symbol, lastChunkCandles, frameEndTime, when, context, { signalId });
16284
+ if ("__error__" in result) {
16285
+ return result;
16286
+ }
16287
+ if (result.action === "active") {
16288
+ const message = `signal ${signalId} still active after closePending`;
16289
+ console.error(`backtestLogicPrivateService CLOSE_PENDING_FN: ${message} symbol=${symbol}`);
16290
+ await errorEmitter.next(new Error(message));
16291
+ return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "CLOSE_PENDING_FN", message };
16292
+ }
16293
+ return result;
16294
+ };
16295
+ const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialResult, bufferMs, signalId, frameEndTime) => {
16256
16296
  let backtestResult = initialResult;
16257
16297
  const CHUNK = GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST;
16258
16298
  let lastChunkCandles = [];
@@ -16263,25 +16303,14 @@ const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialRe
16263
16303
  return chunkCandles;
16264
16304
  }
16265
16305
  if (!chunkCandles.length) {
16266
- await self.strategyCoreService.closePending(true, symbol, context);
16267
- const result = await BACKTEST_FN(self, symbol, lastChunkCandles, when, context, { signalId });
16268
- if ("__error__" in result) {
16269
- return result;
16270
- }
16271
- if (result.action === "active") {
16272
- const message = `signal ${signalId} still active after closePending`;
16273
- console.error(`backtestLogicPrivateService RUN_INFINITY_CHUNK_LOOP_FN: ${message} symbol=${symbol}`);
16274
- await errorEmitter.next(new Error(message));
16275
- return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_INFINITY_CHUNK_LOOP_FN", message };
16276
- }
16277
- return result;
16306
+ return await CLOSE_PENDING_FN(self, symbol, context, lastChunkCandles, frameEndTime, when, signalId);
16278
16307
  }
16279
16308
  self.loggerService.info("backtestLogicPrivateService candles fetched for infinity chunk", {
16280
16309
  symbol,
16281
16310
  signalId,
16282
16311
  candlesCount: chunkCandles.length,
16283
16312
  });
16284
- const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, when, context, { signalId });
16313
+ const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, frameEndTime, when, context, { signalId });
16285
16314
  if ("__error__" in chunkResult) {
16286
16315
  return chunkResult;
16287
16316
  }
@@ -16326,7 +16355,7 @@ const EMIT_TIMEFRAME_PERFORMANCE_FN = async (self, symbol, timeframeStartTime, p
16326
16355
  });
16327
16356
  return currentTimestamp;
16328
16357
  };
16329
- const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp) {
16358
+ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp, frameEndTime) {
16330
16359
  const signalStartTime = performance.now();
16331
16360
  const signal = result.signal;
16332
16361
  self.loggerService.info("backtestLogicPrivateService scheduled signal detected", {
@@ -16346,6 +16375,10 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16346
16375
  console.error(`backtestLogicPrivateService scheduled signal: getCandles failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${candles.reason} message=${candles.message}`);
16347
16376
  return candles;
16348
16377
  }
16378
+ // No candles available for this scheduled signal — the frame ends before the signal
16379
+ // could be evaluated. Unlike pending (Infinity) signals that require CLOSE_PENDING_FN,
16380
+ // a scheduled signal that never activated needs no explicit cancellation: it simply
16381
+ // did not start. Returning "skip" moves the backtest to the next timeframe.
16349
16382
  if (!candles.length) {
16350
16383
  return { type: "skip" };
16351
16384
  }
@@ -16387,7 +16420,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16387
16420
  });
16388
16421
  }
16389
16422
  try {
16390
- const firstResult = await BACKTEST_FN(self, symbol, candles, when, context, { signalId: signal.id });
16423
+ const firstResult = await BACKTEST_FN(self, symbol, candles, frameEndTime, when, context, { signalId: signal.id });
16391
16424
  if ("__error__" in firstResult) {
16392
16425
  console.error(`backtestLogicPrivateService scheduled signal: backtest failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${firstResult.reason} message=${firstResult.message}`);
16393
16426
  return firstResult;
@@ -16399,7 +16432,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16399
16432
  }
16400
16433
  if (backtestResult.action === "active" && signal.minuteEstimatedTime === Infinity) {
16401
16434
  const bufferMs = bufferMinutes * 60000;
16402
- const chunkResult = await RUN_INFINITY_CHUNK_LOOP_FN(self, symbol, when, context, backtestResult, bufferMs, signal.id);
16435
+ const chunkResult = await RUN_INFINITY_CHUNK_LOOP_FN(self, symbol, when, context, backtestResult, bufferMs, signal.id, frameEndTime);
16403
16436
  if ("__error__" in chunkResult) {
16404
16437
  console.error(`backtestLogicPrivateService scheduled signal: infinity chunk loop failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${chunkResult.reason} message=${chunkResult.message}`);
16405
16438
  return chunkResult;
@@ -16429,7 +16462,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16429
16462
  yield backtestResult;
16430
16463
  return { type: "closed", previousEventTimestamp: newTimestamp, closeTimestamp: backtestResult.closeTimestamp, shouldStop };
16431
16464
  };
16432
- const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStartTime, bufferMs, signalId) => {
16465
+ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStartTime, bufferMs, signalId, frameEndTime) => {
16433
16466
  const CHUNK = GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST;
16434
16467
  let chunkStart = bufferStartTime;
16435
16468
  let lastChunkCandles = [];
@@ -16450,29 +16483,14 @@ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStart
16450
16483
  await errorEmitter.next(new Error(message));
16451
16484
  return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_OPENED_CHUNK_LOOP_FN", message };
16452
16485
  }
16453
- await self.strategyCoreService.closePending(true, symbol, context);
16454
- const result = await BACKTEST_FN(self, symbol, lastChunkCandles, when, context, { signalId });
16455
- if ("__error__" in result) {
16456
- return result;
16457
- }
16458
- if (result.action === "active") {
16459
- const message = `signal ${signalId} still active after closePending`;
16460
- console.error(`backtestLogicPrivateService RUN_OPENED_CHUNK_LOOP_FN: ${message} symbol=${symbol}`);
16461
- self.loggerService.warn("backtestLogicPrivateService opened infinity: signal still active after closePending", {
16462
- symbol,
16463
- signalId,
16464
- });
16465
- await errorEmitter.next(new Error(message));
16466
- return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_OPENED_CHUNK_LOOP_FN", message };
16467
- }
16468
- return result;
16486
+ return await CLOSE_PENDING_FN(self, symbol, context, lastChunkCandles, frameEndTime, when, signalId);
16469
16487
  }
16470
16488
  self.loggerService.info("backtestLogicPrivateService candles fetched", {
16471
16489
  symbol,
16472
16490
  signalId,
16473
16491
  candlesCount: chunkCandles.length,
16474
16492
  });
16475
- const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, when, context, { signalId });
16493
+ const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, frameEndTime, when, context, { signalId });
16476
16494
  if ("__error__" in chunkResult) {
16477
16495
  return chunkResult;
16478
16496
  }
@@ -16483,7 +16501,7 @@ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStart
16483
16501
  chunkStart = new Date(chunkResult._backtestLastTimestamp + 60000 - bufferMs);
16484
16502
  }
16485
16503
  };
16486
- const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp) {
16504
+ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp, frameEndTime) {
16487
16505
  const signalStartTime = performance.now();
16488
16506
  const signal = result.signal;
16489
16507
  self.loggerService.info("backtestLogicPrivateService signal opened", {
@@ -16514,7 +16532,7 @@ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, pr
16514
16532
  signalId: signal.id,
16515
16533
  candlesCount: candles.length,
16516
16534
  });
16517
- const firstResult = await BACKTEST_FN(self, symbol, candles, when, context, { signalId: signal.id });
16535
+ const firstResult = await BACKTEST_FN(self, symbol, candles, frameEndTime, when, context, { signalId: signal.id });
16518
16536
  if ("__error__" in firstResult) {
16519
16537
  console.error(`backtestLogicPrivateService opened signal: backtest failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${firstResult.reason} message=${firstResult.message}`);
16520
16538
  return firstResult;
@@ -16523,7 +16541,7 @@ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, pr
16523
16541
  }
16524
16542
  else {
16525
16543
  const bufferMs = bufferMinutes * 60000;
16526
- const chunkResult = await RUN_OPENED_CHUNK_LOOP_FN(self, symbol, when, context, bufferStartTime, bufferMs, signal.id);
16544
+ const chunkResult = await RUN_OPENED_CHUNK_LOOP_FN(self, symbol, when, context, bufferStartTime, bufferMs, signal.id, frameEndTime);
16527
16545
  if ("__error__" in chunkResult) {
16528
16546
  console.error(`backtestLogicPrivateService opened signal: chunk loop failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${chunkResult.reason} message=${chunkResult.message}`);
16529
16547
  return chunkResult;
@@ -16589,86 +16607,103 @@ class BacktestLogicPrivateService {
16589
16607
  symbol,
16590
16608
  });
16591
16609
  const backtestStartTime = performance.now();
16610
+ let _fatalError = null;
16611
+ let previousEventTimestamp = null;
16592
16612
  const timeframes = await this.frameCoreService.getTimeframe(symbol, this.methodContextService.context.frameName);
16593
16613
  const totalFrames = timeframes.length;
16614
+ let frameEndTime = timeframes[totalFrames - 1].getTime();
16594
16615
  let i = 0;
16595
- let previousEventTimestamp = null;
16596
- while (i < timeframes.length) {
16597
- const timeframeStartTime = performance.now();
16598
- const when = timeframes[i];
16599
- await EMIT_PROGRESS_FN(this, symbol, totalFrames, i);
16600
- if (await CHECK_STOPPED_FN(this, symbol, "before tick", { when: when.toISOString(), processedFrames: i, totalFrames })) {
16601
- break;
16602
- }
16603
- const result = await TICK_FN(this, symbol, when);
16604
- if ("__error__" in result) {
16605
- break;
16606
- }
16607
- if (result.action === "idle" &&
16608
- await and(Promise.resolve(true), this.strategyCoreService.getStopped(true, symbol, {
16609
- strategyName: this.methodContextService.context.strategyName,
16610
- exchangeName: this.methodContextService.context.exchangeName,
16611
- frameName: this.methodContextService.context.frameName,
16612
- }))) {
16613
- this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
16614
- symbol,
16615
- when: when.toISOString(),
16616
- processedFrames: i,
16617
- totalFrames,
16618
- });
16619
- break;
16620
- }
16621
- if (result.action === "scheduled") {
16622
- yield result;
16623
- const r = yield* PROCESS_SCHEDULED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp);
16624
- if (r.type === "error") {
16616
+ try {
16617
+ while (i < timeframes.length) {
16618
+ const timeframeStartTime = performance.now();
16619
+ const when = timeframes[i];
16620
+ await EMIT_PROGRESS_FN(this, symbol, totalFrames, i);
16621
+ if (await CHECK_STOPPED_FN(this, symbol, "before tick", { when: when.toISOString(), processedFrames: i, totalFrames })) {
16625
16622
  break;
16626
16623
  }
16627
- if (r.type === "closed") {
16628
- previousEventTimestamp = r.previousEventTimestamp;
16629
- while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16630
- i++;
16631
- }
16632
- if (r.shouldStop) {
16633
- break;
16634
- }
16624
+ const result = await TICK_FN(this, symbol, when);
16625
+ if ("__error__" in result) {
16626
+ _fatalError = new Error(`[${result.reason}] ${result.message}`);
16627
+ break;
16635
16628
  }
16636
- }
16637
- if (result.action === "opened") {
16638
- yield result;
16639
- const r = yield* PROCESS_OPENED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp);
16640
- if (r.type === "error") {
16629
+ if (result.action === "idle" &&
16630
+ await and(Promise.resolve(true), this.strategyCoreService.getStopped(true, symbol, {
16631
+ strategyName: this.methodContextService.context.strategyName,
16632
+ exchangeName: this.methodContextService.context.exchangeName,
16633
+ frameName: this.methodContextService.context.frameName,
16634
+ }))) {
16635
+ this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
16636
+ symbol,
16637
+ when: when.toISOString(),
16638
+ processedFrames: i,
16639
+ totalFrames,
16640
+ });
16641
16641
  break;
16642
16642
  }
16643
- if (r.type === "closed") {
16644
- previousEventTimestamp = r.previousEventTimestamp;
16645
- while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16646
- i++;
16643
+ if (result.action === "scheduled") {
16644
+ yield result;
16645
+ const r = yield* PROCESS_SCHEDULED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp, frameEndTime);
16646
+ if (r.type === "error") {
16647
+ _fatalError = new Error(`[${r.reason}] ${r.message}`);
16648
+ break;
16649
+ }
16650
+ if (r.type === "closed") {
16651
+ previousEventTimestamp = r.previousEventTimestamp;
16652
+ while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16653
+ i++;
16654
+ }
16655
+ if (r.shouldStop) {
16656
+ break;
16657
+ }
16647
16658
  }
16648
- if (r.shouldStop) {
16659
+ }
16660
+ if (result.action === "opened") {
16661
+ yield result;
16662
+ const r = yield* PROCESS_OPENED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp, frameEndTime);
16663
+ if (r.type === "error") {
16664
+ _fatalError = new Error(`[${r.reason}] ${r.message}`);
16649
16665
  break;
16650
16666
  }
16667
+ if (r.type === "closed") {
16668
+ previousEventTimestamp = r.previousEventTimestamp;
16669
+ while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16670
+ i++;
16671
+ }
16672
+ if (r.shouldStop) {
16673
+ break;
16674
+ }
16675
+ }
16651
16676
  }
16677
+ previousEventTimestamp = await EMIT_TIMEFRAME_PERFORMANCE_FN(this, symbol, timeframeStartTime, previousEventTimestamp);
16678
+ i++;
16652
16679
  }
16653
- previousEventTimestamp = await EMIT_TIMEFRAME_PERFORMANCE_FN(this, symbol, timeframeStartTime, previousEventTimestamp);
16654
- i++;
16655
- }
16656
- // Emit final progress event (100%)
16657
- await EMIT_PROGRESS_FN(this, symbol, totalFrames, totalFrames);
16658
- // Track total backtest duration
16659
- const backtestEndTime = performance.now();
16660
- const currentTimestamp = Date.now();
16661
- await performanceEmitter.next({
16662
- timestamp: currentTimestamp,
16663
- previousTimestamp: previousEventTimestamp,
16664
- metricType: "backtest_total",
16665
- duration: backtestEndTime - backtestStartTime,
16666
- strategyName: this.methodContextService.context.strategyName,
16667
- exchangeName: this.methodContextService.context.exchangeName,
16668
- frameName: this.methodContextService.context.frameName,
16669
- symbol,
16670
- backtest: true,
16671
- });
16680
+ // Emit final progress event (100%)
16681
+ await EMIT_PROGRESS_FN(this, symbol, totalFrames, totalFrames);
16682
+ // Track total backtest duration
16683
+ const backtestEndTime = performance.now();
16684
+ const currentTimestamp = Date.now();
16685
+ await performanceEmitter.next({
16686
+ timestamp: currentTimestamp,
16687
+ previousTimestamp: previousEventTimestamp,
16688
+ metricType: "backtest_total",
16689
+ duration: backtestEndTime - backtestStartTime,
16690
+ strategyName: this.methodContextService.context.strategyName,
16691
+ exchangeName: this.methodContextService.context.exchangeName,
16692
+ frameName: this.methodContextService.context.frameName,
16693
+ symbol,
16694
+ backtest: true,
16695
+ });
16696
+ }
16697
+ catch (error) {
16698
+ _fatalError = error;
16699
+ }
16700
+ finally {
16701
+ if (_fatalError !== null) {
16702
+ console.error(`[BacktestLogicPrivateService] Fatal error — backtest sequence broken for symbol=${symbol} ` +
16703
+ `strategy=${this.methodContextService.context.strategyName}`, _fatalError);
16704
+ process.exit(-1);
16705
+ }
16706
+ }
16672
16707
  }
16673
16708
  }
16674
16709
 
@@ -33324,54 +33359,144 @@ const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommi
33324
33359
  const BROKER_BASE_METHOD_NAME_ON_TRAILING_TAKE = "BrokerBase.onTrailingTakeCommit";
33325
33360
  const BROKER_BASE_METHOD_NAME_ON_BREAKEVEN = "BrokerBase.onBreakevenCommit";
33326
33361
  const BROKER_BASE_METHOD_NAME_ON_AVERAGE_BUY = "BrokerBase.onAverageBuyCommit";
33362
+ /**
33363
+ * Wrapper around a `Partial<IBroker>` adapter instance.
33364
+ *
33365
+ * Implements the full `IBroker` interface but guards every method call —
33366
+ * if the underlying adapter does not implement a given method, an error is thrown.
33367
+ * `waitForInit` is the only exception: it is silently skipped when not implemented.
33368
+ *
33369
+ * Created internally by `BrokerAdapter.useBrokerAdapter` and stored as
33370
+ * `_brokerInstance`. All `BrokerAdapter.commit*` methods delegate here
33371
+ * after backtest-mode and enable-state checks pass.
33372
+ */
33327
33373
  class BrokerProxy {
33328
33374
  constructor(_instance) {
33329
33375
  this._instance = _instance;
33376
+ /**
33377
+ * Calls `waitForInit` on the underlying adapter exactly once (singleshot).
33378
+ * If the adapter does not implement `waitForInit`, the call is silently skipped.
33379
+ *
33380
+ * @returns Resolves when initialization is complete (or immediately if not implemented).
33381
+ */
33330
33382
  this.waitForInit = singleshot(async () => {
33331
33383
  if (this._instance.waitForInit) {
33332
33384
  await this._instance.waitForInit();
33385
+ return;
33333
33386
  }
33334
33387
  });
33335
33388
  }
33389
+ /**
33390
+ * Forwards a signal-open event to the underlying adapter.
33391
+ * Throws if the adapter does not implement `onSignalOpenCommit`.
33392
+ *
33393
+ * @param payload - Signal open details: symbol, cost, position, prices, context, backtest flag.
33394
+ * @throws {Error} If the adapter does not implement `onSignalOpenCommit`.
33395
+ */
33336
33396
  async onSignalOpenCommit(payload) {
33337
33397
  if (this._instance.onSignalOpenCommit) {
33338
33398
  await this._instance.onSignalOpenCommit(payload);
33399
+ return;
33339
33400
  }
33401
+ throw new Error("BrokerProxy onSignalOpenCommit is not implemented");
33340
33402
  }
33403
+ /**
33404
+ * Forwards a signal-close event to the underlying adapter.
33405
+ * Throws if the adapter does not implement `onSignalCloseCommit`.
33406
+ *
33407
+ * @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, context, backtest flag.
33408
+ * @throws {Error} If the adapter does not implement `onSignalCloseCommit`.
33409
+ */
33341
33410
  async onSignalCloseCommit(payload) {
33342
33411
  if (this._instance.onSignalCloseCommit) {
33343
33412
  await this._instance.onSignalCloseCommit(payload);
33413
+ return;
33344
33414
  }
33415
+ throw new Error("BrokerProxy onSignalCloseCommit is not implemented");
33345
33416
  }
33417
+ /**
33418
+ * Forwards a partial-profit close event to the underlying adapter.
33419
+ * Throws if the adapter does not implement `onPartialProfitCommit`.
33420
+ *
33421
+ * @param payload - Partial profit details: symbol, percentToClose, cost, currentPrice, context, backtest flag.
33422
+ * @throws {Error} If the adapter does not implement `onPartialProfitCommit`.
33423
+ */
33346
33424
  async onPartialProfitCommit(payload) {
33347
33425
  if (this._instance.onPartialProfitCommit) {
33348
33426
  await this._instance.onPartialProfitCommit(payload);
33427
+ return;
33349
33428
  }
33429
+ throw new Error("BrokerProxy onPartialProfitCommit is not implemented");
33350
33430
  }
33431
+ /**
33432
+ * Forwards a partial-loss close event to the underlying adapter.
33433
+ * Throws if the adapter does not implement `onPartialLossCommit`.
33434
+ *
33435
+ * @param payload - Partial loss details: symbol, percentToClose, cost, currentPrice, context, backtest flag.
33436
+ * @throws {Error} If the adapter does not implement `onPartialLossCommit`.
33437
+ */
33351
33438
  async onPartialLossCommit(payload) {
33352
33439
  if (this._instance.onPartialLossCommit) {
33353
33440
  await this._instance.onPartialLossCommit(payload);
33441
+ return;
33354
33442
  }
33443
+ throw new Error("BrokerProxy onPartialLossCommit is not implemented");
33355
33444
  }
33445
+ /**
33446
+ * Forwards a trailing stop-loss update event to the underlying adapter.
33447
+ * Throws if the adapter does not implement `onTrailingStopCommit`.
33448
+ *
33449
+ * @param payload - Trailing stop details: symbol, percentShift, currentPrice, newStopLossPrice, context, backtest flag.
33450
+ * @throws {Error} If the adapter does not implement `onTrailingStopCommit`.
33451
+ */
33356
33452
  async onTrailingStopCommit(payload) {
33357
33453
  if (this._instance.onTrailingStopCommit) {
33358
33454
  await this._instance.onTrailingStopCommit(payload);
33455
+ return;
33359
33456
  }
33457
+ throw new Error("BrokerProxy onTrailingStopCommit is not implemented");
33360
33458
  }
33459
+ /**
33460
+ * Forwards a trailing take-profit update event to the underlying adapter.
33461
+ * Throws if the adapter does not implement `onTrailingTakeCommit`.
33462
+ *
33463
+ * @param payload - Trailing take details: symbol, percentShift, currentPrice, newTakeProfitPrice, context, backtest flag.
33464
+ * @throws {Error} If the adapter does not implement `onTrailingTakeCommit`.
33465
+ */
33361
33466
  async onTrailingTakeCommit(payload) {
33362
33467
  if (this._instance.onTrailingTakeCommit) {
33363
33468
  await this._instance.onTrailingTakeCommit(payload);
33469
+ return;
33364
33470
  }
33471
+ throw new Error("BrokerProxy onTrailingTakeCommit is not implemented");
33365
33472
  }
33473
+ /**
33474
+ * Forwards a breakeven event to the underlying adapter.
33475
+ * Throws if the adapter does not implement `onBreakevenCommit`.
33476
+ *
33477
+ * @param payload - Breakeven details: symbol, currentPrice, newStopLossPrice (= effectivePriceOpen), newTakeProfitPrice, context, backtest flag.
33478
+ * @throws {Error} If the adapter does not implement `onBreakevenCommit`.
33479
+ */
33366
33480
  async onBreakevenCommit(payload) {
33367
33481
  if (this._instance.onBreakevenCommit) {
33368
33482
  await this._instance.onBreakevenCommit(payload);
33483
+ return;
33369
33484
  }
33485
+ throw new Error("BrokerProxy onBreakevenCommit is not implemented");
33370
33486
  }
33487
+ /**
33488
+ * Forwards a DCA average-buy entry event to the underlying adapter.
33489
+ * Throws if the adapter does not implement `onAverageBuyCommit`.
33490
+ *
33491
+ * @param payload - Average buy details: symbol, currentPrice, cost, context, backtest flag.
33492
+ * @throws {Error} If the adapter does not implement `onAverageBuyCommit`.
33493
+ */
33371
33494
  async onAverageBuyCommit(payload) {
33372
33495
  if (this._instance.onAverageBuyCommit) {
33373
33496
  await this._instance.onAverageBuyCommit(payload);
33497
+ return;
33374
33498
  }
33499
+ throw new Error("BrokerProxy onAverageBuyCommit is not implemented");
33375
33500
  }
33376
33501
  }
33377
33502
  /**
@@ -43143,7 +43268,7 @@ class MemoryLocalInstance {
43143
43268
  * @param value - Value to store and index
43144
43269
  * @param index - Optional BM25 index string; defaults to JSON.stringify(value)
43145
43270
  */
43146
- async writeMemory(memoryId, value, index) {
43271
+ async writeMemory(memoryId, value, description) {
43147
43272
  bt.loggerService.debug(MEMORY_LOCAL_INSTANCE_METHOD_NAME_WRITE, {
43148
43273
  signalId: this.signalId,
43149
43274
  bucketName: this.bucketName,
@@ -43152,7 +43277,7 @@ class MemoryLocalInstance {
43152
43277
  this._index.upsert({
43153
43278
  id: memoryId,
43154
43279
  content: value,
43155
- index: index ?? JSON.stringify(value),
43280
+ index: description,
43156
43281
  priority: Date.now(),
43157
43282
  });
43158
43283
  }
@@ -43451,7 +43576,7 @@ class MemoryAdapter {
43451
43576
  * @param dto.value - Value to store
43452
43577
  * @param dto.signalId - Signal identifier
43453
43578
  * @param dto.bucketName - Bucket name
43454
- * @param dto.index - Optional BM25 index string; defaults to JSON.stringify(value)
43579
+ * @param dto.description - Optional BM25 index string; defaults to JSON.stringify(value)
43455
43580
  */
43456
43581
  this.writeMemory = async (dto) => {
43457
43582
  if (!this.enable.hasValue()) {
@@ -43466,7 +43591,7 @@ class MemoryAdapter {
43466
43591
  const isInitial = !this.getInstance.has(key);
43467
43592
  const instance = this.getInstance(dto.signalId, dto.bucketName);
43468
43593
  await instance.waitForInit(isInitial);
43469
- return await instance.writeMemory(dto.memoryId, dto.value, dto.index);
43594
+ return await instance.writeMemory(dto.memoryId, dto.value, dto.description);
43470
43595
  };
43471
43596
  /**
43472
43597
  * Search memory using BM25 full-text scoring.
@@ -43617,7 +43742,7 @@ const REMOVE_MEMORY_METHOD_NAME = "memory.removeMemory";
43617
43742
  * ```
43618
43743
  */
43619
43744
  async function writeMemory(dto) {
43620
- const { bucketName, memoryId, value } = dto;
43745
+ const { bucketName, memoryId, value, description } = dto;
43621
43746
  bt.loggerService.info(WRITE_MEMORY_METHOD_NAME, {
43622
43747
  bucketName,
43623
43748
  memoryId,
@@ -43641,6 +43766,7 @@ async function writeMemory(dto) {
43641
43766
  value,
43642
43767
  signalId: signal.id,
43643
43768
  bucketName,
43769
+ description,
43644
43770
  });
43645
43771
  }
43646
43772
  /**
@@ -44084,7 +44210,7 @@ class DumpMemoryInstance {
44084
44210
  bucketName: this.bucketName,
44085
44211
  signalId: this.signalId,
44086
44212
  value: { messages },
44087
- index: description,
44213
+ description,
44088
44214
  });
44089
44215
  }
44090
44216
  /**
@@ -44105,7 +44231,7 @@ class DumpMemoryInstance {
44105
44231
  bucketName: this.bucketName,
44106
44232
  signalId: this.signalId,
44107
44233
  value: record,
44108
- index: description,
44234
+ description,
44109
44235
  });
44110
44236
  }
44111
44237
  /**
@@ -44127,7 +44253,7 @@ class DumpMemoryInstance {
44127
44253
  bucketName: this.bucketName,
44128
44254
  signalId: this.signalId,
44129
44255
  value: { rows },
44130
- index: description,
44256
+ description,
44131
44257
  });
44132
44258
  }
44133
44259
  /**
@@ -44148,7 +44274,7 @@ class DumpMemoryInstance {
44148
44274
  bucketName: this.bucketName,
44149
44275
  signalId: this.signalId,
44150
44276
  value: { content },
44151
- index: description,
44277
+ description,
44152
44278
  });
44153
44279
  }
44154
44280
  /**
@@ -44169,7 +44295,7 @@ class DumpMemoryInstance {
44169
44295
  bucketName: this.bucketName,
44170
44296
  signalId: this.signalId,
44171
44297
  value: { content },
44172
- index: description,
44298
+ description,
44173
44299
  });
44174
44300
  }
44175
44301
  /**
@@ -44191,7 +44317,7 @@ class DumpMemoryInstance {
44191
44317
  bucketName: this.bucketName,
44192
44318
  signalId: this.signalId,
44193
44319
  value: json,
44194
- index: description,
44320
+ description,
44195
44321
  });
44196
44322
  }
44197
44323
  /** Releases resources held by this instance. */