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.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
|
|
@@ -1669,6 +1675,8 @@ const walkerCompleteSubject = new functoolsKit.Subject();
|
|
|
1669
1675
|
/**
|
|
1670
1676
|
* Walker stop emitter for walker cancellation events.
|
|
1671
1677
|
* Emits when a walker comparison is stopped/cancelled.
|
|
1678
|
+
*
|
|
1679
|
+
* Includes walkerName to support multiple walkers running on the same symbol.
|
|
1672
1680
|
*/
|
|
1673
1681
|
const walkerStopSubject = new functoolsKit.Subject();
|
|
1674
1682
|
/**
|
|
@@ -1821,18 +1829,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1821
1829
|
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
1822
1830
|
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
1823
1831
|
}
|
|
1824
|
-
// ЗАЩИТА ОТ
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
//
|
|
1828
|
-
if (
|
|
1829
|
-
errors.push(`Long: currentPrice (${currentPrice})
|
|
1830
|
-
`Signal would be immediately
|
|
1832
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
1833
|
+
if (!isScheduled && isFinite(currentPrice)) {
|
|
1834
|
+
// LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
|
|
1835
|
+
// SL < currentPrice < TP
|
|
1836
|
+
if (currentPrice <= signal.priceStopLoss) {
|
|
1837
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1838
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
1831
1839
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1840
|
+
if (currentPrice >= signal.priceTakeProfit) {
|
|
1841
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1842
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
1846
|
+
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
1847
|
+
// LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
|
|
1848
|
+
// SL < priceOpen < TP
|
|
1849
|
+
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
1850
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1851
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
1852
|
+
}
|
|
1853
|
+
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
1854
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1855
|
+
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
1836
1856
|
}
|
|
1837
1857
|
}
|
|
1838
1858
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1844,8 +1864,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1844
1864
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1845
1865
|
}
|
|
1846
1866
|
}
|
|
1867
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
1868
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1869
|
+
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1870
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1871
|
+
errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1872
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
1873
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1847
1876
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1848
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1877
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1849
1878
|
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1850
1879
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1851
1880
|
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -1862,18 +1891,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1862
1891
|
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
1863
1892
|
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
1864
1893
|
}
|
|
1865
|
-
// ЗАЩИТА ОТ
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
//
|
|
1869
|
-
if (
|
|
1870
|
-
errors.push(`Short: currentPrice (${currentPrice})
|
|
1871
|
-
`Signal would be immediately
|
|
1894
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
1895
|
+
if (!isScheduled && isFinite(currentPrice)) {
|
|
1896
|
+
// SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
|
|
1897
|
+
// TP < currentPrice < SL
|
|
1898
|
+
if (currentPrice >= signal.priceStopLoss) {
|
|
1899
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1900
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
1901
|
+
}
|
|
1902
|
+
if (currentPrice <= signal.priceTakeProfit) {
|
|
1903
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1904
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
1908
|
+
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
1909
|
+
// SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
|
|
1910
|
+
// TP < priceOpen < SL
|
|
1911
|
+
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
1912
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1913
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
1872
1914
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
`Signal is invalid - the profit opportunity has already passed.`);
|
|
1915
|
+
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
1916
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1917
|
+
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
1877
1918
|
}
|
|
1878
1919
|
}
|
|
1879
1920
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1885,8 +1926,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1885
1926
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1886
1927
|
}
|
|
1887
1928
|
}
|
|
1929
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
1930
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1931
|
+
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1932
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1933
|
+
errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1934
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
1935
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1888
1938
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1889
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1939
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1890
1940
|
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1891
1941
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1892
1942
|
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -2577,8 +2627,14 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
2577
2627
|
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
2578
2628
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2579
2629
|
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
2630
|
+
const bufferCandlesCount = candlesCount - 1;
|
|
2580
2631
|
for (let i = 0; i < candles.length; i++) {
|
|
2581
2632
|
const candle = candles[i];
|
|
2633
|
+
// КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
|
|
2634
|
+
// BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
|
|
2635
|
+
if (i < bufferCandlesCount) {
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2582
2638
|
const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
|
|
2583
2639
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2584
2640
|
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
@@ -2644,11 +2700,21 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
2644
2700
|
};
|
|
2645
2701
|
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
2646
2702
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2647
|
-
|
|
2648
|
-
|
|
2703
|
+
const bufferCandlesCount = candlesCount - 1;
|
|
2704
|
+
// КРИТИЧНО: проверяем TP/SL на КАЖДОЙ свече начиная после буфера
|
|
2705
|
+
// Первые bufferCandlesCount свечей - это буфер для VWAP
|
|
2706
|
+
for (let i = 0; i < candles.length; i++) {
|
|
2707
|
+
const currentCandle = candles[i];
|
|
2708
|
+
const currentCandleTimestamp = currentCandle.timestamp;
|
|
2709
|
+
// КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
|
|
2710
|
+
// BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
|
|
2711
|
+
if (i < bufferCandlesCount) {
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
// Берем последние candlesCount свечей для VWAP (включая буфер)
|
|
2715
|
+
const startIndex = Math.max(0, i - (candlesCount - 1));
|
|
2716
|
+
const recentCandles = candles.slice(startIndex, i + 1);
|
|
2649
2717
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2650
|
-
const currentCandleTimestamp = recentCandles[recentCandles.length - 1].timestamp;
|
|
2651
|
-
const currentCandle = recentCandles[recentCandles.length - 1];
|
|
2652
2718
|
let shouldClose = false;
|
|
2653
2719
|
let closeReason;
|
|
2654
2720
|
// Check time expiration FIRST (КРИТИЧНО!)
|
|
@@ -2845,6 +2911,23 @@ class ClientStrategy {
|
|
|
2845
2911
|
});
|
|
2846
2912
|
return this._pendingSignal;
|
|
2847
2913
|
}
|
|
2914
|
+
/**
|
|
2915
|
+
* Returns the stopped state of the strategy.
|
|
2916
|
+
*
|
|
2917
|
+
* Indicates whether the strategy has been explicitly stopped and should
|
|
2918
|
+
* not continue processing new ticks or signals.
|
|
2919
|
+
*
|
|
2920
|
+
* @param symbol - Trading pair symbol
|
|
2921
|
+
* @param strategyName - Name of the strategy
|
|
2922
|
+
* @returns Promise resolving to true if strategy is stopped, false otherwise
|
|
2923
|
+
*/
|
|
2924
|
+
async getStopped(symbol, strategyName) {
|
|
2925
|
+
this.params.logger.debug("ClientStrategy getStopped", {
|
|
2926
|
+
symbol,
|
|
2927
|
+
strategyName,
|
|
2928
|
+
});
|
|
2929
|
+
return this._isStopped;
|
|
2930
|
+
}
|
|
2848
2931
|
/**
|
|
2849
2932
|
* Performs a single tick of strategy execution.
|
|
2850
2933
|
*
|
|
@@ -2948,9 +3031,10 @@ class ClientStrategy {
|
|
|
2948
3031
|
* 4. If cancelled: returns closed result with closeReason "cancelled"
|
|
2949
3032
|
*
|
|
2950
3033
|
* For pending signals:
|
|
2951
|
-
* 1. Iterates through candles
|
|
2952
|
-
* 2.
|
|
2953
|
-
* 3.
|
|
3034
|
+
* 1. Iterates through ALL candles starting from the first one
|
|
3035
|
+
* 2. Checks TP/SL using candle.high/low (immediate detection)
|
|
3036
|
+
* 3. VWAP calculated with dynamic window (1 to CC_AVG_PRICE_CANDLES_COUNT candles)
|
|
3037
|
+
* 4. Returns closed result (either TP/SL or time_expired)
|
|
2954
3038
|
*
|
|
2955
3039
|
* @param candles - Array of candles to process
|
|
2956
3040
|
* @returns Promise resolving to closed signal result with PNL
|
|
@@ -3086,18 +3170,24 @@ class ClientStrategy {
|
|
|
3086
3170
|
* // Existing signal will continue until natural close
|
|
3087
3171
|
* ```
|
|
3088
3172
|
*/
|
|
3089
|
-
async stop(symbol, strategyName) {
|
|
3173
|
+
async stop(symbol, strategyName, backtest) {
|
|
3090
3174
|
this.params.logger.debug("ClientStrategy stop", {
|
|
3091
3175
|
symbol,
|
|
3092
3176
|
strategyName,
|
|
3093
3177
|
hasPendingSignal: this._pendingSignal !== null,
|
|
3094
3178
|
hasScheduledSignal: this._scheduledSignal !== null,
|
|
3179
|
+
backtest,
|
|
3095
3180
|
});
|
|
3096
3181
|
this._isStopped = true;
|
|
3097
3182
|
// Clear scheduled signal if exists
|
|
3098
|
-
if (this._scheduledSignal) {
|
|
3099
|
-
|
|
3183
|
+
if (!this._scheduledSignal) {
|
|
3184
|
+
return;
|
|
3100
3185
|
}
|
|
3186
|
+
this._scheduledSignal = null;
|
|
3187
|
+
if (backtest) {
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, this.params.execution.context.symbol, this.params.strategyName);
|
|
3101
3191
|
}
|
|
3102
3192
|
}
|
|
3103
3193
|
|
|
@@ -3180,6 +3270,24 @@ class StrategyConnectionService {
|
|
|
3180
3270
|
const strategy = this.getStrategy(symbol, strategyName);
|
|
3181
3271
|
return await strategy.getPendingSignal(symbol, strategyName);
|
|
3182
3272
|
};
|
|
3273
|
+
/**
|
|
3274
|
+
* Retrieves the stopped state of the strategy.
|
|
3275
|
+
*
|
|
3276
|
+
* Delegates to the underlying strategy instance to check if it has been
|
|
3277
|
+
* marked as stopped and should cease operation.
|
|
3278
|
+
*
|
|
3279
|
+
* @param symbol - Trading pair symbol
|
|
3280
|
+
* @param strategyName - Name of the strategy
|
|
3281
|
+
* @returns Promise resolving to true if strategy is stopped, false otherwise
|
|
3282
|
+
*/
|
|
3283
|
+
this.getStopped = async (symbol, strategyName) => {
|
|
3284
|
+
this.loggerService.log("strategyConnectionService getStopped", {
|
|
3285
|
+
symbol,
|
|
3286
|
+
strategyName,
|
|
3287
|
+
});
|
|
3288
|
+
const strategy = this.getStrategy(symbol, strategyName);
|
|
3289
|
+
return await strategy.getStopped(symbol, strategyName);
|
|
3290
|
+
};
|
|
3183
3291
|
/**
|
|
3184
3292
|
* Executes live trading tick for current strategy.
|
|
3185
3293
|
*
|
|
@@ -3247,12 +3355,12 @@ class StrategyConnectionService {
|
|
|
3247
3355
|
* @param strategyName - Name of strategy to stop
|
|
3248
3356
|
* @returns Promise that resolves when stop flag is set
|
|
3249
3357
|
*/
|
|
3250
|
-
this.stop = async (ctx) => {
|
|
3358
|
+
this.stop = async (ctx, backtest) => {
|
|
3251
3359
|
this.loggerService.log("strategyConnectionService stop", {
|
|
3252
3360
|
ctx
|
|
3253
3361
|
});
|
|
3254
3362
|
const strategy = this.getStrategy(ctx.symbol, ctx.strategyName);
|
|
3255
|
-
await strategy.stop(ctx.symbol, ctx.strategyName);
|
|
3363
|
+
await strategy.stop(ctx.symbol, ctx.strategyName, backtest);
|
|
3256
3364
|
};
|
|
3257
3365
|
/**
|
|
3258
3366
|
* Clears the memoized ClientStrategy instance from cache.
|
|
@@ -4117,6 +4225,24 @@ class StrategyGlobalService {
|
|
|
4117
4225
|
await this.validate(symbol, strategyName);
|
|
4118
4226
|
return await this.strategyConnectionService.getPendingSignal(symbol, strategyName);
|
|
4119
4227
|
};
|
|
4228
|
+
/**
|
|
4229
|
+
* Checks if the strategy has been stopped.
|
|
4230
|
+
*
|
|
4231
|
+
* Validates strategy existence and delegates to connection service
|
|
4232
|
+
* to retrieve the stopped state from the strategy instance.
|
|
4233
|
+
*
|
|
4234
|
+
* @param symbol - Trading pair symbol
|
|
4235
|
+
* @param strategyName - Name of the strategy
|
|
4236
|
+
* @returns Promise resolving to true if strategy is stopped, false otherwise
|
|
4237
|
+
*/
|
|
4238
|
+
this.getStopped = async (symbol, strategyName) => {
|
|
4239
|
+
this.loggerService.log("strategyGlobalService getStopped", {
|
|
4240
|
+
symbol,
|
|
4241
|
+
strategyName,
|
|
4242
|
+
});
|
|
4243
|
+
await this.validate(symbol, strategyName);
|
|
4244
|
+
return await this.strategyConnectionService.getStopped(symbol, strategyName);
|
|
4245
|
+
};
|
|
4120
4246
|
/**
|
|
4121
4247
|
* Checks signal status at a specific timestamp.
|
|
4122
4248
|
*
|
|
@@ -4183,12 +4309,13 @@ class StrategyGlobalService {
|
|
|
4183
4309
|
* @param strategyName - Name of strategy to stop
|
|
4184
4310
|
* @returns Promise that resolves when stop flag is set
|
|
4185
4311
|
*/
|
|
4186
|
-
this.stop = async (ctx) => {
|
|
4312
|
+
this.stop = async (ctx, backtest) => {
|
|
4187
4313
|
this.loggerService.log("strategyGlobalService stop", {
|
|
4188
4314
|
ctx,
|
|
4315
|
+
backtest,
|
|
4189
4316
|
});
|
|
4190
4317
|
await this.validate(ctx.symbol, ctx.strategyName);
|
|
4191
|
-
return await this.strategyConnectionService.stop(ctx);
|
|
4318
|
+
return await this.strategyConnectionService.stop(ctx, backtest);
|
|
4192
4319
|
};
|
|
4193
4320
|
/**
|
|
4194
4321
|
* Clears the memoized ClientStrategy instance from cache.
|
|
@@ -4880,6 +5007,16 @@ class BacktestLogicPrivateService {
|
|
|
4880
5007
|
progress: totalFrames > 0 ? i / totalFrames : 0,
|
|
4881
5008
|
});
|
|
4882
5009
|
}
|
|
5010
|
+
// Check if strategy should stop before processing next frame
|
|
5011
|
+
if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
|
|
5012
|
+
this.loggerService.info("backtestLogicPrivateService stopped by user request (before tick)", {
|
|
5013
|
+
symbol,
|
|
5014
|
+
when: when.toISOString(),
|
|
5015
|
+
processedFrames: i,
|
|
5016
|
+
totalFrames,
|
|
5017
|
+
});
|
|
5018
|
+
break;
|
|
5019
|
+
}
|
|
4883
5020
|
let result;
|
|
4884
5021
|
try {
|
|
4885
5022
|
result = await this.strategyGlobalService.tick(symbol, when, true);
|
|
@@ -4895,6 +5032,16 @@ class BacktestLogicPrivateService {
|
|
|
4895
5032
|
i++;
|
|
4896
5033
|
continue;
|
|
4897
5034
|
}
|
|
5035
|
+
// Check if strategy should stop when idle (no active signal)
|
|
5036
|
+
if (await functoolsKit.and(Promise.resolve(result.action === "idle"), this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName))) {
|
|
5037
|
+
this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
|
|
5038
|
+
symbol,
|
|
5039
|
+
when: when.toISOString(),
|
|
5040
|
+
processedFrames: i,
|
|
5041
|
+
totalFrames,
|
|
5042
|
+
});
|
|
5043
|
+
break;
|
|
5044
|
+
}
|
|
4898
5045
|
// Если scheduled signal создан - обрабатываем через backtest()
|
|
4899
5046
|
if (result.action === "scheduled") {
|
|
4900
5047
|
const signalStartTime = performance.now();
|
|
@@ -4906,13 +5053,17 @@ class BacktestLogicPrivateService {
|
|
|
4906
5053
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4907
5054
|
});
|
|
4908
5055
|
// Запрашиваем минутные свечи для мониторинга активации/отмены
|
|
4909
|
-
// КРИТИЧНО:
|
|
4910
|
-
//
|
|
4911
|
-
//
|
|
4912
|
-
|
|
5056
|
+
// КРИТИЧНО: запрашиваем:
|
|
5057
|
+
// - CC_AVG_PRICE_CANDLES_COUNT-1 для буфера VWAP (ДО when)
|
|
5058
|
+
// - CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
5059
|
+
// - minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
5060
|
+
// - +1 потому что when включается как первая свеча
|
|
5061
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
5062
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
5063
|
+
const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
|
|
4913
5064
|
let candles;
|
|
4914
5065
|
try {
|
|
4915
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded,
|
|
5066
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
|
|
4916
5067
|
}
|
|
4917
5068
|
catch (error) {
|
|
4918
5069
|
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 +5071,7 @@ class BacktestLogicPrivateService {
|
|
|
4920
5071
|
symbol,
|
|
4921
5072
|
signalId: signal.id,
|
|
4922
5073
|
candlesNeeded,
|
|
5074
|
+
bufferMinutes,
|
|
4923
5075
|
error: functoolsKit.errorData(error), message: functoolsKit.getErrorMessage(error),
|
|
4924
5076
|
});
|
|
4925
5077
|
await errorEmitter.next(error);
|
|
@@ -4982,6 +5134,16 @@ class BacktestLogicPrivateService {
|
|
|
4982
5134
|
i++;
|
|
4983
5135
|
}
|
|
4984
5136
|
yield backtestResult;
|
|
5137
|
+
// Check if strategy should stop after signal is closed
|
|
5138
|
+
if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
|
|
5139
|
+
this.loggerService.info("backtestLogicPrivateService stopped by user request (after scheduled signal closed)", {
|
|
5140
|
+
symbol,
|
|
5141
|
+
signalId: backtestResult.signal.id,
|
|
5142
|
+
processedFrames: i,
|
|
5143
|
+
totalFrames,
|
|
5144
|
+
});
|
|
5145
|
+
break;
|
|
5146
|
+
}
|
|
4985
5147
|
}
|
|
4986
5148
|
// Если обычный сигнал открыт, вызываем backtest
|
|
4987
5149
|
if (result.action === "opened") {
|
|
@@ -4992,16 +5154,23 @@ class BacktestLogicPrivateService {
|
|
|
4992
5154
|
signalId: signal.id,
|
|
4993
5155
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4994
5156
|
});
|
|
4995
|
-
// Получаем свечи для
|
|
5157
|
+
// КРИТИЧНО: Получаем свечи включая буфер для VWAP
|
|
5158
|
+
// Сдвигаем начало назад на CC_AVG_PRICE_CANDLES_COUNT-1 минут для буфера VWAP
|
|
5159
|
+
// Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
|
|
5160
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
5161
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
5162
|
+
const totalCandles = signal.minuteEstimatedTime + bufferMinutes;
|
|
4996
5163
|
let candles;
|
|
4997
5164
|
try {
|
|
4998
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m",
|
|
5165
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
|
|
4999
5166
|
}
|
|
5000
5167
|
catch (error) {
|
|
5001
5168
|
console.warn(`backtestLogicPrivateService getNextCandles failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
|
|
5002
5169
|
this.loggerService.warn("backtestLogicPrivateService getNextCandles failed for opened signal", {
|
|
5003
5170
|
symbol,
|
|
5004
5171
|
signalId: signal.id,
|
|
5172
|
+
totalCandles,
|
|
5173
|
+
bufferMinutes,
|
|
5005
5174
|
error: functoolsKit.errorData(error), message: functoolsKit.getErrorMessage(error),
|
|
5006
5175
|
});
|
|
5007
5176
|
await errorEmitter.next(error);
|
|
@@ -5058,6 +5227,16 @@ class BacktestLogicPrivateService {
|
|
|
5058
5227
|
i++;
|
|
5059
5228
|
}
|
|
5060
5229
|
yield backtestResult;
|
|
5230
|
+
// Check if strategy should stop after signal is closed
|
|
5231
|
+
if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
|
|
5232
|
+
this.loggerService.info("backtestLogicPrivateService stopped by user request (after signal closed)", {
|
|
5233
|
+
symbol,
|
|
5234
|
+
signalId: backtestResult.signal.id,
|
|
5235
|
+
processedFrames: i,
|
|
5236
|
+
totalFrames,
|
|
5237
|
+
});
|
|
5238
|
+
break;
|
|
5239
|
+
}
|
|
5061
5240
|
}
|
|
5062
5241
|
// Track timeframe processing duration
|
|
5063
5242
|
const timeframeEndTime = performance.now();
|
|
@@ -5165,7 +5344,8 @@ class LiveLogicPrivateService {
|
|
|
5165
5344
|
this.loggerService.warn("liveLogicPrivateService tick failed, retrying after sleep", {
|
|
5166
5345
|
symbol,
|
|
5167
5346
|
when: when.toISOString(),
|
|
5168
|
-
error: functoolsKit.errorData(error),
|
|
5347
|
+
error: functoolsKit.errorData(error),
|
|
5348
|
+
message: functoolsKit.getErrorMessage(error),
|
|
5169
5349
|
});
|
|
5170
5350
|
await errorEmitter.next(error);
|
|
5171
5351
|
await functoolsKit.sleep(TICK_TTL);
|
|
@@ -5189,11 +5369,19 @@ class LiveLogicPrivateService {
|
|
|
5189
5369
|
backtest: false,
|
|
5190
5370
|
});
|
|
5191
5371
|
previousEventTimestamp = currentTimestamp;
|
|
5192
|
-
if (
|
|
5372
|
+
// Check if strategy should stop when idle (no active signal)
|
|
5373
|
+
if (result.action === "idle") {
|
|
5374
|
+
if (await functoolsKit.and(Promise.resolve(true), this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName))) {
|
|
5375
|
+
this.loggerService.info("liveLogicPrivateService stopped by user request (idle state)", {
|
|
5376
|
+
symbol,
|
|
5377
|
+
when: when.toISOString(),
|
|
5378
|
+
});
|
|
5379
|
+
break;
|
|
5380
|
+
}
|
|
5193
5381
|
await functoolsKit.sleep(TICK_TTL);
|
|
5194
5382
|
continue;
|
|
5195
5383
|
}
|
|
5196
|
-
if (result.action === "
|
|
5384
|
+
if (result.action === "active") {
|
|
5197
5385
|
await functoolsKit.sleep(TICK_TTL);
|
|
5198
5386
|
continue;
|
|
5199
5387
|
}
|
|
@@ -5203,12 +5391,21 @@ class LiveLogicPrivateService {
|
|
|
5203
5391
|
}
|
|
5204
5392
|
// Yield opened, closed results
|
|
5205
5393
|
yield result;
|
|
5394
|
+
// Check if strategy should stop after signal is closed
|
|
5395
|
+
if (result.action === "closed") {
|
|
5396
|
+
if (await this.strategyGlobalService.getStopped(symbol, this.methodContextService.context.strategyName)) {
|
|
5397
|
+
this.loggerService.info("liveLogicPrivateService stopped by user request (after signal closed)", {
|
|
5398
|
+
symbol,
|
|
5399
|
+
signalId: result.signal.id,
|
|
5400
|
+
});
|
|
5401
|
+
break;
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5206
5404
|
await functoolsKit.sleep(TICK_TTL);
|
|
5207
5405
|
}
|
|
5208
5406
|
}
|
|
5209
5407
|
}
|
|
5210
5408
|
|
|
5211
|
-
const CANCEL_SYMBOL = Symbol("CANCEL_SYMBOL");
|
|
5212
5409
|
/**
|
|
5213
5410
|
* Private service for walker orchestration (strategy comparison).
|
|
5214
5411
|
*
|
|
@@ -5266,112 +5463,121 @@ class WalkerLogicPrivateService {
|
|
|
5266
5463
|
let strategiesTested = 0;
|
|
5267
5464
|
let bestMetric = null;
|
|
5268
5465
|
let bestStrategy = null;
|
|
5269
|
-
|
|
5270
|
-
const
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
.
|
|
5278
|
-
.
|
|
5279
|
-
// Run backtest for each strategy
|
|
5280
|
-
for (const strategyName of strategies) {
|
|
5281
|
-
// Call onStrategyStart callback if provided
|
|
5282
|
-
if (walkerSchema.callbacks?.onStrategyStart) {
|
|
5283
|
-
walkerSchema.callbacks.onStrategyStart(strategyName, symbol);
|
|
5284
|
-
}
|
|
5285
|
-
this.loggerService.info("walkerLogicPrivateService testing strategy", {
|
|
5286
|
-
strategyName,
|
|
5466
|
+
// Track stopped strategies in Set for efficient lookup
|
|
5467
|
+
const stoppedStrategies = new Set();
|
|
5468
|
+
// Subscribe to stop signals and collect them in Set
|
|
5469
|
+
// Filter by both symbol AND walkerName to support multiple walkers on same symbol
|
|
5470
|
+
// connect() returns destructor function
|
|
5471
|
+
const unsubscribe = walkerStopSubject
|
|
5472
|
+
.filter((data) => data.symbol === symbol && data.walkerName === context.walkerName)
|
|
5473
|
+
.connect((data) => {
|
|
5474
|
+
stoppedStrategies.add(data.strategyName);
|
|
5475
|
+
this.loggerService.info("walkerLogicPrivateService received stop signal for strategy", {
|
|
5287
5476
|
symbol,
|
|
5477
|
+
walkerName: context.walkerName,
|
|
5478
|
+
strategyName: data.strategyName,
|
|
5479
|
+
stoppedCount: stoppedStrategies.size,
|
|
5288
5480
|
});
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5481
|
+
});
|
|
5482
|
+
try {
|
|
5483
|
+
// Run backtest for each strategy
|
|
5484
|
+
for (const strategyName of strategies) {
|
|
5485
|
+
// Check if this strategy should be stopped before starting
|
|
5486
|
+
if (stoppedStrategies.has(strategyName)) {
|
|
5487
|
+
this.loggerService.info("walkerLogicPrivateService skipping stopped strategy", {
|
|
5488
|
+
symbol,
|
|
5489
|
+
strategyName,
|
|
5490
|
+
});
|
|
5491
|
+
break;
|
|
5492
|
+
}
|
|
5493
|
+
// Call onStrategyStart callback if provided
|
|
5494
|
+
if (walkerSchema.callbacks?.onStrategyStart) {
|
|
5495
|
+
walkerSchema.callbacks.onStrategyStart(strategyName, symbol);
|
|
5496
|
+
}
|
|
5497
|
+
this.loggerService.info("walkerLogicPrivateService testing strategy", {
|
|
5305
5498
|
strategyName,
|
|
5306
5499
|
symbol,
|
|
5307
|
-
error: functoolsKit.errorData(error), message: functoolsKit.getErrorMessage(error),
|
|
5308
5500
|
});
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5501
|
+
const iterator = this.backtestLogicPublicService.run(symbol, {
|
|
5502
|
+
strategyName,
|
|
5503
|
+
exchangeName: context.exchangeName,
|
|
5504
|
+
frameName: context.frameName,
|
|
5505
|
+
});
|
|
5506
|
+
try {
|
|
5507
|
+
await functoolsKit.resolveDocuments(iterator);
|
|
5313
5508
|
}
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5509
|
+
catch (error) {
|
|
5510
|
+
console.warn(`walkerLogicPrivateService backtest failed symbol=${symbol} strategyName=${strategyName} exchangeName=${context.exchangeName}`);
|
|
5511
|
+
this.loggerService.warn("walkerLogicPrivateService backtest failed for strategy, skipping", {
|
|
5512
|
+
strategyName,
|
|
5513
|
+
symbol,
|
|
5514
|
+
error: functoolsKit.errorData(error), message: functoolsKit.getErrorMessage(error),
|
|
5515
|
+
});
|
|
5516
|
+
await errorEmitter.next(error);
|
|
5517
|
+
// Call onStrategyError callback if provided
|
|
5518
|
+
if (walkerSchema.callbacks?.onStrategyError) {
|
|
5519
|
+
walkerSchema.callbacks.onStrategyError(strategyName, symbol, error);
|
|
5520
|
+
}
|
|
5521
|
+
continue;
|
|
5522
|
+
}
|
|
5523
|
+
this.loggerService.info("walkerLogicPrivateService backtest complete", {
|
|
5524
|
+
strategyName,
|
|
5525
|
+
symbol,
|
|
5319
5526
|
});
|
|
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
|
-
|
|
5368
|
-
|
|
5369
|
-
// Call onStrategyComplete callback if provided
|
|
5370
|
-
if (walkerSchema.callbacks?.onStrategyComplete) {
|
|
5371
|
-
walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
|
|
5527
|
+
// Get statistics from BacktestMarkdownService
|
|
5528
|
+
const stats = await this.backtestMarkdownService.getData(symbol, strategyName);
|
|
5529
|
+
// Extract metric value
|
|
5530
|
+
const value = stats[metric];
|
|
5531
|
+
const metricValue = value !== null &&
|
|
5532
|
+
value !== undefined &&
|
|
5533
|
+
typeof value === "number" &&
|
|
5534
|
+
!isNaN(value) &&
|
|
5535
|
+
isFinite(value)
|
|
5536
|
+
? value
|
|
5537
|
+
: null;
|
|
5538
|
+
// Update best strategy if needed
|
|
5539
|
+
const isBetter = bestMetric === null ||
|
|
5540
|
+
(metricValue !== null && metricValue > bestMetric);
|
|
5541
|
+
if (isBetter && metricValue !== null) {
|
|
5542
|
+
bestMetric = metricValue;
|
|
5543
|
+
bestStrategy = strategyName;
|
|
5544
|
+
}
|
|
5545
|
+
strategiesTested++;
|
|
5546
|
+
const walkerContract = {
|
|
5547
|
+
walkerName: context.walkerName,
|
|
5548
|
+
exchangeName: context.exchangeName,
|
|
5549
|
+
frameName: context.frameName,
|
|
5550
|
+
symbol,
|
|
5551
|
+
strategyName,
|
|
5552
|
+
stats,
|
|
5553
|
+
metricValue,
|
|
5554
|
+
metric,
|
|
5555
|
+
bestMetric,
|
|
5556
|
+
bestStrategy,
|
|
5557
|
+
strategiesTested,
|
|
5558
|
+
totalStrategies: strategies.length,
|
|
5559
|
+
};
|
|
5560
|
+
// Emit progress event
|
|
5561
|
+
await progressWalkerEmitter.next({
|
|
5562
|
+
walkerName: context.walkerName,
|
|
5563
|
+
exchangeName: context.exchangeName,
|
|
5564
|
+
frameName: context.frameName,
|
|
5565
|
+
symbol,
|
|
5566
|
+
totalStrategies: strategies.length,
|
|
5567
|
+
processedStrategies: strategiesTested,
|
|
5568
|
+
progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
|
|
5569
|
+
});
|
|
5570
|
+
// Call onStrategyComplete callback if provided
|
|
5571
|
+
if (walkerSchema.callbacks?.onStrategyComplete) {
|
|
5572
|
+
await walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
|
|
5573
|
+
}
|
|
5574
|
+
await walkerEmitter.next(walkerContract);
|
|
5575
|
+
yield walkerContract;
|
|
5372
5576
|
}
|
|
5373
|
-
|
|
5374
|
-
|
|
5577
|
+
}
|
|
5578
|
+
finally {
|
|
5579
|
+
// Unsubscribe from stop signals by calling destructor
|
|
5580
|
+
unsubscribe();
|
|
5375
5581
|
}
|
|
5376
5582
|
const finalResults = {
|
|
5377
5583
|
walkerName: context.walkerName,
|
|
@@ -13193,6 +13399,7 @@ async function dumpSignal(signalId, history, signal, outputDir = "./dump/strateg
|
|
|
13193
13399
|
|
|
13194
13400
|
const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
|
|
13195
13401
|
const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
|
|
13402
|
+
const BACKTEST_METHOD_NAME_STOP = "BacktestUtils.stop";
|
|
13196
13403
|
const BACKTEST_METHOD_NAME_GET_REPORT = "BacktestUtils.getReport";
|
|
13197
13404
|
const BACKTEST_METHOD_NAME_DUMP = "BacktestUtils.dump";
|
|
13198
13405
|
/**
|
|
@@ -13287,7 +13494,7 @@ class BacktestUtils {
|
|
|
13287
13494
|
};
|
|
13288
13495
|
task().catch((error) => exitEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
|
|
13289
13496
|
return () => {
|
|
13290
|
-
backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName });
|
|
13497
|
+
backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName }, true);
|
|
13291
13498
|
backtest$1.strategyGlobalService
|
|
13292
13499
|
.getPendingSignal(symbol, context.strategyName)
|
|
13293
13500
|
.then(async (pendingSignal) => {
|
|
@@ -13307,6 +13514,30 @@ class BacktestUtils {
|
|
|
13307
13514
|
isStopped = true;
|
|
13308
13515
|
};
|
|
13309
13516
|
};
|
|
13517
|
+
/**
|
|
13518
|
+
* Stops the strategy from generating new signals.
|
|
13519
|
+
*
|
|
13520
|
+
* Sets internal flag to prevent strategy from opening new signals.
|
|
13521
|
+
* Current active signal (if any) will complete normally.
|
|
13522
|
+
* Backtest will stop at the next safe point (idle state or after signal closes).
|
|
13523
|
+
*
|
|
13524
|
+
* @param symbol - Trading pair symbol
|
|
13525
|
+
* @param strategyName - Strategy name to stop
|
|
13526
|
+
* @returns Promise that resolves when stop flag is set
|
|
13527
|
+
*
|
|
13528
|
+
* @example
|
|
13529
|
+
* ```typescript
|
|
13530
|
+
* // Stop strategy after some condition
|
|
13531
|
+
* await Backtest.stop("BTCUSDT", "my-strategy");
|
|
13532
|
+
* ```
|
|
13533
|
+
*/
|
|
13534
|
+
this.stop = async (symbol, strategyName) => {
|
|
13535
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_STOP, {
|
|
13536
|
+
symbol,
|
|
13537
|
+
strategyName,
|
|
13538
|
+
});
|
|
13539
|
+
await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
|
|
13540
|
+
};
|
|
13310
13541
|
/**
|
|
13311
13542
|
* Gets statistical data from all closed signals for a symbol-strategy pair.
|
|
13312
13543
|
*
|
|
@@ -13395,6 +13626,7 @@ const Backtest = new BacktestUtils();
|
|
|
13395
13626
|
|
|
13396
13627
|
const LIVE_METHOD_NAME_RUN = "LiveUtils.run";
|
|
13397
13628
|
const LIVE_METHOD_NAME_BACKGROUND = "LiveUtils.background";
|
|
13629
|
+
const LIVE_METHOD_NAME_STOP = "LiveUtils.stop";
|
|
13398
13630
|
const LIVE_METHOD_NAME_GET_REPORT = "LiveUtils.getReport";
|
|
13399
13631
|
const LIVE_METHOD_NAME_DUMP = "LiveUtils.dump";
|
|
13400
13632
|
/**
|
|
@@ -13502,7 +13734,7 @@ class LiveUtils {
|
|
|
13502
13734
|
};
|
|
13503
13735
|
task().catch((error) => exitEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
|
|
13504
13736
|
return () => {
|
|
13505
|
-
backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName });
|
|
13737
|
+
backtest$1.strategyGlobalService.stop({ symbol, strategyName: context.strategyName }, false);
|
|
13506
13738
|
backtest$1.strategyGlobalService
|
|
13507
13739
|
.getPendingSignal(symbol, context.strategyName)
|
|
13508
13740
|
.then(async (pendingSignal) => {
|
|
@@ -13522,6 +13754,30 @@ class LiveUtils {
|
|
|
13522
13754
|
isStopped = true;
|
|
13523
13755
|
};
|
|
13524
13756
|
};
|
|
13757
|
+
/**
|
|
13758
|
+
* Stops the strategy from generating new signals.
|
|
13759
|
+
*
|
|
13760
|
+
* Sets internal flag to prevent strategy from opening new signals.
|
|
13761
|
+
* Current active signal (if any) will complete normally.
|
|
13762
|
+
* Live trading will stop at the next safe point (idle/closed state).
|
|
13763
|
+
*
|
|
13764
|
+
* @param symbol - Trading pair symbol
|
|
13765
|
+
* @param strategyName - Strategy name to stop
|
|
13766
|
+
* @returns Promise that resolves when stop flag is set
|
|
13767
|
+
*
|
|
13768
|
+
* @example
|
|
13769
|
+
* ```typescript
|
|
13770
|
+
* // Stop live trading gracefully
|
|
13771
|
+
* await Live.stop("BTCUSDT", "my-strategy");
|
|
13772
|
+
* ```
|
|
13773
|
+
*/
|
|
13774
|
+
this.stop = async (symbol, strategyName) => {
|
|
13775
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_STOP, {
|
|
13776
|
+
symbol,
|
|
13777
|
+
strategyName,
|
|
13778
|
+
});
|
|
13779
|
+
await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, false);
|
|
13780
|
+
};
|
|
13525
13781
|
/**
|
|
13526
13782
|
* Gets statistical data from all live trading events for a symbol-strategy pair.
|
|
13527
13783
|
*
|
|
@@ -13829,6 +14085,7 @@ class Performance {
|
|
|
13829
14085
|
|
|
13830
14086
|
const WALKER_METHOD_NAME_RUN = "WalkerUtils.run";
|
|
13831
14087
|
const WALKER_METHOD_NAME_BACKGROUND = "WalkerUtils.background";
|
|
14088
|
+
const WALKER_METHOD_NAME_STOP = "WalkerUtils.stop";
|
|
13832
14089
|
const WALKER_METHOD_NAME_GET_DATA = "WalkerUtils.getData";
|
|
13833
14090
|
const WALKER_METHOD_NAME_GET_REPORT = "WalkerUtils.getReport";
|
|
13834
14091
|
const WALKER_METHOD_NAME_DUMP = "WalkerUtils.dump";
|
|
@@ -13939,8 +14196,8 @@ class WalkerUtils {
|
|
|
13939
14196
|
task().catch((error) => exitEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
|
|
13940
14197
|
return () => {
|
|
13941
14198
|
for (const strategyName of walkerSchema.strategies) {
|
|
13942
|
-
backtest$1.strategyGlobalService.stop({ symbol, strategyName });
|
|
13943
|
-
walkerStopSubject.next({ symbol, strategyName });
|
|
14199
|
+
backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
|
|
14200
|
+
walkerStopSubject.next({ symbol, strategyName, walkerName: context.walkerName });
|
|
13944
14201
|
}
|
|
13945
14202
|
if (!isDone) {
|
|
13946
14203
|
doneWalkerSubject.next({
|
|
@@ -13954,6 +14211,40 @@ class WalkerUtils {
|
|
|
13954
14211
|
isStopped = true;
|
|
13955
14212
|
};
|
|
13956
14213
|
};
|
|
14214
|
+
/**
|
|
14215
|
+
* Stops all strategies in the walker from generating new signals.
|
|
14216
|
+
*
|
|
14217
|
+
* Iterates through all strategies defined in walker schema and:
|
|
14218
|
+
* 1. Sends stop signal via walkerStopSubject (interrupts current running strategy)
|
|
14219
|
+
* 2. Sets internal stop flag for each strategy (prevents new signals)
|
|
14220
|
+
*
|
|
14221
|
+
* Current active signals (if any) will complete normally.
|
|
14222
|
+
* Walker will stop at the next safe point.
|
|
14223
|
+
*
|
|
14224
|
+
* Supports multiple walkers running on the same symbol simultaneously.
|
|
14225
|
+
* Stop signal is filtered by walkerName to prevent interference.
|
|
14226
|
+
*
|
|
14227
|
+
* @param symbol - Trading pair symbol
|
|
14228
|
+
* @param walkerName - Walker name to stop
|
|
14229
|
+
* @returns Promise that resolves when all stop flags are set
|
|
14230
|
+
*
|
|
14231
|
+
* @example
|
|
14232
|
+
* ```typescript
|
|
14233
|
+
* // Stop walker and all its strategies
|
|
14234
|
+
* await Walker.stop("BTCUSDT", "my-walker");
|
|
14235
|
+
* ```
|
|
14236
|
+
*/
|
|
14237
|
+
this.stop = async (symbol, walkerName) => {
|
|
14238
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_STOP, {
|
|
14239
|
+
symbol,
|
|
14240
|
+
walkerName,
|
|
14241
|
+
});
|
|
14242
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
|
|
14243
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
14244
|
+
await walkerStopSubject.next({ symbol, strategyName, walkerName });
|
|
14245
|
+
await backtest$1.strategyGlobalService.stop({ symbol, strategyName }, true);
|
|
14246
|
+
}
|
|
14247
|
+
};
|
|
13957
14248
|
/**
|
|
13958
14249
|
* Gets walker results data from all strategy comparisons.
|
|
13959
14250
|
*
|