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.cjs CHANGED
@@ -6345,7 +6345,7 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
6345
6345
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
6346
6346
  return result;
6347
6347
  };
6348
- const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
6348
+ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, frameEndTime) => {
6349
6349
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
6350
6350
  const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
6351
6351
  const bufferCandlesCount = candlesCount - 1;
@@ -6358,6 +6358,11 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
6358
6358
  }
6359
6359
  const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
6360
6360
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
6361
+ // Если timestamp свечи вышел за frameEndTime — отменяем scheduled сигнал
6362
+ if (candle.timestamp > frameEndTime) {
6363
+ const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "timeout");
6364
+ return { outcome: "cancelled", result };
6365
+ }
6361
6366
  // КРИТИЧНО: Проверяем был ли сигнал отменен пользователем через cancel()
6362
6367
  if (self._cancelledSignal) {
6363
6368
  // Сигнал был отменен через cancel() в onSchedulePing
@@ -6505,7 +6510,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
6505
6510
  }
6506
6511
  return { outcome: "pending" };
6507
6512
  };
6508
- const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6513
+ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEndTime) => {
6509
6514
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
6510
6515
  const bufferCandlesCount = candlesCount - 1;
6511
6516
  // КРИТИЧНО: проверяем TP/SL на КАЖДОЙ свече начиная после буфера
@@ -6522,6 +6527,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6522
6527
  const startIndex = Math.max(0, i - (candlesCount - 1));
6523
6528
  const recentCandles = candles.slice(startIndex, i + 1);
6524
6529
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
6530
+ // Если timestamp свечи вышел за frameEndTime — закрываем pending сигнал по time_expired
6531
+ if (currentCandleTimestamp > frameEndTime) {
6532
+ const result = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, averagePrice, "time_expired", currentCandleTimestamp);
6533
+ if (!result) {
6534
+ throw new Error(`ClientStrategy backtest: frameEndTime time_expired close rejected by sync (signalId=${signal.id}).`);
6535
+ }
6536
+ return result;
6537
+ }
6525
6538
  // КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
