Auf GitHub ansehen

Trade on Qualified RSI Strategy

Overview

This strategy reproduces the MetaTrader "Trade on qualified RSI" expert advisor using StockSharp's high-level API. It behaves as a contrarian system: it interprets extended Relative Strength Index (RSI) readings as exhaustion and opens a position against the prevailing move after the momentum persists for several candles. Trailing stops are managed in price steps so that the stop follows the trade only when price moves in the trade's favor.

Signal Logic

Indicator

  • Relative Strength Index with a configurable period (default: 28).
  • Calculated on the selected candle subscription (default: 15-minute candles).

Short Entry

  1. The last closed candle has RSI greater than or equal to the upper threshold (default: 55).
  2. Each of the previous CountBars closed candles also had RSI above the same threshold. Internally the strategy counts consecutive bars; the signal triggers once the counter reaches CountBars + 1.
  3. No open position is active. When triggered, the strategy sells at market with the configured volume and stores the candle close as the entry price.

Long Entry

  1. The last closed candle has RSI lower than or equal to the lower threshold (default: 45).
  2. Each of the previous CountBars closed candles also had RSI below the same threshold (CountBars + 1 consecutive readings are required).
  3. No open position exists. When triggered, the strategy buys at market with the configured volume and records the entry price.

Position Management

  • Initial stop: right after entry the stop price is placed StopLossPoints price steps away from the entry close (below for longs, above for shorts). Price steps are obtained from Security.PriceStep; if the security does not define it the strategy falls back to 1.
  • Trailing: on each finished candle the stop is tightened towards the current close. For long positions the stop becomes Close - StopLossPoints * PriceStep when that value is above the previous stop. For short positions the stop becomes Close + StopLossPoints * PriceStep when that value is below the previous stop.
  • Exit: if the candle low crosses below the stop while long, or the candle high crosses above the stop while short, the strategy exits the entire position at market. There are no additional profit targets or reverse signals; new entries occur only after the previous position is closed.

Parameters

Name Description Default
RsiPeriod Lookback length for the RSI indicator. 28
UpperThreshold RSI level that qualifies a short setup. 55
LowerThreshold RSI level that qualifies a long setup. 45
CountBars How many previous bars must stay beyond the threshold (CountBars + 1 consecutive bars in total). 5
StopLossPoints Stop distance expressed in price steps. The actual price offset equals StopLossPoints * PriceStep. 21
TradeVolume Volume submitted with each entry order. 1
CandleType Candle subscription used for indicator calculations. 15-minute candles

All parameters can be optimized. The thresholds allow decimal values, so fine-grained tuning of the RSI boundaries is possible.

Implementation Notes

  • The strategy uses SubscribeCandles(...).Bind(...) to feed the RSI indicator and to react only when the candle is fully formed.
  • RSI values are not read back from the indicator by index; instead, counters track how many consecutive finished candles respect the thresholds.
  • Protective stops are simulated inside the strategy. Orders are closed at market when the stop level is crossed instead of placing separate stop orders.
  • Logging messages are produced for entries and exits, mirroring the verbose output of the original expert advisor.

