View on GitHub

MA MACD Position Averaging

This strategy is a faithful conversion of the MetaTrader expert advisor "MA MACD Position averaging". It combines a weighted moving average filter with a MACD ratio check and adds a martingale-style averaging module that increases the position size whenever price moves adversely by a configurable number of pips. All risk parameters are configured in pip units and internally converted to price offsets using the instrument metadata provided by StockSharp.

Trading Logic

  1. Indicator preparation
    • A configurable moving average (MaPeriod, MaMethod, MaAppliedPrice) is sampled on completed candles. The SignalBar and MaShift parameters emulate MetaTrader's ability to look back by a given number of bars and to plot the moving average with a horizontal offset.
    • A MACD indicator (MacdFastPeriod, MacdSlowPeriod, MacdSignalPeriod, MacdAppliedPrice) is processed on the same candles. The strategy stores the MACD main and signal lines in a small rolling buffer so that historical values can be accessed without calling indicator APIs directly.
  2. Entry conditions
    • Long: both MACD lines are below zero, the ratio MACDmain / MACDsignal is greater than or equal to MacdRatio, the candle close is above the sampled moving average and the distance between price and the average is at least IndentPips pips.
    • Short: both MACD lines are above zero, the ratio is above MacdRatio, the candle close is below the moving average and the distance between them is at least IndentPips pips.
    • New entries are only allowed when the strategy has no exposure. When an averaging cycle is already in progress, the signal logic is skipped and only the averaging rules apply.
  3. Averaging module
    • When a long position exists and price moves down by at least StepLossingPips from the best (lowest) long entry, the strategy opens an additional long trade whose volume equals the last leg volume multiplied by LotCoefficient (rounded by the instrument volume step).
    • When a short position exists and price moves up by at least StepLossingPips from the best (highest) short entry, a new short leg is added using the same LotCoefficient multiplier.
    • If exposure is detected in both directions (should never happen under normal conditions) the strategy immediately closes every leg to restore consistency.
  4. Protective exits
    • Each leg stores individual stop-loss and take-profit levels expressed in price units (StopLossPips, TakeProfitPips). On every finished candle the strategy checks whether the candle range crossed any of the stored levels and, if so, exits the leg with a market order.
    • A trailing stop (TrailingStopPips, TrailingStepPips) is optional. Once price advances in favour of a leg by TrailingStopPips + TrailingStepPips, the stop is moved to TrailingStopPips pips behind the current close. The stop only tightens if price makes additional progress of at least TrailingStepPips pips.
  5. Housekeeping
    • Volume commands are aligned to the instrument volume step and clipped to the allowed minimum/maximum. The strategy executes only on fully formed candles (CandleStates.Finished) to avoid double processing.

Parameters

Parameter Type Default Description
CandleType DataType TimeSpan.FromHours(1).TimeFrame() Timeframe used for indicator calculations.
OrderVolume decimal 0.1 Base lot size for the initial entry.
StopLossPips int 50 Stop-loss distance in pips (0 disables the stop).
TakeProfitPips int 50 Take-profit distance in pips (0 disables the target).
TrailingStopPips int 5 Trailing stop offset in pips. Must be positive to enable trailing.
TrailingStepPips int 5 Extra pip distance required before the trailing stop moves again.
StepLossingPips int 30 Price setback in pips that triggers a new averaging leg.
LotCoefficient decimal 2.0 Multiplier applied to the previous leg volume when averaging.
SignalBar int 0 Number of completed bars to look back when sampling indicators.
MaPeriod int 15 Moving average length in bars.
MaShift int 0 Horizontal shift (in bars) applied to the moving average values.
MaMethod MovingAverageMethod Weighted Moving average smoothing algorithm (simple, exponential, smoothed, weighted).
MaAppliedPrice AppliedPriceType Weighted Candle price used as input for the moving average.
IndentPips int 4 Minimum pip gap required between price and the moving average before entering.
MacdFastPeriod int 12 Fast EMA length of the MACD filter.
MacdSlowPeriod int 26 Slow EMA length of the MACD filter.
MacdSignalPeriod int 9 Signal line length of the MACD filter.
MacdAppliedPrice AppliedPriceType Weighted Applied price used for the MACD calculation.
MacdRatio decimal 0.9 Minimum MACD main/signal ratio required to allow trading.

Pip conversion

