View on GitHub

MA + RSI Wizard Strategy

Overview

This strategy is the StockSharp port of the MetaTrader 5 "MQL5 Wizard MA RSI" expert from folder MQL/17489. The original robot combines a moving average filter with an RSI filter and opens trades when the weighted sum of the filters crosses configurable thresholds. The C# version keeps the same structure while expressing the logic with StockSharp's high level API and modern risk management helpers.

The bot works on any instrument that provides OHLCV candles. It evaluates one moving average that can be lagged by a user defined number of bars and an RSI that can be fed with different price sources. Both indicators contribute to a composite score. A position is opened once the score exceeds the open threshold and closed when the opposite score reaches the close threshold. Optional distance, stop loss and take profit settings replicate the money management parameters of the original Expert Advisor.

Indicators and scoring

  • Moving Average – configurable period, method (simple, exponential, smoothed, linear weighted), price source and forward shift. When the closing price is above the shifted average the MA score equals 100, otherwise it is 0.
  • Relative Strength Index (RSI) – configurable period and price source. The RSI contribution grows linearly from 0 when RSI = 50 to 100 when RSI = 100 for long signals, and mirrors the same behaviour for short signals.
  • Composite score – the MA and RSI scores are weighted by MaWeight and RsiWeight. The final value is the weighted average score = (maScore * MaWeight + rsiScore * RsiWeight) / (MaWeight + RsiWeight) which stays inside the [0;100] interval just like in the MetaTrader version.
  • Price distance filterPriceLevelPoints defines the minimum distance between the candle close and the shifted moving average (converted to price using the instrument step). Signals closer than the threshold are ignored.

Trade rules

  1. Every finished candle updates the indicators and scores.
  2. If the opposite score breaches ThresholdClose, the current position is closed at market.
  3. Long entry – allowed when there is no long exposure, the long score is at least ThresholdOpen, the cooldown (ExpirationBars) has passed, and the price distance filter is satisfied. The order size equals Volume + |Position|, which automatically flips a short position if needed.
  4. Short entry – symmetrical to the long logic.
  5. Optional StartProtection applies stop loss and take profit using absolute price points.

Risk management

The strategy activates StartProtection once it starts. Distances are defined in price points (StopLevelPoints, TakeLevelPoints) and are translated with the current Security.PriceStep. Both values can be set to zero to disable the corresponding protection. The cooldown parameter prevents immediate re-entries in the same direction, emulating the pending order expiration setting of the original EA.

Parameters

Parameter Description Default
CandleType Data series used for analysis. 15-minute time frame
ThresholdOpen Minimum weighted score to open a position. 55
ThresholdClose Minimum opposite score to close a position. 100
PriceLevelPoints Required distance between price and shifted MA (in points). 0
StopLevelPoints Stop loss distance (points). 50
TakeLevelPoints Take profit distance (points). 50
ExpirationBars Cooldown in bars before re-entering in the same direction. 4
MaPeriod Moving average period. 20
MaShift Forward shift applied to the MA output (bars). 3
MaMethods Moving average method (Simple, Exponential, Smoothed, LinearWeighted). Simple
MaAppliedPrice Price source for the MA. Close
MaWeight Weight assigned to the MA score. 0.8
RsiPeriod RSI period. 3
RsiAppliedPrice Price source for the RSI. Close
RsiWeight Weight assigned to the RSI score. 0.5

