backtest-kit 1.5.1 → 1.5.3
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/README.md +177 -7
- package/build/index.cjs +443 -152
- package/build/index.mjs +444 -153
- package/package.json +1 -1
- package/types.d.ts +123 -6
package/build/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createActivator } from 'di-kit';
|
|
2
2
|
import { scoped } from 'di-scoped';
|
|
3
|
-
import { errorData, getErrorMessage, sleep, memoize, makeExtendable, singleshot, not, trycatch, retry, Subject, randomString, ToolRegistry, isObject, resolveDocuments, str, iterateDocuments, distinctDocuments, queued } from 'functools-kit';
|
|
3
|
+
import { errorData, getErrorMessage, sleep, memoize, makeExtendable, singleshot, not, trycatch, retry, Subject, randomString, ToolRegistry, isObject, and, resolveDocuments, str, iterateDocuments, distinctDocuments, queued } from 'functools-kit';
|
|
4
4
|
import fs, { mkdir, writeFile } from 'fs/promises';
|
|
5
5
|
import path, { join } from 'path';
|
|
6
6
|
import crypto from 'crypto';
|
|
@@ -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
|
|
@@ -1667,6 +1673,8 @@ const walkerCompleteSubject = new Subject();
|
|
|
1667
1673
|
/**
|
|
1668
1674
|
* Walker stop emitter for walker cancellation events.
|
|
1669
1675
|
* Emits when a walker comparison is stopped/cancelled.
|
|
1676
|
+
*
|
|
1677
|
+
* Includes walkerName to support multiple walkers running on the same symbol.
|
|
1670
1678
|
*/
|
|
1671
1679
|
const walkerStopSubject = new Subject();
|
|
1672
1680
|
/**
|
|
@@ -1819,18 +1827,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1819
1827
|
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
1820
1828
|
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
1821
1829
|
}
|
|
1822
|
-
// ЗАЩИТА ОТ
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
//
|
|
1826
|
-
if (
|
|
1827
|
-
errors.push(`Long: currentPrice (${currentPrice})
|
|
1828
|
-
`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.`);
|
|
1829
1837
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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.`);
|
|
1850
|
+
}
|
|
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.`);
|
|
1834
1854
|
}
|
|
1835
1855
|
}
|
|
1836
1856
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1842,8 +1862,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1842
1862
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1843
1863
|
}
|
|
1844
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
|
+
}
|
|
1845
1874
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1846
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1875
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1847
1876
|
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1848
1877
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1849
1878
|
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -1860,18 +1889,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1860
1889
|
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
1861
1890
|
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
1862
1891
|
}
|
|
1863
|
-
// ЗАЩИТА ОТ
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
//
|
|
1867
|
-
if (
|
|
1868
|
-
errors.push(`Short: currentPrice (${currentPrice})
|
|
1869
|
-
`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.`);
|
|
1899
|
+
}
|
|
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.`);
|
|
1870
1912
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
`Signal is invalid - the profit opportunity has already passed.`);
|
|
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.`);
|
|
1875
1916
|
}
|
|
1876
1917
|
}
|
|
1877
1918
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1883,8 +1924,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1883
1924
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1884
1925
|
}
|
|
1885
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
|
+
}
|
|
1886
1936
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1887
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1937
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1888
1938
|
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1889
1939
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1890
1940
|
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -2575,8 +2625,14 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
2575
2625
|
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
2576
2626
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2577
2627
|
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
2628
|
+
const bufferCandlesCount = candlesCount - 1;
|
|
2578
2629
|
for (let i = 0; i < candles.length; i++) {
|
|
2579
2630
|
const candle = candles[i];
|
|
2631
|
+
// КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
|
|
2632
|
+
// BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
|
|
2633
|
+
if (i < bufferCandlesCount) {
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2580
2636
|
const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
|
|
2581
2637
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2582
2638
|
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
@@ -2642,11 +2698,21 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
2642
2698
|
};
|
|
2643
2699
|
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
2644
2700
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2645
|
-
|
|
2646
|
-
|
|
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);
|
|
2647
2715
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2648
|
-
const currentCandleTimestamp = recentCandles[recentCandles.length - 1].timestamp;
|
|
2649
|
-
const currentCandle = recentCandles[recentCandles.length - 1];
|
|
2650
2716
|
let shouldClose = false;
|
|
2651
2717
|
let closeReason;
|
|
2652
2718
|
// Check time expiration FIRST (КРИТИЧНО!)
|
|
@@ -2843,6 +2909,23 @@ class ClientStrategy {
|
|
|
2843
2909
|
});
|
|
2844
2910
|
return this._pendingSignal;
|
|
2845
2911
|
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Returns the stopped state of the strategy.
|
|
2914
|
+
*
|
|
2915
|
+
* Indicates whether the strategy has been explicitly stopped and should
|
|
2916
|
+
* not continue processing new ticks or signals.
|
|
2917
|
+
*
|
|
2918
|
+
* @param symbol - Trading pair symbol
|
|
2919
|
+
* @param strategyName - Name of the strategy
|
|
2920
|
+
* @returns Promise resolving to true if strategy is stopped, false otherwise
|
|
2921
|
+
*/
|
|
2922
|
+
async getStopped(symbol, strategyName) {
|
|
2923
|
+
this.params.logger.debug("ClientStrategy getStopped", {
|
|
2924
|
+
symbol,
|
|
2925
|
+
strategyName,
|
|
2926
|
+
});
|
|
2927
|
+
return this._isStopped;
|
|
2928
|
+
}
|
|
2846
2929
|
/**
|
|
2847
2930
|
* Performs a single tick of strategy execution.
|
|
2848
2931
|
*
|
|
@@ -2946,9 +3029,10 @@ class ClientStrategy {
|
|
|
2946
3029
|
* 4. If cancelled: returns closed result with closeReason "cancelled"
|
|
2947
3030
|
*
|
|
2948
3031
|
* For pending signals:
|
|
2949
|
-
* 1. Iterates through candles
|
|
2950
|
-
* 2.
|
|
2951
|
-
* 3.
|
|
3032
|
+
* 1. Iterates through ALL candles starting from the first one
|
|
3033
|
+
* 2. Checks TP/SL using candle.high/low (immediate detection)
|
|
3034
|
+
* 3. VWAP calculated with dynamic window (1 to CC_AVG_PRICE_CANDLES_COUNT candles)
|
|
3035
|
+
* 4. Returns closed result (either TP/SL or time_expired)
|
|
2952
3036
|
*
|
|
2953
3037
|
* @param candles - Array of candles to process
|
|
2954
3038
|
* @returns Promise resolving to closed signal result with PNL
|
|
@@ -3084,18 +3168,24 @@ class ClientStrategy {
|
|
|
3084
3168
|
* // Existing signal will continue until natural close
|
|
3085
3169
|
* ```
|
|
3086
3170
|
*/
|
|
3087
|
-
async stop(symbol, strategyName) {
|
|
3171
|
+
async stop(symbol, strategyName, backtest) {
|
|
3088
3172
|
this.params.logger.debug("ClientStrategy stop", {
|
|
3089
3173
|
symbol,
|
|
3090
3174
|
strategyName,
|
|
3091
3175
|
hasPendingSignal: this._pendingSignal !== null,
|
|
3092
3176
|
hasScheduledSignal: this._scheduledSignal !== null,
|
|
3177
|
+
backtest,
|
|
3093
3178
|
});
|
|
3094
3179
|
this._isStopped = true;
|
|
3095
3180
|
// Clear scheduled signal if exists
|
|
3096
|
-
if (this._scheduledSignal) {
|
|
3097
|
-
|
|
3181
|
+
if (!this._scheduledSignal) {
|
|
3182
|
+
return;
|
|
3098
3183
|
}
|
|
3184
|
+
this._scheduledSignal = null;
|
|
3185
|
+
if (backtest) {
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, this.params.execution.context.symbol, this.params.strategyName);
|
|
3099
3189
|
}
|
|
3100
3190
|
}
|
|
3101
3191
|
|
|
@@ -3178,6 +3268,24 @@ class StrategyConnectionService {
|
|
|
3178
3268
|
const strategy = this.getStrategy(symbol, strategyName);
|
|
3179
3269
|
return await strategy.getPendingSignal(symbol, strategyName);
|
|
3180
3270
|
};
|
|
3271
|
+
/**
|
|
3272
|
+
* Retrieves the stopped state of the strategy.
|
|
3273
|
+
*
|
|
3274
|
+
* Delegates to the underlying strategy instance to check if it has been
|
|
3275
|
+
* marked as stopped and should cease operation.
|
|
3276
|
+
*
|
|
3277
|
+
* @param symbol - Trading pair symbol
|
|
3278
|
+
* @param strategyName - Name of the strategy
|
|
3279
|
+
* @returns Promise resolving to true if strategy is stopped, false otherwise
|
|
3280
|
+
*/
|
|
3281
|
+
this.getStopped = async (symbol, strategyName) => {
|
|
3282
|
+
this.loggerService.log("strategyConnectionService getStopped", {
|
|
3283
|
+
symbol,
|
|
3284
|
+
strategyName,
|
|
3285
|
+
});
|
|
3286
|
+
const strategy = this.getStrategy(symbol, strategyName);
|
|
3287
|
+
return await strategy.getStopped(symbol, strategyName);
|
|
3288
|
+
};
|
|
3181
3289
|
/**
|
|
3182
3290
|
* Executes live trading tick for current strategy.
|
|
3183
3291
|
*
|
|
@@ -3245,12 +3353,12 @@ class StrategyConnectionService {
|
|
|
3245
3353
|
* @param strategyName - Name of strategy to stop
|
|
3246
3354
|
* @returns Promise that resolves when stop flag is set
|
|
3247
3355
|
*/
|
|
3248
|
-
this.stop = async (ctx) => {
|
|
3356
|
+
this.stop = async (ctx, backtest) => {
|
|
3249
3357
|
this.loggerService.log("strategyConnectionService stop", {
|
|
3250
3358
|
ctx
|
|
3251
3359
|
});
|
|
3252
3360
|
const strategy = this.getStrategy(ctx.symbol, ctx.strategyName);
|
|
3253
|
-
await strategy.stop(ctx.symbol, ctx.strategyName);
|
|
3361
|
+
await strategy.stop(ctx.symbol, ctx.strategyName, backtest);
|
|
3254
3362
|
};
|
|
3255
3363
|
/**
|
|
3256
3364
|
* Clears the memoized ClientStrategy instance from cache.
|
|
@@ -4115,6 +4223,24 @@ class StrategyGlobalService {
|
|
|
4115
4223
|
await this.validate(symbol, strategyName);
|
|
4116
4224
|
return await this.strategyConnectionService.getPendingSignal(symbol, strategyName);
|
|
4117
4225
|
};
|
|
4226
|
+
/**
|
|
4227
|
+
* Checks if the strategy has been stopped.
|
|
4228
|
+
*
|
|
4229
|
+
* Validates strategy existence and delegates to connection service
|
|
4230
|
+
* to retrieve the stopped state from the strategy instance.
|
|
4231
|
+
*
|
|
4232
|
+
* @param symbol - Trading pair symbol
|
|
4233
|
+
* @param strategyName - Name of the strategy
|
|
4234
|
+
* @returns Promise resolving to true if strategy is stopped, false otherwise
|
|
4235
|
+
*/
|
|
4236
|
+
this.getStopped = async (symbol, strategyName) => {
|
|
4237
|
+
this.loggerService.log("strategyGlobalService getStopped", {
|
|
4238
|
+
symbol,
|
|
4239
|
+
strategyName,
|
|
4240
|
+
});
|
|
4241
|
+
await this.validate(symbol, strategyName);
|
|
4242
|
+
return await this.strategyConnectionService.getStopped(symbol, strategyName);
|
|
4243
|
+
};
|
|
4118
4244
|
/**
|
|
4119
4245
|
* Checks signal status at a specific timestamp.
|
|
4120
4246
|
*
|
|
@@ -4181,12 +4307,13 @@ class StrategyGlobalService {
|
|
|
4181
4307
|
* @param strategyName - Name of strategy to stop
|
|
4182
4308
|
* @returns Promise that resolves when stop flag is set
|
|
4183
4309
|
*/
|
|
4184
|
-
this.stop = async (ctx) => {
|
|
4310
|
+
this.stop = async (ctx, backtest) => {
|
|
4185
4311
|
this.loggerService.log("strategyGlobalService stop", {
|
|
4186
4312
|
ctx,
|
|
4313
|
+
backtest,
|
|
4187
4314
|
});
|
|
4188
4315
|
await this.validate(ctx.symbol, ctx.strategyName);
|
|
4189
|
-
return await this.strategyConnectionService.stop(ctx);
|
|
4316
|
+
return await this.strategyConnectionService.stop(ctx, backtest);
|
|
4190
4317
|
};
|
|
4191
4318
|
/**
|
|
4192
4319
|
* Clears the memoized ClientStrategy instance from cache.
|
|
@@ -4878,6 +5005,16 @@ class BacktestLogicPrivateService {
|
|
|
4878
5005
|
progress: totalFrames > 0 ? i / totalFrames : 0,
|
|
4879
5006
|
});
|
|
4880
5007
|
}
|
|
5008
|
+
// Check if strategy should stop before processing next frame
|
|
5009
|
+
if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
|
|
5010
|
+
this.loggerService.info("backtestLogicPrivateService stopped by user request (before tick)", {
|
|
5011
|
+
symbol,
|
|
5012
|
+
when: when.toISOString(),
|
|
5013
|
+
processedFrames: i,
|
|
5014
|
+
totalFrames,
|
|
5015
|
+
});
|
|
5016
|
+
break;
|
|
5017
|
+
}
|
|
4881
5018
|
let result;
|
|
4882
5019
|
try {
|
|
4883
5020
|
result = await this.strategyGlobalService.tick(symbol, when, true);
|
|
@@ -4893,6 +5030,16 @@ class BacktestLogicPrivateService {
|
|
|
4893
5030
|
i++;
|
|
4894
5031
|
continue;
|
|
4895
5032
|
}
|
|
5033
|
+
// Check if strategy should stop when idle (no active signal)
|
|
5034
|
+
if (await and(Promise.resolve(result.action === "idle"), this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName))) {
|
|
5035
|
+
this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
|
|
5036
|
+
symbol,
|
|
5037
|
+
when: when.toISOString(),
|
|
5038
|
+
processedFrames: i,
|
|
5039
|
+
totalFrames,
|
|
5040
|
+
});
|
|
5041
|
+
break;
|
|
5042
|
+
}
|
|
4896
5043
|
// Если scheduled signal создан - обрабатываем через backtest()
|
|
4897
5044
|
if (result.action === "scheduled") {
|
|
4898
5045
|
const signalStartTime = performance.now();
|
|
@@ -4904,13 +5051,17 @@ class BacktestLogicPrivateService {
|
|
|
4904
5051
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4905
5052
|
});
|
|
4906
5053
|
// Запрашиваем минутные свечи для мониторинга активации/отмены
|
|
4907
|
-
// КРИТИЧНО:
|
|
4908
|
-
//
|
|
4909
|
-
//
|
|
4910
|
-
|
|
5054
|
+
// КРИТИЧНО: запрашиваем:
|
|
5055
|
+
// - CC_AVG_PRICE_CANDLES_COUNT-1 для буфера VWAP (ДО when)
|
|
5056
|
+
// - CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
5057
|
+
// - minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
5058
|
+
// - +1 потому что when включается как первая свеча
|
|
5059
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
5060
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
5061
|
+
const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
|
|
4911
5062
|
let candles;
|
|
4912
5063
|
try {
|
|
4913
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded,
|
|
5064
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
|
|
4914
5065
|
}
|
|
4915
5066
|
catch (error) {
|
|
4916
5067
|
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 +5069,7 @@ class BacktestLogicPrivateService {
|
|
|
4918
5069
|
symbol,
|
|
4919
5070
|
signalId: signal.id,
|
|
4920
5071
|
candlesNeeded,
|
|
5072
|
+
bufferMinutes,
|
|
4921
5073
|
error: errorData(error), message: getErrorMessage(error),
|
|
4922
5074
|
});
|
|
4923
5075
|
await errorEmitter.next(error);
|
|
@@ -4980,6 +5132,16 @@ class BacktestLogicPrivateService {
|
|
|
4980
5132
|
i++;
|
|
4981
5133
|
}
|
|
4982
5134
|
yield backtestResult;
|
|
5135
|
+
// Check if strategy should stop after signal is closed
|
|
5136
|
+
if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
|
|
5137
|
+
this.loggerService.info("backtestLogicPrivateService stopped by user request (after scheduled signal closed)", {
|
|
5138
|
+
symbol,
|
|
5139
|
+
signalId: backtestResult.signal.id,
|
|
5140
|
+
processedFrames: i,
|
|
5141
|
+
totalFrames,
|
|
5142
|
+
});
|
|
5143
|
+
break;
|
|
5144
|
+
}
|
|
4983
5145
|
}
|
|
4984
5146
|
// Если обычный сигнал открыт, вызываем backtest
|
|
4985
5147
|
if (result.action === "opened") {
|
|
@@ -4990,16 +5152,23 @@ class BacktestLogicPrivateService {
|
|
|
4990
5152
|
signalId: signal.id,
|
|
4991
5153
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4992
5154
|
});
|
|
4993
|
-
// Получаем свечи для
|
|
5155
|
+
// КРИТИЧНО: Получаем свечи включая буфер для VWAP
|
|
5156
|
+
// Сдвигаем начало назад на CC_AVG_PRICE_CANDLES_COUNT-1 минут для буфера VWAP
|
|
5157
|
+
// Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
|
|
5158
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
5159
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
5160
|
+
const totalCandles = signal.minuteEstimatedTime + bufferMinutes;
|
|
4994
5161
|
let candles;
|
|
4995
5162
|
try {
|
|
4996
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m",
|
|
5163
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
|
|
4997
5164
|
}
|
|
4998
5165
|
catch (error) {
|
|
4999
5166
|
console.warn(`backtestLogicPrivateService getNextCandles failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
|
|
5000
5167
|
this.loggerService.warn("backtestLogicPrivateService getNextCandles failed for opened signal", {
|
|
5001
5168
|
symbol,
|
|
5002
5169
|
signalId: signal.id,
|
|
5170
|
+
totalCandles,
|
|
5171
|
+
bufferMinutes,
|
|
5003
5172
|
error: errorData(error), message: getErrorMessage(error),
|
|
5004
5173
|
});
|
|
5005
5174
|
await errorEmitter.next(error);
|
|
@@ -5056,6 +5225,16 @@ class BacktestLogicPrivateService {
|
|
|
5056
5225
|
i++;
|
|
5057
5226
|
}
|
|
5058
5227
|
yield backtestResult;
|
|
5228
|
+
// Check if strategy should stop after signal is closed
|
|
5229
|
+
if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
|
|
5230
|
+
this.loggerService.info("backtestLogicPrivateService stopped by user request (after signal closed)", {
|
|
5231
|
+
symbol,
|
|
5232
|
+
signalId: backtestResult.signal.id,
|
|
5233
|
+
processedFrames: i,
|
|
5234
|
+
totalFrames,
|
|
5235
|
+
});
|
|
5236
|
+
break;
|
|
5237
|
+
}
|
|
5059
5238
|
}
|
|
5060
5239
|
// Track timeframe processing duration
|
|
5061
5240
|
const timeframeEndTime = performance.now();
|
|
@@ -5163,7 +5342,8 @@ class LiveLogicPrivateService {
|
|
|
5163
5342
|
this.loggerService.warn("liveLogicPrivateService tick failed, retrying after sleep", {
|
|
5164
5343
|
symbol,
|
|
5165
5344
|
when: when.toISOString(),
|
|
5166
|
-
error: errorData(error),
|
|
5345
|
+
error: errorData(error),
|
|
5346
|
+
message: getErrorMessage(error),
|
|
5167
5347
|
});
|
|
5168
5348
|
await errorEmitter.next(error);
|
|
5169
5349
|
await sleep(TICK_TTL);
|
|
@@ -5187,11 +5367,19 @@ class LiveLogicPrivateService {
|
|
|
5187
5367
|
backtest: false,
|
|
5188
5368
|
});
|
|
5189
5369
|
previousEventTimestamp = currentTimestamp;
|
|
5190
|
-
if (
|
|
5370
|
+
// Check if strategy should stop when idle (no active signal)
|
|
5371
|
+
if (result.action === "idle") {
|
|
5372
|
+
if (await and(Promise.resolve(true), this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName))) {
|
|
5373
|
+
this.loggerService.info("liveLogicPrivateService stopped by user request (idle state)", {
|
|
5374
|
+
symbol,
|
|
5375
|
+
when: when.toISOString(),
|
|
5376
|
+
});
|
|
5377
|
+
break;
|
|
5378
|
+
}
|
|
5191
5379
|
await sleep(TICK_TTL);
|
|
5192
5380
|
continue;
|
|
5193
5381
|
}
|
|
5194
|
-
if (result.action === "
|
|
5382
|
+
if (result.action === "active") {
|
|
5195
5383
|
await sleep(TICK_TTL);
|
|
5196
5384
|
continue;
|
|
5197
5385
|
}
|
|
@@ -5201,12 +5389,21 @@ class LiveLogicPrivateService {
|
|
|
5201
5389
|
}
|
|
5202
5390
|
// Yield opened, closed results
|
|
5203
5391
|
yield result;
|
|
5392
|
+
// Check if strategy should stop after signal is closed
|
|
5393
|
+
if (result.action === "closed") {
|
|
5394
|
+
if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
|
|
5395
|
+
this.loggerService.info("liveLogicPrivateService stopped by user request (after signal closed)", {
|
|
5396
|
+
symbol,
|
|
5397
|
+
signalId: result.signal.id,
|
|
5398
|
+
});
|
|
5399
|
+
break;
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5204
5402
|
await sleep(TICK_TTL);
|
|
5205
5403
|
}
|
|
5206
5404
|
}
|
|
5207
5405
|
}
|
|
5208
5406
|
|
|
5209
|
-
const CANCEL_SYMBOL = Symbol("CANCEL_SYMBOL");
|
|
5210
5407
|
/**
|
|
5211
5408
|
* Private service for walker orchestration (strategy comparison).
|
|
5212
5409
|
*
|
|
@@ -5264,112 +5461,121 @@ class WalkerLogicPrivateService {
|
|
|
5264
5461
|
let strategiesTested = 0;
|
|
5265
5462
|
let bestMetric = null;
|
|
5266
5463
|
let bestStrategy = null;
|
|
5267
|
-
|
|
5268
|
-
const
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
.
|
|
5276
|
-
.
|
|
5277
|
-
// Run backtest for each strategy
|
|
5278
|
-
for (const strategyName of strategies) {
|
|
5279
|
-
// Call onStrategyStart callback if provided
|
|
5280
|
-
if (walkerSchema.callbacks?.onStrategyStart) {
|
|
5281
|
-
walkerSchema.callbacks.onStrategyStart(strategyName, symbol);
|
|
5282
|
-
}
|
|
5283
|
-
this.loggerService.info("walkerLogicPrivateService testing strategy", {
|
|
5284
|
-
strategyName,
|
|
5464
|
+
// Track stopped strategies in Set for efficient lookup
|
|
5465
|
+
const stoppedStrategies = new Set();
|
|
5466
|
+
// Subscribe to stop signals and collect them in Set
|
|
5467
|
+
// Filter by both symbol AND walkerName to support multiple walkers on same symbol
|
|
5468
|
+
// connect() returns destructor function
|
|
5469
|
+
const unsubscribe = walkerStopSubject
|
|
5470
|
+
.filter((data) => data.symbol === symbol && data.walkerName === context.walkerName)
|
|
5471
|
+
.connect((data) => {
|
|
5472
|
+
stoppedStrategies.add(data.strategyName);
|
|
5473
|
+
this.loggerService.info("walkerLogicPrivateService received stop signal for strategy", {
|
|
5285
5474
|
symbol,
|
|
5475
|
+
walkerName: context.walkerName,
|
|
5476
|
+
strategyName: data.strategyName,
|
|
5477
|
+
stoppedCount: stoppedStrategies.size,
|
|
5286
5478
|
});
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5479
|
+
});
|
|
5480
|
+
try {
|
|
5481
|
+
// Run backtest for each strategy
|
|
5482
|
+
for (const strategyName of strategies) {
|
|
5483
|
+
// Check if this strategy should be stopped before starting
|
|
5484
|
+
if (stoppedStrategies.has(strategyName)) {
|
|
5485
|
+
this.loggerService.info("walkerLogicPrivateService skipping stopped strategy", {
|
|
5486
|
+
symbol,
|
|
5487
|
+
strategyName,
|
|
5488
|
+
});
|
|
5489
|
+
break;
|
|
5490
|
+
}
|
|
5491
|
+
// Call onStrategyStart callback if provided
|
|
5492
|
+
if (walkerSchema.callbacks?.onStrategyStart) {
|
|
5493
|
+
walkerSchema.callbacks.onStrategyStart(strategyName, symbol);
|
|
5494
|
+
}
|
|
5495
|
+
this.loggerService.info("walkerLogicPrivateService testing strategy", {
|
|
5303
5496
|
strategyName,
|
|
5304
5497
|
symbol,
|
|
5305
|
-
error: errorData(error), message: getErrorMessage(error),
|
|
5306
5498
|
});
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5499
|
+
const iterator = this.backtestLogicPublicService.run(symbol, {
|
|
5500
|
+
strategyName,
|
|
5501
|
+
exchangeName: context.exchangeName,
|
|
5502
|
+
frameName: context.frameName,
|
|
5503
|
+
});
|
|
5504
|
+
try {
|
|
5505
|
+
await resolveDocuments(iterator);
|
|
5311
5506
|
}
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5507
|
+
catch (error) {
|
|
5508
|
+
console.warn(`walkerLogicPrivateService backtest failed symbol=${symbol} strategyName=${strategyName} exchangeName=${context.exchangeName}`);
|
|
5509
|
+
this.loggerService.warn("walkerLogicPrivateService backtest failed for strategy, skipping", {
|
|
5510
|
+
strategyName,
|
|
5511
|
+
symbol,
|
|
5512
|
+
error: errorData(error), message: getErrorMessage(error),
|
|
5513
|
+
});
|
|
5514
|
+
await errorEmitter.next(error);
|
|
5515
|
+
// Call onStrategyError callback if provided
|
|
5516
|
+
if (walkerSchema.callbacks?.onStrategyError) {
|
|
5517
|
+
walkerSchema.callbacks.onStrategyError(strategyName, symbol, error);
|
|
5518
|
+
}
|
|
5519
|
+
continue;
|
|
5520
|
+
}
|
|
5521
|
+
this.loggerService.info("walkerLogicPrivateService backtest complete", {
|
|
5522
|
+
strategyName,
|
|
5523
|
+
symbol,
|
|
5317
5524
|
});
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
// Call onStrategyComplete callback if provided
|
|
5368
|
-
if (walkerSchema.callbacks?.onStrategyComplete) {
|
|
5369
|
-
walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
|
|
5525
|
+
// Get statistics from BacktestMarkdownService
|
|
5526
|
+
const stats = await this.backtestMarkdownService.getData(symbol, strategyName);
|
|
5527
|
+
// Extract metric value
|
|
5528
|
+
const value = stats[metric];
|
|
5529
|
+
const metricValue = value !== null &&
|
|
5530
|
+
value !== undefined &&
|
|
5531
|
+
typeof value === "number" &&
|
|
5532
|
+
!isNaN(value) &&
|
|
5533
|
+
isFinite(value)
|
|
5534
|
+
? value
|
|
5535
|
+
: null;
|
|
5536
|
+
// Update best strategy if needed
|
|
5537
|
+
const isBetter = bestMetric === null ||
|
|
5538
|
+
(metricValue !== null && metricValue > bestMetric);
|
|
5539
|
+
if (isBetter && metricValue !== null) {
|
|
5540
|
+
bestMetric = metricValue;
|
|
5541
|
+
bestStrategy = strategyName;
|
|
5542
|
+
}
|
|
5543
|
+
strategiesTested++;
|
|
5544
|
+
const walkerContract = {
|
|
5545
|
+
walkerName: context.walkerName,
|
|
5546
|
+
exchangeName: context.exchangeName,
|
|
5547
|
+
frameName: context.frameName,
|
|
5548
|
+
symbol,
|
|
5549
|
+
strategyName,
|
|
5550
|
+
stats,
|
|
5551
|
+
metricValue,
|
|
5552
|
+
metric,
|
|
5553
|
+
bestMetric,
|
|
5554
|
+
bestStrategy,
|
|
5555
|
+
strategiesTested,
|
|
5556
|
+
totalStrategies: strategies.length,
|
|
5557
|
+
};
|
|
5558
|
+
// Emit progress event
|
|
5559
|
+
await progressWalkerEmitter.next({
|
|
5560
|
+
walkerName: context.walkerName,
|
|
5561
|
+
exchangeName: context.exchangeName,
|
|
5562
|
+
frameName: context.frameName,
|
|
5563
|
+
symbol,
|
|
5564
|
+
totalStrategies: strategies.length,
|
|
5565
|
+
processedStrategies: strategiesTested,
|
|
5566
|
+
progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
|
|
5567
|
+
});
|
|
5568
|
+
// Call onStrategyComplete callback if provided
|
|
5569
|
+
if (walkerSchema.callbacks?.onStrategyComplete) {
|
|
5570
|
+
await walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
|
|
5571
|
+
}
|
|
5572
|
+
await walkerEmitter.next(walkerContract);
|
|
5573
|
+
yield walkerContract;
|
|
5370
5574
|
}
|
|
5371
|
-
|
|
5372
|
-
|
|
5575
|
+
}
|
|
5576
|
+
finally {
|
|
5577
|
+
// Unsubscribe from stop signals by calling destructor
|
|
5578
|
+
unsubscribe();
|
|
5373
5579
|
}
|
|
5374
5580
|
const finalResults = {
|
|
5375
5581
|
walkerName: context.walkerName,
|
|
@@ -13191,6 +13397,7 @@ async function dumpSignal(signalId, history, signal, outputDir = "./dump/strateg
|
|
|
13191
13397
|
|
|
13192
13398
|
const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
|
|
13193
13399
|
const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
|
|
13400
|
+
const BACKTEST_METHOD_NAME_STOP = "BacktestUtils.stop";
|
|
13194
13401
|
const BACKTEST_METHOD_NAME_GET_REPORT = "BacktestUtils.getReport";
|
|
13195
13402
|
const BACKTEST_METHOD_NAME_DUMP = "BacktestUtils.dump";
|
|
13196
13403
|
/**
|
|
@@ -13285,7 +13492,7 @@ class BacktestUtils {
|
|
|
13285
13492
|
};
|
|
13286
13493
|
task().catch((error) => exitEmitter.next(new Error(getErrorMessage(error))));
|
|
13287
13494
|
return () => {
|
|
13288
|
-
backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName });
|
|
13495
|
+
backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName }, true);
|
|
13289
13496
|
backtest$1.strategyGlobalService
|
|
13290
13497
|
.getPendingSignal(symbol, context.strategyName)
|
|
13291
13498
|
.then(async (pendingSignal) => {
|
|
@@ -13305,6 +13512,30 @@ class BacktestUtils {
|
|
|
13305
13512
|
isStopped = true;
|
|
13306
13513
|
};
|
|
13307
13514
|
};
|
|
13515
|
+
/**
|
|
13516
|
+
* Stops the strategy from generating new signals.
|
|
13517
|
+
*
|
|
13518
|
+
* Sets internal flag to prevent strategy from opening new signals.
|
|
13519
|
+
* Current active signal (if any) will complete normally.
|
|
13520
|
+
* Backtest will stop at the next safe point (idle state or after signal closes).
|
|
13521
|
+
*
|
|
13522
|
+
* @param symbol - Trading pair symbol
|
|
13523
|
+
* @param strategyName - Strategy name to stop
|
|
13524
|
+
* @returns Promise that resolves when stop flag is set
|
|
13525
|
+
*
|
|
13526
|
+
* @example
|
|
13527
|
+
* ```typescript
|
|
13528
|
+
* // Stop strategy after some condition
|
|
13529
|
+
* await Backtest.stop("BTCUSDT", "my-strategy");
|
|
13530
|
+
* ```
|
|
13531
|
+
*/
|
|
13532
|
+
this.stop = async (symbol, strategyName) => {
|
|
13533
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_STOP, {
|
|
13534
|
+
symbol,
|
|
13535
|
+
strategyName,
|
|
13536
|
+
});
|
|
13537
|
+
await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
|
|
13538
|
+
};
|
|
13308
13539
|
/**
|
|
13309
13540
|
* Gets statistical data from all closed signals for a symbol-strategy pair.
|
|
13310
13541
|
*
|
|
@@ -13393,6 +13624,7 @@ const Backtest = new BacktestUtils();
|
|
|
13393
13624
|
|
|
13394
13625
|
const LIVE_METHOD_NAME_RUN = "LiveUtils.run";
|
|
13395
13626
|
const LIVE_METHOD_NAME_BACKGROUND = "LiveUtils.background";
|
|
13627
|
+
const LIVE_METHOD_NAME_STOP = "LiveUtils.stop";
|
|
13396
13628
|
const LIVE_METHOD_NAME_GET_REPORT = "LiveUtils.getReport";
|
|
13397
13629
|
const LIVE_METHOD_NAME_DUMP = "LiveUtils.dump";
|
|
13398
13630
|
/**
|
|
@@ -13500,7 +13732,7 @@ class LiveUtils {
|
|
|
13500
13732
|
};
|
|
13501
13733
|
task().catch((error) => exitEmitter.next(new Error(getErrorMessage(error))));
|
|
13502
13734
|
return () => {
|
|
13503
|
-
backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName });
|
|
13735
|
+
backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName }, false);
|
|
13504
13736
|
backtest$1.strategyGlobalService
|
|
13505
13737
|
.getPendingSignal(symbol, context.strategyName)
|
|
13506
13738
|
.then(async (pendingSignal) => {
|
|
@@ -13520,6 +13752,30 @@ class LiveUtils {
|
|
|
13520
13752
|
isStopped = true;
|
|
13521
13753
|
};
|
|
13522
13754
|
};
|
|
13755
|
+
/**
|
|
13756
|
+
* Stops the strategy from generating new signals.
|
|
13757
|
+
*
|
|
13758
|
+
* Sets internal flag to prevent strategy from opening new signals.
|
|
13759
|
+
* Current active signal (if any) will complete normally.
|
|
13760
|
+
* Live trading will stop at the next safe point (idle/closed state).
|
|
13761
|
+
*
|
|
13762
|
+
* @param symbol - Trading pair symbol
|
|
13763
|
+
* @param strategyName - Strategy name to stop
|
|
13764
|
+
* @returns Promise that resolves when stop flag is set
|
|
13765
|
+
*
|
|
13766
|
+
* @example
|
|
13767
|
+
* ```typescript
|
|
13768
|
+
* // Stop live trading gracefully
|
|
13769
|
+
* await Live.stop("BTCUSDT", "my-strategy");
|
|
13770
|
+
* ```
|
|
13771
|
+
*/
|
|
13772
|
+
this.stop = async (symbol, strategyName) => {
|
|
13773
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_STOP, {
|
|
13774
|
+
symbol,
|
|
13775
|
+
strategyName,
|
|
13776
|
+
});
|
|
13777
|
+
await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, false);
|
|
13778
|
+
};
|
|
13523
13779
|
/**
|
|
13524
13780
|
* Gets statistical data from all live trading events for a symbol-strategy pair.
|
|
13525
13781
|
*
|
|
@@ -13827,6 +14083,7 @@ class Performance {
|
|
|
13827
14083
|
|
|
13828
14084
|
const WALKER_METHOD_NAME_RUN = "WalkerUtils.run";
|
|
13829
14085
|
const WALKER_METHOD_NAME_BACKGROUND = "WalkerUtils.background";
|
|
14086
|
+
const WALKER_METHOD_NAME_STOP = "WalkerUtils.stop";
|
|
13830
14087
|
const WALKER_METHOD_NAME_GET_DATA = "WalkerUtils.getData";
|
|
13831
14088
|
const WALKER_METHOD_NAME_GET_REPORT = "WalkerUtils.getReport";
|
|
13832
14089
|
const WALKER_METHOD_NAME_DUMP = "WalkerUtils.dump";
|
|
@@ -13937,8 +14194,8 @@ class WalkerUtils {
|
|
|
13937
14194
|
task().catch((error) => exitEmitter.next(new Error(getErrorMessage(error))));
|
|
13938
14195
|
return () => {
|
|
13939
14196
|
for (const strategyName of walkerSchema.strategies) {
|
|
13940
|
-
backtest$1.strategyGlobalService.stop({ symbol, strategyName });
|
|
13941
|
-
walkerStopSubject.next({ symbol, strategyName });
|
|
14197
|
+
backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
|
|
14198
|
+
walkerStopSubject.next({ symbol, strategyName, walkerName: context.walkerName });
|
|
13942
14199
|
}
|
|
13943
14200
|
if (!isDone) {
|
|
13944
14201
|
doneWalkerSubject.next({
|
|
@@ -13952,6 +14209,40 @@ class WalkerUtils {
|
|
|
13952
14209
|
isStopped = true;
|
|
13953
14210
|
};
|
|
13954
14211
|
};
|
|
14212
|
+
/**
|
|
14213
|
+
* Stops all strategies in the walker from generating new signals.
|
|
14214
|
+
*
|
|
14215
|
+
* Iterates through all strategies defined in walker schema and:
|
|
14216
|
+
* 1. Sends stop signal via walkerStopSubject (interrupts current running strategy)
|
|
14217
|
+
* 2. Sets internal stop flag for each strategy (prevents new signals)
|
|
14218
|
+
*
|
|
14219
|
+
* Current active signals (if any) will complete normally.
|
|
14220
|
+
* Walker will stop at the next safe point.
|
|
14221
|
+
*
|
|
14222
|
+
* Supports multiple walkers running on the same symbol simultaneously.
|
|
14223
|
+
* Stop signal is filtered by walkerName to prevent interference.
|
|
14224
|
+
*
|
|
14225
|
+
* @param symbol - Trading pair symbol
|
|
14226
|
+
* @param walkerName - Walker name to stop
|
|
14227
|
+
* @returns Promise that resolves when all stop flags are set
|
|
14228
|
+
*
|
|
14229
|
+
* @example
|
|
14230
|
+
* ```typescript
|
|
14231
|
+
* // Stop walker and all its strategies
|
|
14232
|
+
* await Walker.stop("BTCUSDT", "my-walker");
|
|
14233
|
+
* ```
|
|
14234
|
+
*/
|
|
14235
|
+
this.stop = async (symbol, walkerName) => {
|
|
14236
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_STOP, {
|
|
14237
|
+
symbol,
|
|
14238
|
+
walkerName,
|
|
14239
|
+
});
|
|
14240
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
|
|
14241
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
14242
|
+
await walkerStopSubject.next({ symbol, strategyName, walkerName });
|
|
14243
|
+
await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
|
|
14244
|
+
}
|
|
14245
|
+
};
|
|
13955
14246
|
/**
|
|
13956
14247
|
* Gets walker results data from all strategy comparisons.
|
|
13957
14248
|
*
|