Auf GitHub ansehen

Bull Row Breakout Strategy

Overview

The Bull Row Breakout strategy is a C# conversion of the MetaTrader 5 expert advisor "BULL row full EA". The original robot was built with a block-constructor and combines price action patterns with momentum confirmation. The StockSharp port reproduces the same logic on a single configurable timeframe and keeps the trading commentary in English as required.

The strategy opens long-only positions after a sequence of bearish candles is followed by bullish momentum and a breakout above recent highs. Stochastic Oscillator filters control the momentum strength while dynamic stop loss and target levels recreate the risk settings from the MQL version.

Signal Logic

  1. Wait for a new candle to close ("once per bar" execution).
  2. Verify that no long position is currently open.
  3. Detect a bearish row:
    • BearRowSize consecutive candles starting at BearShift bars back must be bearish.
    • Each candle body must be at least BearMinBody price steps.
    • Body progression must satisfy the selected BearRowMode (normal / bigger / smaller).
  4. Detect a bullish row:
    • BullRowSize consecutive candles starting at BullShift bars back must be bullish.
    • Each candle body must be at least BullMinBody price steps.
    • Body progression must satisfy BullRowMode.
  5. Breakout confirmation: the close of the latest finished candle must be higher than the highest high recorded from bar 2 up to BreakoutLookback bars ago.
  6. Stochastic confirmation:
    • Current %K (StochasticKPeriod) must be above %D (StochasticDPeriod).
    • The last StochasticRangePeriod %K values must stay between StochasticLowerLevel and StochasticUpperLevel.
  7. Risk management:
    • Stop price is the lowest low among the last StopLossLookback candles (starting from the latest closed bar).
    • Take profit is placed at a distance equal to TakeProfitPercent percent of the stop distance.
    • The stop and target are monitored on every closed candle; if either level is reached intrabar, the position is closed at market on the next update.

Parameters

Parameter Description
Volume Fixed trade volume used for each entry.
CandleTimeFrame Timeframe of the processed candles.
StopLossLookback Number of bars used to calculate the dynamic stop price.
TakeProfitPercent Reward distance expressed as a percentage of the stop distance.
BearRowSize, BearMinBody, BearRowMode, BearShift Configuration of the bearish row that precedes the breakout.
BullRowSize, BullMinBody, BullRowMode, BullShift Configuration of the bullish row that immediately precedes the signal.
BreakoutLookback Length of the rolling high used for breakout confirmation.
StochasticKPeriod, StochasticDPeriod, StochasticSlowing Stochastic Oscillator settings.
StochasticRangePeriod Number of historical Stochastic values that must stay inside the bounds.
StochasticUpperLevel, StochasticLowerLevel Oscillator channel limits applied to %K.

All body sizes are expressed in price steps to mirror the toDigits helper from the original code. When the instrument does not provide a price step, a default value of 1 is used.

Differences from the MQL Version

  • The MT5 project allowed separate timeframes for the block inputs. The StockSharp port operates on one timeframe defined by CandleTimeFrame, matching the common usage of the original EA (all blocks at chart timeframe).
  • Virtual stops and pending order handling from the generic block library are not required and therefore omitted.
  • Protective stop-loss and take-profit levels are emulated by monitoring candles and closing the position with SellMarket once a level is breached.
  • Logging and chart decorations from the MQL environment are not replicated.

Usage Tips

  • Optimise the row sizes and shifts for the traded instrument. The default values mimic the original preset (three bearish candles starting three bars back followed by two bullish candles starting one bar back).
  • Adjust StochasticLowerLevel and StochasticUpperLevel to tune how restrictive the oscillator filter should be.
  • Because the stop is based on recent lows, instruments with large gaps may require widening the lookback or adding additional filters.
namespace StockSharp.Samples.Strategies;

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;

using StockSharp.Algo;

/// <summary>
/// Strategy converted from the "BULL row full EA" expert advisor.
/// </summary>
public class BullRowBreakoutStrategy : Strategy
{
	private readonly List<ICandleMessage> _candles = new();
	private readonly Queue<decimal> _stochasticHistory = new();
	private StochasticOscillator _stochastic = null!;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	private readonly StrategyParam<TimeSpan> _candleTimeFrame;
	private readonly StrategyParam<int> _stopLossLookback;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<int> _bearRowSize;
	private readonly StrategyParam<decimal> _bearMinBody;
	private readonly StrategyParam<RowSequenceModes> _bearRowMode;
	private readonly StrategyParam<int> _bearShift;
	private readonly StrategyParam<int> _bullRowSize;
	private readonly StrategyParam<decimal> _bullMinBody;
	private readonly StrategyParam<RowSequenceModes> _bullRowMode;
	private readonly StrategyParam<int> _bullShift;
	private readonly StrategyParam<int> _breakoutLookback;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<int> _stochasticSlowing;
	private readonly StrategyParam<int> _stochasticRangePeriod;
	private readonly StrategyParam<decimal> _stochasticUpperLevel;
	private readonly StrategyParam<decimal> _stochasticLowerLevel;