All pip-based settings (StopLossPips, TakeProfitPips, TrailingStopPips, TrailingStepPips, StepLossingPips, IndentPips) are multiplied by the security PriceStep. When the instrument has 3 or 5 decimal places the value is further multiplied by 10 to reproduce MetaTrader's "pip" definition for fractional quotes. If no price step is available a fallback of 0.0001 is used.

Implementation Notes

  • The strategy keeps an internal list of position legs because StockSharp operates in netting mode. Each leg tracks its own entry price, stop and take levels so that averaging behaves like the original MetaTrader EA.
  • Protective orders are simulated in software: when a candle touches a stop-loss or take-profit level the position is closed with a market order on that bar.
  • Averaging is disabled automatically when StepLossingPips is zero. Otherwise, every additional leg uses the previous leg volume multiplied by LotCoefficient and rounded down to the nearest volume step.
  • Trailing stop updates use the candle close as the current price proxy. The stop never moves in the adverse direction and remains inactive until the price progress exceeds TrailingStopPips + TrailingStepPips.
  • Indicator buffers honour the SignalBar and MaShift offsets so the decision logic sees exactly the same values that the MetaTrader expert would obtain from its indicator buffers.
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;

public enum MaMacdMovingAverageMethod
{
	Simple,
	Exponential,
	Smoothed,
	Weighted,
}

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

