View on GitHub

Two MA RSI Strategy

Overview

Two MA RSI Strategy is a conversion of the original MetaTrader "2MA_RSI" expert advisor. It uses a fast and a slow exponential moving average (EMA) crossover confirmed by a Relative Strength Index (RSI) filter. Orders are sized with a martingale-style money management block that increases the next order volume after a loss. The StockSharp version works entirely on finished candles and reproduces the original take-profit and stop-loss behaviour in price points.

Data and indicators

  • The strategy subscribes to a single candle series defined by CandleType (5-minute candles by default).
  • Three indicators are calculated on every completed bar:
    • FastLength EMA (applied to the candle close).
    • SlowLength EMA.
    • RSI with length RsiLength.
  • Historical indicator values are stored internally to detect EMA crossovers without pulling data from indicator buffers.

Entry logic

  1. The previous candle must be finished to avoid intrabar re-evaluation.
  2. No active position is allowed (Position == 0).
  3. Long entry:
    • The fast EMA crosses above the slow EMA (fast EMA on the current bar is greater than the slow EMA while the previous bar had fast EMA < slow EMA).
    • The RSI value is below RsiOversold, confirming an oversold market.
  4. Short entry:
    • The fast EMA crosses below the slow EMA with the analogous condition (fast EMA now below slow EMA, previously above).
    • RSI is above RsiOverbought, signalling an overbought market.
  5. When all conditions are satisfied the strategy sends a market order sized according to the martingale module.

Exit logic

  • A protective stop loss and a take profit are calculated immediately after each entry. Distances are defined in "points" and converted through the instrument PriceStep:
    • Long:
      • Stop loss = entry price - StopLossPoints * PriceStep.
      • Take profit = entry price + TakeProfitPoints * PriceStep.
    • Short:
      • Stop loss = entry price + StopLossPoints * PriceStep.
      • Take profit = entry price - TakeProfitPoints * PriceStep.
  • Only these protective levels close a trade. The strategy waits for the next candle to confirm whether the low/high touched the target or stop and sends a market ClosePosition() order accordingly.
  • Exit priority matches the conservative behaviour of the original robot: a stop loss is evaluated before a take profit if both levels fall inside the same candle range.

Position sizing and martingale

  1. The base volume is calculated on every entry as floor(balance / BalanceDivider) * VolumeStep. The value always stays at or above one volume step and uses portfolio CurrentValue (falling back to BeginValue when necessary).
  2. After each losing exit the martingale stage increases by one up to MaxDoublings. The next order volume is multiplied by 2^stage.
  3. Any winning trade or reaching the maximum number of doublings resets the stage to zero, returning to the base volume.
  4. If MaxDoublings is zero or negative the size never increases and equals the base volume.

Additional behaviour

  • The strategy keeps track of previous EMA values internally and does not request historical indicator values.
  • Orders are executed only when the strategy is online, indicators are formed, and trading is allowed.
  • Chart output draws price candles, own trades, and the three indicators for visual analysis.

Parameters

Parameter Description Default
FastLength Length of the fast EMA. 5
SlowLength Length of the slow EMA. 20
RsiLength Number of bars used in RSI calculation. 14
RsiOverbought RSI level that blocks new longs and allows shorts. 70
RsiOversold RSI level that allows longs. 30
StopLossPoints Stop loss distance expressed in price steps. 500
TakeProfitPoints Take profit distance in price steps. 1500
BalanceDivider Divides portfolio value to obtain the base order size. 1000
MaxDoublings Maximum number of martingale doublings after consecutive losses. 1
CandleType Candle series used by the strategy. 5-minute timeframe

