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/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.1,
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
- // ЗАЩИТА ОТ EDGE CASE: для immediate сигналов проверяем что текущая цена не пробила SL/TP
1825
- // Для scheduled сигналов эта проверка избыточна т.к. priceOpen уже проверен выше
1826
- if (!isScheduled) {
1827
- // Текущая цена уже пробила StopLoss - позиция откроется и сразу закроется по SL
1828
- if (isFinite(currentPrice) && currentPrice < signal.priceStopLoss) {
1829
- errors.push(`Long: currentPrice (${currentPrice}) < priceStopLoss (${signal.priceStopLoss}). ` +
1830
- `Signal would be immediately cancelled. This signal is invalid.`);
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
- // Текущая цена уже достигла TakeProfit - профит упущен
1833
- if (isFinite(currentPrice) && currentPrice > signal.priceTakeProfit) {
1834
- errors.push(`Long: currentPrice (${currentPrice}) > priceTakeProfit (${signal.priceTakeProfit}). ` +
1835
- `Signal is invalid - the profit opportunity has already passed.`);
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 && 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
- // ЗАЩИТА ОТ EDGE CASE: для immediate сигналов проверяем что текущая цена не пробила SL/TP
1866
- // Для scheduled сигналов эта проверка избыточна т.к. priceOpen уже проверен выше
1867
- if (!isScheduled) {
1868
- // Текущая цена уже пробила StopLoss - позиция откроется и сразу закроется по SL
1869
- if (isFinite(currentPrice) && currentPrice > signal.priceStopLoss) {
1870
- errors.push(`Short: currentPrice (${currentPrice}) > priceStopLoss (${signal.priceStopLoss}). ` +
1871
- `Signal would be immediately cancelled. This signal is invalid.`);
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
- // Текущая цена уже достигла TakeProfit - профит упущен
1874
- if (isFinite(currentPrice) && currentPrice < signal.priceTakeProfit) {
1875
- errors.push(`Short: currentPrice (${currentPrice}) < priceTakeProfit (${signal.priceTakeProfit}). ` +
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 && 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
- for (let i = candlesCount - 1; i < candles.length; i++) {
2648
- const recentCandles = candles.slice(i - (candlesCount - 1), i + 1);
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 checking VWAP against TP/SL on each timeframe
2952
- * 2. Starts from index 4 (needs 5 candles for VWAP calculation)
2953
- * 3. Returns closed result (either TP/SL or time_expired)
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
- await this.setScheduledSignal(null);
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
- // КРИТИЧНО: запрашиваем CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
4910
- // + minuteEstimatedTime для работы сигнала ПОСЛЕ активации
4911
- // +1 потому что when включается как первая свеча (timestamp начинается с when, а не after when)
4912
- const candlesNeeded = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
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, when, true);
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", signal.minuteEstimatedTime, when, true);
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), message: functoolsKit.getErrorMessage(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 (result.action === "active") {
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 === "idle") {
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
- let pendingStrategy;
5270
- const listenStop = walkerStopSubject
5271
- .filter((data) => {
5272
- let isOk = true;
5273
- isOk = isOk && data.symbol === symbol;
5274
- isOk = isOk && data.strategyName === pendingStrategy;
5275
- return isOk;
5276
- })
5277
- .map(() => CANCEL_SYMBOL)
5278
- .toPromise();
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
- const iterator = this.backtestLogicPublicService.run(symbol, {
5290
- strategyName,
5291
- exchangeName: context.exchangeName,
5292
- frameName: context.frameName,
5293
- });
5294
- pendingStrategy = strategyName;
5295
- let result;
5296
- try {
5297
- result = await Promise.race([
5298
- await functoolsKit.resolveDocuments(iterator),
5299
- listenStop,
5300
- ]);
5301
- }
5302
- catch (error) {
5303
- console.warn(`walkerLogicPrivateService backtest failed symbol=${symbol} strategyName=${strategyName} exchangeName=${context.exchangeName}`);
5304
- this.loggerService.warn("walkerLogicPrivateService backtest failed for strategy, skipping", {
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
- await errorEmitter.next(error);
5310
- // Call onStrategyError callback if provided
5311
- if (walkerSchema.callbacks?.onStrategyError) {
5312
- walkerSchema.callbacks.onStrategyError(strategyName, symbol, error);
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
- continue;
5315
- }
5316
- if (result === CANCEL_SYMBOL) {
5317
- this.loggerService.info("walkerLogicPrivateService received stop signal, cancelling walker", {
5318
- context,
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
- break;
5321
- }
5322
- this.loggerService.info("walkerLogicPrivateService backtest complete", {
5323
- strategyName,
5324
- symbol,
5325
- });
5326
- // Get statistics from BacktestMarkdownService
5327
- const stats = await this.backtestMarkdownService.getData(symbol, strategyName);
5328
- // Extract metric value
5329
- const value = stats[metric];
5330
- const metricValue = value !== null &&
5331
- value !== undefined &&
5332
- typeof value === "number" &&
5333
- !isNaN(value) &&
5334
- isFinite(value)
5335
- ? value
5336
- : null;
5337
- // Update best strategy if needed
5338
- const isBetter = bestMetric === null ||
5339
- (metricValue !== null && metricValue > bestMetric);
5340
- if (isBetter && metricValue !== null) {
5341
- bestMetric = metricValue;
5342
- bestStrategy = strategyName;
5343
- }
5344
- strategiesTested++;
5345
- const walkerContract = {
5346
- walkerName: context.walkerName,
5347
- exchangeName: context.exchangeName,
5348
- frameName: context.frameName,
5349
- symbol,
5350
- strategyName,
5351
- stats,
5352
- metricValue,
5353
- metric,
5354
- bestMetric,
5355
- bestStrategy,
5356
- strategiesTested,
5357
- totalStrategies: strategies.length,
5358
- };
5359
- // Emit progress event
5360
- await progressWalkerEmitter.next({
5361
- walkerName: context.walkerName,
5362
- exchangeName: context.exchangeName,
5363
- frameName: context.frameName,
5364
- symbol,
5365
- totalStrategies: strategies.length,
5366
- processedStrategies: strategiesTested,
5367
- progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
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
- await walkerEmitter.next(walkerContract);
5374
- yield walkerContract;
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
  *