Открыть на GitHub

Стратегия «Пауза после серии убытков»

Стратегия «Пауза после серии убытков» переносит на StockSharp логику советника MetaTrader 4 Pause Trading On Consecutive Loss. Исходный код анализировал историю сделок, находил последнюю последовательность убыточных ордеров и блокировал открытие новых позиций, если серия достигала заданной длины за ограниченный промежуток времени. В порте сохранён тот же принцип, но вокруг него построена минимальная модель входа по импульсу, чтобы механизм паузы можно было тестировать отдельно от других систем.

Алгоритм работы

  1. Стратегия подписывается на свечи типа CandleType. Для каждой завершённой свечи закрытие сравнивается с предыдущим закрытием: рост провоцирует покупку, падение — продажу. Если у бычьей позиции появляется свеча с закрытием ниже открытия, она закрывается; для короткой позиции действует зеркальное правило.
  2. После полного закрытия позиции анализируется реализованный результат (PnLManager.RealizedPnL). Отрицательные значения добавляют время закрытия в очередь FIFO, хранящую только последовательные убытки. Положительные или нулевые результаты очищают очередь — аналогично тому, как MQL-версия прекращала просмотр истории при первом неубыточном ордере.
  3. Когда длина очереди достигает ConsecutiveLosses, вычисляется разница между самой ранней и самой поздней убыточной сделкой. Если она не превышает WithinMinutes, стратегия ставит торговлю на паузу на PauseMinutes минут, отсчитывая их от времени последнего закрытия. Во время паузы новые заявки не отправляются, но существующие позиции могут закрываться по обычным правилам.
  4. После истечения паузы очередь сбрасывается, и торговля автоматически возобновляется. Таким образом воспроизводится поведение функций CheckLastNLossDifference и lastOrderCloseTime без повторного обхода всей истории ордеров.

Стратегия использует высокоуровневую подписку на свечи (SubscribeCandles) и встроенный менеджер прибыли StockSharp для отслеживания реализованного результата. Очередь Queue<DateTimeOffset> хранит временные метки серии убытков, что позволяет обойтись без ручного опроса истории сделок.

Параметры

Параметр Значение по умолчанию Описание
CandleType Свечи с периодом 5 минут Тип и таймфрейм свечей, на которых формируется импульсный сигнал.
OrderVolume 0.1 Объём заявки для входа и выхода из позиции.
ConsecutiveLosses 3 Число последовательных убытков, необходимое для запуска паузы.
WithinMinutes 20 Максимальное количество минут между первой и последней убыточной сделкой в серии (0 — отключить проверку).
PauseMinutes 20 Длительность паузы после срабатывания фильтра.

Дополнительные детали

  • Очередь пополняется только тогда, когда стратегия полностью выходит из позиции с отрицательным результатом. Частичные фиксации и прибыльные сделки не продлевают серию и не вызывают ложных срабатываний.
  • Проверка на окончание паузы выполняется при обработке каждой завершённой свечи. Если PauseMinutes истекли в период простоя, следующая свеча моментально разблокирует торговлю.
  • В варианте StockSharp используется неттинговая позиция, поэтому величина реализованной прибыли берётся как прирост PnLManager.RealizedPnL. Это повторяет логику MQL без многократного обращения к истории ордеров.
using System;
using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "Pause Trading On Consecutive Loss" MetaTrader expert.
/// Uses simple momentum entries (close vs previous close) with a pause mechanism
/// that halts trading after consecutive losing trades within a time window.
/// </summary>
public class PauseTradingOnConsecutiveLossStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _consecutiveLosses;
	private readonly StrategyParam<int> _pauseBars;

	private decimal? _previousClose;
	private int _lossStreak;
	private int _pauseCountdown;
	private decimal _entryPrice;
	private Sides? _entryDirection;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int ConsecutiveLosses
	{
		get => _consecutiveLosses.Value;
		set => _consecutiveLosses.Value = value;
	}

	public int PauseBars
	{
		get => _pauseBars.Value;
		set => _pauseBars.Value = value;
	}

	public PauseTradingOnConsecutiveLossStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for momentum entries", "General");

		_consecutiveLosses = Param(nameof(ConsecutiveLosses), 3)
			.SetGreaterThanZero()
			.SetDisplay("Consecutive Losses", "Losses before pausing", "Risk");

		_pauseBars = Param(nameof(PauseBars), 8)
			.SetGreaterThanZero()
			.SetDisplay("Pause Bars", "Number of bars to pause after loss streak", "Risk");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_previousClose = null;
		_lossStreak = 0;
		_pauseCountdown = 0;
		_entryPrice = 0;
		_entryDirection = null;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var close = candle.ClosePrice;

		if (_previousClose is null)
		{
			_previousClose = close;
			return;
		}

		var volume = Volume;
		if (volume <= 0)
			volume = 1;
		var momentumThreshold = _previousClose.Value * 0.003m;

		// Check if we should pause
		if (_pauseCountdown > 0)
		{
			_pauseCountdown--;
			_previousClose = close;
			return;
		}

		// Check for exit and track wins/losses
		if (Position != 0)
		{
			var shouldExit = false;

			if (Position > 0 && close < _previousClose.Value - momentumThreshold)
				shouldExit = true;
			else if (Position < 0 && close > _previousClose.Value + momentumThreshold)
				shouldExit = true;

			if (shouldExit)
			{
				// Determine if this was a winning or losing trade
				var isLoss = false;
				if (_entryDirection == Sides.Buy && close < _entryPrice)
					isLoss = true;
				else if (_entryDirection == Sides.Sell && close > _entryPrice)
					isLoss = true;

				if (isLoss)
				{
					_lossStreak++;
					if (_lossStreak >= ConsecutiveLosses)
					{
						_pauseCountdown = PauseBars;
						_lossStreak = 0;
					}
				}
				else
				{
					_lossStreak = 0;
				}

				// Close position
				if (Position > 0)
					SellMarket(Position);
				else if (Position < 0)
					BuyMarket(Math.Abs(Position));

				_entryDirection = null;
			}
		}

		// New entry: momentum - close > prev close -> buy, close < prev close -> sell
		if (Position == 0 && _entryDirection is null)
		{
			if (close > _previousClose.Value + momentumThreshold)
			{
				BuyMarket(volume);
				_entryPrice = close;
				_entryDirection = Sides.Buy;
			}
			else if (close < _previousClose.Value - momentumThreshold)
			{
				SellMarket(volume);
				_entryPrice = close;
				_entryDirection = Sides.Sell;
			}
		}

		_previousClose = close;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_previousClose = null;
		_lossStreak = 0;
		_pauseCountdown = 0;
		_entryPrice = 0;
		_entryDirection = null;

		base.OnReseted();
	}
}