Auf GitHub ansehen

Smoothing Average Crossover Strategy

Overview

The Smoothing Average Crossover strategy replicates the logic of the original Smoothing Average (barabashkakvn's edition) MQL5 Expert Advisor. It combines a configurable moving average with a price distance filter measured in pips. When the market moves far enough away from the smoothed average, the strategy opens a position in the direction of the move (or the opposite side if reversal mode is enabled). Positions are closed once price reverts through an expanded channel around the moving average.

Trading Logic

Default mode (ReverseSignals = false)

  • Entry long: the close price rises above the moving average minus Entry Delta (pips).
  • Entry short: the close price falls below the moving average plus Entry Delta (pips).
  • Exit short: the close price climbs above the moving average plus Entry Delta (pips) × Close Delta Coefficient.
  • Exit long: the close price drops below the moving average minus Entry Delta (pips) × Close Delta Coefficient.

Reverse mode (ReverseSignals = true)

  • Entry long: the close price falls below the moving average plus Entry Delta (pips).
  • Entry short: the close price rises above the moving average minus Entry Delta (pips).
  • Exit long: the close price drops below the moving average minus Entry Delta (pips) × Close Delta Coefficient.
  • Exit short: the close price climbs above the moving average plus Entry Delta (pips) × Close Delta Coefficient.

The moving average can be shifted forward by several candles. The strategy emulates this behaviour by keeping a small buffer of the most recent indicator values and using the value from MaShift bars ago. This matches the shifted line produced by the original MetaTrader implementation.

Parameters

  • Candle Type – data series used for calculations.
  • MA Length – period of the smoothing average.
  • MA Shift – number of bars that the moving average is shifted forward.
  • MA Type – moving average method (simple, exponential, smoothed, or linear weighted).
  • Price Source – candle price fed into the moving average (default: typical price).
  • Entry Delta (pips) – distance from the moving average required to trigger entries. Converted to price using the instrument pip size.
  • Close Delta Coefficient – multiplier applied to the entry delta when checking exit conditions.
  • Reverse Signals – inverts long/short entry logic.
  • Trade Volume – order size used for both long and short entries.

Risk Management

  • Orders are sent with the fixed Trade Volume parameter. The strategy does not scale in while a position is open.
  • All exits are rule-based. No hard stop-loss or take-profit orders are submitted, but StartProtection() is invoked to enable the platform-level safety net.
  • Reverse mode is available for counter-trend behaviour without altering other settings.

Implementation Notes

  • The pip size is derived from Security.PriceStep. Three- or five-digit FX symbols receive the same 10× adjustment as in the MQL5 code.
  • The moving average uses the Price Source selection so that typical, median, or other candle prices can be matched to the original EA settings.
  • Entry and exit comparisons use the candle close as a stable proxy for bid/ask checks in the source Expert Advisor.
  • All comments inside the C# code are provided in English, as required by the conversion guidelines.
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>
/// Smoothing Average strategy converted from MQL5.
/// Opens trades when price moves away from the moving average by a configurable delta.
/// Supports reversing the signals and shifting the moving average output.
/// </summary>
public class SmoothingAverageCrossoverStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MovingAverageKinds> _maType;
	private readonly StrategyParam<CandlePrices> _priceSource;
	private readonly StrategyParam<decimal> _entryDeltaPips;
	private readonly StrategyParam<decimal> _closeDeltaCoefficient;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<decimal> _tradeVolume;

	private readonly Queue<decimal> _maShiftBuffer = new();

	private decimal _entryDelta;
	private decimal _closeDelta;

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
        public SmoothingAverageCrossoverStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "General");

		_maLength = Param(nameof(MaLength), 60)
			.SetGreaterThanZero()
			.SetDisplay("MA Length", "Period of the smoothing average", "Moving Average");

		_maShift = Param(nameof(MaShift), 3)
			.SetNotNegative()
			.SetDisplay("MA Shift", "Horizontal shift applied to the average", "Moving Average");

		_maType = Param(nameof(MaType), MovingAverageKinds.Simple)
			.SetDisplay("MA Type", "Type of smoothing applied", "Moving Average");

		_priceSource = Param(nameof(PriceSource), CandlePrices.Typical)
			.SetDisplay("Price Source", "Price used for the moving average", "Moving Average");

		_entryDeltaPips = Param(nameof(EntryDeltaPips), 60m)
			.SetNotNegative()
			.SetDisplay("Entry Delta (pips)", "Distance from MA to trigger entries", "Trading Rules");

		_closeDeltaCoefficient = Param(nameof(CloseDeltaCoefficient), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Close Delta Coefficient", "Multiplier applied to entry delta for exits", "Trading Rules");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert long and short logic", "Trading Rules");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Order volume for each entry", "Risk");
	}

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

	/// <summary>
	/// Moving average period.
	/// </summary>
	public int MaLength
	{
		get => _maLength.Value;
		set => _maLength.Value = value;
	}

	/// <summary>
	/// Number of candles used to shift the moving average output.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average type.
	/// </summary>
	public MovingAverageKinds MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Candle price source for the moving average.
	/// </summary>
	public CandlePrices PriceSource
	{
		get => _priceSource.Value;
		set => _priceSource.Value = value;
	}

	/// <summary>
	/// Delta in pip units used to open new positions.
	/// </summary>
	public decimal EntryDeltaPips
	{
		get => _entryDeltaPips.Value;
		set => _entryDeltaPips.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the entry delta when evaluating exits.
	/// </summary>
	public decimal CloseDeltaCoefficient
	{
		get => _closeDeltaCoefficient.Value;
		set => _closeDeltaCoefficient.Value = value;
	}

	/// <summary>
	/// If true, swaps long and short signals.
	/// </summary>
	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	/// <summary>
	/// Volume sent with market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

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

		_maShiftBuffer.Clear();
		_entryDelta = 0m;
		_closeDelta = 0m;
	}

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

		// Sync the base strategy volume with the parameter value.
		Volume = TradeVolume;

		// Calculate pip-based offsets once at the start to avoid repeated computations.
		_entryDelta = CalculateEntryDelta();
		_closeDelta = _entryDelta * CloseDeltaCoefficient;

		var movingAverage = CreateMovingAverage(MaType, MaLength);
		//movingAverage.CandlePrice = PriceSource;

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

		// Enable built-in protection helpers (no additional parameters required).
		StartProtection(null, null);
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var shiftedMa = ApplyShift(maValue);

		// Use candle close as a proxy for bid/ask checks from the original Expert Advisor.
		var askPrice = candle.ClosePrice;
		var bidPrice = candle.ClosePrice;

		var entryUpper = shiftedMa + _entryDelta;
		var entryLower = shiftedMa - _entryDelta;
		var closeUpper = shiftedMa + _closeDelta;
		var closeLower = shiftedMa - _closeDelta;

		if (Position == 0m)
		{
			if (!ReverseSignals)
			{
				if (askPrice > entryUpper)
				{
					OpenLong();
					return;
				}

				if (bidPrice < entryLower)
				{
					OpenShort();
					return;
				}
			}
			else
			{
				if (askPrice > entryUpper)
				{
					OpenShort();
					return;
				}

				if (bidPrice < entryLower)
				{
					OpenLong();
					return;
				}
			}
		}
		else
		{
			if (!ReverseSignals)
			{
				if (Position < 0m && bidPrice > closeUpper)
					CloseShort();

				if (Position > 0m && askPrice < closeLower)
					CloseLong();
			}
			else
			{
				if (Position > 0m && askPrice < closeLower)
					CloseLong();

				if (Position < 0m && bidPrice > closeUpper)
					CloseShort();
			}
		}
	}

	private decimal ApplyShift(decimal currentValue)
	{
		if (MaShift <= 0)
			return currentValue;

		var shifted = _maShiftBuffer.Count < MaShift ? currentValue : _maShiftBuffer.Peek();

		_maShiftBuffer.Enqueue(currentValue);

		if (_maShiftBuffer.Count > MaShift)
			_maShiftBuffer.Dequeue();

		return shifted;
	}

	private decimal CalculateEntryDelta()
	{
		var pip = CalculatePipSize();
		return pip * EntryDeltaPips;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 0.0001m;

		var digits = (int)Math.Round(Math.Log10((double)(1m / step)));
		return digits == 3 || digits == 5 ? step * 10m : step;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MovingAverageKinds type, int length)
	{
		return type switch
		{
			MovingAverageKinds.Simple => new SMA { Length = length },
			MovingAverageKinds.Exponential => new EMA { Length = length },
			MovingAverageKinds.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageKinds.LinearWeighted => new WeightedMovingAverage { Length = length },
			_ => new SMA { Length = length }
		};
	}

	private void OpenLong()
	{
		var volume = TradeVolume + Math.Max(0m, -Position);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
	}

	private void OpenShort()
	{
		var volume = TradeVolume + Math.Max(0m, Position);
		if (volume <= 0m)
			return;

		SellMarket(volume);
	}

	private void CloseLong()
	{
		if (Position <= 0m)
			return;

		SellMarket(Position);
	}

	private void CloseShort()
	{
		if (Position >= 0m)
			return;

		BuyMarket(Math.Abs(Position));
	}

	/// <summary>
	/// Supported moving average types replicating the MQL5 enumeration.
	/// </summary>
	public enum MovingAverageKinds
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	public enum CandlePrices
	{
		Open,
		Close,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
}