Usage notes

  • Provide a portfolio and security with valid PriceStep and VolumeStep metadata so that point-based risk management and position sizing remain consistent.
  • Because market orders are used for exits, slippage and spreads are still possible compared with the limit orders of the MetaTrader version, but the logic of stop/take evaluation is preserved.
  • The strategy does not create a Python version; only the C# implementation is supplied as requested.
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 crossover strategy with RSI confirmation and martingale sizing.
/// </summary>
public class TwoMaRsiStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<decimal> _rsiOverbought;
	private readonly StrategyParam<decimal> _rsiOversold;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _balanceDivider;
	private readonly StrategyParam<int> _maxDoublings;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _slowEma;
	private RelativeStrengthIndex _rsi;

	private decimal? _previousFast;
	private decimal? _previousSlow;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;
	private int _martingaleStage;
	private bool _isClosing;

	/// <summary>
	/// Initializes a new instance of the <see cref="TwoMaRsiStrategy"/> class.
	/// </summary>
	public TwoMaRsiStrategy()
	{
		_fastLength = Param(nameof(FastLength), 5)
			.SetDisplay("Fast EMA Length", "Length of the fast exponential moving average", "Indicators")
			
			.SetOptimize(2, 20, 1);

		_slowLength = Param(nameof(SlowLength), 20)
			.SetDisplay("Slow EMA Length", "Length of the slow exponential moving average", "Indicators")
			
			.SetOptimize(10, 60, 5);

		_rsiLength = Param(nameof(RsiLength), 14)
			.SetDisplay("RSI Length", "Number of bars for the RSI calculation", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_rsiOverbought = Param(nameof(RsiOverbought), 50m)
			.SetDisplay("RSI Overbought", "Upper RSI threshold for short entries", "Signals");

		_rsiOversold = Param(nameof(RsiOversold), 50m)
			.SetDisplay("RSI Oversold", "Lower RSI threshold for long entries", "Signals");

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetDisplay("Stop Loss (points)", "Stop loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 1500m)
			.SetDisplay("Take Profit (points)", "Take profit distance in price steps", "Risk");

		_balanceDivider = Param(nameof(BalanceDivider), 1000m)
			.SetDisplay("Balance Divider", "Divides portfolio value to estimate base order volume", "Money Management");

		_maxDoublings = Param(nameof(MaxDoublings), 1)
			.SetDisplay("Max Doublings", "Maximum number of martingale doublings", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series for the strategy", "General");
	}

	/// <summary>
	/// Fast EMA length.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	/// <summary>
	/// RSI period.
	/// </summary>
	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.Value = value;
	}

	/// <summary>
	/// Overbought threshold for RSI.
	/// </summary>
	public decimal RsiOverbought
	{
		get => _rsiOverbought.Value;
		set => _rsiOverbought.Value = value;
	}

	/// <summary>
	/// Oversold threshold for RSI.
	/// </summary>
	public decimal RsiOversold
	{
		get => _rsiOversold.Value;
		set => _rsiOversold.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Divider applied to the portfolio value to calculate the base order volume.
	/// </summary>
	public decimal BalanceDivider
	{
		get => _balanceDivider.Value;
		set => _balanceDivider.Value = value;
	}

	/// <summary>
	/// Maximum number of martingale doublings.
	/// </summary>
	public int MaxDoublings
	{
		get => _maxDoublings.Value;
		set => _maxDoublings.Value = value;
	}

	/// <summary>
	/// Candle data type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

		_fastEma = null;
		_slowEma = null;
		_rsi = null;
		_previousFast = null;
		_previousSlow = null;
		_entryPrice = default;
		_stopPrice = default;
		_takeProfitPrice = default;
		_martingaleStage = 0;
		_isClosing = false;
	}

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

		_fastEma = new ExponentialMovingAverage
		{
			Length = FastLength
		};

		_slowEma = new ExponentialMovingAverage
		{
			Length = SlowLength
		};

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiLength
		};

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		if (Position == 0 && _isClosing)
		{
			_isClosing = false;
			_entryPrice = default;
			_stopPrice = default;
			_takeProfitPrice = default;
		}

		var fastResult = _fastEma.Process(candle);
		var slowResult = _slowEma.Process(candle);
		var rsiResult = _rsi.Process(candle);

		if (fastResult.IsEmpty || slowResult.IsEmpty || rsiResult.IsEmpty)
		{
			return;
		}

		if (!_fastEma.IsFormed || !_slowEma.IsFormed || !_rsi.IsFormed)
		{
			_previousFast = fastResult.GetValue<decimal>();
			_previousSlow = slowResult.GetValue<decimal>();
			return;
		}

		var fast = fastResult.GetValue<decimal>();
		var slow = slowResult.GetValue<decimal>();
		var rsi = rsiResult.GetValue<decimal>();
		var point = GetPoint();

		if (Position > 0)
		{
			var stopHit = candle.LowPrice <= _stopPrice;
			var takeHit = candle.HighPrice >= _takeProfitPrice;

			if (!_isClosing && stopHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterLoss();
			}
			else if (!_isClosing && takeHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterWin();
			}
		}
		else if (Position < 0)
		{
			var stopHit = candle.HighPrice >= _stopPrice;
			var takeHit = candle.LowPrice <= _takeProfitPrice;

			if (!_isClosing && stopHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterLoss();
			}
			else if (!_isClosing && takeHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterWin();
			}
		}
		else if (!_isClosing)
		{
			if (_previousFast is null || _previousSlow is null)
			{
				_previousFast = fast;
				_previousSlow = slow;
				return;
			}

			var prevFast = _previousFast.Value;
			var prevSlow = _previousSlow.Value;

			var crossUp = prevFast < prevSlow && fast > slow && rsi < RsiOversold;
			var crossDown = prevFast > prevSlow && fast < slow && rsi > RsiOverbought;

			if (crossUp)
			{
				var volume = CalculateOrderVolume();
				if (volume > 0m)
				{
					BuyMarket(volume);
					_entryPrice = candle.ClosePrice;
					_stopPrice = _entryPrice - StopLossPoints * point;
					_takeProfitPrice = _entryPrice + TakeProfitPoints * point;
				}
			}
			else if (crossDown)
			{
				var volume = CalculateOrderVolume();
				if (volume > 0m)
				{
					SellMarket(volume);
					_entryPrice = candle.ClosePrice;
					_stopPrice = _entryPrice + StopLossPoints * point;
					_takeProfitPrice = _entryPrice - TakeProfitPoints * point;
				}
			}
		}

		_previousFast = fast;
		_previousSlow = slow;
	}

	private decimal GetPoint()
	{
		var step = Security?.PriceStep ?? 1m;
		return step > 0m ? step : 1m;
	}

	private decimal CalculateOrderVolume()
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var baseVolume = step;
		var divider = BalanceDivider;
		var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (divider > 0m && balance > 0m)
		{
			var count = Math.Floor((double)(balance / divider));
			baseVolume = (decimal)count * step;
			if (baseVolume < step)
				baseVolume = step;
		}

		var multiplier = CalculateMartingaleMultiplier();
		var volume = baseVolume * multiplier;

		if (volume < step)
			volume = step;

		var ratio = volume / step;
		volume = Math.Ceiling(ratio) * step;

		return volume;
	}

	private decimal CalculateMartingaleMultiplier()
	{
		if (MaxDoublings <= 0 || _martingaleStage <= 0)
			return 1m;

		var stage = Math.Min(_martingaleStage, MaxDoublings);
		return (decimal)Math.Pow(2d, stage);
	}

	private void RegisterWin()
	{
		_martingaleStage = 0;
	}

	private void ClosePosition()
	{
		if (Position > 0)
			SellMarket(Position);
		else if (Position < 0)
			BuyMarket(-Position);
	}

	private void RegisterLoss()
	{
		if (MaxDoublings <= 0)
		{
			_martingaleStage = 0;
			return;
		}

		if (_martingaleStage < MaxDoublings)
		{
			_martingaleStage++;
		}
		else
		{
			_martingaleStage = 0;
		}
	}
}