Auf GitHub ansehen

N Candles v6 Strategy

Overview

The N Candles v6 strategy monitors the most recent finished candles and looks for streaks of identical direction. When the market prints N bullish candles in a row the strategy opens a long position, while a string of N bearish candles produces a short entry. The logic is inspired by the MetaTrader expert advisor N Candles v6.mq5 and is adapted to the StockSharp high-level API.

The algorithm is designed for any symbol that delivers standard time-based candles. A configurable trading window keeps the strategy inactive outside of the desired session, but active trailing and exit logic continues to protect an open position even during the blocked hours.

Trading Logic

  1. Subscribe to the configured candle type and process only finished candles.
  2. Count consecutive bullish (Close > Open) and bearish (Close < Open) candles. Dojis reset the counters.
  3. When CandlesCount bullish candles appear:
    • Verify that the projected net position stays below MaxPositionVolume.
    • Send a market buy order. If a short position exists, the order size is increased to flip the position long in one trade.
  4. When CandlesCount bearish candles appear:
    • Ensure the new short exposure will not exceed MaxPositionVolume.
    • Send a market sell order and enlarge the order if a long position must be closed.
  5. If the newest candle breaks the streak (the “black sheep”):
    • Apply the selected ClosingMode to close all, opposite, or same-direction positions once.
  6. Trailing and protective exits run on every candle:
    • Stop-loss and take-profit levels are derived from pip distances and the instrument price step.
    • The trailing stop activates after price moves by TrailingStopPips + TrailingStepPips and only ratchets in the favorable direction.
    • Any breach of the stop, take-profit, or trailing level closes the entire position immediately.

Risk Management

  • Stop Loss (pips) – converts pip distance into an absolute price offset using the symbol price step (5- and 3-digit instruments are automatically scaled).
  • Take Profit (pips) – closes the position after a favorable move of the specified size.
  • Trailing Stop / Step (pips) – enables dynamic protection once the trade reaches the configured profit threshold. The step must be non-zero when trailing is active.
  • Max Position Volume – caps the absolute net position. Signals that would breach the cap are ignored.
  • Closing Mode – determines how to react when a non-conforming candle appears:
    • All – flat the entire position.
    • Opposite – close positions against the streak direction (e.g., close shorts after bullish run breaks).
    • Unidirectional – close positions in the streak direction only.
  • Trading Window – the strategy opens new trades only when the candle open time hour lies between StartHour and EndHour (inclusive). Protective exits continue to operate even when new trades are blocked.

Parameters

Name Default Description
CandlesCount 3 Number of identical candles required for a signal.
OrderVolume 0.01 Base market order size. Opposite exposure is closed before establishing a new trade.
TakeProfitPips 50 Take-profit distance in pips. 0 disables the target.
StopLossPips 50 Stop-loss distance in pips. 0 disables the stop.
TrailingStopPips 10 Trailing stop distance in pips. 0 disables trailing.
TrailingStepPips 4 Minimum price improvement before the trailing level moves. Must be > 0 when trailing is enabled.
MaxPositionVolume 2 Maximum absolute net position.
UseTradingHours true Enables trading window filtering.
StartHour 11 Beginning of the trading session (0-23).
EndHour 18 End of the trading session (0-23).
ClosingMode All Behaviour when a black sheep candle appears.
CandleType 1 hour candles Data type used for signal generation.

Notes

  • The pip conversion is based on the instrument PriceStep. For 5- and 3-digit quotes the strategy multiplies the step by ten to match the traditional pip definition.
  • Call StartProtection() during startup to enable StockSharp safeguard services (cancel-on-stop, reconnection safety, etc.).
  • The logic uses the net position (Strategy.Position) and therefore operates correctly on netting accounts. Hedging-style behaviour can be emulated by setting a high MaxPositionVolume.
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Detects runs of identical candles and trades in the direction of the streak.
/// </summary>
public class NCandlesV6Strategy : Strategy
{
	/// <summary>
	/// Defines how positions are closed when a candle breaks the streak.
	/// </summary>
	public enum BlackSheepCloseModes
	{
		/// <summary>
		/// Close every open position regardless of direction.
		/// </summary>
		All,

		/// <summary>
		/// Close only positions that oppose the detected streak.
		/// </summary>
		Opposite,

		/// <summary>
		/// Close only positions that follow the detected streak.
		/// </summary>
		Unidirectional,
	}

