Auf GitHub ansehen

Rsi Test Strategy

Overview

RsiTestStrategy converts the MetaTrader 4 expert advisor RSI_Test into StockSharp's high level API. The strategy combines a fast RSI momentum filter with simple candle confirmation and risk-aware position sizing. It trades a single instrument defined by the host strategy and uses completed candles only, mirroring the tick-to-close logic of the original code.

Trading Rules

  1. Calculate the Relative Strength Index with the configurable RsiPeriod.
  2. Go long when the RSI is rising from an oversold region (BuyLevel) and the current candle opens above the previous one.
  3. Go short when the RSI is falling from an overbought region (SellLevel) and the current candle opens below the previous one.
  4. Respect the MaxOpenPositions limit. A value of 0 disables the cap; otherwise the net exposure cannot exceed MaxOpenPositions * Volume.
  5. Manage exits through a stair-style trailing stop that activates once price advances by TrailingDistanceSteps ticks beyond the average entry price.
  6. No explicit take-profit is used. Positions exit when the trailing stop is triggered or when the trading session terminates the strategy.

Position Sizing and Risk

  • The strategy derives a tentative order size from RiskPercentage of the portfolio's current value. When the instrument provides margin data (Security.MarginBuy/Security.MarginSell) the required capital per lot is honoured; otherwise the amount is divided by the latest close price as a conservative fallback.
  • Volumes are rounded to Security.VolumeStep (or two decimals if the step is unknown) and clamped inside the Security.MinVolume/Security.MaxVolume range.
  • Set RiskPercentage to zero to disable dynamic sizing and always trade the configured Volume.

Trailing Stop Behaviour

  • TrailingDistanceSteps expresses the distance in price steps (Security.PriceStep). If the instrument does not expose a step, the distance is treated as a direct price offset.
  • Once the close or intrabar high crosses the activation level (entry + distance for longs, entry - distance for shorts) the strategy arms the trailing stop at the same offset beyond the entry price.
  • The protective stop is applied only once per position, exactly like the original EA that moves the stop from break-even to the first stair and keeps it there.

Parameters

Name Description Default
RsiPeriod RSI lookback period. 14
BuyLevel Oversold threshold that prepares a long setup. 12
SellLevel Overbought threshold that prepares a short setup. 88
RiskPercentage Portfolio share used for position sizing. Set 0 to ignore. 10
TrailingDistanceSteps Distance (in price steps) required to arm the trailing stop. 50
MaxOpenPositions Maximum simultaneous positions; 0 removes the limit. 1
CandleType Primary timeframe for calculations. 15 minutes
Volume Base volume when risk sizing cannot be resolved. 1