Notes

  • The strategy runs strictly on finished candles and ignores partial updates.
  • Setting both indicator weights to zero disables trading because the combined score can no longer reach the thresholds.
  • Cooldown (ExpirationBars) equal to zero allows multiple entries in the same direction without waiting.
  • Because StockSharp executes market orders by default, pending order expiration from the original EA is represented by the cooldown mechanic instead of actual order cancellation.
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>
/// Moving average plus RSI strategy converted from the MQL5 Wizard template.
/// The strategy computes weighted scores from a shifted moving average and RSI momentum.
/// </summary>
public class MaRsiWizardStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation methods supported by the strategy.
	/// </summary>
	public enum MaMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	/// <summary>
	/// Price sources compatible with the indicators used in the strategy.
	/// </summary>
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _thresholdOpen;
	private readonly StrategyParam<int> _thresholdClose;
	private readonly StrategyParam<decimal> _priceLevelPoints;
	private readonly StrategyParam<int> _stopLevelPoints;
	private readonly StrategyParam<int> _takeLevelPoints;
	private readonly StrategyParam<int> _expirationBars;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MaMethods> _maMethod;
	private readonly StrategyParam<AppliedPrices> _maAppliedPrice;
	private readonly StrategyParam<decimal> _maWeight;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<AppliedPrices> _rsiAppliedPrice;
	private readonly StrategyParam<decimal> _rsiWeight;

	private DecimalLengthIndicator _ma = null!;
	private RelativeStrengthIndex _rsi = null!;
	private readonly Queue<decimal> _maShiftBuffer = new();

	private int _barIndex;
	private int? _lastLongEntryBar;
	private int? _lastShortEntryBar;

	/// <summary>
	/// Initializes a new instance of the <see cref="MaRsiWizardStrategy"/>.
	/// </summary>
	public MaRsiWizardStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for incoming candles", "General");

		_thresholdOpen = Param(nameof(ThresholdOpen), 75)
			.SetRange(0, 100)
			.SetDisplay("Open Threshold", "Weighted score required to open a position", "Signals")
			;

		_thresholdClose = Param(nameof(ThresholdClose), 100)
			.SetRange(0, 100)
			.SetDisplay("Close Threshold", "Weighted score required to exit an existing position", "Signals")
			;

		_priceLevelPoints = Param(nameof(PriceLevelPoints), 0m)
			.SetDisplay("Price Level (points)", "Minimum distance between price and moving average", "Signals")
			;

		_stopLevelPoints = Param(nameof(StopLevelPoints), 50)
			.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price points", "Risk")
			;

		_takeLevelPoints = Param(nameof(TakeLevelPoints), 50)
			.SetDisplay("Take Profit (points)", "Profit target distance expressed in price points", "Risk")
			;

		_expirationBars = Param(nameof(ExpirationBars), 24)
			.SetDisplay("Signal Cooldown (bars)", "Bars to wait before allowing a new trade in the same direction", "Signals")
			;

		_maPeriod = Param(nameof(MaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average period", "Moving Average")
			;

		_maShift = Param(nameof(MaShift), 3)
			.SetRange(0, 100)
			.SetDisplay("MA Shift", "Lag applied to the moving average output", "Moving Average")
			;

		_maMethod = Param(nameof(MaMethods), MaMethods.Simple)
			.SetDisplay("MA Method", "Moving average calculation method", "Moving Average");

		_maAppliedPrice = Param(nameof(MaAppliedPrice), AppliedPrices.Close)
			.SetDisplay("MA Source", "Price type used for the moving average", "Moving Average");

		_maWeight = Param(nameof(MaWeight), 0.8m)
			.SetDisplay("MA Weight", "Contribution of the moving average score", "Signals")
			.SetRange(0m, 1m)
			;

		_rsiPeriod = Param(nameof(RsiPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI calculation length", "RSI")
			;

		_rsiAppliedPrice = Param(nameof(RsiAppliedPrice), AppliedPrices.Close)
			.SetDisplay("RSI Source", "Price type used for RSI", "RSI");

		_rsiWeight = Param(nameof(RsiWeight), 0.5m)
			.SetDisplay("RSI Weight", "Contribution of the RSI score", "Signals")
			.SetRange(0m, 1m)
			;
	}

	/// <summary>
	/// Type of candles used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Weighted score required to open a new position.
	/// </summary>
	public int ThresholdOpen
	{
		get => _thresholdOpen.Value;
		set => _thresholdOpen.Value = value;
	}

	/// <summary>
	/// Weighted score required to close the current position.
	/// </summary>
	public int ThresholdClose
	{
		get => _thresholdClose.Value;
		set => _thresholdClose.Value = value;
	}

	/// <summary>
	/// Minimum price distance from the moving average expressed in points.
	/// </summary>
	public decimal PriceLevelPoints
	{
		get => _priceLevelPoints.Value;
		set => _priceLevelPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in points.
	/// </summary>
	public int StopLevelPoints
	{
		get => _stopLevelPoints.Value;
		set => _stopLevelPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in points.
	/// </summary>
	public int TakeLevelPoints
	{
		get => _takeLevelPoints.Value;
		set => _takeLevelPoints.Value = value;
	}

	/// <summary>
	/// Cooldown measured in bars before a new trade in the same direction is allowed.
	/// </summary>
	public int ExpirationBars
	{
		get => _expirationBars.Value;
		set => _expirationBars.Value = value;
	}

	/// <summary>
	/// Moving average length.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Number of bars used to lag the moving average output.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average calculation method.
	/// </summary>
	public MaMethods MaMethod
	{
		get => _maMethod.Value;
		set => _maMethod.Value = value;
	}

	/// <summary>
	/// Price source used for the moving average.
	/// </summary>
	public AppliedPrices MaAppliedPrice
	{
		get => _maAppliedPrice.Value;
		set => _maAppliedPrice.Value = value;
	}

	/// <summary>
	/// Contribution of the moving average score in the weighted decision.
	/// </summary>
	public decimal MaWeight
	{
		get => _maWeight.Value;
		set => _maWeight.Value = value;
	}

	/// <summary>
	/// RSI calculation length.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Price source used for the RSI indicator.
	/// </summary>
	public AppliedPrices RsiAppliedPrice
	{
		get => _rsiAppliedPrice.Value;
		set => _rsiAppliedPrice.Value = value;
	}

	/// <summary>
	/// Contribution of the RSI score in the weighted decision.
	/// </summary>
	public decimal RsiWeight
	{
		get => _rsiWeight.Value;
		set => _rsiWeight.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_maShiftBuffer.Clear();
		_barIndex = 0;
		_lastLongEntryBar = null;
		_lastShortEntryBar = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_maShiftBuffer.Clear();
		_barIndex = 0;
		_lastLongEntryBar = null;
		_lastShortEntryBar = null;

		_ma = CreateMovingAverage(MaMethod, MaPeriod);
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var step = Security.PriceStep ?? 1m;

		Unit takeProfit = TakeLevelPoints > 0
			? new Unit(TakeLevelPoints * step, UnitTypes.Absolute)
			: null;

		Unit stopLoss = StopLevelPoints > 0
			? new Unit(StopLevelPoints * step, UnitTypes.Absolute)
			: null;

		if (stopLoss != null || takeProfit != null)
			StartProtection(stopLoss ?? new Unit(), takeProfit ?? new Unit());

		var priceArea = CreateChartArea();
		if (priceArea != null)
		{
			DrawCandles(priceArea, subscription);
			DrawIndicator(priceArea, _ma);
			DrawOwnTrades(priceArea);
		}

		var rsiArea = CreateChartArea();
		if (rsiArea != null)
		{
			rsiArea.Title = "RSI";
			DrawIndicator(rsiArea, _rsi);
		}
	}

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

		// removed IsFormedAndOnlineAndAllowTrading for backtesting

		_barIndex++;

		var maInput = SelectAppliedPrice(candle, MaAppliedPrice);
		var maValue = _ma.Process(new DecimalIndicatorValue(_ma, maInput, candle.OpenTime) { IsFinal = true });
		if (!maValue.IsFinal || maValue is not DecimalIndicatorValue maResult)
			return;

		var rsiInput = SelectAppliedPrice(candle, RsiAppliedPrice);
		var rsiValue = _rsi.Process(new DecimalIndicatorValue(_rsi, rsiInput, candle.OpenTime) { IsFinal = true });
		if (!rsiValue.IsFinal || rsiValue is not DecimalIndicatorValue rsiResult)
			return;

		var referenceMa = UpdateAndGetShiftedMa(maResult.Value);
		if (referenceMa == null)
			return;

		var currentPrice = candle.ClosePrice;
		var step = Security.PriceStep ?? 1m;
		var priceOffset = PriceLevelPoints * step;

		if (PriceLevelPoints > 0 && Math.Abs(currentPrice - referenceMa.Value) < priceOffset)
			return;

		var maLongSignal = currentPrice > referenceMa.Value ? 100m : 0m;
		var maShortSignal = currentPrice < referenceMa.Value ? 100m : 0m;

		var rsi = rsiResult.Value;
		var rsiLongSignal = rsi > 50m ? Math.Min(100m, (rsi - 50m) * 2m) : 0m;
		var rsiShortSignal = rsi < 50m ? Math.Min(100m, (50m - rsi) * 2m) : 0m;

		var weightSum = MaWeight + RsiWeight;
		if (weightSum <= 0m)
			return;

		var longScore = (MaWeight * maLongSignal + RsiWeight * rsiLongSignal) / weightSum;
		var shortScore = (MaWeight * maShortSignal + RsiWeight * rsiShortSignal) / weightSum;

		if (Position > 0 && shortScore >= ThresholdClose)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (Position < 0 && longScore >= ThresholdClose)
		{
			BuyMarket(Math.Abs(Position));
		}

		var allowLong = ExpirationBars <= 0 || _lastLongEntryBar == null || _barIndex - _lastLongEntryBar >= ExpirationBars;
		var allowShort = ExpirationBars <= 0 || _lastShortEntryBar == null || _barIndex - _lastShortEntryBar >= ExpirationBars;

		if (Position <= 0 && longScore >= ThresholdOpen && allowLong)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0)
			{
				BuyMarket(volume);
				_lastLongEntryBar = _barIndex;
			}
			return;
		}

		if (Position >= 0 && shortScore >= ThresholdOpen && allowShort)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0)
			{
				SellMarket(volume);
				_lastShortEntryBar = _barIndex;
			}
		}
	}

	private decimal? UpdateAndGetShiftedMa(decimal maValue)
	{
		var shift = Math.Max(0, MaShift);
		if (shift == 0)
		{
			return maValue;
		}

		_maShiftBuffer.Enqueue(maValue);

		if (_maShiftBuffer.Count <= shift)
			return null;

		if (_maShiftBuffer.Count > shift + 1)
			_maShiftBuffer.Dequeue();

		return _maShiftBuffer.Count == shift + 1 ? _maShiftBuffer.Peek() : (decimal?)null;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MaMethods method, int period)
	{
		return method switch
		{
			MaMethods.Simple => new SMA { Length = period },
			MaMethods.Exponential => new EMA { Length = period },
			MaMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MaMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
			_ => new SMA { Length = period }
		};
	}

	private static decimal SelectAppliedPrice(ICandleMessage candle, AppliedPrices price)
	{
		return price switch
		{
			AppliedPrices.Open => candle.OpenPrice,
			AppliedPrices.High => candle.HighPrice,
			AppliedPrices.Low => candle.LowPrice,
			AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}
}