GitHub で見る

Pause Trading On Consecutive Loss Strategy

The Pause Trading On Consecutive Loss Strategy reproduces the risk control logic of the MetaTrader 4 expert advisor "Pause Trading On Consecutive Loss". The original script monitored the most recent closed trades, counted how many of them ended with a negative profit, and suspended new orders when the losing streak exceeded a user-defined limit within a short time window. The StockSharp port keeps that behaviour while wrapping it around a minimal momentum entry model so the pause mechanism can be evaluated inside the standalone strategy.

How it works

  1. The strategy subscribes to time-frame candles specified by CandleType. Whenever a finished candle arrives, the closing price is compared to the previous close. If it increased, the strategy attempts a long entry; if it decreased, a short entry is considered. Positions exit whenever a bullish position faces a bearish candle (close below open) or a bearish position faces a bullish candle (close above open).
  2. After every closed position the realised profit of the strategy is inspected. Losing results enqueue their closing timestamp in an internal FIFO list that only stores consecutive losses. Profitable or breakeven exits wipe the list, just as the MQL loop aborted once it encountered a non-losing deal.
  3. When the list reaches ConsecutiveLosses items, the strategy checks whether the time difference between the oldest and the newest loss is within WithinMinutes. If it is, trading is paused until PauseMinutes elapse from the last closing time. During the pause no new market orders are submitted, but the existing position management continues operating so the book can flatten naturally.
  4. Once the pause expires, the list of losses is cleared and trading resumes automatically. The behaviour mimics the original CheckLastNLossDifference and lastOrderCloseTime functions without relying on a persistent order history scan.

The implementation uses StockSharp's high-level candle subscriptions (SubscribeCandles) and the built-in PnL manager to monitor realised profits. A simple queue (Queue<DateTimeOffset>) captures the timestamps of the loss streak while respecting the prohibition on redundant manual history traversal.

Parameters

Parameter Default Description
CandleType 5-minute time frame Candle aggregation used for the simple momentum entries.
OrderVolume 0.1 Volume (in lots/contracts) sent with each entry and exit order.
ConsecutiveLosses 3 Number of back-to-back losing positions required before new trades are paused.
WithinMinutes 20 Maximum number of minutes allowed between the first and the last loss in the streak. A value of zero disables the window check.
PauseMinutes 20 Duration of the trading suspension after the loss streak is detected.

Notes

  • The queue of loss timestamps is only populated when the strategy is flat and has just realised a loss. Partial closes or profitable trades do not extend the streak, preventing false positives.
  • The pause timer is evaluated against each finished candle. If PauseMinutes elapse while the strategy is idle, the next candle immediately unlocks trading.
  • Because the StockSharp version operates on a netting position, the realised PnL difference is derived from PnLManager.RealizedPnL, faithfully mirroring the MetaTrader history lookup without reprocessing the entire order log.
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();
	}
}