Usage Notes

  1. Attach the strategy to a security that exposes accurate PriceStep, VolumeStep, and margin metadata for the best match with the MQL behaviour.
  2. The algorithm checks only completed candles (CandleStates.Finished), so backtests should use the same timeframe as production.
  3. StartProtection() from the base class is enabled in OnStarted, allowing StockSharp's built-in risk control to manage unexpected position remnants.
  4. Because the original expert advisor launched MetaTrader optimizations through GlobalVariableGet, that behaviour is intentionally omitted. Configure the parameters directly within StockSharp.
  5. Combine the strategy with a portfolio that updates Portfolio.CurrentValue for dynamic risk sizing. Without it the strategy gracefully falls back to the static Volume.
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// RSI-based strategy with volume sizing and stair-like trailing stop.
/// </summary>
public class RsiTestStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _buyLevel;
	private readonly StrategyParam<decimal> _sellLevel;
	private readonly StrategyParam<decimal> _riskPercentage;
	private readonly StrategyParam<int> _trailingDistanceSteps;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private decimal? _previousRsi;
	private decimal? _previousOpen;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private bool _trailingArmed;
	private decimal _priceStep;

	/// <summary>
	/// Initialize <see cref="RsiTestStrategy"/>.
	/// </summary>
	public RsiTestStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 7)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Lookback period for RSI", "Indicators")

			.SetOptimize(7, 28, 1);

		_buyLevel = Param(nameof(BuyLevel), 40m)
			.SetDisplay("RSI Buy Level", "Oversold threshold for long entries", "Trading");

		_sellLevel = Param(nameof(SellLevel), 60m)
			.SetDisplay("RSI Sell Level", "Overbought threshold for short entries", "Trading");

		_riskPercentage = Param(nameof(RiskPercentage), 10m)
			.SetDisplay("Risk Percentage", "Portfolio percentage used for sizing", "Risk");

		_trailingDistanceSteps = Param(nameof(TrailingDistanceSteps), 50)
			.SetDisplay("Trailing Distance Steps", "Steps before activating trailing stop", "Risk");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
			.SetDisplay("Max Open Positions", "Maximum simultaneous positions. 0 disables the limit.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "Data");
	}

	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	public decimal BuyLevel
	{
		get => _buyLevel.Value;
		set => _buyLevel.Value = value;
	}

	public decimal SellLevel
	{
		get => _sellLevel.Value;
		set => _sellLevel.Value = value;
	}

	public decimal RiskPercentage
	{
		get => _riskPercentage.Value;
		set => _riskPercentage.Value = value;
	}

	public int TrailingDistanceSteps
	{
		get => _trailingDistanceSteps.Value;
		set => _trailingDistanceSteps.Value = value;
	}

	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_previousRsi = null;
		_previousOpen = null;
		_entryPrice = null;
		_stopPrice = null;
		_trailingArmed = false;
		_priceStep = 0m;
	}

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

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		_priceStep = Security?.PriceStep ?? 0m;

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

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

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
	{
		// Only react to fully formed candles to match the MQL implementation.
		if (candle.State != CandleStates.Finished)
		return;

		// Manage trailing logic and exits before attempting fresh entries.
		ManagePosition(candle);

		if (!_rsi.IsFormed)
		{
			_previousRsi = rsiValue;
			_previousOpen = candle.OpenPrice;
			return;
		}

		if (_previousRsi is null || _previousOpen is null)
		{
			_previousRsi = rsiValue;
			_previousOpen = candle.OpenPrice;
			return;
		}

		if (rsiValue < BuyLevel && Position <= 0)
		{
			TryEnterLong(candle);
		}
		else if (rsiValue > SellLevel && Position >= 0)
		{
			TryEnterShort(candle);
		}

		_previousRsi = rsiValue;
		_previousOpen = candle.OpenPrice;
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		// Close short position first if needed
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			ResetPositionState();
		}

		if (Position == 0)
		{
			BuyMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = null;
			_trailingArmed = false;
		}
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		// Close long position first if needed
		if (Position > 0)
		{
			SellMarket(Math.Abs(Position));
			ResetPositionState();
		}

		if (Position == 0)
		{
			SellMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = null;
			_trailingArmed = false;
		}
	}

	private void ManagePosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetPositionState();
			return;
		}

		var avgPrice = _entryPrice;
		if (avgPrice > 0m)
		_entryPrice = avgPrice;

		if (Position > 0)
		{
			UpdateTrailingForLong(candle);
			TryExitLong(candle);
		}
		else if (Position < 0)
		{
			UpdateTrailingForShort(candle);
			TryExitShort(candle);
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingDistanceSteps <= 0 || _entryPrice is null || _trailingArmed)
		return;

		var trailingDistance = GetPriceOffset(TrailingDistanceSteps);
		if (trailingDistance <= 0m)
		return;

		var activationPrice = _entryPrice.Value + trailingDistance;
		if (candle.HighPrice < activationPrice)
		return;

		_stopPrice = _entryPrice.Value + trailingDistance;
		_trailingArmed = true;
		LogInfo($"Activated long trailing stop at {_stopPrice:0.#####}.");
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingDistanceSteps <= 0 || _entryPrice is null || _trailingArmed)
		return;

		var trailingDistance = GetPriceOffset(TrailingDistanceSteps);
		if (trailingDistance <= 0m)
		return;

		var activationPrice = _entryPrice.Value - trailingDistance;
		if (candle.LowPrice > activationPrice)
		return;

		_stopPrice = _entryPrice.Value - trailingDistance;
		_trailingArmed = true;
		LogInfo($"Activated short trailing stop at {_stopPrice:0.#####}.");
	}

	private void TryExitLong(ICandleMessage candle)
	{
		if (_stopPrice is null)
		return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (candle.LowPrice > _stopPrice.Value)
		return;

		SellMarket(volume);
		ResetPositionState();
	}

	private void TryExitShort(ICandleMessage candle)
	{
		if (_stopPrice is null)
		return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (candle.HighPrice < _stopPrice.Value)
		return;

		BuyMarket(volume);
		ResetPositionState();
	}

	private decimal CalculateOrderVolume(decimal referencePrice)
	{
		var volume = Volume;

		if (RiskPercentage > 0m)
		{
			var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			var riskCapital = portfolioValue * RiskPercentage / 100m;

			if (riskCapital > 0m)
			{
				var margin = GetSecurityValue<decimal?>(Level1Fields.MarginBuy) ?? GetSecurityValue<decimal?>(Level1Fields.MarginSell) ?? 0m;

				if (margin > 0m)
				{
					volume = riskCapital / margin;
				}
				else if (referencePrice > 0m)
				{
					volume = riskCapital / referencePrice;
				}
			}
		}

		volume = RoundVolume(volume);

		// Ensure volume is at least the base Volume when calculation produces too small a value
		if (volume <= 0m)
			volume = Volume;

		var minVolume = Security?.MinVolume;
		if (minVolume != null && minVolume.Value > 0m && volume < minVolume.Value)
		{
			volume = minVolume.Value;
		}

		var maxVolume = Security?.MaxVolume;
		if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
		{
			volume = maxVolume.Value;
		}

		return volume;
	}

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

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			if (steps <= 0m)
			{
				return step;
			}

			return steps * step;
		}

		return Math.Round(volume, 2, MidpointRounding.ToZero);
	}

	private bool HasCapacityForNewPosition(decimal volume)
	{
		if (MaxOpenPositions <= 0)
		{
			return true;
		}

		if (volume <= 0m)
		{
			return false;
		}

		var exposure = Math.Abs(Position);
		var maxExposure = MaxOpenPositions * volume;

		return exposure + volume <= maxExposure + volume * 0.0001m;
	}

	private decimal GetPriceOffset(int steps)
	{
		if (steps <= 0)
		{
			return 0m;
		}

		if (_priceStep > 0m)
		{
			return steps * _priceStep;
		}

		return steps;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_trailingArmed = false;
	}
}