Ver en GitHub

Smart Trend Follower Strategy

Overview

The Smart Trend Follower Strategy is a StockSharp port of the MetaTrader 5 expert advisor Smart Trend Follower. The original system alternates between a contrarian moving-average crossover and a trend-following setup that uses stochastic confirmation. It scales into positions with a martingale-like volume multiplier and maintains a shared take-profit/stop-loss for each directional basket. The StockSharp version keeps the same behaviour while using the high-level API (candle subscriptions, indicator bindings and market orders).

Signal Logic

Two independent signal engines are available and can be switched with the SignalMode parameter:

  1. CrossMa – replicates the original contrarian crossover. When the fast SMA crosses below the slow SMA (fast < slow but previously fast > slow) the strategy opens or averages long positions. When the fast SMA crosses above the slow SMA (fast > slow but previously fast < slow) it opens or averages shorts.
  2. Trend – follows the original trend mode that requires confirmation from the stochastic oscillator. A bullish signal appears when the fast SMA stays above the slow SMA, the candle closes higher than it opened, and the stochastic %K value is at or below 30. A bearish signal requires fast < slow, a bearish candle body, and stochastic %K at or above 70.

Signals are evaluated on finished candles only. Whenever a new signal arrives while opposite positions are still open, the strategy first liquidates the opposing basket and only then processes new entries to stay aligned with the direction of the current signal.

Position Scaling

The strategy reproduces the MQL martingale logic:

  • The first order uses InitialVolume lots.
  • Every additional averaging order multiplies the previous volume by Multiplier (values ≤ 1 disable volume growth).
  • A new averaging order for the active direction is allowed only after the market moves by LayerDistancePips pips away from the best entry price of the current basket (lowest long fill or highest short fill).
  • Volumes are normalised using the instrument VolumeStep, VolumeMin and VolumeMax limits when available.

Risk Management

For each directional basket the strategy tracks a shared breakeven price (volume-weighted average of all fills):

  • TakeProfitPips defines the distance between the average entry price and a basket take-profit. Long baskets exit when the candle high touches that level, short baskets when the candle low reaches it. Set to 0 to disable take-profit targets.
  • StopLossPips mirrors the behaviour for protective exits. Long baskets close when the candle low breaks below the stop, short baskets when the candle high crosses above it. Set to 0 to disable the protective stop.

Exit orders are executed via market orders when the next finished candle confirms that the level has been reached. The strategy maintains _longExitRequested and _shortExitRequested flags to avoid duplicated exit submissions while fills are still pending.

Parameters

Parameter Type Default Description
SignalMode enum (CrossMa, Trend) CrossMa Selects the signal engine (contrarian crossover or trend with stochastic filter).
CandleType DataType 30-minute time frame Primary candle series used for calculations and signal generation.
InitialVolume decimal 0.01 Base order size in lots for the first entry of any basket.
Multiplier decimal 2 Volume multiplier applied to each additional averaging order.
LayerDistancePips decimal 200 Minimum pip distance from the best entry before adding another order in the same direction.
FastPeriod int 14 Period of the fast simple moving average.
SlowPeriod int 28 Period of the slow simple moving average (must be greater than FastPeriod).
StochasticKPeriod int 10 Lookback length for the stochastic oscillator %K line.
StochasticDPeriod int 3 Smoothing length for the stochastic %D line.
StochasticSlowing int 3 Additional smoothing applied to %K before %D calculation.
TakeProfitPips decimal 500 Distance in pips from the average entry where the basket take-profit is placed. Set 0 to disable.
StopLossPips decimal 0 Protective stop distance in pips. Set 0 to disable the hard stop.

Implementation Notes

  • Pip size is derived from the instrument PriceStep and Decimals, matching the MetaTrader notion of “point” (e.g. 0.0001 for 5-digit FX quotes).
  • Position tracking uses two lists of PositionEntry objects to mirror MetaTrader’s per-ticket accounting. Entries are reduced FIFO-style when opposite trades close part of a basket.
  • All indicator calculations rely on StockSharp’s high-level binding API (SubscribeCandles().BindEx(...)). No manual calls to GetValue are required and indicators are never injected into Strategy.Indicators.
  • The strategy calls StartProtection() on start, allowing StockSharp to manage global risk-control modules (break-even, margin checks, etc.).
  • Because StockSharp consolidates positions net-by-direction, opposite positions are fully closed before new entries are evaluated. This keeps the implementation deterministic and closely aligned with the original EA behaviour.