	private readonly StrategyParam<int> _candlesCount;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _maxPositionVolume;
	private readonly StrategyParam<bool> _useTradingHours;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<BlackSheepCloseModes> _blackSheepMode;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal? _trailingLong;
	private decimal? _trailingShort;
	private int _streakDirection;
	private int _bullCount;
	private int _bearCount;
	private bool _blackSheepTriggered;

	public int CandlesCount { get => _candlesCount.Value; set => _candlesCount.Value = value; }
	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
	public decimal TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
	public decimal StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public decimal TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
	public decimal TrailingStepPips { get => _trailingStepPips.Value; set => _trailingStepPips.Value = value; }
	public decimal MaxPositionVolume { get => _maxPositionVolume.Value; set => _maxPositionVolume.Value = value; }
	public bool UseTradingHours { get => _useTradingHours.Value; set => _useTradingHours.Value = value; }
	public int StartHour { get => _startHour.Value; set => _startHour.Value = value; }
	public int EndHour { get => _endHour.Value; set => _endHour.Value = value; }
	public BlackSheepCloseModes ClosingMode { get => _blackSheepMode.Value; set => _blackSheepMode.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public NCandlesV6Strategy()
	{
		_candlesCount = Param(nameof(CandlesCount), 4)
		.SetGreaterThanZero()
		.SetDisplay("Candles", "Number of identical candles", "Pattern");

		_orderVolume = Param(nameof(OrderVolume), 0.01m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Base order size", "Orders");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
		.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
		.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
		.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 4m)
		.SetDisplay("Trailing Step (pips)", "Minimum move before trailing updates", "Risk");

		_maxPositionVolume = Param(nameof(MaxPositionVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Max Position Volume", "Maximum absolute net position", "Risk");

		_useTradingHours = Param(nameof(UseTradingHours), false)
		.SetDisplay("Use Trading Hours", "Enable trading window", "Timing");

		_startHour = Param(nameof(StartHour), 11)
		.SetDisplay("Start Hour", "Hour when trading can start", "Timing");

		_endHour = Param(nameof(EndHour), 18)
		.SetDisplay("End Hour", "Hour when trading stops", "Timing");

		_blackSheepMode = Param(nameof(ClosingMode), BlackSheepCloseModes.All)
		.SetDisplay("Closing Mode", "Reaction to a black sheep candle", "Pattern");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe", "Pattern");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		if (UseTradingHours && StartHour >= EndHour)
		throw new InvalidOperationException("Start hour must be less than end hour when trading window is enabled.");

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
		throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");

		Volume = OrderVolume;

		_pipSize = CalculatePipSize();

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		ResetCounters();
		ResetPositionState();
		_blackSheepTriggered = false;
	}

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

		UpdateTrailingLevels(candle);

		if (ApplyRiskManagement(candle))
		return;

		var direction = GetDirection(candle);

		if (direction == 0)
		{
			if (_streakDirection != 0 && !_blackSheepTriggered)
			HandleBlackSheep(_streakDirection);

			ResetCounters();
			return;
		}

		if (_streakDirection == direction)
		{
			if (direction == 1)
			{
				_bullCount = Math.Min(CandlesCount, _bullCount + 1);
				_bearCount = 0;
			}
			else
			{
				_bearCount = Math.Min(CandlesCount, _bearCount + 1);
				_bullCount = 0;
			}
		}
		else
		{
			if (_streakDirection != 0 && !_blackSheepTriggered)
			HandleBlackSheep(_streakDirection);

			_streakDirection = direction;
			_bullCount = direction == 1 ? 1 : 0;
			_bearCount = direction == -1 ? 1 : 0;
		}

		var allowTrading = !UseTradingHours || IsWithinTradingHours(candle.OpenTime);

		if (_bullCount >= CandlesCount && allowTrading)
		{
			EnterLong(candle.ClosePrice);
		}
		else if (_bearCount >= CandlesCount && allowTrading)
		{
			EnterShort(candle.ClosePrice);
		}
	}

	private void EnterLong(decimal price)
	{
		if (OrderVolume <= 0m)
		return;

		var volume = OrderVolume;

		if (Position < 0m)
		volume += Math.Abs(Position);

		var projected = Position + volume;

		if (projected > MaxPositionVolume)
		return;

		BuyMarket(volume);

		_entryPrice = price;
		_stopLossPrice = StopLossPips > 0m ? price - GetPriceOffset(StopLossPips) : null;
		_takeProfitPrice = TakeProfitPips > 0m ? price + GetPriceOffset(TakeProfitPips) : null;
		_trailingLong = null;
		_trailingShort = null;
		_blackSheepTriggered = false;
	}

	private void EnterShort(decimal price)
	{
		if (OrderVolume <= 0m)
		return;

		var volume = OrderVolume;

		if (Position > 0m)
		volume += Math.Abs(Position);

		var projected = Position - volume;

		if (Math.Abs(projected) > MaxPositionVolume)
		return;

		SellMarket(volume);

		_entryPrice = price;
		_stopLossPrice = StopLossPips > 0m ? price + GetPriceOffset(StopLossPips) : null;
		_takeProfitPrice = TakeProfitPips > 0m ? price - GetPriceOffset(TakeProfitPips) : null;
		_trailingLong = null;
		_trailingShort = null;
		_blackSheepTriggered = false;
	}

	private void HandleBlackSheep(int direction)
	{
		if (direction == 0 || _blackSheepTriggered)
		return;

		switch (ClosingMode)
		{
			case BlackSheepCloseModes.All:
			{
				ClosePosition();
				break;
			}

			case BlackSheepCloseModes.Opposite:
			{
				if (direction == 1 && Position < 0m)
				{
					BuyMarket(Math.Abs(Position));
					ResetPositionState();
				}
				else if (direction == -1 && Position > 0m)
				{
					SellMarket(Math.Abs(Position));
					ResetPositionState();
				}

				break;
			}

			case BlackSheepCloseModes.Unidirectional:
			{
				if (direction == 1 && Position > 0m)
				{
					SellMarket(Math.Abs(Position));
					ResetPositionState();
				}
				else if (direction == -1 && Position < 0m)
				{
					BuyMarket(Math.Abs(Position));
					ResetPositionState();
				}

				break;
			}
		}

		_blackSheepTriggered = true;
	}

	private void ClosePosition()
	{
		if (Position > 0m)
		{
			SellMarket(Math.Abs(Position));
			ResetPositionState();
		}
		else if (Position < 0m)
		{
			BuyMarket(Math.Abs(Position));
			ResetPositionState();
		}
	}

	private void UpdateTrailingLevels(ICandleMessage candle)
	{
		var trailingStop = GetPriceOffset(TrailingStopPips);

		if (trailingStop <= 0m)
		return;

		var trailingStep = GetPriceOffset(TrailingStepPips);

		if (Position > 0m)
		{
			var profit = candle.ClosePrice - _entryPrice;

			if (profit > trailingStop + trailingStep)
			{
				var candidate = candle.ClosePrice - trailingStop;

				if (_trailingLong == null || candidate > _trailingLong.Value + trailingStep)
				_trailingLong = candidate;
			}
		}
		else if (Position < 0m)
		{
			var profit = _entryPrice - candle.ClosePrice;

			if (profit > trailingStop + trailingStep)
			{
				var candidate = candle.ClosePrice + trailingStop;

				if (_trailingShort == null || candidate < _trailingShort.Value - trailingStep)
				_trailingShort = candidate;
			}
		}
	}

	private bool ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopLossPrice is decimal longSl && candle.LowPrice <= longSl)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal longTp && candle.HighPrice >= longTp)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_trailingLong is decimal trail && candle.LowPrice <= trail)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			var absPosition = Math.Abs(Position);

			if (_stopLossPrice is decimal shortSl && candle.HighPrice >= shortSl)
			{
				BuyMarket(absPosition);
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal shortTp && candle.LowPrice <= shortTp)
			{
				BuyMarket(absPosition);
				ResetPositionState();
				return true;
			}

			if (_trailingShort is decimal trail && candle.HighPrice >= trail)
			{
				BuyMarket(absPosition);
				ResetPositionState();
				return true;
			}
		}

		return false;
	}

	private void ResetCounters()
	{
		_streakDirection = 0;
		_bullCount = 0;
		_bearCount = 0;
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_trailingLong = null;
		_trailingShort = null;
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.TimeOfDay.Hours;
		return hour >= StartHour && hour <= EndHour;
	}

	private decimal GetPriceOffset(decimal pips)
	{
		if (pips <= 0m)
		return 0m;

		return pips * _pipSize;
	}

	private static int GetDirection(ICandleMessage candle)
	{
		if (candle.ClosePrice > candle.OpenPrice)
		return 1;

		if (candle.ClosePrice < candle.OpenPrice)
		return -1;

		return 0;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;

		if (step <= 0m)
		return 1m;

		var decimals = CountDecimals(step);

		return decimals == 3 || decimals == 5
		? step * 10m
		: step;
	}

	private static int CountDecimals(decimal value)
	{
		value = Math.Abs(value);
		var count = 0;

		while (value != Math.Truncate(value) && count < 10)
		{
			value *= 10m;
			count++;
		}

		return count;
	}
}