	/// <summary>
	/// Initializes a new instance of the <see cref="BullRowBreakoutStrategy"/> class.
	/// </summary>
	public BullRowBreakoutStrategy()
	{
		_candleTimeFrame = Param(nameof(CandleTimeFrame), TimeSpan.FromMinutes(5))
		.SetDisplay("Timeframe", "Primary candle timeframe", "Market")
		;

		_stopLossLookback = Param(nameof(StopLossLookback), 10)
		.SetDisplay("Stop loss bars", "Bars used to locate protective stop", "Risk")
		;

		_takeProfitPercent = Param(nameof(TakeProfitPercent), 100m)
		.SetDisplay("Take profit %", "Reward distance relative to stop", "Risk")
		;

		_bearRowSize = Param(nameof(BearRowSize), 3)
		.SetDisplay("Bear row size", "Required consecutive bearish candles", "Pattern")
		;

		_bearMinBody = Param(nameof(BearMinBody), 0m)
		.SetDisplay("Bear min body", "Minimum bearish candle body (price steps)", "Pattern")
		;

		_bearRowMode = Param(nameof(BearRowMode), RowSequenceModes.Normal)
		.SetDisplay("Bear row mode", "Body size progression for bearish row", "Pattern")
		;

		_bearShift = Param(nameof(BearShift), 3)
		.SetDisplay("Bear row shift", "How many bars back the bearish row starts", "Pattern")
		;

		_bullRowSize = Param(nameof(BullRowSize), 2)
		.SetDisplay("Bull row size", "Required consecutive bullish candles", "Pattern")
		;

		_bullMinBody = Param(nameof(BullMinBody), 0m)
		.SetDisplay("Bull min body", "Minimum bullish candle body (price steps)", "Pattern")
		;

		_bullRowMode = Param(nameof(BullRowMode), RowSequenceModes.Normal)
		.SetDisplay("Bull row mode", "Body size progression for bullish row", "Pattern")
		;

		_bullShift = Param(nameof(BullShift), 1)
		.SetDisplay("Bull row shift", "How many bars back the bullish row starts", "Pattern")
		;

		_breakoutLookback = Param(nameof(BreakoutLookback), 10)
		.SetDisplay("Breakout lookback", "Bars checked for the breakout filter", "Pattern")
		;

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 40)
		.SetDisplay("Stochastic %K", "%K period", "Indicators")
		;

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 8)
		.SetDisplay("Stochastic %D", "%D period", "Indicators")
		;

		_stochasticSlowing = Param(nameof(StochasticSlowing), 10)
		.SetDisplay("Stochastic slowing", "Smoothing applied to %K", "Indicators")
		;

		_stochasticRangePeriod = Param(nameof(StochasticRangePeriod), 3)
		.SetDisplay("Stochastic bars", "Bars that must remain inside the oscillator channel", "Indicators")
		;

		_stochasticUpperLevel = Param(nameof(StochasticUpperLevel), 70m)
		.SetDisplay("Stochastic upper", "Upper bound for the oscillator", "Indicators")
		;

		_stochasticLowerLevel = Param(nameof(StochasticLowerLevel), 30m)
		.SetDisplay("Stochastic lower", "Lower bound for the oscillator", "Indicators")
		;
	}

	/// <summary>
	/// Primary candle timeframe.
	/// </summary>
	public TimeSpan CandleTimeFrame
	{
		get => _candleTimeFrame.Value;
		set => _candleTimeFrame.Value = value;
	}

	/// <summary>
	/// Bars used to locate the stop price.
	/// </summary>
	public int StopLossLookback
	{
		get => _stopLossLookback.Value;
		set => _stopLossLookback.Value = value;
	}

	/// <summary>
	/// Take profit distance relative to the stop in percent.
	/// </summary>
	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}

	/// <summary>
	/// Bearish row length in candles.
	/// </summary>
	public int BearRowSize
	{
		get => _bearRowSize.Value;
		set => _bearRowSize.Value = value;
	}

	/// <summary>
	/// Minimum bearish body expressed in price steps.
	/// </summary>
	public decimal BearMinBody
	{
		get => _bearMinBody.Value;
		set => _bearMinBody.Value = value;
	}

	/// <summary>
	/// Bearish row body progression requirement.
	/// </summary>
	public RowSequenceModes BearRowMode
	{
		get => _bearRowMode.Value;
		set => _bearRowMode.Value = value;
	}

	/// <summary>
	/// Offset in bars where the bearish row starts.
	/// </summary>
	public int BearShift
	{
		get => _bearShift.Value;
		set => _bearShift.Value = value;
	}

	/// <summary>
	/// Bullish row length in candles.
	/// </summary>
	public int BullRowSize
	{
		get => _bullRowSize.Value;
		set => _bullRowSize.Value = value;
	}

	/// <summary>
	/// Minimum bullish body expressed in price steps.
	/// </summary>
	public decimal BullMinBody
	{
		get => _bullMinBody.Value;
		set => _bullMinBody.Value = value;
	}

	/// <summary>
	/// Bullish row body progression requirement.
	/// </summary>
	public RowSequenceModes BullRowMode
	{
		get => _bullRowMode.Value;
		set => _bullRowMode.Value = value;
	}

	/// <summary>
	/// Offset in bars where the bullish row starts.
	/// </summary>
	public int BullShift
	{
		get => _bullShift.Value;
		set => _bullShift.Value = value;
	}

	/// <summary>
	/// Lookback used to determine the breakout high.
	/// </summary>
	public int BreakoutLookback
	{
		get => _breakoutLookback.Value;
		set => _breakoutLookback.Value = value;
	}

	/// <summary>
	/// Stochastic %K length.
	/// </summary>
	public int StochasticKPeriod
	{
		get => _stochasticKPeriod.Value;
		set => _stochasticKPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic %D length.
	/// </summary>
	public int StochasticDPeriod
	{
		get => _stochasticDPeriod.Value;
		set => _stochasticDPeriod.Value = value;
	}

	/// <summary>
	/// Additional smoothing applied to %K.
	/// </summary>
	public int StochasticSlowing
	{
		get => _stochasticSlowing.Value;
		set => _stochasticSlowing.Value = value;
	}

	/// <summary>
	/// Number of candles that must remain inside the Stochastic channel.
	/// </summary>
	public int StochasticRangePeriod
	{
		get => _stochasticRangePeriod.Value;
		set => _stochasticRangePeriod.Value = value;
	}

	/// <summary>
	/// Upper bound for the Stochastic filter.
	/// </summary>
	public decimal StochasticUpperLevel
	{
		get => _stochasticUpperLevel.Value;
		set => _stochasticUpperLevel.Value = value;
	}

	/// <summary>
	/// Lower bound for the Stochastic filter.
	/// </summary>
	public decimal StochasticLowerLevel
	{
		get => _stochasticLowerLevel.Value;
		set => _stochasticLowerLevel.Value = value;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_candles.Clear();
		_stochasticHistory.Clear();
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_candles.Clear();
		_stochasticHistory.Clear();
		_stopPrice = null;
		_takeProfitPrice = null;

		_stochastic = new StochasticOscillator
		{
			K = { Length = StochasticKPeriod },
			D = { Length = StochasticDPeriod },
		};

		var subscription = SubscribeCandles(CandleTimeFrame.TimeFrame());
		subscription
		.BindEx(_stochastic, ProcessCandle)
		.Start();

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

		StartProtection(null, null);
	}

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

		var stoch = (StochasticOscillatorValue)stochasticValue;
		if (!stochasticValue.IsFinal || stoch.K is not decimal kValue || stoch.D is not decimal dValue)
		return;

		_candles.Add(candle);
		var maxNeeded = Math.Max(Math.Max(BearShift + BearRowSize - 1, BullShift + BullRowSize - 1), Math.Max(StopLossLookback, BreakoutLookback));
		if (_candles.Count > Math.Max(maxNeeded + 5, StochasticRangePeriod + 5))
		_candles.RemoveAt(0);

		_stochasticHistory.Enqueue(kValue);
		while (_stochasticHistory.Count > Math.Max(StochasticRangePeriod, 1))
		_stochasticHistory.Dequeue();

		ManageProtectiveLevels(candle);

		if (Position > 0m)
		return;

		if (!HasEnoughHistory())
		return;

		if (!HasBearRow())
		return;

		if (!HasBullRow())
		return;

		if (!HasBreakout())
		return;

		if (!HasStochasticCross(kValue, dValue))
		return;

		if (!IsStochasticContained())
		return;

		var volume = Volume;
		if (volume <= 0m)
		return;

		var stopPrice = CalculateStopPrice();
		if (stopPrice is null)
		return;

		var entryPrice = candle.ClosePrice;
		var risk = entryPrice - stopPrice.Value;
		if (risk <= 0m)
		return;

		var reward = risk * TakeProfitPercent / 100m;
		var takeProfitPrice = entryPrice + reward;

		if (BuyMarket(volume) is null)
		return;

		_stopPrice = stopPrice;
		_takeProfitPrice = takeProfitPrice;
	}

	private void ManageProtectiveLevels(ICandleMessage candle)
	{
		if (Position <= 0m)
		return;

		if (_stopPrice is decimal stop && candle.LowPrice <= stop)
		{
			SellMarket(Math.Abs(Position));
			ResetProtection();
			return;
		}

		if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
		{
			SellMarket(Math.Abs(Position));
			ResetProtection();
			return;
		}
	}
	private void ResetProtection()
	{
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private bool HasEnoughHistory()
	{
		if (_candles.Count < Math.Max(BreakoutLookback, StopLossLookback))
		return false;

		var bearRequirement = BearShift + BearRowSize - 1;
		var bullRequirement = BullShift + BullRowSize - 1;
		var minCandles = Math.Max(bearRequirement, bullRequirement);
		return _candles.Count >= Math.Max(minCandles, 2);
	}

	private bool HasBearRow() => HasRow(BearRowSize, BearMinBody, BearRowMode, BearShift, isBullish: false);

	private bool HasBullRow() => HasRow(BullRowSize, BullMinBody, BullRowMode, BullShift, isBullish: true);

	private bool HasRow(int size, decimal minBody, RowSequenceModes mode, int shift, bool isBullish)
	{
		if (size <= 0 || shift <= 0)
		return false;

		var maxShift = shift + size - 1;
		if (_candles.Count < maxShift)
		return false;

		var bodyStep = Security?.PriceStep ?? 0m;
		if (bodyStep <= 0m)
		bodyStep = 1m;

		var minBodyValue = minBody * bodyStep;
		decimal previousBody = 0m;

		for (var i = 0; i < size; i++)
		{
			var candle = GetCandle(shift + i);
			var body = isBullish ? candle.ClosePrice - candle.OpenPrice : candle.OpenPrice - candle.ClosePrice;

			if (body <= 0m)
			return false;

			if (body < minBodyValue)
			return false;

			if (mode == RowSequenceModes.Bigger && previousBody > 0m && body <= previousBody)
			return false;

			if (mode == RowSequenceModes.Smaller && previousBody > 0m && body >= previousBody)
			return false;

			previousBody = body;
		}

		return true;
	}

	private bool HasBreakout()
	{
		if (BreakoutLookback <= 2)
		return false;

		var prevClose = GetCandle(1).ClosePrice;
		var highest = decimal.MinValue;

		for (var shift = 2; shift <= BreakoutLookback; shift++)
		{
			var candle = GetCandle(shift);
			highest = Math.Max(highest, candle.HighPrice);
		}

		return prevClose > highest;
	}

	private bool HasStochasticCross(decimal kValue, decimal dValue)
	{
		return kValue > dValue;
	}

	private bool IsStochasticContained()
	{
		if (StochasticRangePeriod <= 0)
		return true;

		if (_stochasticHistory.Count < StochasticRangePeriod)
		return false;

		var history = _stochasticHistory.ToArray();
		return history.All(v => v <= StochasticUpperLevel && v >= StochasticLowerLevel);
	}

	private decimal? CalculateStopPrice()
	{
		if (StopLossLookback <= 0)
		return null;

		var lowest = decimal.MaxValue;
		var bars = Math.Min(StopLossLookback, _candles.Count);
		for (var shift = 1; shift <= bars; shift++)
		{
			var candle = GetCandle(shift);
			lowest = Math.Min(lowest, candle.LowPrice);
		}

		return lowest == decimal.MaxValue ? null : lowest;
	}

	private ICandleMessage GetCandle(int shift)
	{
		return _candles[^shift];
	}

	public enum RowSequenceModes
	{
		/// <summary>
		/// Only direction and minimum body size are checked.
		/// </summary>
		Normal,

		/// <summary>
		/// Each candle must have a larger body than the previous one.
		/// </summary>
		Bigger,

		/// <summary>
		/// Each candle must have a smaller body than the previous one.
		/// </summary>
		Smaller
	}
}