MA MACD Position Averaging v2 Strategy
Overview
The MA MACD Position Averaging v2 Strategy is a direct translation of Vladimir Karputov's MetaTrader expert advisor. It combines a weighted moving average filter, a MACD confirmation block, and an averaging module that increases exposure when existing trades move against the position. The StockSharp version keeps the original signal hierarchy, processes indicators on finished candles, and manages protective logic (stop loss, take profit, trailing) in code to reproduce broker-side behaviour from MQL.
Trading Logic
- Indicator preparation
- A configurable moving average calculates on the selected candle type and price component. The
MaShift parameter emulates MetaTrader's forward shift by reading values from older candles, while BarOffset lets you evaluate the current or a previous bar.
- A MACD signal indicator produces the main and signal lines using customisable fast, slow, and signal periods and an applied price matching the original expert advisor.
- Signal validation
- Long setups require both MACD lines to be negative, the price to sit above the shifted moving average, and the price distance to the average to exceed
MaIndentPips (converted to absolute price using the instrument pip size).
- Short setups mirror the conditions: both MACD lines must be positive, price must stay below the shifted moving average, and the gap to the average must be at least
MaIndentPips.
- The ratio filter
MacdRatio enforces MACD_main / MACD_signal >= MacdRatio (using absolute decimal division) before a trade is allowed.
- When
ReverseSignals = true the direction of the market order is inverted after all conditions pass.
- Position lifecycle
- If no position exists, the strategy opens a market order with the configured
OrderVolume (rounded by the security volume step) in the computed direction. Stop-loss and take-profit levels are applied immediately according to StopLossPips and TakeProfitPips.
- If an exposure already exists, the strategy never opens the opposite side. Instead it either:
- Closes everything if longs and shorts are detected simultaneously (safety net mirroring the MQL check), or
- Invokes the averaging block for the current side.
- Averaging module
- For longs the algorithm finds the lowest-priced open leg whose unrealised loss exceeds
StepLossPips. For shorts it selects the highest-priced losing leg.
- Once a candidate is found a new market order is sent with volume
CandidateVolume × LotCoefficient (after adjusting to the allowed volume step/min/max). This reproduces the geometric progression from the original expert.
- New legs inherit the same stop-loss and take-profit distances and become eligible for trailing updates.
- Risk controls
- A trailing stop activates only when both
TrailingStopPips and TrailingStepPips are greater than zero. For longs the stop moves to Close - TrailingStopPips once profit exceeds TrailingStopPips + TrailingStepPips; shorts behave symmetrically.
- Manual stop-loss and take-profit checks are performed on each finished candle. When triggered, a market order closes the exact leg and removes it from the averaging list.
Parameters
| Parameter |
Description |
| OrderVolume |
Base volume for the very first trade in a cycle. |
| StopLossPips |
Stop-loss distance in pips. Set to zero to disable the stop. |
| TakeProfitPips |
Take-profit distance in pips. Set to zero to disable the target. |
| TrailingStopPips |
Distance between price and the trailing stop. Works together with TrailingStepPips. |
| TrailingStepPips |
Additional favourable move required before the trailing stop is updated. |
| StepLossPips |
Minimal loss (in pips) required before an averaging leg is added. |
| LotCoefficient |
Multiplier applied to the selected losing leg volume when averaging. |
| BarOffset |
Number of bars back to read indicator values (0 = current finished bar). |
| ReverseSignals |
Inverts long/short execution while keeping the same filters. |
| MaPeriod |
Moving average period. |
| MaShift |
Forward shift applied to the moving average (MetaTrader style). |
| MaMethod |
Moving average smoothing method (Simple, Exponential, Smoothed, Weighted). |
| MaPrice |
Candle price component used for the moving average. |
| MaIndentPips |
Minimal price distance from the moving average before entering. |
| MacdFastPeriod |
Fast EMA period for MACD. |
| MacdSlowPeriod |
Slow EMA period for MACD. |
| MacdSignalPeriod |
Signal EMA period for MACD. |
| MacdPrice |
Applied price used in the MACD calculation. |
| MacdRatio |
Minimal ratio between MACD main and signal lines. |
| CandleType |
Candle series used for all calculations. |
Implementation Notes
- Pip size is calculated from the security's price step, reproducing the 3/5-digit adjustment from the MQL version. This keeps pip-based distances identical across Forex symbols.
- All indicator buffers use queues to emulate MetaTrader's
ma_shift and bar indexing without calling historical lookup methods prohibited by the project rules.
- Volume adjustments respect
Security.VolumeStep, Security.MinVolume, and Security.MaxVolume, preventing invalid order sizes when LotCoefficient multiplies the exposure.
- Protective logic (stops, takes, trailing) runs entirely in the strategy layer, so there is no dependency on broker-side position modification APIs.
- The class resides in the
StockSharp.Samples.Strategies namespace and follows the repository requirement of using tab indentation and English comments exclusively.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// MA MACD Position Averaging v2 strategy using WMA crossover with EMA trend filter.
/// Buys when fast WMA crosses above slow WMA and price above EMA.
/// Sells on reverse conditions.
/// </summary>
public class MaMacdPositionAveragingV2Strategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private WeightedMovingAverage _fast;
private WeightedMovingAverage _slow;
private ExponentialMovingAverage _ema;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
private int _cooldown;
/// <summary>
/// Fast WMA period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow WMA period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// EMA trend filter period.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public MaMacdPositionAveragingV2Strategy()
{
_fastPeriod = Param(nameof(FastPeriod), 15)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Fast WMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Slow WMA period", "Indicator");
_emaPeriod = Param(nameof(EmaPeriod), 200)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "EMA trend filter period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fast = null;
_slow = null;
_ema = null;
_prevFast = 0;
_prevSlow = 0;
_entryPrice = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new WeightedMovingAverage { Length = FastPeriod };
_slow = new WeightedMovingAverage { Length = SlowPeriod };
_ema = new ExponentialMovingAverage { Length = EmaPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _slow, _ema, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_fast.IsFormed || !_slow.IsFormed || !_ema.IsFormed)
{
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
// Check SL/TP
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 80;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 80;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 80;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 80;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
// WMA crossover with EMA trend filter
if (_prevFast <= _prevSlow && fastValue > slowValue && close > emaValue && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = close;
_cooldown = 80;
}
else if (_prevFast >= _prevSlow && fastValue < slowValue && close < emaValue && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = close;
_cooldown = 80;
}
_prevFast = fastValue;
_prevSlow = slowValue;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import WeightedMovingAverage, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class ma_macd_position_averaging_v2_strategy(Strategy):
def __init__(self):
super(ma_macd_position_averaging_v2_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 15) \
.SetDisplay("Fast Period", "Fast WMA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 100) \
.SetDisplay("Slow Period", "Slow WMA period", "Indicator")
self._ema_period = self.Param("EmaPeriod", 200) \
.SetDisplay("EMA Period", "EMA trend filter period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit in price steps", "Risk")
self._fast = None
self._slow = None
self._ema = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def ema_period(self):
return self._ema_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(ma_macd_position_averaging_v2_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._ema = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(ma_macd_position_averaging_v2_strategy, self).OnStarted2(time)
self._fast = WeightedMovingAverage()
self._fast.Length = self.fast_period
self._slow = WeightedMovingAverage()
self._slow.Length = self.slow_period
self._ema = ExponentialMovingAverage()
self._ema.Length = self.ema_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._ema, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value, ema_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
ema_val = float(ema_value)
if not self._fast.IsFormed or not self._slow.IsFormed or not self._ema.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
# Check SL/TP
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 80
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 80
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 80
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 80
self._prev_fast = fast_val
self._prev_slow = slow_val
return
# WMA crossover with EMA trend filter
if self._prev_fast <= self._prev_slow and fast_val > slow_val and close > ema_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 80
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and close < ema_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 80
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return ma_macd_position_averaging_v2_strategy()