backtest-kit 1.2.3 → 1.3.1

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
@@ -1460,8 +1460,15 @@ const INTERVAL_MINUTES$1 = {
1460
1460
  "30m": 30,
1461
1461
  "1h": 60,
1462
1462
  };
1463
- const VALIDATE_SIGNAL_FN = (signal) => {
1463
+ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1464
1464
  const errors = [];
1465
+ // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
1466
+ if (!isFinite(currentPrice)) {
1467
+ errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
1468
+ }
1469
+ if (isFinite(currentPrice) && currentPrice <= 0) {
1470
+ errors.push(`currentPrice must be positive, got ${currentPrice}`);
1471
+ }
1465
1472
  // ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
1466
1473
  if (!isFinite(signal.priceOpen)) {
1467
1474
  errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
@@ -1490,6 +1497,20 @@ const VALIDATE_SIGNAL_FN = (signal) => {
1490
1497
  if (signal.priceStopLoss >= signal.priceOpen) {
1491
1498
  errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
1492
1499
  }
1500
+ // ЗАЩИТА ОТ EDGE CASE: для immediate сигналов проверяем что текущая цена не пробила SL/TP
1501
+ // Для scheduled сигналов эта проверка избыточна т.к. priceOpen уже проверен выше
1502
+ if (!isScheduled) {
1503
+ // Текущая цена уже пробила StopLoss - позиция откроется и сразу закроется по SL
1504
+ if (isFinite(currentPrice) && currentPrice < signal.priceStopLoss) {
1505
+ errors.push(`Long: currentPrice (${currentPrice}) < priceStopLoss (${signal.priceStopLoss}). ` +
1506
+ `Signal would be immediately cancelled. This signal is invalid.`);
1507
+ }
1508
+ // Текущая цена уже достигла TakeProfit - профит упущен
1509
+ if (isFinite(currentPrice) && currentPrice > signal.priceTakeProfit) {
1510
+ errors.push(`Long: currentPrice (${currentPrice}) > priceTakeProfit (${signal.priceTakeProfit}). ` +
1511
+ `Signal is invalid - the profit opportunity has already passed.`);
1512
+ }
1513
+ }
1493
1514
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
1494
1515
  if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
1495
1516
  const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
@@ -1517,6 +1538,20 @@ const VALIDATE_SIGNAL_FN = (signal) => {
1517
1538
  if (signal.priceStopLoss <= signal.priceOpen) {
1518
1539
  errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
1519
1540
  }
1541
+ // ЗАЩИТА ОТ EDGE CASE: для immediate сигналов проверяем что текущая цена не пробила SL/TP
1542
+ // Для scheduled сигналов эта проверка избыточна т.к. priceOpen уже проверен выше
1543
+ if (!isScheduled) {
1544
+ // Текущая цена уже пробила StopLoss - позиция откроется и сразу закроется по SL
1545
+ if (isFinite(currentPrice) && currentPrice > signal.priceStopLoss) {
1546
+ errors.push(`Short: currentPrice (${currentPrice}) > priceStopLoss (${signal.priceStopLoss}). ` +
1547
+ `Signal would be immediately cancelled. This signal is invalid.`);
1548
+ }
1549
+ // Текущая цена уже достигла TakeProfit - профит упущен
1550
+ if (isFinite(currentPrice) && currentPrice < signal.priceTakeProfit) {
1551
+ errors.push(`Short: currentPrice (${currentPrice}) < priceTakeProfit (${signal.priceTakeProfit}). ` +
1552
+ `Signal is invalid - the profit opportunity has already passed.`);
1553
+ }
1554
+ }
1520
1555
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
1521
1556
  if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
1522
1557
  const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
@@ -1590,8 +1625,39 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1590
1625
  if (!signal) {
1591
1626
  return null;
1592
1627
  }
1593
- // Если priceOpen указан - создаем scheduled signal (risk check при активации)
1628
+ if (self._isStopped) {
1629
+ return null;
1630
+ }
1631
+ // Если priceOpen указан - проверяем нужно ли ждать активации или открыть сразу
1594
1632
  if (signal.priceOpen !== undefined) {
1633
+ // КРИТИЧЕСКАЯ ПРОВЕРКА: достигнут ли priceOpen?
1634
+ // LONG: если currentPrice <= priceOpen - цена уже упала достаточно, открываем сразу
1635
+ // SHORT: если currentPrice >= priceOpen - цена уже выросла достаточно, открываем сразу
1636
+ const shouldActivateImmediately = (signal.position === "long" && currentPrice <= signal.priceOpen) ||
1637
+ (signal.position === "short" && currentPrice >= signal.priceOpen);
1638
+ if (shouldActivateImmediately) {
1639
+ // НЕМЕДЛЕННАЯ АКТИВАЦИЯ: priceOpen уже достигнут
1640
+ // Создаем активный сигнал напрямую (БЕЗ scheduled фазы)
1641
+ const signalRow = {
1642
+ id: functoolsKit.randomString(),
1643
+ priceOpen: signal.priceOpen, // Используем priceOpen из сигнала
1644
+ position: signal.position,
1645
+ note: signal.note,
1646
+ priceTakeProfit: signal.priceTakeProfit,
1647
+ priceStopLoss: signal.priceStopLoss,
1648
+ minuteEstimatedTime: signal.minuteEstimatedTime,
1649
+ symbol: self.params.execution.context.symbol,
1650
+ exchangeName: self.params.method.context.exchangeName,
1651
+ strategyName: self.params.method.context.strategyName,
1652
+ scheduledAt: currentTime,
1653
+ pendingAt: currentTime, // Для immediate signal оба времени одинаковые
1654
+ _isScheduled: false,
1655
+ };
1656
+ // Валидируем сигнал перед возвратом
1657
+ VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
1658
+ return signalRow;
1659
+ }
1660
+ // ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
1595
1661
  const scheduledSignalRow = {
1596
1662
  id: functoolsKit.randomString(),
1597
1663
  priceOpen: signal.priceOpen,
@@ -1608,7 +1674,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1608
1674
  _isScheduled: true,
1609
1675
  };
1610
1676
  // Валидируем сигнал перед возвратом
1611
- VALIDATE_SIGNAL_FN(scheduledSignalRow);
1677
+ VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
1612
1678
  return scheduledSignalRow;
1613
1679
  }
1614
1680
  const signalRow = {
@@ -1623,7 +1689,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1623
1689
  _isScheduled: false,
1624
1690
  };
1625
1691
  // Валидируем сигнал перед возвратом
1626
- VALIDATE_SIGNAL_FN(signalRow);
1692
+ VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
1627
1693
  return signalRow;
1628
1694
  }, {
1629
1695
  defaultValue: null,
@@ -2288,6 +2354,14 @@ class ClientStrategy {
2288
2354
  }
2289
2355
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
2290
2356
  }
2357
+ /**
2358
+ * Retrieves the current pending signal.
2359
+ * If no signal is pending, returns null.
2360
+ * @returns Promise resolving to the pending signal or null.
2361
+ */
2362
+ async getPendingSignal() {
2363
+ return this._pendingSignal;
2364
+ }
2291
2365
  /**
2292
2366
  * Performs a single tick of strategy execution.
2293
2367
  *
@@ -2593,6 +2667,17 @@ class StrategyConnectionService {
2593
2667
  callbacks,
2594
2668
  });
2595
2669
  });
2670
+ /**
2671
+ * Retrieves the currently active pending signal for the strategy.
2672
+ * If no active signal exists, returns null.
2673
+ * Used internally for monitoring TP/SL and time expiration.
2674
+ * @returns Promise resolving to pending signal or null
2675
+ */
2676
+ this.getPendingSignal = async () => {
2677
+ this.loggerService.log("strategyConnectionService getPendingSignal");
2678
+ const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
2679
+ return await strategy.getPendingSignal();
2680
+ };
2596
2681
  /**
2597
2682
  * Executes live trading tick for current strategy.
2598
2683
  *
@@ -3495,6 +3580,31 @@ class StrategyGlobalService {
3495
3580
  riskName &&
3496
3581
  this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE);
3497
3582
  });
3583
+ /**
3584
+ * Retrieves the currently active pending signal for the symbol.
3585
+ * If no active signal exists, returns null.
3586
+ * Used internally for monitoring TP/SL and time expiration.
3587
+ *
3588
+ * @param symbol - Trading pair symbol
3589
+ * @param when - Timestamp for tick evaluation
3590
+ * @param backtest - Whether running in backtest mode
3591
+ * @returns Promise resolving to pending signal or null
3592
+ */
3593
+ this.getPendingSignal = async (symbol, when, backtest) => {
3594
+ this.loggerService.log("strategyGlobalService getPendingSignal", {
3595
+ symbol,
3596
+ when,
3597
+ backtest,
3598
+ });
3599
+ await this.validate(this.methodContextService.context.strategyName);
3600
+ return await ExecutionContextService.runInContext(async () => {
3601
+ return await this.strategyConnectionService.getPendingSignal();
3602
+ }, {
3603
+ symbol,
3604
+ when,
3605
+ backtest,
3606
+ });
3607
+ };
3498
3608
  /**
3499
3609
  * Checks signal status at a specific timestamp.
3500
3610
  *
@@ -9422,22 +9532,39 @@ class LiveUtils {
9422
9532
  context,
9423
9533
  });
9424
9534
  let isStopped = false;
9535
+ let isDone = false;
9425
9536
  const task = async () => {
9426
9537
  for await (const signal of this.run(symbol, context)) {
9427
9538
  if (signal?.action === "closed" && isStopped) {
9428
9539
  break;
9429
9540
  }
9430
9541
  }
9431
- await doneLiveSubject.next({
9432
- exchangeName: context.exchangeName,
9433
- strategyName: context.strategyName,
9434
- backtest: false,
9435
- symbol,
9436
- });
9542
+ if (!isDone) {
9543
+ await doneLiveSubject.next({
9544
+ exchangeName: context.exchangeName,
9545
+ strategyName: context.strategyName,
9546
+ backtest: false,
9547
+ symbol,
9548
+ });
9549
+ }
9550
+ isDone = true;
9437
9551
  };
9438
9552
  task().catch((error) => errorEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
9439
9553
  return () => {
9440
9554
  backtest$1.strategyGlobalService.stop(context.strategyName);
9555
+ backtest$1.strategyGlobalService
9556
+ .getPendingSignal(symbol, new Date(), false)
9557
+ .then(async () => {
9558
+ if (!isDone) {
9559
+ await doneLiveSubject.next({
9560
+ exchangeName: context.exchangeName,
9561
+ strategyName: context.strategyName,
9562
+ backtest: false,
9563
+ symbol,
9564
+ });
9565
+ }
9566
+ isDone = true;
9567
+ });
9441
9568
  isStopped = true;
9442
9569
  };
9443
9570
  };
package/build/index.mjs CHANGED
@@ -1458,8 +1458,15 @@ const INTERVAL_MINUTES$1 = {
1458
1458
  "30m": 30,
1459
1459
  "1h": 60,
1460
1460
  };
1461
- const VALIDATE_SIGNAL_FN = (signal) => {
1461
+ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1462
1462
  const errors = [];
1463
+ // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
1464
+ if (!isFinite(currentPrice)) {
1465
+ errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
1466
+ }
1467
+ if (isFinite(currentPrice) && currentPrice <= 0) {
1468
+ errors.push(`currentPrice must be positive, got ${currentPrice}`);
1469
+ }
1463
1470
  // ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
1464
1471
  if (!isFinite(signal.priceOpen)) {
1465
1472
  errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
@@ -1488,6 +1495,20 @@ const VALIDATE_SIGNAL_FN = (signal) => {
1488
1495
  if (signal.priceStopLoss >= signal.priceOpen) {
1489
1496
  errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
1490
1497
  }
1498
+ // ЗАЩИТА ОТ EDGE CASE: для immediate сигналов проверяем что текущая цена не пробила SL/TP
1499
+ // Для scheduled сигналов эта проверка избыточна т.к. priceOpen уже проверен выше
1500
+ if (!isScheduled) {
1501
+ // Текущая цена уже пробила StopLoss - позиция откроется и сразу закроется по SL
1502
+ if (isFinite(currentPrice) && currentPrice < signal.priceStopLoss) {
1503
+ errors.push(`Long: currentPrice (${currentPrice}) < priceStopLoss (${signal.priceStopLoss}). ` +
1504
+ `Signal would be immediately cancelled. This signal is invalid.`);
1505
+ }
1506
+ // Текущая цена уже достигла TakeProfit - профит упущен
1507
+ if (isFinite(currentPrice) && currentPrice > signal.priceTakeProfit) {
1508
+ errors.push(`Long: currentPrice (${currentPrice}) > priceTakeProfit (${signal.priceTakeProfit}). ` +
1509
+ `Signal is invalid - the profit opportunity has already passed.`);
1510
+ }
1511
+ }
1491
1512
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
1492
1513
  if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
1493
1514
  const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
@@ -1515,6 +1536,20 @@ const VALIDATE_SIGNAL_FN = (signal) => {
1515
1536
  if (signal.priceStopLoss <= signal.priceOpen) {
1516
1537
  errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
1517
1538
  }
1539
+ // ЗАЩИТА ОТ EDGE CASE: для immediate сигналов проверяем что текущая цена не пробила SL/TP
1540
+ // Для scheduled сигналов эта проверка избыточна т.к. priceOpen уже проверен выше
1541
+ if (!isScheduled) {
1542
+ // Текущая цена уже пробила StopLoss - позиция откроется и сразу закроется по SL
1543
+ if (isFinite(currentPrice) && currentPrice > signal.priceStopLoss) {
1544
+ errors.push(`Short: currentPrice (${currentPrice}) > priceStopLoss (${signal.priceStopLoss}). ` +
1545
+ `Signal would be immediately cancelled. This signal is invalid.`);
1546
+ }
1547
+ // Текущая цена уже достигла TakeProfit - профит упущен
1548
+ if (isFinite(currentPrice) && currentPrice < signal.priceTakeProfit) {
1549
+ errors.push(`Short: currentPrice (${currentPrice}) < priceTakeProfit (${signal.priceTakeProfit}). ` +
1550
+ `Signal is invalid - the profit opportunity has already passed.`);
1551
+ }
1552
+ }
1518
1553
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
1519
1554
  if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
1520
1555
  const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
@@ -1588,8 +1623,39 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1588
1623
  if (!signal) {
1589
1624
  return null;
1590
1625
  }
1591
- // Если priceOpen указан - создаем scheduled signal (risk check при активации)
1626
+ if (self._isStopped) {
1627
+ return null;
1628
+ }
1629
+ // Если priceOpen указан - проверяем нужно ли ждать активации или открыть сразу
1592
1630
  if (signal.priceOpen !== undefined) {
1631
+ // КРИТИЧЕСКАЯ ПРОВЕРКА: достигнут ли priceOpen?
1632
+ // LONG: если currentPrice <= priceOpen - цена уже упала достаточно, открываем сразу
1633
+ // SHORT: если currentPrice >= priceOpen - цена уже выросла достаточно, открываем сразу
1634
+ const shouldActivateImmediately = (signal.position === "long" && currentPrice <= signal.priceOpen) ||
1635
+ (signal.position === "short" && currentPrice >= signal.priceOpen);
1636
+ if (shouldActivateImmediately) {
1637
+ // НЕМЕДЛЕННАЯ АКТИВАЦИЯ: priceOpen уже достигнут
1638
+ // Создаем активный сигнал напрямую (БЕЗ scheduled фазы)
1639
+ const signalRow = {
1640
+ id: randomString(),
1641
+ priceOpen: signal.priceOpen, // Используем priceOpen из сигнала
1642
+ position: signal.position,
1643
+ note: signal.note,
1644
+ priceTakeProfit: signal.priceTakeProfit,
1645
+ priceStopLoss: signal.priceStopLoss,
1646
+ minuteEstimatedTime: signal.minuteEstimatedTime,
1647
+ symbol: self.params.execution.context.symbol,
1648
+ exchangeName: self.params.method.context.exchangeName,
1649
+ strategyName: self.params.method.context.strategyName,
1650
+ scheduledAt: currentTime,
1651
+ pendingAt: currentTime, // Для immediate signal оба времени одинаковые
1652
+ _isScheduled: false,
1653
+ };
1654
+ // Валидируем сигнал перед возвратом
1655
+ VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
1656
+ return signalRow;
1657
+ }
1658
+ // ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
1593
1659
  const scheduledSignalRow = {
1594
1660
  id: randomString(),
1595
1661
  priceOpen: signal.priceOpen,
@@ -1606,7 +1672,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1606
1672
  _isScheduled: true,
1607
1673
  };
1608
1674
  // Валидируем сигнал перед возвратом
1609
- VALIDATE_SIGNAL_FN(scheduledSignalRow);
1675
+ VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
1610
1676
  return scheduledSignalRow;
1611
1677
  }
1612
1678
  const signalRow = {
@@ -1621,7 +1687,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1621
1687
  _isScheduled: false,
1622
1688
  };
1623
1689
  // Валидируем сигнал перед возвратом
1624
- VALIDATE_SIGNAL_FN(signalRow);
1690
+ VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
1625
1691
  return signalRow;
1626
1692
  }, {
1627
1693
  defaultValue: null,
@@ -2286,6 +2352,14 @@ class ClientStrategy {
2286
2352
  }
2287
2353
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
2288
2354
  }
2355
+ /**
2356
+ * Retrieves the current pending signal.
2357
+ * If no signal is pending, returns null.
2358
+ * @returns Promise resolving to the pending signal or null.
2359
+ */
2360
+ async getPendingSignal() {
2361
+ return this._pendingSignal;
2362
+ }
2289
2363
  /**
2290
2364
  * Performs a single tick of strategy execution.
2291
2365
  *
@@ -2591,6 +2665,17 @@ class StrategyConnectionService {
2591
2665
  callbacks,
2592
2666
  });
2593
2667
  });
2668
+ /**
2669
+ * Retrieves the currently active pending signal for the strategy.
2670
+ * If no active signal exists, returns null.
2671
+ * Used internally for monitoring TP/SL and time expiration.
2672
+ * @returns Promise resolving to pending signal or null
2673
+ */
2674
+ this.getPendingSignal = async () => {
2675
+ this.loggerService.log("strategyConnectionService getPendingSignal");
2676
+ const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
2677
+ return await strategy.getPendingSignal();
2678
+ };
2594
2679
  /**
2595
2680
  * Executes live trading tick for current strategy.
2596
2681
  *
@@ -3493,6 +3578,31 @@ class StrategyGlobalService {
3493
3578
  riskName &&
3494
3579
  this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE);
3495
3580
  });
3581
+ /**
3582
+ * Retrieves the currently active pending signal for the symbol.
3583
+ * If no active signal exists, returns null.
3584
+ * Used internally for monitoring TP/SL and time expiration.
3585
+ *
3586
+ * @param symbol - Trading pair symbol
3587
+ * @param when - Timestamp for tick evaluation
3588
+ * @param backtest - Whether running in backtest mode
3589
+ * @returns Promise resolving to pending signal or null
3590
+ */
3591
+ this.getPendingSignal = async (symbol, when, backtest) => {
3592
+ this.loggerService.log("strategyGlobalService getPendingSignal", {
3593
+ symbol,
3594
+ when,
3595
+ backtest,
3596
+ });
3597
+ await this.validate(this.methodContextService.context.strategyName);
3598
+ return await ExecutionContextService.runInContext(async () => {
3599
+ return await this.strategyConnectionService.getPendingSignal();
3600
+ }, {
3601
+ symbol,
3602
+ when,
3603
+ backtest,
3604
+ });
3605
+ };
3496
3606
  /**
3497
3607
  * Checks signal status at a specific timestamp.
3498
3608
  *
@@ -9420,22 +9530,39 @@ class LiveUtils {
9420
9530
  context,
9421
9531
  });
9422
9532
  let isStopped = false;
9533
+ let isDone = false;
9423
9534
  const task = async () => {
9424
9535
  for await (const signal of this.run(symbol, context)) {
9425
9536
  if (signal?.action === "closed" && isStopped) {
9426
9537
  break;
9427
9538
  }
9428
9539
  }
9429
- await doneLiveSubject.next({
9430
- exchangeName: context.exchangeName,
9431
- strategyName: context.strategyName,
9432
- backtest: false,
9433
- symbol,
9434
- });
9540
+ if (!isDone) {
9541
+ await doneLiveSubject.next({
9542
+ exchangeName: context.exchangeName,
9543
+ strategyName: context.strategyName,
9544
+ backtest: false,
9545
+ symbol,
9546
+ });
9547
+ }
9548
+ isDone = true;
9435
9549
  };
9436
9550
  task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
9437
9551
  return () => {
9438
9552
  backtest$1.strategyGlobalService.stop(context.strategyName);
9553
+ backtest$1.strategyGlobalService
9554
+ .getPendingSignal(symbol, new Date(), false)
9555
+ .then(async () => {
9556
+ if (!isDone) {
9557
+ await doneLiveSubject.next({
9558
+ exchangeName: context.exchangeName,
9559
+ strategyName: context.strategyName,
9560
+ backtest: false,
9561
+ symbol,
9562
+ });
9563
+ }
9564
+ isDone = true;
9565
+ });
9439
9566
  isStopped = true;
9440
9567
  };
9441
9568
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -825,6 +825,15 @@ interface IStrategy {
825
825
  * @returns Promise resolving to tick result (idle | opened | active | closed)
826
826
  */
827
827
  tick: (symbol: string) => Promise<IStrategyTickResult>;
828
+ /**
829
+ * Retrieves the currently active pending signal for the symbol.
830
+ * If no active signal exists, returns null.
831
+ * Used internally for monitoring TP/SL and time expiration.
832
+ *
833
+ * @param symbol
834
+ * @returns
835
+ */
836
+ getPendingSignal: (symbol: string) => Promise<ISignalRow | null>;
828
837
  /**
829
838
  * Fast backtest using historical candles.
830
839
  * Iterates through candles, calculates VWAP, checks TP/SL on each candle.
@@ -4718,6 +4727,13 @@ declare class StrategyConnectionService implements IStrategy {
4718
4727
  * @returns Configured ClientStrategy instance
4719
4728
  */
4720
4729
  private getStrategy;
4730
+ /**
4731
+ * Retrieves the currently active pending signal for the strategy.
4732
+ * If no active signal exists, returns null.
4733
+ * Used internally for monitoring TP/SL and time expiration.
4734
+ * @returns Promise resolving to pending signal or null
4735
+ */
4736
+ getPendingSignal: () => Promise<ISignalRow | null>;
4721
4737
  /**
4722
4738
  * Executes live trading tick for current strategy.
4723
4739
  *
@@ -5165,6 +5181,17 @@ declare class StrategyGlobalService {
5165
5181
  * @returns Promise that resolves when validation is complete
5166
5182
  */
5167
5183
  private validate;
5184
+ /**
5185
+ * Retrieves the currently active pending signal for the symbol.
5186
+ * If no active signal exists, returns null.
5187
+ * Used internally for monitoring TP/SL and time expiration.
5188
+ *
5189
+ * @param symbol - Trading pair symbol
5190
+ * @param when - Timestamp for tick evaluation
5191
+ * @param backtest - Whether running in backtest mode
5192
+ * @returns Promise resolving to pending signal or null
5193
+ */
5194
+ getPendingSignal: (symbol: string, when: Date, backtest: boolean) => Promise<ISignalRow | null>;
5168
5195
  /**
5169
5196
  * Checks signal status at a specific timestamp.
5170
5197
  *