Files

  • CS/SmartTrendFollowerStrategy.cs – C# implementation of the strategy using the StockSharp high-level API.
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>
/// Port of the "Smart Trend Follower" MetaTrader 5 expert advisor that combines moving average signals
/// with stochastic confirmation and a martingale-style layering engine.
/// </summary>
public class SmartTrendFollowerStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<SignalModes> _signalMode;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<decimal> _multiplier;
	private readonly StrategyParam<decimal> _layerDistancePips;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<int> _stochasticSlowing;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;

	private SimpleMovingAverage _fastSma;
	private SimpleMovingAverage _slowSma;
	private StochasticOscillator _stochastic;

	private readonly List<PositionEntry> _longEntries = new();
	private readonly List<PositionEntry> _shortEntries = new();

	private decimal? _prevFast;
	private decimal? _prevSlow;
	private decimal _pipSize;
	private bool _longExitRequested;
	private bool _shortExitRequested;

	/// <summary>
	/// Trading signal mode.
	/// </summary>
	public SignalModes SignalMode
	{
		get => _signalMode.Value;
		set => _signalMode.Value = value;
	}

	/// <summary>
	/// Base candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initial order volume expressed in lots.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the volume of every additional averaging order.
	/// </summary>
	public decimal Multiplier
	{
		get => _multiplier.Value;
		set => _multiplier.Value = value;
	}

	/// <summary>
	/// Distance in pips required before stacking another order in the same direction.
	/// </summary>
	public decimal LayerDistancePips
	{
		get => _layerDistancePips.Value;
		set => _layerDistancePips.Value = value;
	}

	/// <summary>
	/// Fast simple moving average period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow simple moving average period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

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

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

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

	/// <summary>
	/// Take-profit distance in pips relative to the average entry price.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips relative to the average entry price.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="SmartTrendFollowerStrategy"/>.
	/// </summary>
	public SmartTrendFollowerStrategy()
	{
		_signalMode = Param(nameof(SignalMode), SignalModes.CrossMa)
		.SetDisplay("Signal Mode", "Trading logic selection", "Signals");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe", "General");

		_initialVolume = Param(nameof(InitialVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Initial Volume", "Starting order volume in lots", "Money Management");

		_multiplier = Param(nameof(Multiplier), 2m)
		.SetNotNegative()
		.SetDisplay("Volume Multiplier", "Martingale multiplier applied to additional entries", "Money Management");

		_layerDistancePips = Param(nameof(LayerDistancePips), 200m)
		.SetNotNegative()
		.SetDisplay("Layer Distance", "Pip distance before adding another order", "Money Management");

		_fastPeriod = Param(nameof(FastPeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("Fast MA", "Fast moving average period", "Indicators")
		;

		_slowPeriod = Param(nameof(SlowPeriod), 28)
		.SetGreaterThanZero()
		.SetDisplay("Slow MA", "Slow moving average period", "Indicators")
		;

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 10)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %K", "%K lookback length", "Indicators");

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %D", "%D smoothing length", "Indicators");

		_stochasticSlowing = Param(nameof(StochasticSlowing), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic Slowing", "Extra smoothing for %K", "Indicators");

		_takeProfitPips = Param(nameof(TakeProfitPips), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit", "Target distance in pips", "Risk Management");

		_stopLossPips = Param(nameof(StopLossPips), 0m)
		.SetNotNegative()
		.SetDisplay("Stop Loss", "Protective distance in pips", "Risk Management");
	}

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

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

		_fastSma = null;
		_slowSma = null;
		_stochastic = null;

		_longEntries.Clear();
		_shortEntries.Clear();

		_prevFast = null;
		_prevSlow = null;
		_pipSize = 0m;
		_longExitRequested = false;
		_shortExitRequested = false;
	}

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

		_fastSma = new SimpleMovingAverage { Length = Math.Max(1, FastPeriod) };
		_slowSma = new SimpleMovingAverage { Length = Math.Max(1, SlowPeriod) };
		_stochastic = new StochasticOscillator();
		_stochastic.K.Length = Math.Max(1, StochasticKPeriod);
		_stochastic.D.Length = Math.Max(1, StochasticDPeriod);

		var subscription = SubscribeCandles(CandleType);
		subscription
		.BindEx(_fastSma, _slowSma, _stochastic, ProcessCandle)
		.Start();

		_pipSize = CalculatePipSize();

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

		// protection managed manually via ManageExits
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var price = trade.Trade.Price;
		var volume = trade.Trade.Volume;

		if (trade.Order.Side == Sides.Buy)
		{
			ReduceEntries(_shortEntries, ref volume);

			if (volume > 0m)
			{
				_longEntries.Add(new PositionEntry(price, volume));
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			ReduceEntries(_longEntries, ref volume);

			if (volume > 0m)
			{
				_shortEntries.Add(new PositionEntry(price, volume));
			}
		}

		if (GetTotalVolume(_longEntries) <= 0m)
		{
			_longEntries.Clear();
			_longExitRequested = false;
		}

		if (GetTotalVolume(_shortEntries) <= 0m)
		{
			_shortEntries.Clear();
			_shortExitRequested = false;
		}
	}

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

		var fast = fastValue.ToDecimal();
		var slow = slowValue.ToDecimal();

		ManageExits(candle);

		var signal = SignalDirections.None;

		if (SignalMode == SignalModes.CrossMa)
		{
			if (_prevFast.HasValue && _prevSlow.HasValue)
			{
				var crossBuy = fast < slow && _prevSlow.Value < _prevFast.Value;
				var crossSell = fast > slow && _prevSlow.Value > _prevFast.Value;

				if (crossBuy)
					signal = SignalDirections.Buy;
				else if (crossSell)
					signal = SignalDirections.Sell;
			}
		}
		else if (_stochastic?.IsFormed == true)
		{
			var kValue = stochasticValue.ToDecimal();
			var bullish = candle.ClosePrice > candle.OpenPrice;
			var bearish = candle.ClosePrice < candle.OpenPrice;

			if (fast > slow && bullish && kValue <= 30m)
				signal = SignalDirections.Buy;
			else if (fast < slow && bearish && kValue >= 70m)
				signal = SignalDirections.Sell;
		}

		if (signal != SignalDirections.None)
		{
			ProcessSignal(signal, candle.ClosePrice);
		}

		_prevFast = fast;
		_prevSlow = slow;
	}

	private void ProcessSignal(SignalDirections signal, decimal referencePrice)
	{
		switch (signal)
		{
			case SignalDirections.Buy:
			{
				var shortVolume = GetTotalVolume(_shortEntries);
				if (shortVolume > 0m)
				{
					if (!_shortExitRequested)
					{
						_shortExitRequested = true;
						BuyMarket(shortVolume);
					}
					return;
				}

				var longCount = _longEntries.Count;
				var requested = CalculateRequestedVolume(longCount);
				var volume = PrepareNextVolume(requested);
				if (volume <= 0m)
					return;

				if (longCount == 0)
				{
					BuyMarket(volume);
					return;
				}

				var lowest = GetExtremePrice(_longEntries, true);
				var threshold = lowest - LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);

				if (referencePrice <= threshold)
				{
					BuyMarket(volume);
				}

				break;
			}
			case SignalDirections.Sell:
			{
				var longVolume = GetTotalVolume(_longEntries);
				if (longVolume > 0m)
				{
					if (!_longExitRequested)
					{
						_longExitRequested = true;
						SellMarket(longVolume);
					}
					return;
				}

				var shortCount = _shortEntries.Count;
				var requested = CalculateRequestedVolume(shortCount);
				var volume = PrepareNextVolume(requested);
				if (volume <= 0m)
					return;

				if (shortCount == 0)
				{
					SellMarket(volume);
					return;
				}

				var highest = GetExtremePrice(_shortEntries, false);
				var threshold = highest + LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);

				if (referencePrice >= threshold)
				{
					SellMarket(volume);
				}

				break;
			}
		}
	}

	private void ManageExits(ICandleMessage candle)
	{
		var longVolume = GetTotalVolume(_longEntries);
		if (longVolume > 0m && !_longExitRequested)
		{
			var average = GetAveragePrice(_longEntries);
			var takeProfit = TakeProfitPips > 0m ? average + TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
			var stopLoss = StopLossPips > 0m ? average - StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;

			if (takeProfit.HasValue && candle.HighPrice >= takeProfit.Value)
			{
				_longExitRequested = true;
				SellMarket(longVolume);
				return;
			}

			if (stopLoss.HasValue && candle.LowPrice <= stopLoss.Value)
			{
				_longExitRequested = true;
				SellMarket(longVolume);
				return;
			}
		}

		var shortVolume = GetTotalVolume(_shortEntries);
		if (shortVolume > 0m && !_shortExitRequested)
		{
			var average = GetAveragePrice(_shortEntries);
			var takeProfit = TakeProfitPips > 0m ? average - TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
			var stopLoss = StopLossPips > 0m ? average + StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;

			if (takeProfit.HasValue && candle.LowPrice <= takeProfit.Value)
			{
				_shortExitRequested = true;
				BuyMarket(shortVolume);
				return;
			}

			if (stopLoss.HasValue && candle.HighPrice >= stopLoss.Value)
			{
				_shortExitRequested = true;
				BuyMarket(shortVolume);
			}
		}
	}

	private decimal CalculateRequestedVolume(int existingCount)
	{
		if (InitialVolume <= 0m)
			return 0m;

		var result = InitialVolume;

		if (existingCount > 0 && Multiplier > 0m)
		{
			result *= (decimal)Math.Pow((double)Math.Max(Multiplier, 1m), existingCount);
		}

		return result;
	}

	private decimal PrepareNextVolume(decimal requested)
	{
		if (requested <= 0m)
			return 0m;

		var security = Security;
		if (security == null)
			return requested;

		var step = security.VolumeStep ?? 0m;
		if (step > 0m)
		{
			requested = step * Math.Round(requested / step, MidpointRounding.AwayFromZero);
		}

		var min = security.MinVolume ?? 0m;
		if (min > 0m && requested < min)
			return 0m;

		var max = security.MaxVolume ?? decimal.MaxValue;
		if (requested > max)
		{
			requested = max;
		}

		return requested;
	}

	private void ReduceEntries(List<PositionEntry> entries, ref decimal volume)
	{
		var index = 0;
		while (volume > 0m && index < entries.Count)
		{
			var entry = entries[index];
			if (volume >= entry.Volume)
			{
				volume -= entry.Volume;
				entries.RemoveAt(index);
			}
			else
			{
				entry.Volume -= volume;
				volume = 0m;
				entries[index] = entry;
			}
		}
	}

	private static decimal GetTotalVolume(List<PositionEntry> entries)
	{
		var total = 0m;
		for (var i = 0; i < entries.Count; i++)
			total += entries[i].Volume;
		return total;
	}

	private static decimal GetAveragePrice(List<PositionEntry> entries)
	{
		var totalVolume = GetTotalVolume(entries);
		if (totalVolume <= 0m)
			return 0m;

		var weighted = 0m;
		for (var i = 0; i < entries.Count; i++)
			weighted += entries[i].Price * entries[i].Volume;

		return weighted / totalVolume;
	}

	private static decimal GetExtremePrice(List<PositionEntry> entries, bool forLong)
	{
		if (entries.Count == 0)
			return 0m;

		var extreme = entries[0].Price;
		for (var i = 1; i < entries.Count; i++)
		{
			var price = entries[i].Price;
			if (forLong)
			{
				if (price < extreme)
					extreme = price;
			}
			else if (price > extreme)
			{
				extreme = price;
			}
		}

		return extreme;
	}

	private decimal CalculatePipSize()
	{
		var security = Security;
		if (security == null)
			return 0m;

		var step = security.PriceStep ?? 0m;
		if (step <= 0m)
			return 0m;

		var decimals = security.Decimals;
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private enum SignalDirections
	{
		None,
		Buy,
		Sell
	}

	/// <summary>
	/// Signal selector for the strategy.
	/// </summary>
	public enum SignalModes
	{
		/// <summary>
		/// Use moving average crossovers in a contrarian fashion.
		/// </summary>
		CrossMa,

		/// <summary>
		/// Follow trend direction using moving averages with stochastic confirmation.
		/// </summary>
		Trend
	}

	private sealed class PositionEntry
	{
		public PositionEntry(decimal price, decimal volume)
		{
			Price = price;
			Volume = volume;
		}

		public decimal Price { get; }

		public decimal Volume { get; set; }
	}
}