Auf GitHub ansehen

RSI Expert Trend Filter Strategy

Overview

  • Conversion of the MetaTrader 5 expert advisor RSI_Expert_v2.0 to StockSharp's high level strategy API.
  • Generates signals on the configured CandleType (default 1 hour) and executes trades at candle close.
  • Designed for net positions: the strategy keeps a single aggregate position instead of hedging multiple tickets.

Entry Logic

  1. RSI crossover – a long setup appears when the latest RSI value rises above RsiLevelDown while the previous finished candle was below the level. A short setup is triggered when RSI falls back under RsiLevelUp after being above it.
  2. Moving average filter – the original expert allows trading with or against a moving average crossover. The MaMode parameter reproduces the choices:
    • Off: ignore moving averages and trade only on RSI triggers.
    • Forward: allow longs only when the fast MA is above the slow MA, shorts only when it is below.
    • Reverse: invert the filter so that longs require the fast MA below the slow MA, matching the EA's “reverse” mode.

Both conditions must agree before the strategy opens a new market order. If a position is already open or an order is waiting, new signals are ignored until it finishes.

Trade Management

  • Initial stop loss and take profit are expressed in pips using the instrument PriceStep. Both are optional; setting a value of zero disables the respective exit.
  • When TrailingStopPips is greater than zero the stop will trail the price once profit exceeds TrailingStopPips + TrailingStepPips. The step value must be strictly positive when trailing is enabled (the strategy throws otherwise).
  • If UseMartingale is enabled, the next order volume doubles after the previous position closed with a loss (detected via realized PnL). Winning trades reset the multiplier.

Money Management

  • MoneyMode = FixedVolume keeps the same VolumeOrRiskValue for every entry.
  • MoneyMode = RiskPercent treats VolumeOrRiskValue as a percentage of portfolio equity and derives the quantity from the configured stop-loss distance. When no stop loss is specified the strategy falls back to the raw value.
  • Volumes are normalised to exchange rules using Security.MinVolume and Security.VolumeStep to avoid invalid order sizes.

Additional Implementation Notes

  • Trailing logic and stop/target checks are evaluated on finished candles to replicate the “new bar” behaviour of the MQL version.
  • The martingale flag uses realized PnL changes when a position is closed externally, so manual closures are also tracked.
  • Because StockSharp uses aggregate positions, simultaneous long and short trades (MT5 hedging mode) are not supported.

Parameters

Name Description
CandleType Timeframe used for indicator updates and signal generation.
StopLossPips Initial stop-loss distance in pips; zero disables the stop.
TakeProfitPips Initial take-profit distance in pips; zero disables the target.
TrailingStopPips Trailing stop distance. Requires a positive TrailingStepPips.
TrailingStepPips Extra pips needed before the trailing stop moves again.
MoneyMode Selects fixed lot sizing or risk-percent calculation.
VolumeOrRiskValue Lot size in fixed mode or percent risk in risk mode.
UseMartingale Doubles the next order volume after a losing trade.
FastMaPeriod Period of the fast moving average used by the trend filter.
SlowMaPeriod Period of the slow moving average used by the trend filter.
RsiPeriod Averaging length for the RSI indicator.
RsiLevelUp Upper RSI threshold that triggers short setups.
RsiLevelDown Lower RSI threshold that triggers long setups.
MaMode Enables or inverts the moving average confirmation filter.
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>
/// RSI crossover expert advisor converted from the "RSI_Expert_v2.0" MetaTrader 5 strategy.
/// Combines RSI threshold crosses with an optional moving average trend filter, fixed/percentage risk sizing, and martingale recovery.
/// </summary>
public class RsiExpertTrendFilterStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<MoneyManagementModes> _moneyMode;
	private readonly StrategyParam<decimal> _volumeOrRiskValue;
	private readonly StrategyParam<bool> _useMartingale;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiLevelUp;
	private readonly StrategyParam<decimal> _rsiLevelDown;
	private readonly StrategyParam<MaTradeModes> _maMode;

	private RelativeStrengthIndex _rsi = null!;
	private SimpleMovingAverage _fastMa = null!;
	private SimpleMovingAverage _slowMa = null!;

	private decimal? _previousRsi;
	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _pipSize;
	private bool _closeRequested;
	private bool _closeByStop;
	private bool _lastTradeWasLoss;
	private decimal _prevRealizedPnL;

	/// <summary>
	/// Money management modes supported by the strategy.
	/// </summary>
	public enum MoneyManagementModes
	{
		/// <summary>
		/// Use a fixed volume for every trade.
		/// </summary>
		FixedVolume,

		/// <summary>
		/// Calculate volume from risk percent and stop-loss distance.
		/// </summary>
		RiskPercent
	}

	/// <summary>
	/// Moving average filter configuration copied from the original EA.
	/// </summary>
	public enum MaTradeModes
	{
		/// <summary>
		/// Ignore the moving average filter.
		/// </summary>
		Off,

		/// <summary>
		/// Trade in the direction of the fast and slow moving average crossover.
		/// </summary>
		Forward,

		/// <summary>
		/// Trade in the opposite direction of the moving average crossover.
		/// </summary>
		Reverse
	}

	/// <summary>