6526
6539
  if (self._closedSignal) {
6527
6540
  return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
@@ -7687,7 +7700,7 @@ class ClientStrategy {
7687
7700
  * console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired" | "cancelled"
7688
7701
  * ```
7689
7702
  */
7690
- async backtest(symbol, strategyName, candles) {
7703
+ async backtest(symbol, strategyName, candles, frameEndTime) {
7691
7704
  this.params.logger.debug("ClientStrategy backtest", {
7692
7705
  symbol,
7693
7706
  strategyName,
@@ -7695,6 +7708,7 @@ class ClientStrategy {
7695
7708
  candlesCount: candles.length,
7696
7709
  hasScheduled: !!this._scheduledSignal,
7697
7710
  hasPending: !!this._pendingSignal,
7711
+ frameEndTime,
7698
7712
  });
7699
7713
  if (!this.params.execution.context.backtest) {
7700
7714
  throw new Error("ClientStrategy backtest: running in live context");
@@ -7813,7 +7827,7 @@ class ClientStrategy {
7813
7827
  priceOpen: scheduled.priceOpen,
7814
7828
  position: scheduled.position,
7815
7829
  });
7816
- const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
7830
+ const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles, frameEndTime);
7817
7831
  if (scheduledResult.outcome === "cancelled") {
7818
7832
  return scheduledResult.result;
7819
7833
  }
@@ -7890,7 +7904,7 @@ class ClientStrategy {
7890
7904
  if (candles.length < candlesCount) {
7891
7905
  this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
7892
7906
  }
7893
- return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
7907
+ return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles, frameEndTime);
7894
7908
  }
7895
7909
  /**
7896
7910
  * Stops the strategy from generating new signals.
@@ -10283,17 +10297,18 @@ class StrategyConnectionService {
10283
10297
  * @param candles - Array of historical candle data to backtest
10284
10298
  * @returns Promise resolving to backtest result (signal or idle)
10285
10299
  */
10286
- this.backtest = async (symbol, context, candles) => {
10300
+ this.backtest = async (symbol, context, candles, frameEndTime) => {
10287
10301
  const backtest = this.executionContextService.context.backtest;
10288
10302
  this.loggerService.log("strategyConnectionService backtest", {
10289
10303
  symbol,
10290
10304
  context,
10291
10305
  candleCount: candles.length,
10306
+ frameEndTime,
10292
10307
  backtest,
10293
10308
  });
10294
10309
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
10295
10310
  await strategy.waitForInit();
10296
- const tick = await strategy.backtest(symbol, context.strategyName, candles);
10311
+ const tick = await strategy.backtest(symbol, context.strategyName, candles, frameEndTime);
10297
10312
  {
10298
10313
  await CALL_SIGNAL_EMIT_FN(this, tick, context, backtest, symbol);
10299
10314
  }
@@ -14228,17 +14243,18 @@ class StrategyCoreService {
14228
14243
  * @param context - Execution context with strategyName, exchangeName, frameName
14229
14244
  * @returns Closed signal result with PNL
14230
14245
  */
14231
- this.backtest = async (symbol, candles, when, backtest, context) => {
14246
+ this.backtest = async (symbol, candles, frameEndTime, when, backtest, context) => {
14232
14247
  this.loggerService.log("strategyCoreService backtest", {
14233
14248
  symbol,
14234
14249
  candleCount: candles.length,
14235
14250
  when,
14236
14251
  backtest,
14237
14252
  context,
14253
+ frameEndTime,
14238
14254
  });
14239
14255
  await this.validate(context);
14240
14256
  return await ExecutionContextService.runInContext(async () => {
14241
- return await this.strategyConnectionService.backtest(symbol, context, candles);
14257
+ return await this.strategyConnectionService.backtest(symbol, context, candles, frameEndTime);
14242
14258
  }, {
14243
14259
  symbol,
14244
14260
  when,
@@ -16231,7 +16247,9 @@ const TICK_FN = async (self, symbol, when) => {
16231
16247
  });
16232
16248
  }
16233
16249
  catch (error) {
16234
- console.error(`backtestLogicPrivateService tick failed symbol=${symbol} when=${when.toISOString()} strategyName=${self.methodContextService.context.strategyName} exchangeName=${self.methodContextService.context.exchangeName}`);
16250
+ console.error(`backtestLogicPrivateService tick failed symbol=${symbol} when=${when.toISOString()} strategyName=${self.methodContextService.context.strategyName} exchangeName=${self.methodContextService.context.exchangeName} error=${functoolsKit.getErrorMessage(error)}`, {
16251
+ error: functoolsKit.errorData(error),
16252
+ });
16235
16253
  self.loggerService.warn("backtestLogicPrivateService tick failed", {
16236
16254
  symbol,
16237
16255
  when: when.toISOString(),
@@ -16257,9 +16275,9 @@ const GET_CANDLES_FN = async (self, symbol, candlesNeeded, bufferStartTime, logM
16257
16275
  return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "GET_CANDLES_FN", message: functoolsKit.getErrorMessage(error) };
16258
16276
  }
16259
16277
  };
16260
- const BACKTEST_FN = async (self, symbol, candles, when, context, logMeta) => {
16278
+ const BACKTEST_FN = async (self, symbol, candles, frameEndTime, when, context, logMeta) => {
16261
16279
  try {
16262
- return await self.strategyCoreService.backtest(symbol, candles, when, true, context);
16280
+ return await self.strategyCoreService.backtest(symbol, candles, frameEndTime, when, true, context);
16263
16281
  }
16264
16282
  catch (error) {
16265
16283
  console.error(`backtestLogicPrivateService backtest failed symbol=${symbol} when=${when.toISOString()} strategyName=${context.strategyName} exchangeName=${context.exchangeName}`);
@@ -16272,7 +16290,29 @@ const BACKTEST_FN = async (self, symbol, candles, when, context, logMeta) => {
16272
16290
  return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "BACKTEST_FN", message: functoolsKit.getErrorMessage(error) };
16273
16291
  }
16274
16292
  };
16275
- const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialResult, bufferMs, signalId) => {
16293
+ const CLOSE_PENDING_FN = async (self, symbol, context, lastChunkCandles, frameEndTime, when, signalId) => {
16294
+ try {
16295
+ await self.strategyCoreService.closePending(true, symbol, context);
16296
+ }
16297
+ catch (error) {
16298
+ const message = `closePending failed: ${functoolsKit.getErrorMessage(error)}`;
16299
+ console.error(`backtestLogicPrivateService CLOSE_PENDING_FN: ${message} symbol=${symbol}`);
16300
+ await errorEmitter.next(error instanceof Error ? error : new Error(message));
16301
+ return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "CLOSE_PENDING_FN", message };
16302
+ }
16303
+ const result = await BACKTEST_FN(self, symbol, lastChunkCandles, frameEndTime, when, context, { signalId });
16304
+ if ("__error__" in result) {
16305
+ return result;
16306
+ }
16307
+ if (result.action === "active") {
16308
+ const message = `signal ${signalId} still active after closePending`;
16309
+ console.error(`backtestLogicPrivateService CLOSE_PENDING_FN: ${message} symbol=${symbol}`);
16310
+ await errorEmitter.next(new Error(message));
16311
+ return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "CLOSE_PENDING_FN", message };
16312
+ }
16313
+ return result;
16314
+ };
16315
+ const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialResult, bufferMs, signalId, frameEndTime) => {
16276
16316
  let backtestResult = initialResult;
16277
16317
  const CHUNK = GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST;
16278
16318
  let lastChunkCandles = [];
@@ -16283,25 +16323,14 @@ const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialRe
16283
16323
  return chunkCandles;
16284
16324
  }
16285
16325
  if (!chunkCandles.length) {
16286
- await self.strategyCoreService.closePending(true, symbol, context);
16287
- const result = await BACKTEST_FN(self, symbol, lastChunkCandles, when, context, { signalId });
16288
- if ("__error__" in result) {
16289
- return result;
16290
- }
16291
- if (result.action === "active") {
16292
- const message = `signal ${signalId} still active after closePending`;
16293
- console.error(`backtestLogicPrivateService RUN_INFINITY_CHUNK_LOOP_FN: ${message} symbol=${symbol}`);
16294
- await errorEmitter.next(new Error(message));
16295
- return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_INFINITY_CHUNK_LOOP_FN", message };
16296
- }
16297
- return result;
16326
+ return await CLOSE_PENDING_FN(self, symbol, context, lastChunkCandles, frameEndTime, when, signalId);
16298
16327
  }
16299
16328
  self.loggerService.info("backtestLogicPrivateService candles fetched for infinity chunk", {
16300
16329
  symbol,
16301
16330
  signalId,
16302
16331
  candlesCount: chunkCandles.length,
16303
16332
  });
16304
- const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, when, context, { signalId });
16333
+ const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, frameEndTime, when, context, { signalId });
16305
16334
  if ("__error__" in chunkResult) {
16306
16335
  return chunkResult;
16307
16336
  }
@@ -16346,7 +16375,7 @@ const EMIT_TIMEFRAME_PERFORMANCE_FN = async (self, symbol, timeframeStartTime, p
16346
16375
  });
16347
16376
  return currentTimestamp;
16348
16377
  };
16349
- const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp) {
16378
+ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp, frameEndTime) {
16350
16379
  const signalStartTime = performance.now();
16351
16380
  const signal = result.signal;
16352
16381
  self.loggerService.info("backtestLogicPrivateService scheduled signal detected", {
@@ -16366,6 +16395,10 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16366
16395
  console.error(`backtestLogicPrivateService scheduled signal: getCandles failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${candles.reason} message=${candles.message}`);
16367
16396
  return candles;
16368
16397
  }
16398
+ // No candles available for this scheduled signal — the frame ends before the signal
16399
+ // could be evaluated. Unlike pending (Infinity) signals that require CLOSE_PENDING_FN,
16400
+ // a scheduled signal that never activated needs no explicit cancellation: it simply
16401
+ // did not start. Returning "skip" moves the backtest to the next timeframe.
16369
16402
  if (!candles.length) {
16370
16403
  return { type: "skip" };
16371
16404
  }
@@ -16407,7 +16440,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16407
16440
  });
16408
16441
  }
16409
16442
  try {
16410
- const firstResult = await BACKTEST_FN(self, symbol, candles, when, context, { signalId: signal.id });
16443
+ const firstResult = await BACKTEST_FN(self, symbol, candles, frameEndTime, when, context, { signalId: signal.id });
16411
16444
  if ("__error__" in firstResult) {
16412
16445
  console.error(`backtestLogicPrivateService scheduled signal: backtest failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${firstResult.reason} message=${firstResult.message}`);
16413
16446
  return firstResult;
@@ -16419,7 +16452,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16419
16452
  }
16420
16453
  if (backtestResult.action === "active" && signal.minuteEstimatedTime === Infinity) {
16421
16454
  const bufferMs = bufferMinutes * 60000;
16422
- const chunkResult = await RUN_INFINITY_CHUNK_LOOP_FN(self, symbol, when, context, backtestResult, bufferMs, signal.id);
16455
+ const chunkResult = await RUN_INFINITY_CHUNK_LOOP_FN(self, symbol, when, context, backtestResult, bufferMs, signal.id, frameEndTime);
16423
16456
  if ("__error__" in chunkResult) {
16424
16457
  console.error(`backtestLogicPrivateService scheduled signal: infinity chunk loop failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${chunkResult.reason} message=${chunkResult.message}`);
16425
16458
  return chunkResult;
@@ -16449,7 +16482,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16449
16482
  yield backtestResult;
16450
16483
  return { type: "closed", previousEventTimestamp: newTimestamp, closeTimestamp: backtestResult.closeTimestamp, shouldStop };
16451
16484
  };
16452
- const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStartTime, bufferMs, signalId) => {
16485
+ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStartTime, bufferMs, signalId, frameEndTime) => {
16453
16486
  const CHUNK = GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST;
16454
16487
  let chunkStart = bufferStartTime;
16455
16488
  let lastChunkCandles = [];
@@ -16470,29 +16503,14 @@ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStart
16470
16503
  await errorEmitter.next(new Error(message));
16471
16504
  return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_OPENED_CHUNK_LOOP_FN", message };
16472
16505
  }
16473
- await self.strategyCoreService.closePending(true, symbol, context);
16474
- const result = await BACKTEST_FN(self, symbol, lastChunkCandles, when, context, { signalId });
16475
- if ("__error__" in result) {
16476
- return result;
16477
- }
16478
- if (result.action === "active") {
16479
- const message = `signal ${signalId} still active after closePending`;
16480
- console.error(`backtestLogicPrivateService RUN_OPENED_CHUNK_LOOP_FN: ${message} symbol=${symbol}`);
16481
- self.loggerService.warn("backtestLogicPrivateService opened infinity: signal still active after closePending", {
16482
- symbol,
16483
- signalId,
16484
- });
16485
- await errorEmitter.next(new Error(message));
16486
- return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_OPENED_CHUNK_LOOP_FN", message };
16487
- }
16488
- return result;
16506
+ return await CLOSE_PENDING_FN(self, symbol, context, lastChunkCandles, frameEndTime, when, signalId);
16489
16507
  }
16490
16508
  self.loggerService.info("backtestLogicPrivateService candles fetched", {
16491
16509
  symbol,
16492
16510
  signalId,
16493
16511
  candlesCount: chunkCandles.length,
16494
16512
  });
16495
- const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, when, context, { signalId });
16513
+ const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, frameEndTime, when, context, { signalId });
16496
16514
  if ("__error__" in chunkResult) {
16497
16515
  return chunkResult;
16498
16516
  }
@@ -16503,7 +16521,7 @@ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStart
16503
16521
  chunkStart = new Date(chunkResult._backtestLastTimestamp + 60000 - bufferMs);
16504
16522
  }
16505
16523
  };
16506
- const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp) {
16524
+ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp, frameEndTime) {
16507
16525
  const signalStartTime = performance.now();
16508
16526
  const signal = result.signal;
16509
16527
  self.loggerService.info("backtestLogicPrivateService signal opened", {
@@ -16534,7 +16552,7 @@ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, pr
16534
16552
  signalId: signal.id,
16535
16553
  candlesCount: candles.length,
16536
16554
  });
16537
- const firstResult = await BACKTEST_FN(self, symbol, candles, when, context, { signalId: signal.id });
16555
+ const firstResult = await BACKTEST_FN(self, symbol, candles, frameEndTime, when, context, { signalId: signal.id });
16538
16556
  if ("__error__" in firstResult) {
16539
16557
  console.error(`backtestLogicPrivateService opened signal: backtest failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${firstResult.reason} message=${firstResult.message}`);
16540
16558
  return firstResult;
@@ -16543,7 +16561,7 @@ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, pr
16543
16561
  }
16544
16562
  else {
16545
16563
  const bufferMs = bufferMinutes * 60000;
16546
- const chunkResult = await RUN_OPENED_CHUNK_LOOP_FN(self, symbol, when, context, bufferStartTime, bufferMs, signal.id);
16564
+ const chunkResult = await RUN_OPENED_CHUNK_LOOP_FN(self, symbol, when, context, bufferStartTime, bufferMs, signal.id, frameEndTime);
16547
16565
  if ("__error__" in chunkResult) {
16548
16566
  console.error(`backtestLogicPrivateService opened signal: chunk loop failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${chunkResult.reason} message=${chunkResult.message}`);
16549
16567
  return chunkResult;
@@ -16609,86 +16627,103 @@ class BacktestLogicPrivateService {
16609
16627
  symbol,
16610
16628
  });
16611
16629
  const backtestStartTime = performance.now();
16630
+ let _fatalError = null;
16631
+ let previousEventTimestamp = null;
16612
16632
  const timeframes = await this.frameCoreService.getTimeframe(symbol, this.methodContextService.context.frameName);
16613
16633
  const totalFrames = timeframes.length;
16634
+ let frameEndTime = timeframes[totalFrames - 1].getTime();
16614
16635
  let i = 0;
16615
- let previousEventTimestamp = null;
16616
- while (i < timeframes.length) {
16617
- const timeframeStartTime = performance.now();
16618
- const when = timeframes[i];
16619
- await EMIT_PROGRESS_FN(this, symbol, totalFrames, i);
16620
- if (await CHECK_STOPPED_FN(this, symbol, "before tick", { when: when.toISOString(), processedFrames: i, totalFrames })) {
16621
- break;
16622
- }
16623
- const result = await TICK_FN(this, symbol, when);
16624
- if ("__error__" in result) {
16625
- break;
16626
- }
16627
- if (result.action === "idle" &&
16628
- await functoolsKit.and(Promise.resolve(true), this.strategyCoreService.getStopped(true, symbol, {
16629
- strategyName: this.methodContextService.context.strategyName,
16630
- exchangeName: this.methodContextService.context.exchangeName,
16631
- frameName: this.methodContextService.context.frameName,
16632
- }))) {
16633
- this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
16634
- symbol,
16635
- when: when.toISOString(),
16636
- processedFrames: i,
16637
- totalFrames,
16638
- });
16639
- break;
16640
- }
16641
- if (result.action === "scheduled") {
16642
- yield result;
16643
- const r = yield* PROCESS_SCHEDULED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp);
16644
- if (r.type === "error") {
16636
+ try {
16637
+ while (i < timeframes.length) {
16638
+ const timeframeStartTime = performance.now();
16639
+ const when = timeframes[i];
16640
+ await EMIT_PROGRESS_FN(this, symbol, totalFrames, i);
16641
+ if (await CHECK_STOPPED_FN(this, symbol, "before tick", { when: when.toISOString(), processedFrames: i, totalFrames })) {
16645
16642
  break;
16646
16643
  }
16647
- if (r.type === "closed") {
16648
- previousEventTimestamp = r.previousEventTimestamp;
16649
- while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16650
- i++;
16651
- }
16652
- if (r.shouldStop) {
16653
- break;
16654
- }
16644
+ const result = await TICK_FN(this, symbol, when);
16645
+ if ("__error__" in result) {
16646
+ _fatalError = new Error(`[${result.reason}] ${result.message}`);
16647
+ break;
16655
16648
  }
16656
- }
16657
- if (result.action === "opened") {
16658
- yield result;
16659
- const r = yield* PROCESS_OPENED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp);
16660
- if (r.type === "error") {
16649
+ if (result.action === "idle" &&
16650
+ await functoolsKit.and(Promise.resolve(true), this.strategyCoreService.getStopped(true, symbol, {
16651
+ strategyName: this.methodContextService.context.strategyName,
16652
+ exchangeName: this.methodContextService.context.exchangeName,
16653
+ frameName: this.methodContextService.context.frameName,
16654
+ }))) {
16655
+ this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
16656
+ symbol,
16657
+ when: when.toISOString(),
16658
+ processedFrames: i,
16659
+ totalFrames,
16660
+ });
16661
16661
  break;
16662
16662
  }
16663
- if (r.type === "closed") {
16664
- previousEventTimestamp = r.previousEventTimestamp;
16665
- while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16666
- i++;
16663
+ if (result.action === "scheduled") {
16664
+ yield result;
16665
+ const r = yield* PROCESS_SCHEDULED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp, frameEndTime);
16666
+ if (r.type === "error") {
16667
+ _fatalError = new Error(`[${r.reason}] ${r.message}`);
16668
+ break;
16669
+ }
16670
+ if (r.type === "closed") {
16671
+ previousEventTimestamp = r.previousEventTimestamp;
16672
+ while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16673
+ i++;
16674
+ }
16675
+ if (r.shouldStop) {
16676
+ break;
16677
+ }
16667
16678
  }
16668
- if (r.shouldStop) {
16679
+ }
16680
+ if (result.action === "opened") {
16681
+ yield result;
16682
+ const r = yield* PROCESS_OPENED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp, frameEndTime);
16683
+ if (r.type === "error") {
16684
+ _fatalError = new Error(`[${r.reason}] ${r.message}`);
16669
16685
  break;
16670
16686
  }
16687
+ if (r.type === "closed") {
16688
+ previousEventTimestamp = r.previousEventTimestamp;
16689
+ while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16690
+ i++;
16691
+ }
16692
+ if (r.shouldStop) {
16693
+ break;
16694
+ }
16695
+ }
16671
16696
  }
16697
+ previousEventTimestamp = await EMIT_TIMEFRAME_PERFORMANCE_FN(this, symbol, timeframeStartTime, previousEventTimestamp);
16698
+ i++;
16672
16699
  }
16673
- previousEventTimestamp = await EMIT_TIMEFRAME_PERFORMANCE_FN(this, symbol, timeframeStartTime, previousEventTimestamp);
16674
- i++;
16675
- }
16676
- // Emit final progress event (100%)
16677
- await EMIT_PROGRESS_FN(this, symbol, totalFrames, totalFrames);
16678
- // Track total backtest duration
16679
- const backtestEndTime = performance.now();
16680
- const currentTimestamp = Date.now();
16681
- await performanceEmitter.next({
16682
- timestamp: currentTimestamp,
16683
- previousTimestamp: previousEventTimestamp,
16684
- metricType: "backtest_total",
16685
- duration: backtestEndTime - backtestStartTime,
16686
- strategyName: this.methodContextService.context.strategyName,
16687
- exchangeName: this.methodContextService.context.exchangeName,
16688
- frameName: this.methodContextService.context.frameName,
16689
- symbol,
16690
- backtest: true,
16691
- });
16700
+ // Emit final progress event (100%)
16701
+ await EMIT_PROGRESS_FN(this, symbol, totalFrames, totalFrames);
16702
+ // Track total backtest duration
16703
+ const backtestEndTime = performance.now();
16704
+ const currentTimestamp = Date.now();
16705
+ await performanceEmitter.next({
16706
+ timestamp: currentTimestamp,
16707
+ previousTimestamp: previousEventTimestamp,
16708
+ metricType: "backtest_total",
16709
+ duration: backtestEndTime - backtestStartTime,
16710
+ strategyName: this.methodContextService.context.strategyName,
16711
+ exchangeName: this.methodContextService.context.exchangeName,
16712
+ frameName: this.methodContextService.context.frameName,
16713
+ symbol,
16714
+ backtest: true,
16715
+ });
16716
+ }
16717
+ catch (error) {
16718
+ _fatalError = error;
16719
+ }
16720
+ finally {
16721
+ if (_fatalError !== null) {
16722
+ console.error(`[BacktestLogicPrivateService] Fatal error — backtest sequence broken for symbol=${symbol} ` +
16723
+ `strategy=${this.methodContextService.context.strategyName}`, _fatalError);
16724
+ process.exit(-1);
16725
+ }
16726
+ }
16692
16727
  }
16693
16728
  }
16694
16729
 
@@ -33344,54 +33379,144 @@ const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommi
33344
33379
  const BROKER_BASE_METHOD_NAME_ON_TRAILING_TAKE = "BrokerBase.onTrailingTakeCommit";
33345
33380
  const BROKER_BASE_METHOD_NAME_ON_BREAKEVEN = "BrokerBase.onBreakevenCommit";
33346
33381
  const BROKER_BASE_METHOD_NAME_ON_AVERAGE_BUY = "BrokerBase.onAverageBuyCommit";
33382
+ /**
33383
+ * Wrapper around a `Partial<IBroker>` adapter instance.
33384
+ *
33385
+ * Implements the full `IBroker` interface but guards every method call —
33386
+ * if the underlying adapter does not implement a given method, an error is thrown.
33387
+ * `waitForInit` is the only exception: it is silently skipped when not implemented.
33388
+ *
33389
+ * Created internally by `BrokerAdapter.useBrokerAdapter` and stored as
33390
+ * `_brokerInstance`. All `BrokerAdapter.commit*` methods delegate here
33391
+ * after backtest-mode and enable-state checks pass.
33392
+ */
33347
33393
  class BrokerProxy {
33348
33394
  constructor(_instance) {
33349
33395
  this._instance = _instance;
33396
+ /**
33397
+ * Calls `waitForInit` on the underlying adapter exactly once (singleshot).
33398
+ * If the adapter does not implement `waitForInit`, the call is silently skipped.
33399
+ *
33400
+ * @returns Resolves when initialization is complete (or immediately if not implemented).
33401
+ */
33350
33402
  this.waitForInit = functoolsKit.singleshot(async () => {
33351
33403
  if (this._instance.waitForInit) {
33352
33404
  await this._instance.waitForInit();
33405
+ return;
33353
33406
  }
33354
33407
  });
33355
33408
  }
33409
+ /**
33410
+ * Forwards a signal-open event to the underlying adapter.
33411
+ * Throws if the adapter does not implement `onSignalOpenCommit`.
33412
+ *
33413
+ * @param payload - Signal open details: symbol, cost, position, prices, context, backtest flag.
33414
+ * @throws {Error} If the adapter does not implement `onSignalOpenCommit`.
33415
+ */
33356
33416
  async onSignalOpenCommit(payload) {
33357
33417
  if (this._instance.onSignalOpenCommit) {
33358
33418
  await this._instance.onSignalOpenCommit(payload);
33419
+ return;
33359
33420
  }
33421
+ throw new Error("BrokerProxy onSignalOpenCommit is not implemented");
33360
33422
  }
33423
+ /**
33424
+ * Forwards a signal-close event to the underlying adapter.
33425
+ * Throws if the adapter does not implement `onSignalCloseCommit`.
33426
+ *
33427
+ * @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, context, backtest flag.
33428
+ * @throws {Error} If the adapter does not implement `onSignalCloseCommit`.
33429
+ */
33361
33430
  async onSignalCloseCommit(payload) {
33362
33431
  if (this._instance.onSignalCloseCommit) {
33363
33432
  await this._instance.onSignalCloseCommit(payload);
33433
+ return;
33364
33434
  }
33435
+ throw new Error("BrokerProxy onSignalCloseCommit is not implemented");
33365
33436
  }
33437
+ /**
33438
+ * Forwards a partial-profit close event to the underlying adapter.
33439
+ * Throws if the adapter does not implement `onPartialProfitCommit`.
33440
+ *
33441
+ * @param payload - Partial profit details: symbol, percentToClose, cost, currentPrice, context, backtest flag.
33442
+ * @throws {Error} If the adapter does not implement `onPartialProfitCommit`.
33443
+ */
33366
33444
  async onPartialProfitCommit(payload) {
33367
33445
  if (this._instance.onPartialProfitCommit) {
33368
33446
  await this._instance.onPartialProfitCommit(payload);
33447
+ return;
33369
33448
  }
33449
+ throw new Error("BrokerProxy onPartialProfitCommit is not implemented");
33370
33450
  }
33451
+ /**
33452
+ * Forwards a partial-loss close event to the underlying adapter.
33453
+ * Throws if the adapter does not implement `onPartialLossCommit`.
33454
+ *
33455
+ * @param payload - Partial loss details: symbol, percentToClose, cost, currentPrice, context, backtest flag.
33456
+ * @throws {Error} If the adapter does not implement `onPartialLossCommit`.
33457
+ */
33371
33458
  async onPartialLossCommit(payload) {
33372
33459
  if (this._instance.onPartialLossCommit) {
33373
33460
  await this._instance.onPartialLossCommit(payload);
33461
+ return;
33374
33462
  }
33463
+ throw new Error("BrokerProxy onPartialLossCommit is not implemented");
33375
33464
  }
33465
+ /**
33466
+ * Forwards a trailing stop-loss update event to the underlying adapter.
33467
+ * Throws if the adapter does not implement `onTrailingStopCommit`.
33468
+ *
33469
+ * @param payload - Trailing stop details: symbol, percentShift, currentPrice, newStopLossPrice, context, backtest flag.
33470
+ * @throws {Error} If the adapter does not implement `onTrailingStopCommit`.
33471
+ */
33376
33472
  async onTrailingStopCommit(payload) {
33377
33473
  if (this._instance.onTrailingStopCommit) {
33378
33474
  await this._instance.onTrailingStopCommit(payload);
33475
+ return;
33379
33476
  }
33477
+ throw new Error("BrokerProxy onTrailingStopCommit is not implemented");
33380
33478
  }
33479
+ /**
33480
+ * Forwards a trailing take-profit update event to the underlying adapter.
33481
+ * Throws if the adapter does not implement `onTrailingTakeCommit`.
33482
+ *
33483
+ * @param payload - Trailing take details: symbol, percentShift, currentPrice, newTakeProfitPrice, context, backtest flag.
33484
+ * @throws {Error} If the adapter does not implement `onTrailingTakeCommit`.
33485
+ */
33381
33486
  async onTrailingTakeCommit(payload) {
33382
33487
  if (this._instance.onTrailingTakeCommit) {
33383
33488
  await this._instance.onTrailingTakeCommit(payload);
33489
+ return;
33384
33490
  }
33491
+ throw new Error("BrokerProxy onTrailingTakeCommit is not implemented");
33385
33492
  }
33493
+ /**
33494
+ * Forwards a breakeven event to the underlying adapter.
33495
+ * Throws if the adapter does not implement `onBreakevenCommit`.
33496
+ *
33497
+ * @param payload - Breakeven details: symbol, currentPrice, newStopLossPrice (= effectivePriceOpen), newTakeProfitPrice, context, backtest flag.
33498
+ * @throws {Error} If the adapter does not implement `onBreakevenCommit`.
33499
+ */
33386
33500
  async onBreakevenCommit(payload) {
33387
33501
  if (this._instance.onBreakevenCommit) {
33388
33502
  await this._instance.onBreakevenCommit(payload);
33503
+ return;
33389
33504
  }
33505
+ throw new Error("BrokerProxy onBreakevenCommit is not implemented");
33390
33506
  }
33507
+ /**
33508
+ * Forwards a DCA average-buy entry event to the underlying adapter.
33509
+ * Throws if the adapter does not implement `onAverageBuyCommit`.
33510
+ *
33511
+ * @param payload - Average buy details: symbol, currentPrice, cost, context, backtest flag.
33512
+ * @throws {Error} If the adapter does not implement `onAverageBuyCommit`.
33513
+ */
33391
33514
  async onAverageBuyCommit(payload) {
33392
33515
  if (this._instance.onAverageBuyCommit) {
33393
33516
  await this._instance.onAverageBuyCommit(payload);
33517
+ return;
33394
33518
  }
33519
+ throw new Error("BrokerProxy onAverageBuyCommit is not implemented");
33395
33520
  }
33396
33521
  }
33397
33522
  /**
@@ -43163,7 +43288,7 @@ class MemoryLocalInstance {
43163
43288
  * @param value - Value to store and index
43164
43289
  * @param index - Optional BM25 index string; defaults to JSON.stringify(value)
43165
43290
  */
43166
- async writeMemory(memoryId, value, index) {
43291
+ async writeMemory(memoryId, value, description) {
43167
43292
  bt.loggerService.debug(MEMORY_LOCAL_INSTANCE_METHOD_NAME_WRITE, {
43168
43293
  signalId: this.signalId,
43169
43294
  bucketName: this.bucketName,
@@ -43172,7 +43297,7 @@ class MemoryLocalInstance {
43172
43297
  this._index.upsert({
43173
43298
  id: memoryId,
43174
43299
  content: value,
43175
- index: index ?? JSON.stringify(value),
43300
+ index: description,
43176
43301
  priority: Date.now(),
43177
43302
  });
43178
43303
  }
@@ -43471,7 +43596,7 @@ class MemoryAdapter {
43471
43596
  * @param dto.value - Value to store
43472
43597
  * @param dto.signalId - Signal identifier
43473
43598
  * @param dto.bucketName - Bucket name
43474
- * @param dto.index - Optional BM25 index string; defaults to JSON.stringify(value)
43599
+ * @param dto.description - Optional BM25 index string; defaults to JSON.stringify(value)
43475
43600
  */
43476
43601
  this.writeMemory = async (dto) => {
43477
43602
  if (!this.enable.hasValue()) {
@@ -43486,7 +43611,7 @@ class MemoryAdapter {
43486
43611
  const isInitial = !this.getInstance.has(key);
43487
43612
  const instance = this.getInstance(dto.signalId, dto.bucketName);
43488
43613
  await instance.waitForInit(isInitial);
43489
- return await instance.writeMemory(dto.memoryId, dto.value, dto.index);
43614
+ return await instance.writeMemory(dto.memoryId, dto.value, dto.description);
43490
43615
  };
43491
43616
  /**
43492
43617
  * Search memory using BM25 full-text scoring.
@@ -43637,7 +43762,7 @@ const REMOVE_MEMORY_METHOD_NAME = "memory.removeMemory";
43637
43762
  * ```
43638
43763
  */
43639
43764
  async function writeMemory(dto) {
43640
- const { bucketName, memoryId, value } = dto;
43765
+ const { bucketName, memoryId, value, description } = dto;
43641
43766
  bt.loggerService.info(WRITE_MEMORY_METHOD_NAME, {
43642
43767
  bucketName,
43643
43768
  memoryId,
@@ -43661,6 +43786,7 @@ async function writeMemory(dto) {
43661
43786
  value,
43662
43787
  signalId: signal.id,
43663
43788
  bucketName,
43789
+ description,
43664
43790
  });
43665
43791
  }
43666
43792
  /**
@@ -44104,7 +44230,7 @@ class DumpMemoryInstance {
44104
44230
  bucketName: this.bucketName,
44105
44231
  signalId: this.signalId,
44106
44232
  value: { messages },
44107
- index: description,
44233
+ description,
44108
44234
  });
44109
44235
  }
44110
44236
  /**
@@ -44125,7 +44251,7 @@ class DumpMemoryInstance {
44125
44251
  bucketName: this.bucketName,
44126
44252
  signalId: this.signalId,
44127
44253
  value: record,
44128
- index: description,
44254
+ description,
44129
44255
  });
44130
44256
  }
44131
44257
  /**
@@ -44147,7 +44273,7 @@ class DumpMemoryInstance {
44147
44273
  bucketName: this.bucketName,
44148
44274
  signalId: this.signalId,
44149
44275
  value: { rows },
44150
- index: description,
44276
+ description,
44151
44277
  });
44152
44278
  }
44153
44279
  /**
@@ -44168,7 +44294,7 @@ class DumpMemoryInstance {
44168
44294
  bucketName: this.bucketName,
44169
44295
  signalId: this.signalId,
44170
44296
  value: { content },
44171
- index: description,
44297
+ description,
44172
44298
  });
44173
44299
  }
44174
44300
  /**
@@ -44189,7 +44315,7 @@ class DumpMemoryInstance {
44189
44315
  bucketName: this.bucketName,
44190
44316
  signalId: this.signalId,
44191
44317
  value: { content },
44192
- index: description,
44318
+ description,
44193
44319
  });
44194
44320
  }
44195
44321
  /**
@@ -44211,7 +44337,7 @@ class DumpMemoryInstance {
44211
44337
  bucketName: this.bucketName,
44212
44338
  signalId: this.signalId,
44213
44339
  value: json,
44214
- index: description,
44340
+ description,
44215
44341
  });
44216
44342
  }
44217
44343
  /** Releases resources held by this instance. */