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 +67 -4
- package/build/index.mjs +67 -4
- package/package.json +1 -1
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 указан -
|
|
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 указан -
|
|
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,
|