The Above Below MA Rejoin strategy is a StockSharp conversion of the MetaTrader 4 expert advisor "AboveBelowMA". The original script monitors the 15-minute chart of GBP/USD and compares the current price with a one-period exponential moving average (EMA) calculated on the typical price. When price trades on the opposite side of a rising or falling average, the strategy attempts to fade that excursion and rejoin the underlying direction of the EMA. This port keeps the signal structure intact while leveraging StockSharp high-level APIs (SubscribeCandles + Bind).
Trading logic
Subscribe to the configured candle type (15-minute by default) and feed an exponential moving average that uses the typical price (High + Low + Close) / 3.
Track the latest and previous EMA values to understand the short-term slope. A bullish bias requires the EMA to rise, while a bearish bias requires it to fall.
Long setup: when the candle opens at least one price step below the EMA, closes below the EMA, and the previous EMA value is lower than the current EMA value, close any short exposure and prepare to buy. If no position remains, submit a market buy order.
Short setup: when the candle opens at least one price step above the EMA, closes above the EMA, and the previous EMA value is higher than the current EMA value, close any long exposure and prepare to sell. If the position is flat, submit a market sell order.
Orders are issued only on finished candles to avoid premature signals on partially formed bars.
Position sizing
The MetaTrader version sizes trades using AccountFreeMargin / 10000 capped at 5 lots. The StockSharp implementation offers an equivalent behaviour: when UseDynamicVolume is enabled, the strategy divides the current portfolio value by BalanceToVolumeDivider (default 10000).
The calculated size is limited by MaxVolume, mirroring the hard 5-lot cap from the expert advisor. If dynamic sizing is disabled, the InitialVolume parameter is used as a fixed volume.
All volumes are aligned to the instrument's volume step and min/max volume constraints to avoid rejection by the broker or simulator.
Parameters
Parameter
Description
EmaLength
Period of the exponential moving average (defaults to 1, matching the EA).
CandleType
Timeframe used to build the candles that feed the EMA (default 15 minutes).
InitialVolume
Fixed order volume when dynamic sizing is disabled.
UseDynamicVolume
Enables portfolio-based position sizing (Balance / BalanceToVolumeDivider).
BalanceToVolumeDivider
Divider applied to the portfolio value to emulate AccountFreeMargin / 10000.
MaxVolume
Maximum order volume allowed by the strategy.
Notes
The strategy uses ClosePosition() before opening a trade in the opposite direction, matching the MetaTrader logic that closes opposing orders via CheckOrders.
Because signals are evaluated on finished candles, entries may occur slightly later than the tick-based MetaTrader version. This change improves stability when running in backtests or live trading with candle data.
Ensure that the selected security provides meaningful PriceStep, VolumeStep, and portfolio valuation information for the dynamic volume block to work as expected.
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Above/Below MA Rejoin strategy.
/// Buys when price is below a rising EMA (pullback in uptrend).
/// Sells when price is above a falling EMA (pullback in downtrend).
/// </summary>
public class AboveBelowMaRejoinStrategy : Strategy
{
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevEma;
private decimal _prevClose;
private bool _hasPrev;
public int EmaLength { get => _emaLength.Value; set => _emaLength.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public AboveBelowMaRejoinStrategy()
{
_emaLength = Param(nameof(EmaLength), 20)
.SetDisplay("EMA Period", "EMA lookback period", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevEma = 0m;
_prevClose = 0m;
_hasPrev = false;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
var ema = new ExponentialMovingAverage { Length = EmaLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
if (!_hasPrev)
{
_prevEma = emaValue;
_prevClose = close;
_hasPrev = true;
return;
}
var emaRising = emaValue > _prevEma;
var emaFalling = emaValue < _prevEma;
// Price rejoins from below in uptrend - buy
if (emaRising && _prevClose < _prevEma && close >= emaValue && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
}
// Price rejoins from above in downtrend - sell
else if (emaFalling && _prevClose > _prevEma && close <= emaValue && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
}
_prevEma = emaValue;
_prevClose = close;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class above_below_ma_rejoin_strategy(Strategy):
"""Above/Below MA Rejoin strategy.
Buys when price rejoins from below a rising EMA (pullback in uptrend).
Sells when price rejoins from above a falling EMA (pullback in downtrend)."""
def __init__(self):
super(above_below_ma_rejoin_strategy, self).__init__()
self._ema_length = self.Param("EmaLength", 20) \
.SetDisplay("EMA Period", "EMA lookback period", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_ema = 0.0
self._prev_close = 0.0
self._has_prev = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def EmaLength(self):
return self._ema_length.Value
def OnReseted(self):
super(above_below_ma_rejoin_strategy, self).OnReseted()
self._prev_ema = 0.0
self._prev_close = 0.0
self._has_prev = False
def OnStarted2(self, time):
super(above_below_ma_rejoin_strategy, self).OnStarted2(time)
self._has_prev = False
ema = ExponentialMovingAverage()
ema.Length = self.EmaLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self._process_candle).Start()
def _process_candle(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
ema_val = float(ema_value)
if not self._has_prev:
self._prev_ema = ema_val
self._prev_close = close
self._has_prev = True
return
ema_rising = ema_val > self._prev_ema
ema_falling = ema_val < self._prev_ema
# Price rejoins from below in uptrend - buy
if ema_rising and self._prev_close < self._prev_ema and close >= ema_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
# Price rejoins from above in downtrend - sell
elif ema_falling and self._prev_close > self._prev_ema and close <= ema_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_ema = ema_val
self._prev_close = close
def CreateClone(self):
return above_below_ma_rejoin_strategy()