View on GitHub

Bruno Strategy

The Bruno expert advisor is a trend-following system originally written for MetaTrader 5. The port keeps the same confirmation chain: Average Directional Index (ADX) with directional movement, a pair of exponential moving averages (EMA 8/21), MACD (13, 34, 8), a Stochastic Oscillator (21, 3, 3) and the slope of a Parabolic SAR (0.055, 0.21). Every filter that agrees with the direction multiplies the order size by a configurable factor. If both long and short signals are amplified on the same candle, trading is skipped to avoid conflicting orders.

Trading logic

  • Directional bias
    • Long pressure is strengthened when +DI > -DI and +DI > 20.
    • Short pressure is strengthened when +DI < -DI and +DI < 40.
  • Momentum alignment
    • Long preference requires EMA(8) above EMA(21), Stochastic %K above %D and %K below the overbought threshold (default 80).
    • Short preference requires EMA(8) below EMA(21), Stochastic %K below %D and %K above the oversold threshold (default 20).
  • MACD filter
    • Long bias: MACD histogram above zero and MACD main line above the signal line.
    • Short bias: MACD histogram below zero and MACD main line below the signal line.
  • Parabolic SAR slope
    • Long bias is reinforced when the previous SAR values are rising while EMA(8) > EMA(21).
    • Short bias is reinforced when the previous SAR values are falling while EMA(8) < EMA(21).

Each satisfied condition multiplies the base lot size by SignalMultiplier (default 1.6). Only one side may be active at a time. When a final signal is generated, the strategy closes any opposite position, submits the market order with the multiplied volume, and stores the current close as the entry price.

Position management

  • Stop-loss / take-profit – fixed distances expressed in adjusted pips, matching the MetaTrader version. If either level is hit intrabar the position is closed immediately.
  • Trailing stop – activates once floating profit exceeds TrailingStop + TrailingStep pips. The stop is then pulled behind price by TrailingStop pips and only advances when the gain increases by at least TrailingStep more pips.
  • Conflict handling – if both long and short filters fire on the same candle, no new trade is taken.

Parameters

Group Name Description
Trading BaseVolume Initial lot size before multipliers.
Trading SignalMultiplier Volume multiplier applied by each agreeing filter.
Risk Management StopLossPips / TakeProfitPips Protective distances in adjusted pips. Set to zero to disable.
Risk Management TrailingStopPips / TrailingStepPips Trailing distance and minimum step in adjusted pips.
Indicators AdxPeriod, AdxPositiveThreshold, AdxNegativeThreshold ADX length and DI thresholds.
Indicators FastEmaPeriod, SlowEmaPeriod EMA lengths used in trend confirmation.
Indicators MacdFastPeriod, MacdSlowPeriod, MacdSignalPeriod MACD configuration.
Indicators StochasticPeriod, StochasticKsmoothing, StochasticDsmoothing, StochasticOverbought, StochasticOversold Stochastic oscillator settings.
General CandleType Timeframe used for the entire signal chain (default 1 hour).

Notes

  • Adjusted pip size follows the MetaTrader convention: instruments with 3 or 5 decimal digits are multiplied by 10.
  • Parabolic SAR operates with acceleration step 0.055 and maximum 0.21, mirroring the expert advisor defaults.
  • The port keeps the original money-management style (volume stacking) but aggregates the exposure into a single StockSharp position.