/// <summary>
/// MA + MACD position averaging strategy.
/// </summary>
public class MaMacdPositionAveragingStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _stepLossingPips;
	private readonly StrategyParam<decimal> _lotCoefficient;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MaMacdMovingAverageMethod> _maMethod;
	private readonly StrategyParam<MaMacdAppliedPrice> _maAppliedPrice;
	private readonly StrategyParam<int> _indentPips;
	private readonly StrategyParam<int> _macdFastPeriod;
	private readonly StrategyParam<int> _macdSlowPeriod;
	private readonly StrategyParam<int> _macdSignalPeriod;
	private readonly StrategyParam<MaMacdAppliedPrice> _macdAppliedPrice;
	private readonly StrategyParam<decimal> _macdRatio;

	private readonly List<decimal> _maInputs = [];
	private readonly List<decimal> _maValues = [];
	private readonly List<(decimal Main, decimal Signal)> _macdValues = [];
	private readonly List<Leg> _legs = [];

	private decimal? _macdFast;
	private decimal? _macdSlow;
	private decimal? _macdSignal;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.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 StepLossingPips { get => _stepLossingPips.Value; set => _stepLossingPips.Value = value; }
	public decimal LotCoefficient { get => _lotCoefficient.Value; set => _lotCoefficient.Value = value; }
	public int SignalBar { get => _signalBar.Value; set => _signalBar.Value = value; }
	public int MaPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
	public int MaShift { get => _maShift.Value; set => _maShift.Value = value; }
	public MaMacdMovingAverageMethod MaMethod { get => _maMethod.Value; set => _maMethod.Value = value; }
	public MaMacdAppliedPrice MaAppliedPrice { get => _maAppliedPrice.Value; set => _maAppliedPrice.Value = value; }
	public int IndentPips { get => _indentPips.Value; set => _indentPips.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 MaMacdAppliedPrice MacdAppliedPrice { get => _macdAppliedPrice.Value; set => _macdAppliedPrice.Value = value; }
	public decimal MacdRatio { get => _macdRatio.Value; set => _macdRatio.Value = value; }

	public MaMacdPositionAveragingStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame());
		_orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero();
		_stopLossPips = Param(nameof(StopLossPips), 50).SetNotNegative();
		_takeProfitPips = Param(nameof(TakeProfitPips), 50).SetNotNegative();
		_trailingStopPips = Param(nameof(TrailingStopPips), 5).SetNotNegative();
		_trailingStepPips = Param(nameof(TrailingStepPips), 5).SetNotNegative();
		_stepLossingPips = Param(nameof(StepLossingPips), 30).SetNotNegative();
		_lotCoefficient = Param(nameof(LotCoefficient), 2m).SetGreaterThanZero();
		_signalBar = Param(nameof(SignalBar), 0).SetNotNegative();
		_maPeriod = Param(nameof(MaPeriod), 15).SetGreaterThanZero();
		_maShift = Param(nameof(MaShift), 0).SetNotNegative();
		_maMethod = Param(nameof(MaMethod), MaMacdMovingAverageMethod.Weighted);
		_maAppliedPrice = Param(nameof(MaAppliedPrice), MaMacdAppliedPrice.Weighted);
		_indentPips = Param(nameof(IndentPips), 4).SetNotNegative();
		_macdFastPeriod = Param(nameof(MacdFastPeriod), 12).SetGreaterThanZero();
		_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 26).SetGreaterThanZero();
		_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 9).SetGreaterThanZero();
		_macdAppliedPrice = Param(nameof(MacdAppliedPrice), MaMacdAppliedPrice.Weighted);
		_macdRatio = Param(nameof(MacdRatio), 0.9m).SetNotNegative();
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_maInputs.Clear();
		_maValues.Clear();
		_macdValues.Clear();
		_legs.Clear();
		_macdFast = null;
		_macdSlow = null;
		_macdSignal = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

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

		UpdateIndicators(candle);

		if (_legs.Select(l => l.Side).Distinct().Count() > 1)
		{
			FlattenAll();
			return;
		}

		ApplyProtection(candle);

		if (_legs.Count > 0)
		{
			TryAverage(candle.ClosePrice, candle.OpenTime);
			return;
		}

		if (Position != 0)
			return;

		var maIndex = _maValues.Count - 1 - SignalBar - MaShift;
		var macdIndex = _macdValues.Count - 1 - SignalBar;

		if (maIndex < 0 || macdIndex < 0)
			return;

		var ma = _maValues[maIndex];
		var (main, signal) = _macdValues[macdIndex];

		if (signal == 0m)
			return;

		var ratio = main / signal;
		var pip = GetPipSize();
		var indent = IndentPips * pip;
		var close = candle.ClosePrice;

		if (main < 0m && signal < 0m && ratio >= MacdRatio && close > ma && close - ma >= indent)
			AddLeg(Sides.Buy, NormalizeVolume(OrderVolume), close, candle.OpenTime);
		else if (main > 0m && signal > 0m && ratio >= MacdRatio && close < ma && ma - close >= indent)
			AddLeg(Sides.Sell, NormalizeVolume(OrderVolume), close, candle.OpenTime);
	}

	private void UpdateIndicators(ICandleMessage candle)
	{
		var maPrice = GetAppliedPrice(candle, MaAppliedPrice);
		_maInputs.Add(maPrice);
		_maValues.Add(CalculateMa(_maInputs, MaPeriod, MaMethod));

		var macdPrice = GetAppliedPrice(candle, MacdAppliedPrice);
		_macdFast = Ema(_macdFast, macdPrice, MacdFastPeriod, 2m);
		_macdSlow = Ema(_macdSlow, macdPrice, MacdSlowPeriod, 2m);
		var main = _macdFast.Value - _macdSlow.Value;
		_macdSignal = Ema(_macdSignal, main, MacdSignalPeriod, 2m);
		_macdValues.Add((main, _macdSignal.Value));

		var keep = Math.Max(MaPeriod + MaShift + SignalBar + 10, MacdSlowPeriod + MacdSignalPeriod + SignalBar + 10);
		if (_maInputs.Count > keep)
		{
			var remove = _maInputs.Count - keep;
			_maInputs.RemoveRange(0, remove);
			_maValues.RemoveRange(0, remove);
			_macdValues.RemoveRange(0, remove);
		}
	}

	private void TryAverage(decimal close, DateTimeOffset candleTime)
	{
		if (StepLossingPips <= 0 || _legs.Count == 0)
			return;

		var side = _legs[0].Side;
		if (_legs.Any(l => l.Side != side))
			return;

		var pip = GetPipSize();
		var distance = StepLossingPips * pip;
		var lastVolume = _legs[^1].Volume;
		var volume = NormalizeVolume(lastVolume * LotCoefficient);

		if (side == Sides.Buy)
		{
			var bestEntry = _legs.Min(l => l.EntryPrice);
			if (close <= bestEntry - distance)
				AddLeg(Sides.Buy, volume, close, candleTime);
		}
		else
		{
			var bestEntry = _legs.Max(l => l.EntryPrice);
			if (close >= bestEntry + distance)
				AddLeg(Sides.Sell, volume, close, candleTime);
		}
	}

	private void ApplyProtection(ICandleMessage candle)
	{
		if (_legs.Count == 0)
			return;

		var pip = GetPipSize();
		var exitBuy = 0m;
		var exitSell = 0m;

		for (var i = _legs.Count - 1; i >= 0; i--)
		{
			var leg = _legs[i];
			if (candle.OpenTime <= leg.EntryTime)
				continue;

			var stopPrice = leg.StopPrice;
			var takePrice = leg.TakePrice;
			var hit = leg.Side == Sides.Buy
				? (stopPrice is decimal stop && candle.LowPrice <= stop) || (takePrice is decimal take && candle.HighPrice >= take)
				: (stopPrice is decimal shortStop && candle.HighPrice >= shortStop) || (takePrice is decimal shortTake && candle.LowPrice <= shortTake);

			if (!hit)
			{
				// A stop moved on this close only guards the candles that follow.
				UpdateTrailing(leg, candle.ClosePrice, pip);
				continue;
			}

			if (leg.Side == Sides.Buy)
				exitSell += leg.Volume;
			else
				exitBuy += leg.Volume;

			_legs.RemoveAt(i);
		}

		if (exitSell > 0m)
			SellMarket(exitSell);
		if (exitBuy > 0m)
			BuyMarket(exitBuy);
	}

	private void UpdateTrailing(Leg leg, decimal close, decimal pip)
	{
		if (TrailingStopPips <= 0 || TrailingStepPips < 0)
			return;

		var trail = TrailingStopPips * pip;
		var step = TrailingStepPips * pip;

		if (leg.Side == Sides.Buy)
		{
			if (close - leg.EntryPrice < trail + step)
				return;

			var candidate = close - trail;
			if (leg.StopPrice is null || candidate >= leg.StopPrice.Value + step)
				leg.StopPrice = candidate;
		}
		else
		{
			if (leg.EntryPrice - close < trail + step)
				return;

			var candidate = close + trail;
			if (leg.StopPrice is null || candidate <= leg.StopPrice.Value - step)
				leg.StopPrice = candidate;
		}
	}

	private void AddLeg(Sides side, decimal volume, decimal price, DateTimeOffset entryTime)
	{
		if (volume <= 0m)
			return;

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

		var pip = GetPipSize();
		var stopDistance = StopLossPips * pip;
		var takeDistance = TakeProfitPips * pip;

		_legs.Add(new Leg
		{
			Side = side,
			Volume = volume,
			EntryPrice = price,
			EntryTime = entryTime,
			StopPrice = StopLossPips > 0 ? side == Sides.Buy ? price - stopDistance : price + stopDistance : null,
			TakePrice = TakeProfitPips > 0 ? side == Sides.Buy ? price + takeDistance : price - takeDistance : null,
		});
	}

	private void FlattenAll()
	{
		if (Position > 0)
			SellMarket(Math.Abs(Position));
		else if (Position < 0)
			BuyMarket(Math.Abs(Position));

		_legs.Clear();
	}

	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 GetAppliedPrice(ICandleMessage candle, MaMacdAppliedPrice type)
		=> type switch
		{
			MaMacdAppliedPrice.Open => candle.OpenPrice,
			MaMacdAppliedPrice.High => candle.HighPrice,
			MaMacdAppliedPrice.Low => candle.LowPrice,
			MaMacdAppliedPrice.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			MaMacdAppliedPrice.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			MaMacdAppliedPrice.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};

	private static decimal CalculateMa(List<decimal> values, int period, MaMacdMovingAverageMethod method)
	{
		var count = Math.Min(period, values.Count);
		var start = values.Count - count;

		if (method == MaMacdMovingAverageMethod.Weighted)
		{
			var weighted = 0m;
			var weights = 0m;
			for (var i = 0; i < count; i++)
			{
				var weight = i + 1m;
				weighted += values[start + i] * weight;
				weights += weight;
			}
			return weighted / weights;
		}

		if (method == MaMacdMovingAverageMethod.Simple)
			return values.Skip(start).Average();

		var alpha = method == MaMacdMovingAverageMethod.Smoothed ? 1m / period : 2m / (period + 1m);
		var result = values[start];
		for (var i = start + 1; i < values.Count; i++)
			result += alpha * (values[i] - result);
		return result;
	}

	private static decimal Ema(decimal? previous, decimal value, int period, decimal numerator)
	{
		if (previous is null)
			return value;

		var alpha = numerator / (period + 1m);
		return previous.Value + alpha * (value - previous.Value);
	}

	private sealed class Leg
	{
		public Sides Side { get; init; }
		public decimal Volume { get; init; }
		public decimal EntryPrice { get; init; }
		public DateTimeOffset EntryTime { get; init; }
		public decimal? StopPrice { get; set; }
		public decimal? TakePrice { get; init; }
	}
}