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