using System;
using System.Collections.Generic;
using System.Linq;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Bruno multi-filter trend strategy.
/// </summary>
public class BrunoStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _signalMultiplier;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<decimal> _adxPositiveThreshold;
	private readonly StrategyParam<decimal> _adxNegativeThreshold;
	private readonly StrategyParam<int> _fastEmaPeriod;
	private readonly StrategyParam<int> _slowEmaPeriod;
	private readonly StrategyParam<int> _macdFastPeriod;
	private readonly StrategyParam<int> _macdSlowPeriod;
	private readonly StrategyParam<int> _macdSignalPeriod;
	private readonly StrategyParam<int> _stochasticPeriod;
	private readonly StrategyParam<int> _stochasticKsmoothing;
	private readonly StrategyParam<int> _stochasticDsmoothing;
	private readonly StrategyParam<decimal> _stochasticOverbought;
	private readonly StrategyParam<decimal> _stochasticOversold;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<Bar> _bars = [];
	private readonly List<decimal> _rawK = [];
	private readonly List<decimal> _smoothK = [];
	private readonly List<decimal> _sarValues = [];

	private decimal? _fastEma;
	private decimal? _slowEma;
	private decimal? _macdFast;
	private decimal? _macdSlow;
	private decimal? _macdSignal;

	private SarState _sar;

	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal? _bestPrice;
	private DateTimeOffset? _entryCandleTime;

	public decimal BaseVolume { get => _baseVolume.Value; set => _baseVolume.Value = value; }
	public decimal SignalMultiplier { get => _signalMultiplier.Value; set => _signalMultiplier.Value = value; }
	public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public int TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
	public int TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
	public int TrailingStepPips { get => _trailingStepPips.Value; set => _trailingStepPips.Value = value; }
	public int AdxPeriod { get => _adxPeriod.Value; set => _adxPeriod.Value = value; }
	public decimal AdxPositiveThreshold { get => _adxPositiveThreshold.Value; set => _adxPositiveThreshold.Value = value; }
	public decimal AdxNegativeThreshold { get => _adxNegativeThreshold.Value; set => _adxNegativeThreshold.Value = value; }
	public int FastEmaPeriod { get => _fastEmaPeriod.Value; set => _fastEmaPeriod.Value = value; }
	public int SlowEmaPeriod { get => _slowEmaPeriod.Value; set => _slowEmaPeriod.Value = value; }
	public int MacdFastPeriod { get => _macdFastPeriod.Value; set => _macdFastPeriod.Value = value; }
	public int MacdSlowPeriod { get => _macdSlowPeriod.Value; set => _macdSlowPeriod.Value = value; }
	public int MacdSignalPeriod { get => _macdSignalPeriod.Value; set => _macdSignalPeriod.Value = value; }
	public int StochasticPeriod { get => _stochasticPeriod.Value; set => _stochasticPeriod.Value = value; }
	public int StochasticKsmoothing { get => _stochasticKsmoothing.Value; set => _stochasticKsmoothing.Value = value; }
	public int StochasticDsmoothing { get => _stochasticDsmoothing.Value; set => _stochasticDsmoothing.Value = value; }
	public decimal StochasticOverbought { get => _stochasticOverbought.Value; set => _stochasticOverbought.Value = value; }
	public decimal StochasticOversold { get => _stochasticOversold.Value; set => _stochasticOversold.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public BrunoStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 0.1m).SetGreaterThanZero();
		_signalMultiplier = Param(nameof(SignalMultiplier), 1.6m).SetGreaterThanZero();
		_stopLossPips = Param(nameof(StopLossPips), 50).SetNotNegative();
		_takeProfitPips = Param(nameof(TakeProfitPips), 100).SetNotNegative();
		_trailingStopPips = Param(nameof(TrailingStopPips), 30).SetNotNegative();
		_trailingStepPips = Param(nameof(TrailingStepPips), 5).SetNotNegative();
		_adxPeriod = Param(nameof(AdxPeriod), 14).SetGreaterThanZero();
		_adxPositiveThreshold = Param(nameof(AdxPositiveThreshold), 20m);
		_adxNegativeThreshold = Param(nameof(AdxNegativeThreshold), 40m);
		_fastEmaPeriod = Param(nameof(FastEmaPeriod), 8).SetGreaterThanZero();
		_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 21).SetGreaterThanZero();
		_macdFastPeriod = Param(nameof(MacdFastPeriod), 13).SetGreaterThanZero();
		_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 34).SetGreaterThanZero();
		_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 8).SetGreaterThanZero();
		_stochasticPeriod = Param(nameof(StochasticPeriod), 21).SetGreaterThanZero();
		_stochasticKsmoothing = Param(nameof(StochasticKsmoothing), 3).SetGreaterThanZero();
		_stochasticDsmoothing = Param(nameof(StochasticDsmoothing), 3).SetGreaterThanZero();
		_stochasticOverbought = Param(nameof(StochasticOverbought), 80m);
		_stochasticOversold = Param(nameof(StochasticOversold), 20m);
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame());
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_bars.Clear();
		_rawK.Clear();
		_smoothK.Clear();
		_sarValues.Clear();
		_fastEma = _slowEma = _macdFast = _macdSlow = _macdSignal = null;
		_sar = null;
		ResetProtection();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_sar = new(0.055m, 0.21m);
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

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

		_bars.Add(new Bar(candle.HighPrice, candle.LowPrice, candle.ClosePrice));
		var keep = Math.Max(Math.Max(AdxPeriod + 2, MacdSlowPeriod + MacdSignalPeriod + 2), StochasticPeriod + 10);
		if (_bars.Count > keep)
			_bars.RemoveRange(0, _bars.Count - keep);

		_fastEma = Ema(_fastEma, candle.ClosePrice, FastEmaPeriod);
		_slowEma = Ema(_slowEma, candle.ClosePrice, SlowEmaPeriod);
		_macdFast = Ema(_macdFast, candle.ClosePrice, MacdFastPeriod);
		_macdSlow = Ema(_macdSlow, candle.ClosePrice, MacdSlowPeriod);
		var macdMain = _macdFast.Value - _macdSlow.Value;
		_macdSignal = Ema(_macdSignal, macdMain, MacdSignalPeriod);

		var sar = _sar.Process(candle.HighPrice, candle.LowPrice, candle.ClosePrice);
		if (sar is decimal sarValue)
		{
			_sarValues.Add(sarValue);
			if (_sarValues.Count > 4)
				_sarValues.RemoveAt(0);
		}

		UpdateStochastic();

		if (Position != 0m && ApplyProtection(candle))
			return;

		if (_bars.Count < AdxPeriod + 1 || _smoothK.Count < StochasticDsmoothing || _sarValues.Count < 3)
			return;

		var (plusDi, minusDi) = CalculateDirectionalIndex();
		var k = _smoothK[^1];
		var d = _smoothK.Skip(_smoothK.Count - StochasticDsmoothing).Average();
		var histogram = macdMain - _macdSignal.Value;

		var longDirectional = plusDi > minusDi && plusDi > AdxPositiveThreshold;
		var shortDirectional = plusDi < minusDi && plusDi < AdxNegativeThreshold;

		var longMomentum = _fastEma > _slowEma && k > d && k < StochasticOverbought;
		var shortMomentum = _fastEma < _slowEma && k < d && k > StochasticOversold;

		var longMacd = histogram > 0m && macdMain > _macdSignal;
		var shortMacd = histogram < 0m && macdMain < _macdSignal;

		var longSar = _sarValues[^3] < _sarValues[^2] && _sarValues[^2] < _sarValues[^1] && _fastEma > _slowEma;
		var shortSar = _sarValues[^3] > _sarValues[^2] && _sarValues[^2] > _sarValues[^1] && _fastEma < _slowEma;

		var (longVolume, shortVolume) = CalculateSignalVolumes(
			BaseVolume, SignalMultiplier,
			longDirectional, shortDirectional,
			longMomentum, shortMomentum,
			longMacd, shortMacd,
			longSar, shortSar);

		var hasLong = longVolume > BaseVolume;
		var hasShort = shortVolume > BaseVolume;

		if (hasLong == hasShort)
			return;

		if (hasLong)
			Enter(Sides.Buy, longVolume, candle);
		else
			Enter(Sides.Sell, shortVolume, candle);
	}

	private void Enter(Sides side, decimal targetVolume, ICandleMessage candle)
	{
		var opposite = side == Sides.Buy ? Math.Max(0m, -Position) : Math.Max(0m, Position);
		var volume = NormalizeVolume(targetVolume + opposite);
		if (volume <= 0m)
			return;

		if (side == Sides.Buy)
			BuyMarket(volume);
		else
			SellMarket(volume);

		_entryPrice = candle.ClosePrice;
		_entryCandleTime = candle.OpenTime;
		_bestPrice = candle.ClosePrice;

		var pip = GetPipSize();
		_stopPrice = StopLossPips > 0
			? side == Sides.Buy ? _entryPrice - StopLossPips * pip : _entryPrice + StopLossPips * pip
			: null;
		_takePrice = TakeProfitPips > 0
			? side == Sides.Buy ? _entryPrice + TakeProfitPips * pip : _entryPrice - TakeProfitPips * pip
			: null;
	}

	private bool ApplyProtection(ICandleMessage candle)
	{
		if (_entryCandleTime is DateTimeOffset entryTime && candle.OpenTime <= entryTime)
			return false;

		var pip = GetPipSize();

		if (Position > 0m)
		{
			_bestPrice = _bestPrice is decimal best ? Math.Max(best, candle.HighPrice) : candle.HighPrice;

			if (TrailingStopPips > 0 && TrailingStepPips >= 0 &&
				_bestPrice.Value - _entryPrice >= (TrailingStopPips + TrailingStepPips) * pip)
			{
				var candidate = _bestPrice.Value - TrailingStopPips * pip;
				if (_stopPrice is null || candidate >= _stopPrice.Value + TrailingStepPips * pip)
					_stopPrice = candidate;
			}

			if ((_stopPrice is decimal stop && candle.LowPrice <= stop) ||
				(_takePrice is decimal take && candle.HighPrice >= take))
			{
				SellMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}
		}
		else if (Position < 0m)
		{
			_bestPrice = _bestPrice is decimal best ? Math.Min(best, candle.LowPrice) : candle.LowPrice;

			if (TrailingStopPips > 0 && TrailingStepPips >= 0 &&
				_entryPrice - _bestPrice.Value >= (TrailingStopPips + TrailingStepPips) * pip)
			{
				var candidate = _bestPrice.Value + TrailingStopPips * pip;
				if (_stopPrice is null || candidate <= _stopPrice.Value - TrailingStepPips * pip)
					_stopPrice = candidate;
			}

			if ((_stopPrice is decimal stop && candle.HighPrice >= stop) ||
				(_takePrice is decimal take && candle.LowPrice <= take))
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}
		}

		return false;
	}

	private void UpdateStochastic()
	{
		if (_bars.Count < StochasticPeriod)
			return;

		var window = _bars.Skip(_bars.Count - StochasticPeriod).ToArray();
		var high = window.Max(b => b.High);
		var low = window.Min(b => b.Low);
		var raw = high == low ? 50m : (_bars[^1].Close - low) / (high - low) * 100m;

		_rawK.Add(raw);
		if (_rawK.Count > StochasticKsmoothing + StochasticDsmoothing + 2)
			_rawK.RemoveAt(0);

		if (_rawK.Count < StochasticKsmoothing)
			return;

		_smoothK.Add(_rawK.Skip(_rawK.Count - StochasticKsmoothing).Average());
		if (_smoothK.Count > StochasticDsmoothing + 2)
			_smoothK.RemoveAt(0);
	}

	private (decimal plusDi, decimal minusDi) CalculateDirectionalIndex()
	{
		var tr = 0m;
		var plus = 0m;
		var minus = 0m;
		var start = _bars.Count - AdxPeriod;

		for (var i = start; i < _bars.Count; i++)
		{
			var current = _bars[i];
			var previous = _bars[i - 1];
			var up = current.High - previous.High;
			var down = previous.Low - current.Low;

			plus += up > down && up > 0m ? up : 0m;
			minus += down > up && down > 0m ? down : 0m;

			tr += Math.Max(current.High - current.Low,
				Math.Max(Math.Abs(current.High - previous.Close), Math.Abs(current.Low - previous.Close)));
		}

		return tr <= 0m ? (0m, 0m) : (plus / tr * 100m, minus / tr * 100m);
	}

	internal static (decimal longVolume, decimal shortVolume) CalculateSignalVolumes(
		decimal baseVolume,
		decimal multiplier,
		bool longDirectional,
		bool shortDirectional,
		bool longMomentum,
		bool shortMomentum,
		bool longMacd,
		bool shortMacd,
		bool longSar,
		bool shortSar)
	{
		var longVolume = baseVolume;
		var shortVolume = baseVolume;

		if (longDirectional) longVolume *= multiplier;
		if (shortDirectional) shortVolume *= multiplier;
		if (longMomentum) longVolume *= multiplier;
		if (shortMomentum) shortVolume *= multiplier;
		if (longMacd) longVolume *= multiplier;
		if (shortMacd) shortVolume *= multiplier;
		if (longSar) longVolume *= multiplier;
		if (shortSar) shortVolume *= multiplier;

		return (longVolume, shortVolume);
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (Security?.MaxVolume is decimal max && max > 0m) volume = Math.Min(volume, max);
		if (Security?.MinVolume is decimal min && min > 0m) volume = Math.Max(volume, min);
		if (Security?.VolumeStep is decimal step && step > 0m) volume = Math.Floor(volume / step) * step;
		return volume;
	}

	private decimal GetPipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m) return 0.0001m;
		return step is 0.00001m or 0.001m ? step * 10m : step;
	}

	private static decimal Ema(decimal? previous, decimal value, int period)
	{
		if (previous is null) return value;
		var alpha = 2m / (period + 1m);
		return previous.Value + alpha * (value - previous.Value);
	}

	private void ResetProtection()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
		_bestPrice = null;
		_entryCandleTime = null;
	}

	private readonly record struct Bar(decimal High, decimal Low, decimal Close);

	private sealed class SarState(decimal step, decimal maximum)
	{
		private bool _initialized;
		private bool _up;
		private decimal _sar;
		private decimal _ep;
		private decimal _af;
		private decimal _previousHigh;
		private decimal _previousLow;
		private decimal _previousClose;
		private decimal _olderHigh;
		private decimal _olderLow;
		private int _count;

		public void Reset()
		{
			_initialized = false;
			_up = false;
			_sar = _ep = _af = 0m;
			_previousHigh = _previousLow = _previousClose = 0m;
			_olderHigh = _olderLow = 0m;
			_count = 0;
		}

		public decimal? Process(decimal high, decimal low, decimal close)
		{
			_count++;

			if (_count == 1)
			{
				_previousHigh = _olderHigh = high;
				_previousLow = _olderLow = low;
				_previousClose = close;
				return null;
			}

			if (!_initialized)
			{
				_up = close >= _previousClose;
				_sar = _up ? Math.Min(_previousLow, low) : Math.Max(_previousHigh, high);
				_ep = _up ? Math.Max(_previousHigh, high) : Math.Min(_previousLow, low);
				_af = step;
				_initialized = true;
				Shift(high, low, close);
				return _sar;
			}

			var next = _sar + _af * (_ep - _sar);

			if (_up)
			{
				next = Math.Min(next, Math.Min(_previousLow, _olderLow));
				if (low < next)
				{
					_up = false;
					next = _ep;
					_ep = low;
					_af = step;
				}
				else if (high > _ep)
				{
					_ep = high;
					_af = Math.Min(maximum, _af + step);
				}
			}
			else
			{
				next = Math.Max(next, Math.Max(_previousHigh, _olderHigh));
				if (high > next)
				{
					_up = true;
					next = _ep;
					_ep = high;
					_af = step;
				}
				else if (low < _ep)
				{
					_ep = low;
					_af = Math.Min(maximum, _af + step);
				}
			}

			_sar = next;
			Shift(high, low, close);
			return _sar;
		}

		private void Shift(decimal high, decimal low, decimal close)
		{
			_olderHigh = _previousHigh;
			_olderLow = _previousLow;
			_previousHigh = high;
			_previousLow = low;
			_previousClose = close;
		}
	}
}