/// Initializes a new instance of the <see cref="RsiExpertTrendFilterStrategy"/> class.
/// </summary>
public RsiExpertTrendFilterStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for generating signals", "General");

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Distance of the protective stop in pips", "Risk Management")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Distance of the profit target in pips", "Risk Management")
			;

		_trailingStopPips = Param(nameof(TrailingStopPips), 5)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing distance applied after activation", "Risk Management")
			;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Additional pips required before trailing moves again", "Risk Management")
			;

		_moneyMode = Param(nameof(MoneyMode), MoneyManagementModes.FixedVolume)
			.SetDisplay("Money Mode", "Choose fixed volume or percent risk sizing", "Money Management");

		_volumeOrRiskValue = Param(nameof(VolumeOrRiskValue), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume / Risk", "Lot size for fixed mode or percent risk when using risk mode", "Money Management")
			;

		_useMartingale = Param(nameof(UseMartingale), true)
			.SetDisplay("Use Martingale", "Double the next volume after a losing trade", "Money Management");

		_fastMaPeriod = Param(nameof(FastMaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Period of the fast moving average", "Indicators")
			;

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 200)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Period of the slow moving average", "Indicators")
			;

		_rsiPeriod = Param(nameof(RsiPeriod), 21)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Averaging period for RSI", "Indicators")
			;

		_rsiLevelUp = Param(nameof(RsiLevelUp), 70m)
			.SetRange(1m, 99m)
			.SetDisplay("RSI Level Up", "Upper RSI threshold for shorts", "Indicators")
			;

		_rsiLevelDown = Param(nameof(RsiLevelDown), 30m)
			.SetRange(1m, 99m)
			.SetDisplay("RSI Level Down", "Lower RSI threshold for longs", "Indicators")
			;

		_maMode = Param(nameof(MaMode), MaTradeModes.Forward)
			.SetDisplay("MA Trade Mode", "Direction of the moving average confirmation", "Indicators");
	}

	/// <summary>
	/// Candle type that drives indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Extra pips required before the trailing stop is tightened again.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Selected money management approach.
	/// </summary>
	public MoneyManagementModes MoneyMode
	{
		get => _moneyMode.Value;
		set => _moneyMode.Value = value;
	}

	/// <summary>
	/// Lot size in fixed mode or risk percent when money mode equals <see cref="MoneyManagementModes.RiskPercent"/>.
	/// </summary>
	public decimal VolumeOrRiskValue
	{
		get => _volumeOrRiskValue.Value;
		set => _volumeOrRiskValue.Value = value;
	}

	/// <summary>
	/// Enables martingale doubling after losing positions.
	/// </summary>
	public bool UseMartingale
	{
		get => _useMartingale.Value;
		set => _useMartingale.Value = value;
	}

	/// <summary>
	/// Period for the fast moving average.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Period for the slow moving average.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Averaging period for the RSI indicator.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Upper RSI threshold used to detect short entries.
	/// </summary>
	public decimal RsiLevelUp
	{
		get => _rsiLevelUp.Value;
		set => _rsiLevelUp.Value = value;
	}

	/// <summary>
	/// Lower RSI threshold used to detect long entries.
	/// </summary>
	public decimal RsiLevelDown
	{
		get => _rsiLevelDown.Value;
		set => _rsiLevelDown.Value = value;
	}

	/// <summary>
	/// Moving average confirmation mode.
	/// </summary>
	public MaTradeModes MaMode
	{
		get => _maMode.Value;
		set => _maMode.Value = value;
	}

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

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

		_previousRsi = null;
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_pipSize = 0m;
		_closeRequested = false;
		_closeByStop = false;
		_lastTradeWasLoss = false;
		_prevRealizedPnL = 0m;
	}

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

		if (TrailingStopPips > 0 && TrailingStepPips == 0)
			throw new InvalidOperationException("Trailing is not possible when the trailing step is zero.");

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

		_fastMa = new SMA
		{
			Length = FastMaPeriod
		};

		_slowMa = new SMA
		{
			Length = SlowMaPeriod
		};

		_pipSize = CalculatePipSize();
		_prevRealizedPnL = PnL;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_rsi, _fastMa, _slowMa, ProcessCandle)
			.Start();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent));

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

	private void ProcessCandle(ICandleMessage candle, decimal rsiValue, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		ManageActivePosition(candle);

		var currentRsi = rsiValue;
		var previousRsi = _previousRsi;

		if (Position != 0m)
		{
			_previousRsi = currentRsi;
			return;
		}

		var rsiSignal = 0;
		if (previousRsi.HasValue)
		{
			if (currentRsi > RsiLevelDown && previousRsi.Value < RsiLevelDown)
				rsiSignal = 1;
			else if (currentRsi < RsiLevelUp && previousRsi.Value > RsiLevelUp)
				rsiSignal = -1;
		}

		var maSignal = 0;
		switch (MaMode)
		{
			case MaTradeModes.Forward:
				if (fastValue > slowValue)
					maSignal = 1;
				else if (fastValue < slowValue)
					maSignal = -1;
				break;
			case MaTradeModes.Reverse:
				if (fastValue < slowValue)
					maSignal = 1;
				else if (fastValue > slowValue)
					maSignal = -1;
				break;
		}

		var finalSignal = 0;
		if (rsiSignal == 1 && (MaMode == MaTradeModes.Off || maSignal == 1))
			finalSignal = 1;
		else if (rsiSignal == -1 && (MaMode == MaTradeModes.Off || maSignal == -1))
			finalSignal = -1;

		if (finalSignal > 0)
		{
			BuyMarket();
		}
		else if (finalSignal < 0)
		{
			SellMarket();
		}

		_previousRsi = currentRsi;
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position == 0m)
			return;

		if (_entryPrice == null)
			return;

		var isLong = Position > 0m;
		var absPosition = Math.Abs(Position);
		var stepDistance = TrailingStepPips > 0 ? TrailingStepPips * _pipSize : 0m;
		var trailingDistance = TrailingStopPips > 0 ? TrailingStopPips * _pipSize : 0m;

		if (TrailingStopPips > 0 && trailingDistance > 0m)
		{
			if (isLong)
			{
				var profitDistance = candle.ClosePrice - _entryPrice.Value;
				if (profitDistance > trailingDistance + stepDistance)
				{
					var candidate = candle.ClosePrice - trailingDistance;
					if (!_stopLossPrice.HasValue || _stopLossPrice.Value < candidate)
						_stopLossPrice = candidate;
				}
			}
			else
			{
				var profitDistance = _entryPrice.Value - candle.ClosePrice;
				if (profitDistance > trailingDistance + stepDistance)
				{
					var candidate = candle.ClosePrice + trailingDistance;
					if (!_stopLossPrice.HasValue || _stopLossPrice.Value > candidate)
						_stopLossPrice = candidate;
				}
			}
		}

		if (!_closeRequested && _takeProfitPrice.HasValue)
		{
			if (isLong && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Position);
				_closeRequested = true;
				_closeByStop = false;
				return;
			}

			if (!isLong && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(absPosition);
				_closeRequested = true;
				_closeByStop = false;
				return;
			}
		}

		if (!_closeRequested && _stopLossPrice.HasValue)
		{
			if (isLong && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(Position);
				_closeRequested = true;
				_closeByStop = true;
				return;
			}

			if (!isLong && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(absPosition);
				_closeRequested = true;
				_closeByStop = true;
			}
		}
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
		{
			if (_closeRequested)
			{
				_lastTradeWasLoss = _closeByStop;
				_closeRequested = false;
				_closeByStop = false;
				_prevRealizedPnL = PnL;
			}
			else
			{
				var realizedPnL = PnL;
				if (realizedPnL > _prevRealizedPnL)
					_lastTradeWasLoss = false;
				else if (realizedPnL < _prevRealizedPnL)
					_lastTradeWasLoss = true;

				_prevRealizedPnL = realizedPnL;
			}

			ResetPositionState();
		}
		else
		{
			_entryPrice ??= (_entryPrice ?? 0m);

			InitializePositionState(Position > 0m, (_entryPrice ?? 0m));
			_closeRequested = false;
			_closeByStop = false;
		}
	}

	private decimal GetOrderVolume()
	{
		var volume = MoneyMode == MoneyManagementModes.FixedVolume
			? VolumeOrRiskValue
			: CalculateRiskVolume();

		if (UseMartingale && _lastTradeWasLoss)
			volume *= 2m;

		return NormalizeVolume(volume);
	}

	private decimal CalculateRiskVolume()
	{
		if (StopLossPips <= 0)
			return VolumeOrRiskValue;

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return VolumeOrRiskValue;

		var riskAmount = equity * VolumeOrRiskValue / 100m;
		var stopDistance = StopLossPips * _pipSize;

		return stopDistance > 0m ? riskAmount / stopDistance : VolumeOrRiskValue;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var minVolume = Security?.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Max(1m, Math.Round(volume / step, MidpointRounding.AwayFromZero));
			volume = steps * step;
		}

		return volume > 0m ? volume : 0m;
	}

	private void InitializePositionState(bool isLong, decimal entryPrice)
	{
		_entryPrice = entryPrice;

		_stopLossPrice = StopLossPips > 0
			? entryPrice + (isLong ? -1m : 1m) * StopLossPips * _pipSize
			: null;

		_takeProfitPrice = TakeProfitPips > 0
			? entryPrice + (isLong ? 1m : -1m) * TakeProfitPips * _pipSize
			: null;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step > 0m)
			return step;

		var decimals = Security?.Decimals ?? 4;
		decimal pip = 1m;
		for (var i = 0; i < decimals; i++)
			pip /= 10m;

		return pip;
	}

	private bool HasActiveOrders()
	{
		return Orders.Any(o => o.State == OrderStates.Active || o.State == OrderStates.Pending);
	}
}