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 +137 -10
- package/build/index.mjs +137 -10
- package/package.json +1 -1
- package/types.d.ts +27 -0
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
|
-
|
|
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
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
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
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
|
*
|