View on GitHub

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

  1. Candle close is above the moving average.
  2. +DI is greater than or equal to −DI, indicating bullish directional pressure.
  3. Candle close is above the Parabolic SAR value.
  4. 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

  1. Candle close is below the moving average.
  2. +DI is less than or equal to −DI, indicating bearish directional pressure.
  3. Candle close is below the Parabolic SAR value.
  4. 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

  1. Attach the strategy to a security and portfolio in the Designer or Backtester.
  2. Adjust the candle type to match your trading horizon (e.g., M15, H1, or D1 candles).
  3. Tune the moving average period, ADX period, and SAR parameters to fit the instrument’s volatility.
  4. 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.
  5. 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);
	}
}