Ma SAR ADX Bind Strategy
Overview
This strategy is a StockSharp high-level API conversion of the original MaSarADX.mq5 MetaTrader 5 expert advisor. The system combines a simple moving average trend filter with Directional Movement Index (ADX) signals and the Parabolic SAR trailing stop. Trading decisions are evaluated only on completed candles, replicating the "first tick of a new bar" behavior from the MQL version. When the candle close is aligned with both the moving average trend and the ADX directional balance, a position is opened. Parabolic SAR guides both trade direction and exits by forcing a full liquidation when price crosses to the opposite side of the SAR dots.
Indicators and Data
- Simple Moving Average (SMA) – provides the primary trend direction filter. Default length: 100 candles.
- Average Directional Index (ADX) – supplies +DI and −DI to confirm directional strength. Default length: 14.
- Parabolic SAR – acts as a stop-and-reverse overlay and defines exit conditions. Default acceleration: 0.02; maximum acceleration: 0.10.
- Candles – any timeframe can be requested. By default the strategy subscribes to 1-hour candles, but the parameter can be adjusted to match the symbol and testing regime.
The implementation subscribes to StockSharp candle streams and binds all three indicators using the BindEx helper so that every callback receives synchronized values for the same candle.
Trading Logic
Long Entry
- Candle close is above the moving average.
- +DI is greater than or equal to −DI, indicating bullish directional pressure.
- Candle close is above the Parabolic SAR value.
- No long position is currently open (
Position <= 0).
When all rules align, a market buy order is sent for the configured base volume plus any size required to cover a short position.
Short Entry
- Candle close is below the moving average.
- +DI is less than or equal to −DI, indicating bearish directional pressure.
- Candle close is below the Parabolic SAR value.
- No short position is currently open (
Position >= 0).
A market sell order is placed when all short rules match.
Exits
- Long positions are closed immediately once price falls below the Parabolic SAR.
- Short positions are covered when price rises above the Parabolic SAR.
No separate stop-loss or take-profit levels are added; the SAR crossing is the only exit signal, following the original expert advisor. Because exits are evaluated before new entries, the strategy will not flip from short to long (or vice versa) on the same candle, mirroring the two-step open/close cycle of the MQL script.
Parameters
| Name |
Description |
Default |
Notes |
MaPeriod |
Length of the simple moving average used to define the trend filter. |
100 |
Optimizable, must be greater than zero. |
AdxPeriod |
Period of the ADX calculation that produces +DI and −DI. |
14 |
Optimizable, must be greater than zero. |
SarStep |
Acceleration factor and increment for the Parabolic SAR. |
0.02 |
Equivalent to the MQL step parameter. |
SarMax |
Maximum acceleration factor for Parabolic SAR. |
0.10 |
Mirrors the MQL maximum setting. |
Volume |
Base order size for new entries. |
1 |
Replaces the margin-based lot sizing from the MetaTrader version. The actual order size is Volume + |Position| so that reversals flatten existing exposure. |
CandleType |
The candle data type subscribed through StockSharp. |
1 hour |
Adjustable to any timeframe. |
Implementation Notes
- Indicator processing uses StockSharp’s high-level
BindEx pipeline, ensuring that SMA, ADX, and SAR are updated in lock-step without manual buffering.
- Exits are executed even if
AllowTrading is temporarily disabled, keeping risk controls active at all times.
- Charting helpers are included: the primary panel plots price, SMA, and SAR, while a secondary panel plots the ADX indicator for diagnostics.
- Logging statements describe every trade decision with the underlying indicator values to simplify forward testing and debugging.
Usage Guidelines
- Attach the strategy to a security and portfolio in the Designer or Backtester.
- Adjust the candle type to match your trading horizon (e.g., M15, H1, or D1 candles).
- Tune the moving average period, ADX period, and SAR parameters to fit the instrument’s volatility.
- Set the
Volume parameter to your preferred position size. If you need the adaptive money management used in the original script, integrate your own portfolio-based sizing before sending orders.
- Run the strategy. Trades will trigger only after all indicators have produced enough historical values to be formed.
Differences from the Original Expert Advisor
- Margin-based lot calculation has been replaced with a fixed
Volume parameter to keep the strategy broker-neutral inside StockSharp.
- Trade management, indicator values, and the evaluation order (exit before entry) strictly follow the MetaTrader reference logic.
- All comments inside the source code are in English to comply with project 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>
/// Conversion of the MaSarADX MetaTrader strategy to StockSharp high level API.
/// Combines a moving average, ADX directional movement and Parabolic SAR for entries and exits.
/// </summary>
public class MaSarAdxBindStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _sarStep;
private readonly StrategyParam<decimal> _sarMax;
private readonly StrategyParam<DataType> _candleType;
private decimal? _previousHigh;
private decimal? _previousLow;
private decimal? _previousClose;
private decimal _smoothedPlusDm;
private decimal _smoothedMinusDm;
private decimal _smoothedTrueRange;
private int _adxSamples;
/// <summary>
/// Moving average period used for the trend filter.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Average Directional Index calculation period.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// Acceleration step for the Parabolic SAR indicator.
/// </summary>
public decimal SarStep
{
get => _sarStep.Value;
set => _sarStep.Value = value;
}
/// <summary>
/// Maximum acceleration factor for the Parabolic SAR indicator.
/// </summary>
public decimal SarMax
{
get => _sarMax.Value;
set => _sarMax.Value = value;
}
/// <summary>
/// Type of candles processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="MaSarAdxBindStrategy"/>.
/// </summary>
public MaSarAdxBindStrategy()
{
_maPeriod = Param(nameof(MaPeriod), 120)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Length of the trend moving average", "Indicators")
.SetOptimize(20, 200, 10);
_adxPeriod = Param(nameof(AdxPeriod), 18)
.SetGreaterThanZero()
.SetDisplay("ADX Period", "Length of the Average Directional Index", "Indicators")
.SetOptimize(7, 28, 1);
_sarStep = Param(nameof(SarStep), 0.02m)
.SetRange(0.005m, 0.2m)
.SetDisplay("SAR Step", "Acceleration step for Parabolic SAR", "Indicators")
;
_sarMax = Param(nameof(SarMax), 0.1m)
.SetRange(0.05m, 1m)
.SetDisplay("SAR Maximum", "Maximum acceleration for Parabolic SAR", "Indicators")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to request", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousHigh = null;
_previousLow = null;
_previousClose = null;
_smoothedPlusDm = 0m;
_smoothedMinusDm = 0m;
_smoothedTrueRange = 0m;
_adxSamples = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Instantiate indicators used in the original MetaTrader script.
var movingAverage = new SimpleMovingAverage
{
Length = MaPeriod
};
var parabolicSar = new ParabolicSar
{
Acceleration = SarStep,
AccelerationStep = SarStep,
AccelerationMax = SarMax
};
// Subscribe to candle data and bind indicator updates to a single handler.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(movingAverage, parabolicSar, ProcessCandle)
.Start();
// Draw the trading context for visual debugging when charts are available.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, movingAverage);
DrawIndicator(area, parabolicSar);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal movingAverage, decimal sar)
{
// Work only with completed candles to mirror the original first-tick logic.
if (candle.State != CandleStates.Finished)
return;
var (plusDi, minusDi, isReady) = UpdateDirectionalMovement(candle);
if (!isReady)
return;
// Always allow risk exits even if trading is temporarily disabled.
if (Position > 0 && candle.ClosePrice < sar)
{
SellMarket();
return;
}
if (Position < 0 && candle.ClosePrice > sar)
{
BuyMarket();
return;
}
// Entry conditions replicated from the MetaTrader version.
var bullishSignal = candle.ClosePrice > movingAverage && plusDi >= minusDi && candle.ClosePrice > sar;
var bearishSignal = candle.ClosePrice < movingAverage && plusDi <= minusDi && candle.ClosePrice < sar;
if (bullishSignal && Position <= 0)
{
BuyMarket();
return;
}
if (bearishSignal && Position >= 0)
{
SellMarket();
}
}
private (decimal plusDi, decimal minusDi, bool isReady) UpdateDirectionalMovement(ICandleMessage candle)
{
if (_previousHigh is not decimal previousHigh ||
_previousLow is not decimal previousLow ||
_previousClose is not decimal previousClose)
{
_previousHigh = candle.HighPrice;
_previousLow = candle.LowPrice;
_previousClose = candle.ClosePrice;
return (0m, 0m, false);
}
var upMove = candle.HighPrice - previousHigh;
var downMove = previousLow - candle.LowPrice;
var plusDm = upMove > downMove && upMove > 0m ? upMove : 0m;
var minusDm = downMove > upMove && downMove > 0m ? downMove : 0m;
var trueRange = Math.Max(
candle.HighPrice - candle.LowPrice,
Math.Max(
Math.Abs(candle.HighPrice - previousClose),
Math.Abs(candle.LowPrice - previousClose)));
if (_adxSamples < AdxPeriod)
{
_smoothedPlusDm += plusDm;
_smoothedMinusDm += minusDm;
_smoothedTrueRange += trueRange;
_adxSamples++;
}
else
{
_smoothedPlusDm = _smoothedPlusDm - (_smoothedPlusDm / AdxPeriod) + plusDm;
_smoothedMinusDm = _smoothedMinusDm - (_smoothedMinusDm / AdxPeriod) + minusDm;
_smoothedTrueRange = _smoothedTrueRange - (_smoothedTrueRange / AdxPeriod) + trueRange;
}
_previousHigh = candle.HighPrice;
_previousLow = candle.LowPrice;
_previousClose = candle.ClosePrice;
if (_adxSamples < AdxPeriod || _smoothedTrueRange <= 0m)
return (0m, 0m, false);
return (
100m * _smoothedPlusDm / _smoothedTrueRange,
100m * _smoothedMinusDm / _smoothedTrueRange,
true);
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import SimpleMovingAverage, ParabolicSar
from StockSharp.Algo.Strategies import Strategy
class ma_sar_adx_bind_strategy(Strategy):
def __init__(self):
super(ma_sar_adx_bind_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 120)
self._adx_period = self.Param("AdxPeriod", 18)
self._sar_step = self.Param("SarStep", 0.02)
self._sar_max = self.Param("SarMax", 0.1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2)))
self._previous_high = None
self._previous_low = None
self._previous_close = None
self._smoothed_plus_dm = 0.0
self._smoothed_minus_dm = 0.0
self._smoothed_true_range = 0.0
self._adx_samples = 0
@property
def MaPeriod(self):
return self._ma_period.Value
@MaPeriod.setter
def MaPeriod(self, value):
self._ma_period.Value = value
@property
def AdxPeriod(self):
return self._adx_period.Value
@AdxPeriod.setter
def AdxPeriod(self, value):
self._adx_period.Value = value
@property
def SarStep(self):
return self._sar_step.Value
@SarStep.setter
def SarStep(self, value):
self._sar_step.Value = value
@property
def SarMax(self):
return self._sar_max.Value
@SarMax.setter
def SarMax(self, value):
self._sar_max.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(ma_sar_adx_bind_strategy, self).OnStarted2(time)
self._previous_high = None
self._previous_low = None
self._previous_close = None
self._smoothed_plus_dm = 0.0
self._smoothed_minus_dm = 0.0
self._smoothed_true_range = 0.0
self._adx_samples = 0
ma = SimpleMovingAverage()
ma.Length = self.MaPeriod
sar = ParabolicSar()
sar.Acceleration = float(self.SarStep)
sar.AccelerationStep = float(self.SarStep)
sar.AccelerationMax = float(self.SarMax)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ma, sar, self.ProcessCandle).Start()
def ProcessCandle(self, candle, moving_average, sar):
if candle.State != CandleStates.Finished:
return
ma_val = float(moving_average)
sar_val = float(sar)
plus_di, minus_di, is_ready = self._update_directional_movement(candle)
if not is_ready:
return
close = float(candle.ClosePrice)
# Exit conditions
if self.Position > 0 and close < sar_val:
self.SellMarket()
return
if self.Position < 0 and close > sar_val:
self.BuyMarket()
return
# Entry conditions
bullish_signal = close > ma_val and plus_di >= minus_di and close > sar_val
bearish_signal = close < ma_val and plus_di <= minus_di and close < sar_val
if bullish_signal and self.Position <= 0:
self.BuyMarket()
return
if bearish_signal and self.Position >= 0:
self.SellMarket()
def _update_directional_movement(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self._previous_high is None or self._previous_low is None or self._previous_close is None:
self._previous_high = high
self._previous_low = low
self._previous_close = close
return (0.0, 0.0, False)
up_move = high - self._previous_high
down_move = self._previous_low - low
plus_dm = up_move if (up_move > down_move and up_move > 0.0) else 0.0
minus_dm = down_move if (down_move > up_move and down_move > 0.0) else 0.0
true_range = max(high - low, max(abs(high - self._previous_close), abs(low - self._previous_close)))
adx_period = int(self.AdxPeriod)
if self._adx_samples < adx_period:
self._smoothed_plus_dm += plus_dm
self._smoothed_minus_dm += minus_dm
self._smoothed_true_range += true_range
self._adx_samples += 1
else:
self._smoothed_plus_dm = self._smoothed_plus_dm - (self._smoothed_plus_dm / adx_period) + plus_dm
self._smoothed_minus_dm = self._smoothed_minus_dm - (self._smoothed_minus_dm / adx_period) + minus_dm
self._smoothed_true_range = self._smoothed_true_range - (self._smoothed_true_range / adx_period) + true_range
self._previous_high = high
self._previous_low = low
self._previous_close = close
if self._adx_samples < adx_period or self._smoothed_true_range <= 0.0:
return (0.0, 0.0, False)
return (
100.0 * self._smoothed_plus_dm / self._smoothed_true_range,
100.0 * self._smoothed_minus_dm / self._smoothed_true_range,
True)
def OnReseted(self):
super(ma_sar_adx_bind_strategy, self).OnReseted()
self._previous_high = None
self._previous_low = None
self._previous_close = None
self._smoothed_plus_dm = 0.0
self._smoothed_minus_dm = 0.0
self._smoothed_true_range = 0.0
self._adx_samples = 0
def CreateClone(self):
return ma_sar_adx_bind_strategy()