Открыть на GitHub

Стратегия «Trade on Qualified RSI»

Описание

Данная реализация переносит эксперт "Trade on qualified RSI" из MetaTrader в инфраструктуру StockSharp. Алгоритм относится к контртрендовым: он ищет затяжные экстремальные значения индикатора RSI и открывает позицию против направления импульса после подтверждения несколькими свечами. Управление риском выполняется с помощью плавающего стоп-лосса, выраженного в шагах цены инструмента.

Логика сигналов

Индикатор

  • Индекс относительной силы (RSI) с настраиваемым периодом расчёта (по умолчанию 28).
  • Расчёт выполняется по выбранным свечам (по умолчанию 15-минутные свечи).

Условия для короткой позиции

  1. Последняя закрытая свеча имеет значение RSI выше либо равно верхнему порогу (по умолчанию 55).
  2. Каждая из предыдущих CountBars свечей также находилась выше этого порога. Внутри стратегии ведётся счётчик последовательных свечей; сигнал формируется, когда счётчик достигает CountBars + 1.
  3. Открытых позиций нет. При выполнении условий отправляется рыночная заявка на продажу с заданным объёмом, а цена закрытия свечи запоминается как цена входа.

Условия для длинной позиции

  1. Последняя закрытая свеча имеет значение RSI ниже либо равно нижнему порогу (по умолчанию 45).
  2. Каждая из предыдущих CountBars свечей также находилась ниже этого порога (всего требуется CountBars + 1 последовательных наблюдений).
  3. Позиция отсутствует. Стратегия отправляет рыночную заявку на покупку и сохраняет цену закрытия как цену входа.

Управление позицией

  • Начальный стоп: сразу после открытия рассчитывается уровень стоп-лосса на расстоянии StopLossPoints шагов цены от цены входа (ниже для лонга, выше для шорта). Шаг цены берётся из Security.PriceStep; при его отсутствии используется значение 1.
  • Трейлинг: на каждой закрытой свече стоп подтягивается к цене закрытия. Для длинной позиции новый стоп равен Close - StopLossPoints * PriceStep, если это значение выше текущего стопа. Для короткой позиции новый стоп равен Close + StopLossPoints * PriceStep, если это значение ниже текущего стопа.
  • Выход: если минимум свечи опускается ниже стопа в лонге либо максимум поднимается выше стопа в шорте, стратегия полностью закрывает позицию рыночной заявкой. Дополнительных целей по прибыли и разворотов нет; новые входы возможны только после закрытия предыдущей позиции.

Параметры

Имя Назначение Значение по умолчанию
RsiPeriod Период расчёта индикатора RSI. 28
UpperThreshold Порог RSI для квалификации шорт-сигнала. 55
LowerThreshold Порог RSI для квалификации лонг-сигнала. 45
CountBars Количество предыдущих свечей, которые должны удерживаться за порогом (CountBars + 1 последовательных свечей). 5
StopLossPoints Расстояние до стоп-лосса в шагах цены (StopLossPoints * PriceStep). 21
TradeVolume Объём рыночной заявки при входе. 1
CandleType Тип свечей, используемых для анализа. 15-минутные свечи

Все параметры допускают оптимизацию. Пороговые значения задаются в десятичных числах, что позволяет тонко настроить границы RSI под конкретный инструмент.

Особенности реализации

  • Используется высокоуровневый API: подписка на свечи с помощью SubscribeCandles(...).Bind(...), что гарантирует работу только с полностью сформированными свечами.
  • Значения RSI не запрашиваются по индексу — вместо этого применяется счётчик последовательных свечей, удовлетворяющих условиям.
  • Стоп-лоссы моделируются внутри стратегии: при срабатывании условия закрытия отправляется рыночная заявка, что упрощает переносимость.
  • В журнал выводятся сообщения о сделках, что облегчает контроль работы и повторяет подробный лог оригинального эксперта.

Использование

  1. Добавьте стратегию в приложение StockSharp, назначьте инструмент и портфель, настройте источник свечей.
  2. При необходимости измените пороговые значения RSI, количество подтверждающих свечей и размер шага стоп-лосса, чтобы адаптировать стратегию к волатильности инструмента.
  3. Запустите стратегию и наблюдайте за журналом, чтобы оценить моменты входа и траекторию подтягивания стопа.
  4. Для подбора оптимальных параметров воспользуйтесь модулем оптимизации StockSharp.
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;
		}
	}
}