backtest-kit 1.2.3 → 1.3.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
@@ -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,36 @@ 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
+ // Если priceOpen указан - проверяем нужно ли ждать активации или открыть сразу
1594
1629
  if (signal.priceOpen !== undefined) {
1630
+ // КРИТИЧЕСКАЯ ПРОВЕРКА: достигнут ли priceOpen?
1631
+ // LONG: если currentPrice <= priceOpen - цена уже упала достаточно, открываем сразу
1632
+ // SHORT: если currentPrice >= priceOpen - цена уже выросла достаточно, открываем сразу
1633
+ const shouldActivateImmediately = (signal.position === "long" && currentPrice <= signal.priceOpen) ||
1634
+ (signal.position === "short" && currentPrice >= signal.priceOpen);
1635
+ if (shouldActivateImmediately) {
1636
+ // НЕМЕДЛЕННАЯ АКТИВАЦИЯ: priceOpen уже достигнут
1637
+ // Создаем активный сигнал напрямую (БЕЗ scheduled фазы)
1638
+ const signalRow = {
1639
+ id: functoolsKit.randomString(),
1640
+ priceOpen: signal.priceOpen, // Используем priceOpen из сигнала
1641
+ position: signal.position,
1642
+ note: signal.note,
1643
+ priceTakeProfit: signal.priceTakeProfit,
1644
+ priceStopLoss: signal.priceStopLoss,
1645
+ minuteEstimatedTime: signal.minuteEstimatedTime,
1646
+ symbol: self.params.execution.context.symbol,
1647
+ exchangeName: self.params.method.context.exchangeName,
1648
+ strategyName: self.params.method.context.strategyName,
1649
+ scheduledAt: currentTime,
1650
+ pendingAt: currentTime, // Для immediate signal оба времени одинаковые
1651
+ _isScheduled: false,
1652
+ };
1653
+ // Валидируем сигнал перед возвратом
1654
+ VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
1655
+ return signalRow;
1656
+ }
1657
+ // ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
1595
1658
  const scheduledSignalRow = {
1596
1659
  id: functoolsKit.randomString(),
1597
1660
  priceOpen: signal.priceOpen,
@@ -1608,7 +1671,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1608
1671
  _isScheduled: true,
1609
1672
  };
1610
1673
  // Валидируем сигнал перед возвратом
1611
- VALIDATE_SIGNAL_FN(scheduledSignalRow);
1674
+ VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
1612
1675
  return scheduledSignalRow;
1613
1676
  }
1614
1677
  const signalRow = {
@@ -1623,7 +1686,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1623
1686
  _isScheduled: false,
1624
1687
  };
1625
1688
  // Валидируем сигнал перед возвратом
1626
- VALIDATE_SIGNAL_FN(signalRow);
1689
+ VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
1627
1690
  return signalRow;
1628
1691
  }, {
1629
1692
  defaultValue: null,
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,36 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1588
1623
  if (!signal) {
1589
1624
  return null;
1590
1625
  }
1591
- // Если priceOpen указан - создаем scheduled signal (risk check при активации)
1626
+ // Если priceOpen указан - проверяем нужно ли ждать активации или открыть сразу
1592
1627
  if (signal.priceOpen !== undefined) {
1628
+ // КРИТИЧЕСКАЯ ПРОВЕРКА: достигнут ли priceOpen?
1629
+ // LONG: если currentPrice <= priceOpen - цена уже упала достаточно, открываем сразу
1630
+ // SHORT: если currentPrice >= priceOpen - цена уже выросла достаточно, открываем сразу
1631
+ const shouldActivateImmediately = (signal.position === "long" && currentPrice <= signal.priceOpen) ||
1632
+ (signal.position === "short" && currentPrice >= signal.priceOpen);
1633
+ if (shouldActivateImmediately) {
1634
+ // НЕМЕДЛЕННАЯ АКТИВАЦИЯ: priceOpen уже достигнут
1635
+ // Создаем активный сигнал напрямую (БЕЗ scheduled фазы)
1636
+ const signalRow = {
1637
+ id: randomString(),
1638
+ priceOpen: signal.priceOpen, // Используем priceOpen из сигнала
1639
+ position: signal.position,
1640
+ note: signal.note,
1641
+ priceTakeProfit: signal.priceTakeProfit,
1642
+ priceStopLoss: signal.priceStopLoss,
1643
+ minuteEstimatedTime: signal.minuteEstimatedTime,
1644
+ symbol: self.params.execution.context.symbol,
1645
+ exchangeName: self.params.method.context.exchangeName,
1646
+ strategyName: self.params.method.context.strategyName,
1647
+ scheduledAt: currentTime,
1648
+ pendingAt: currentTime, // Для immediate signal оба времени одинаковые
1649
+ _isScheduled: false,
1650
+ };
1651
+ // Валидируем сигнал перед возвратом
1652
+ VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
1653
+ return signalRow;
1654
+ }
1655
+ // ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
1593
1656
  const scheduledSignalRow = {
1594
1657
  id: randomString(),
1595
1658
  priceOpen: signal.priceOpen,
@@ -1606,7 +1669,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1606
1669
  _isScheduled: true,
1607
1670
  };
1608
1671
  // Валидируем сигнал перед возвратом
1609
- VALIDATE_SIGNAL_FN(scheduledSignalRow);
1672
+ VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
1610
1673
  return scheduledSignalRow;
1611
1674
  }
1612
1675
  const signalRow = {
@@ -1621,7 +1684,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1621
1684
  _isScheduled: false,
1622
1685
  };
1623
1686
  // Валидируем сигнал перед возвратом
1624
- VALIDATE_SIGNAL_FN(signalRow);
1687
+ VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
1625
1688
  return signalRow;
1626
1689
  }, {
1627
1690
  defaultValue: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",