backtest-kit 1.5.1 → 1.5.2
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 +116 -39
- package/build/index.mjs +116 -39
- package/package.json +1 -1
- package/types.d.ts +6 -0
package/build/index.cjs
CHANGED
|
@@ -25,7 +25,13 @@ const GLOBAL_CONFIG = {
|
|
|
25
25
|
* Must be greater than trading fees to ensure profitable trades
|
|
26
26
|
* Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
|
|
27
27
|
*/
|
|
28
|
-
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.
|
|
28
|
+
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.3,
|
|
29
|
+
/**
|
|
30
|
+
* Minimum StopLoss distance from priceOpen (percentage)
|
|
31
|
+
* Prevents signals from being immediately stopped out due to price volatility
|
|
32
|
+
* Default: 0.5% (buffer to avoid instant stop loss on normal market fluctuations)
|
|
33
|
+
*/
|
|
34
|
+
CC_MIN_STOPLOSS_DISTANCE_PERCENT: 0.5,
|
|
29
35
|
/**
|
|
30
36
|
* Maximum StopLoss distance from priceOpen (percentage)
|
|
31
37
|
* Prevents catastrophic losses from extreme StopLoss values
|
|
@@ -1821,18 +1827,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1821
1827
|
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
1822
1828
|
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
1823
1829
|
}
|
|
1824
|
-
// ЗАЩИТА ОТ
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
//
|
|
1828
|
-
if (
|
|
1829
|
-
errors.push(`Long: currentPrice (${currentPrice})
|
|
1830
|
-
`Signal would be immediately
|
|
1830
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
1831
|
+
if (!isScheduled && isFinite(currentPrice)) {
|
|
1832
|
+
// LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
|
|
1833
|
+
// SL < currentPrice < TP
|
|
1834
|
+
if (currentPrice <= signal.priceStopLoss) {
|
|
1835
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1836
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
1837
|
+
}
|
|
1838
|
+
if (currentPrice >= signal.priceTakeProfit) {
|
|
1839
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1840
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
1844
|
+
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
1845
|
+
// LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
|
|
1846
|
+
// SL < priceOpen < TP
|
|
1847
|
+
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
1848
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1849
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
1831
1850
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
`Signal is invalid - the profit opportunity has already passed.`);
|
|
1851
|
+
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
1852
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1853
|
+
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
1836
1854
|
}
|
|
1837
1855
|
}
|
|
1838
1856
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1844,8 +1862,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1844
1862
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1845
1863
|
}
|
|
1846
1864
|
}
|
|
1865
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
1866
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1867
|
+
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1868
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1869
|
+
errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1870
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
1871
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1847
1874
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1848
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1875
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1849
1876
|
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1850
1877
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1851
1878
|
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -1862,18 +1889,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1862
1889
|
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
1863
1890
|
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
1864
1891
|
}
|
|
1865
|
-
// ЗАЩИТА ОТ
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
//
|
|
1869
|
-
if (
|
|
1870
|
-
errors.push(`Short: currentPrice (${currentPrice})
|
|
1871
|
-
`Signal would be immediately
|
|
1892
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
1893
|
+
if (!isScheduled && isFinite(currentPrice)) {
|
|
1894
|
+
// SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
|
|
1895
|
+
// TP < currentPrice < SL
|
|
1896
|
+
if (currentPrice >= signal.priceStopLoss) {
|
|
1897
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1898
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
1872
1899
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1900
|
+
if (currentPrice <= signal.priceTakeProfit) {
|
|
1901
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1902
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
1906
|
+
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
1907
|
+
// SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
|
|
1908
|
+
// TP < priceOpen < SL
|
|
1909
|
+
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
1910
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1911
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
1912
|
+
}
|
|
1913
|
+
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
1914
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1915
|
+
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
1877
1916
|
}
|
|
1878
1917
|
}
|
|
1879
1918
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1885,8 +1924,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1885
1924
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1886
1925
|
}
|
|
1887
1926
|
}
|
|
1927
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
1928
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1929
|
+
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1930
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1931
|
+
errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1932
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
1933
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1888
1936
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1889
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1937
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1890
1938
|
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1891
1939
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1892
1940
|
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -2577,8 +2625,14 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
2577
2625
|
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
2578
2626
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2579
2627
|
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
2628
|
+
const bufferCandlesCount = candlesCount - 1;
|
|
2580
2629
|
for (let i = 0; i < candles.length; i++) {
|
|
2581
2630
|
const candle = candles[i];
|
|
2631
|
+
// КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
|
|
2632
|
+
// BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
|
|
2633
|
+
if (i < bufferCandlesCount) {
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2582
2636
|
const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
|
|
2583
2637
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2584
2638
|
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
@@ -2644,11 +2698,21 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
2644
2698
|
};
|
|
2645
2699
|
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
2646
2700
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2647
|
-
|
|
2648
|
-
|
|
2701
|
+
const bufferCandlesCount = candlesCount - 1;
|
|
2702
|
+
// КРИТИЧНО: проверяем TP/SL на КАЖДОЙ свече начиная после буфера
|
|
2703
|
+
// Первые bufferCandlesCount свечей - это буфер для VWAP
|
|
2704
|
+
for (let i = 0; i < candles.length; i++) {
|
|
2705
|
+
const currentCandle = candles[i];
|
|
2706
|
+
const currentCandleTimestamp = currentCandle.timestamp;
|
|
2707
|
+
// КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
|
|
2708
|
+
// BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
|
|
2709
|
+
if (i < bufferCandlesCount) {
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
// Берем последние candlesCount свечей для VWAP (включая буфер)
|
|
2713
|
+
const startIndex = Math.max(0, i - (candlesCount - 1));
|
|
2714
|
+
const recentCandles = candles.slice(startIndex, i + 1);
|
|
2649
2715
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2650
|
-
const currentCandleTimestamp = recentCandles[recentCandles.length - 1].timestamp;
|
|
2651
|
-
const currentCandle = recentCandles[recentCandles.length - 1];
|
|
2652
2716
|
let shouldClose = false;
|
|
2653
2717
|
let closeReason;
|
|
2654
2718
|
// Check time expiration FIRST (КРИТИЧНО!)
|
|
@@ -2948,9 +3012,10 @@ class ClientStrategy {
|
|
|
2948
3012
|
* 4. If cancelled: returns closed result with closeReason "cancelled"
|
|
2949
3013
|
*
|
|
2950
3014
|
* For pending signals:
|
|
2951
|
-
* 1. Iterates through candles
|
|
2952
|
-
* 2.
|
|
2953
|
-
* 3.
|
|
3015
|
+
* 1. Iterates through ALL candles starting from the first one
|
|
3016
|
+
* 2. Checks TP/SL using candle.high/low (immediate detection)
|
|
3017
|
+
* 3. VWAP calculated with dynamic window (1 to CC_AVG_PRICE_CANDLES_COUNT candles)
|
|
3018
|
+
* 4. Returns closed result (either TP/SL or time_expired)
|
|
2954
3019
|
*
|
|
2955
3020
|
* @param candles - Array of candles to process
|
|
2956
3021
|
* @returns Promise resolving to closed signal result with PNL
|
|
@@ -4906,13 +4971,17 @@ class BacktestLogicPrivateService {
|
|
|
4906
4971
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4907
4972
|
});
|
|
4908
4973
|
// Запрашиваем минутные свечи для мониторинга активации/отмены
|
|
4909
|
-
// КРИТИЧНО:
|
|
4910
|
-
//
|
|
4911
|
-
//
|
|
4912
|
-
|
|
4974
|
+
// КРИТИЧНО: запрашиваем:
|
|
4975
|
+
// - CC_AVG_PRICE_CANDLES_COUNT-1 для буфера VWAP (ДО when)
|
|
4976
|
+
// - CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
4977
|
+
// - minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
4978
|
+
// - +1 потому что when включается как первая свеча
|
|
4979
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
4980
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
4981
|
+
const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
|
|
4913
4982
|
let candles;
|
|
4914
4983
|
try {
|
|
4915
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded,
|
|
4984
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
|
|
4916
4985
|
}
|
|
4917
4986
|
catch (error) {
|
|
4918
4987
|
console.warn(`backtestLogicPrivateService getNextCandles failed for scheduled signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
|
|
@@ -4920,6 +4989,7 @@ class BacktestLogicPrivateService {
|
|
|
4920
4989
|
symbol,
|
|
4921
4990
|
signalId: signal.id,
|
|
4922
4991
|
candlesNeeded,
|
|
4992
|
+
bufferMinutes,
|
|
4923
4993
|
error: functoolsKit.errorData(error), message: functoolsKit.getErrorMessage(error),
|
|
4924
4994
|
});
|
|
4925
4995
|
await errorEmitter.next(error);
|
|
@@ -4992,16 +5062,23 @@ class BacktestLogicPrivateService {
|
|
|
4992
5062
|
signalId: signal.id,
|
|
4993
5063
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4994
5064
|
});
|
|
4995
|
-
// Получаем свечи для
|
|
5065
|
+
// КРИТИЧНО: Получаем свечи включая буфер для VWAP
|
|
5066
|
+
// Сдвигаем начало назад на CC_AVG_PRICE_CANDLES_COUNT-1 минут для буфера VWAP
|
|
5067
|
+
// Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
|
|
5068
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
5069
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
5070
|
+
const totalCandles = signal.minuteEstimatedTime + bufferMinutes;
|
|
4996
5071
|
let candles;
|
|
4997
5072
|
try {
|
|
4998
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m",
|
|
5073
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
|
|
4999
5074
|
}
|
|
5000
5075
|
catch (error) {
|
|
5001
5076
|
console.warn(`backtestLogicPrivateService getNextCandles failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
|
|
5002
5077
|
this.loggerService.warn("backtestLogicPrivateService getNextCandles failed for opened signal", {
|
|
5003
5078
|
symbol,
|
|
5004
5079
|
signalId: signal.id,
|
|
5080
|
+
totalCandles,
|
|
5081
|
+
bufferMinutes,
|
|
5005
5082
|
error: functoolsKit.errorData(error), message: functoolsKit.getErrorMessage(error),
|
|
5006
5083
|
});
|
|
5007
5084
|
await errorEmitter.next(error);
|
package/build/index.mjs
CHANGED
|
@@ -23,7 +23,13 @@ const GLOBAL_CONFIG = {
|
|
|
23
23
|
* Must be greater than trading fees to ensure profitable trades
|
|
24
24
|
* Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
|
|
25
25
|
*/
|
|
26
|
-
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.
|
|
26
|
+
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.3,
|
|
27
|
+
/**
|
|
28
|
+
* Minimum StopLoss distance from priceOpen (percentage)
|
|
29
|
+
* Prevents signals from being immediately stopped out due to price volatility
|
|
30
|
+
* Default: 0.5% (buffer to avoid instant stop loss on normal market fluctuations)
|
|
31
|
+
*/
|
|
32
|
+
CC_MIN_STOPLOSS_DISTANCE_PERCENT: 0.5,
|
|
27
33
|
/**
|
|
28
34
|
* Maximum StopLoss distance from priceOpen (percentage)
|
|
29
35
|
* Prevents catastrophic losses from extreme StopLoss values
|
|
@@ -1819,18 +1825,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1819
1825
|
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
1820
1826
|
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
1821
1827
|
}
|
|
1822
|
-
// ЗАЩИТА ОТ
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
//
|
|
1826
|
-
if (
|
|
1827
|
-
errors.push(`Long: currentPrice (${currentPrice})
|
|
1828
|
-
`Signal would be immediately
|
|
1828
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
1829
|
+
if (!isScheduled && isFinite(currentPrice)) {
|
|
1830
|
+
// LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
|
|
1831
|
+
// SL < currentPrice < TP
|
|
1832
|
+
if (currentPrice <= signal.priceStopLoss) {
|
|
1833
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1834
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
1835
|
+
}
|
|
1836
|
+
if (currentPrice >= signal.priceTakeProfit) {
|
|
1837
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1838
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
1842
|
+
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
1843
|
+
// LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
|
|
1844
|
+
// SL < priceOpen < TP
|
|
1845
|
+
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
1846
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1847
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
1829
1848
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
`Signal is invalid - the profit opportunity has already passed.`);
|
|
1849
|
+
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
1850
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1851
|
+
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
1834
1852
|
}
|
|
1835
1853
|
}
|
|
1836
1854
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1842,8 +1860,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1842
1860
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1843
1861
|
}
|
|
1844
1862
|
}
|
|
1863
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
1864
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1865
|
+
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1866
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1867
|
+
errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1868
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
1869
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1845
1872
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1846
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1873
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1847
1874
|
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1848
1875
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1849
1876
|
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -1860,18 +1887,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1860
1887
|
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
1861
1888
|
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
1862
1889
|
}
|
|
1863
|
-
// ЗАЩИТА ОТ
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
//
|
|
1867
|
-
if (
|
|
1868
|
-
errors.push(`Short: currentPrice (${currentPrice})
|
|
1869
|
-
`Signal would be immediately
|
|
1890
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
1891
|
+
if (!isScheduled && isFinite(currentPrice)) {
|
|
1892
|
+
// SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
|
|
1893
|
+
// TP < currentPrice < SL
|
|
1894
|
+
if (currentPrice >= signal.priceStopLoss) {
|
|
1895
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1896
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
1870
1897
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1898
|
+
if (currentPrice <= signal.priceTakeProfit) {
|
|
1899
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1900
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
1904
|
+
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
1905
|
+
// SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
|
|
1906
|
+
// TP < priceOpen < SL
|
|
1907
|
+
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
1908
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1909
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
1910
|
+
}
|
|
1911
|
+
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
1912
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1913
|
+
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
1875
1914
|
}
|
|
1876
1915
|
}
|
|
1877
1916
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1883,8 +1922,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1883
1922
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1884
1923
|
}
|
|
1885
1924
|
}
|
|
1925
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
1926
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1927
|
+
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1928
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1929
|
+
errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1930
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
1931
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1886
1934
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1887
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1935
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1888
1936
|
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1889
1937
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1890
1938
|
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -2575,8 +2623,14 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
2575
2623
|
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
2576
2624
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2577
2625
|
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
2626
|
+
const bufferCandlesCount = candlesCount - 1;
|
|
2578
2627
|
for (let i = 0; i < candles.length; i++) {
|
|
2579
2628
|
const candle = candles[i];
|
|
2629
|
+
// КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
|
|
2630
|
+
// BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
|
|
2631
|
+
if (i < bufferCandlesCount) {
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2580
2634
|
const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
|
|
2581
2635
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2582
2636
|
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
@@ -2642,11 +2696,21 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
2642
2696
|
};
|
|
2643
2697
|
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
2644
2698
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2645
|
-
|
|
2646
|
-
|
|
2699
|
+
const bufferCandlesCount = candlesCount - 1;
|
|
2700
|
+
// КРИТИЧНО: проверяем TP/SL на КАЖДОЙ свече начиная после буфера
|
|
2701
|
+
// Первые bufferCandlesCount свечей - это буфер для VWAP
|
|
2702
|
+
for (let i = 0; i < candles.length; i++) {
|
|
2703
|
+
const currentCandle = candles[i];
|
|
2704
|
+
const currentCandleTimestamp = currentCandle.timestamp;
|
|
2705
|
+
// КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
|
|
2706
|
+
// BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
|
|
2707
|
+
if (i < bufferCandlesCount) {
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
// Берем последние candlesCount свечей для VWAP (включая буфер)
|
|
2711
|
+
const startIndex = Math.max(0, i - (candlesCount - 1));
|
|
2712
|
+
const recentCandles = candles.slice(startIndex, i + 1);
|
|
2647
2713
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2648
|
-
const currentCandleTimestamp = recentCandles[recentCandles.length - 1].timestamp;
|
|
2649
|
-
const currentCandle = recentCandles[recentCandles.length - 1];
|
|
2650
2714
|
let shouldClose = false;
|
|
2651
2715
|
let closeReason;
|
|
2652
2716
|
// Check time expiration FIRST (КРИТИЧНО!)
|
|
@@ -2946,9 +3010,10 @@ class ClientStrategy {
|
|
|
2946
3010
|
* 4. If cancelled: returns closed result with closeReason "cancelled"
|
|
2947
3011
|
*
|
|
2948
3012
|
* For pending signals:
|
|
2949
|
-
* 1. Iterates through candles
|
|
2950
|
-
* 2.
|
|
2951
|
-
* 3.
|
|
3013
|
+
* 1. Iterates through ALL candles starting from the first one
|
|
3014
|
+
* 2. Checks TP/SL using candle.high/low (immediate detection)
|
|
3015
|
+
* 3. VWAP calculated with dynamic window (1 to CC_AVG_PRICE_CANDLES_COUNT candles)
|
|
3016
|
+
* 4. Returns closed result (either TP/SL or time_expired)
|
|
2952
3017
|
*
|
|
2953
3018
|
* @param candles - Array of candles to process
|
|
2954
3019
|
* @returns Promise resolving to closed signal result with PNL
|
|
@@ -4904,13 +4969,17 @@ class BacktestLogicPrivateService {
|
|
|
4904
4969
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4905
4970
|
});
|
|
4906
4971
|
// Запрашиваем минутные свечи для мониторинга активации/отмены
|
|
4907
|
-
// КРИТИЧНО:
|
|
4908
|
-
//
|
|
4909
|
-
//
|
|
4910
|
-
|
|
4972
|
+
// КРИТИЧНО: запрашиваем:
|
|
4973
|
+
// - CC_AVG_PRICE_CANDLES_COUNT-1 для буфера VWAP (ДО when)
|
|
4974
|
+
// - CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
4975
|
+
// - minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
4976
|
+
// - +1 потому что when включается как первая свеча
|
|
4977
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
4978
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
4979
|
+
const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
|
|
4911
4980
|
let candles;
|
|
4912
4981
|
try {
|
|
4913
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded,
|
|
4982
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
|
|
4914
4983
|
}
|
|
4915
4984
|
catch (error) {
|
|
4916
4985
|
console.warn(`backtestLogicPrivateService getNextCandles failed for scheduled signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
|
|
@@ -4918,6 +4987,7 @@ class BacktestLogicPrivateService {
|
|
|
4918
4987
|
symbol,
|
|
4919
4988
|
signalId: signal.id,
|
|
4920
4989
|
candlesNeeded,
|
|
4990
|
+
bufferMinutes,
|
|
4921
4991
|
error: errorData(error), message: getErrorMessage(error),
|
|
4922
4992
|
});
|
|
4923
4993
|
await errorEmitter.next(error);
|
|
@@ -4990,16 +5060,23 @@ class BacktestLogicPrivateService {
|
|
|
4990
5060
|
signalId: signal.id,
|
|
4991
5061
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4992
5062
|
});
|
|
4993
|
-
// Получаем свечи для
|
|
5063
|
+
// КРИТИЧНО: Получаем свечи включая буфер для VWAP
|
|
5064
|
+
// Сдвигаем начало назад на CC_AVG_PRICE_CANDLES_COUNT-1 минут для буфера VWAP
|
|
5065
|
+
// Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
|
|
5066
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
5067
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
5068
|
+
const totalCandles = signal.minuteEstimatedTime + bufferMinutes;
|
|
4994
5069
|
let candles;
|
|
4995
5070
|
try {
|
|
4996
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m",
|
|
5071
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
|
|
4997
5072
|
}
|
|
4998
5073
|
catch (error) {
|
|
4999
5074
|
console.warn(`backtestLogicPrivateService getNextCandles failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
|
|
5000
5075
|
this.loggerService.warn("backtestLogicPrivateService getNextCandles failed for opened signal", {
|
|
5001
5076
|
symbol,
|
|
5002
5077
|
signalId: signal.id,
|
|
5078
|
+
totalCandles,
|
|
5079
|
+
bufferMinutes,
|
|
5003
5080
|
error: errorData(error), message: getErrorMessage(error),
|
|
5004
5081
|
});
|
|
5005
5082
|
await errorEmitter.next(error);
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ declare const GLOBAL_CONFIG: {
|
|
|
19
19
|
* Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
|
|
20
20
|
*/
|
|
21
21
|
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: number;
|
|
22
|
+
/**
|
|
23
|
+
* Minimum StopLoss distance from priceOpen (percentage)
|
|
24
|
+
* Prevents signals from being immediately stopped out due to price volatility
|
|
25
|
+
* Default: 0.5% (buffer to avoid instant stop loss on normal market fluctuations)
|
|
26
|
+
*/
|
|
27
|
+
CC_MIN_STOPLOSS_DISTANCE_PERCENT: number;
|
|
22
28
|
/**
|
|
23
29
|
* Maximum StopLoss distance from priceOpen (percentage)
|
|
24
30
|
* Prevents catastrophic losses from extreme StopLoss values
|