Usage

  1. Add the strategy to a StockSharp application, assign the desired security and portfolio, and configure the candle series.
  2. Adjust the RSI thresholds, number of qualifying bars, and stop distance to match the target instrument's volatility.
  3. Start the strategy. Monitor the log to see when signals occur and how the trailing stop evolves.
  4. Consider running the built-in optimizer to search for better combinations of thresholds or stop distances for specific markets.
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>
/// Contrarian RSI strategy converted from the "Trade on qualified RSI" expert advisor.
/// </summary>
public class TradeOnQualifiedRSIStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _upperThreshold;
	private readonly StrategyParam<decimal> _lowerThreshold;
	private readonly StrategyParam<int> _countBars;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private decimal? _stopPrice;
	private decimal _entryPrice;
	private int _aboveCounter;
	private int _belowCounter;

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

	/// <summary>
	/// Upper RSI threshold used to qualify short entries.
	/// </summary>
	public decimal UpperThreshold
	{
		get => _upperThreshold.Value;
		set => _upperThreshold.Value = value;
	}

	/// <summary>
	/// Lower RSI threshold used to qualify long entries.
	/// </summary>
	public decimal LowerThreshold
	{
		get => _lowerThreshold.Value;
		set => _lowerThreshold.Value = value;
	}

	/// <summary>
	/// Number of previous RSI bars that must stay beyond the threshold.
	/// </summary>
	public int CountBars
	{
		get => _countBars.Value;
		set => _countBars.Value = value;
	}

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

	/// <summary>
	/// Order volume used for entries.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

	/// <summary>
	/// Initialize <see cref="TradeOnQualifiedRSIStrategy"/>.
	/// </summary>
	public TradeOnQualifiedRSIStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 28)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Lookback period for RSI calculation.", "RSI")
			
			.SetOptimize(10, 50, 2);

		_upperThreshold = Param(nameof(UpperThreshold), 65m)
			.SetDisplay("Upper Threshold", "RSI level used to qualify short signals.", "RSI")

			.SetOptimize(50m, 70m, 1m);

		_lowerThreshold = Param(nameof(LowerThreshold), 35m)
			.SetDisplay("Lower Threshold", "RSI level used to qualify long signals.", "RSI")

			.SetOptimize(30m, 50m, 1m);

		_countBars = Param(nameof(CountBars), 8)
			.SetGreaterThanZero()
			.SetDisplay("Qualification Bars", "How many previous RSI bars must stay beyond the threshold.", "Signals")

			.SetOptimize(1, 10, 1);

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss Points", "Stop loss distance expressed in price steps.", "Risk")

			.SetOptimize(5, 50, 5);

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Order volume used for entries.", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Source timeframe for RSI calculation.", "General");
	}

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

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

		Volume = TradeVolume;
		_stopPrice = null;
		_entryPrice = 0m;
		_aboveCounter = 0;
		_belowCounter = 0;
	}

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

		Volume = TradeVolume;

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

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

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

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

		if (_rsi == null || !_rsi.IsFormed)
		{
			_aboveCounter = 0;
			_belowCounter = 0;
			return;
		}

		if (Volume <= 0)
			return;

		var distance = CalculateStopDistance();
		if (distance <= 0)
			return;

		UpdateCounters(rsiValue);

		var requiredBars = CountBars + 1;

		if (Position == 0)
		{
			_stopPrice = null;
			_entryPrice = 0m;

			var shortSignal = rsiValue >= UpperThreshold && _aboveCounter >= requiredBars;
			var longSignal = rsiValue <= LowerThreshold && _belowCounter >= requiredBars;

			if (shortSignal)
			{
				this.LogInfo($"Open short: RSI={rsiValue:F2}, counter={_aboveCounter}");
				SellMarket();
				_entryPrice = candle.ClosePrice;
				_stopPrice = candle.ClosePrice + distance;
				return;
			}

			if (longSignal)
			{
				this.LogInfo($"Open long: RSI={rsiValue:F2}, counter={_belowCounter}");
				BuyMarket();
				_entryPrice = candle.ClosePrice;
				_stopPrice = candle.ClosePrice - distance;
			}

			return;
		}

		if (Position > 0)
		{
			if (_stopPrice == null)
				_stopPrice = _entryPrice - distance;

			var newStop = candle.ClosePrice - distance;
			if (_stopPrice == null || newStop > _stopPrice)
				_stopPrice = newStop;

			if (_stopPrice != null && candle.LowPrice <= _stopPrice)
			{
				this.LogInfo($"Exit long via stop at {_stopPrice:F5}");
				SellMarket();
				_stopPrice = null;
				_entryPrice = 0m;
			}

			return;
		}

		if (Position < 0)
		{
			if (_stopPrice == null)
				_stopPrice = _entryPrice + distance;

			var newStop = candle.ClosePrice + distance;
			if (_stopPrice == null || newStop < _stopPrice)
				_stopPrice = newStop;

			if (_stopPrice != null && candle.HighPrice >= _stopPrice)
			{
				this.LogInfo($"Exit short via stop at {_stopPrice:F5}");
				BuyMarket();
				_stopPrice = null;
				_entryPrice = 0m;
			}
		}
	}

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

		return StopLossPoints * step;
	}

	private void UpdateCounters(decimal rsiValue)
	{
		// Track consecutive closes above and below the thresholds.
		if (rsiValue >= UpperThreshold)
		{
			_aboveCounter++;
		}
		else
		{
			_aboveCounter = 0;
		}

		if (rsiValue <= LowerThreshold)
		{
			_belowCounter++;
		}
		else
		{
			_belowCounter = 0;
		}
	}
}