Открыть на GitHub

Стратегия Hpcs Inter6 RSI

Обзор

Hpcs Inter6 RSI переносит эксперта MetaTrader _HPCS_Inter6_MT4_EA_V01_WE на высокоуровневый API StockSharp. Алгоритм анализирует индекс относительной силы (RSI) на настраиваемых свечах и реагирует на резкие развороты около классических уровней 70/30. Пересечение RSI выше 70 инициирует короткую позицию, пересечение ниже 30 — длинную. Каждая сделка сразу получает симметричные уровни тейк-профита и стоп-лосса, заданные в пунктах.

Данные и индикаторы

  • Источники данных — свечи выбранного таймфрейма (по умолчанию 1 час).
  • Индикатор — RSI с настраиваемым периодом (по умолчанию 14), который пересчитывается через механизм Bind.

Логика входа

  1. Стратегия ожидает закрытия свечи, чтобы работать только с завершёнными данными.
  2. После каждой закрытой свечи сравниваются текущие и предыдущие значения RSI.
  3. Продажа: если RSI пересекает уровень UpperLevel (70) снизу вверх, отправляется рыночная заявка на продажу. Наличие длинной позиции приводит к её закрытию перед открытием шорта, поэтому итоговый объём шорта равен параметру TradeVolume.
  4. Покупка: если RSI пересекает уровень LowerLevel (30) сверху вниз, отправляется рыночная заявка на покупку. Активные короткие позиции закрываются перед открытием лонга, итоговый объём равен TradeVolume.
  5. В течение одной свечи допускается только один сигнал — дополнительная защита от повторных входов на той же свече полностью повторяет логику MQL.

Логика выхода

  • После входа вычисляются уровни тейк-профита и стоп-лосса на одинаковом расстоянии от цены входа.
  • Для длинной позиции выход выполняется, если максимум свечи достигает тейк-профита или минимум опускается до стоп-лосса.
  • Для короткой позиции выход выполняется, если минимум свечи достигает тейк-профита или максимум поднимается к стоп-лоссу.
  • При отсутствии позиции защитные уровни сбрасываются.

Расстояние в пунктах переводится в цену через минимальный шаг котировки инструмента. Для инструментов с тремя или пятью знаками после запятой расстояние дополнительно умножается на десять, что соответствует определению пункта в MetaTrader.

Параметры

Параметр Значение по умолчанию Описание
CandleType Таймфрейм 1 час Источник свечей для расчёта RSI.
RsiLength 14 Период RSI.
UpperLevel 70 Уровень RSI для открытия продаж при пересечении снизу вверх.
LowerLevel 30 Уровень RSI для открытия покупок при пересечении сверху вниз.
TradeVolume 1 Объём рыночных заявок; перед разворотом противоположная позиция закрывается.
OffsetInPips 10 Расстояние до тейк-профита и стоп-лосса в пунктах.

Все параметры оформлены через StrategyParam, поэтому их можно оптимизировать в StockSharp.

Дополнительно

  • Выход по тейк-профиту и стоп-лоссу моделируется по экстремумам свечей, что соответствует фиксированным ценовым уровням в оригинальном советнике.
  • Стратегия не выставляет отложенные ордера — все операции выполняются рыночными заявками.
  • При наличии области на графике автоматически добавляются свечи и кривая RSI для визуального контроля.
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the MetaTrader strategy _HPCS_Inter6_MT4_EA_V01_WE.
/// Trades RSI reversals at the 70/30 levels with symmetric fixed targets and stops.
/// </summary>
public class HpcsInter6RsiStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<decimal> _upperLevel;
	private readonly StrategyParam<decimal> _lowerLevel;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _offsetInPips;
	private readonly StrategyParam<int> _signalCooldownCandles;

	private RelativeStrengthIndex _rsi;
	private decimal? _previousRsi;
	private DateTimeOffset? _lastSignalTime;
	private decimal? _targetPrice;
	private decimal? _stopPrice;
	private bool _isLongPosition;
	private int _candlesSinceTrade;

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

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

	/// <summary>
	/// Upper RSI level that triggers short entries when crossed from below.
	/// </summary>
	public decimal UpperLevel
	{
		get => _upperLevel.Value;
		set => _upperLevel.Value = value;
	}

	/// <summary>
	/// Lower RSI level that triggers long entries when crossed from above.
	/// </summary>
	public decimal LowerLevel
	{
		get => _lowerLevel.Value;
		set => _lowerLevel.Value = value;
	}

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

	/// <summary>
	/// Target and stop distance expressed in pips.
	/// </summary>
	public decimal OffsetInPips
	{
		get => _offsetInPips.Value;
		set => _offsetInPips.Value = value;
	}

	public int SignalCooldownCandles
	{
		get => _signalCooldownCandles.Value;
		set => _signalCooldownCandles.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="HpcsInter6RsiStrategy"/> class.
	/// </summary>
	public HpcsInter6RsiStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for RSI evaluation", "General");

		_rsiLength = Param(nameof(RsiLength), 7)
			.SetGreaterThanZero()
			.SetDisplay("RSI Length", "Lookback period for RSI", "Parameters")

			.SetOptimize(5, 40, 1);

		_upperLevel = Param(nameof(UpperLevel), 65m)
			.SetDisplay("Upper RSI", "Upper RSI level for shorts", "Parameters")

			.SetOptimize(60m, 90m, 5m);

		_lowerLevel = Param(nameof(LowerLevel), 35m)
			.SetDisplay("Lower RSI", "Lower RSI level for longs", "Parameters")

			.SetOptimize(10m, 40m, 5m);

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

		_offsetInPips = Param(nameof(OffsetInPips), 30m)
			.SetGreaterThanZero()
			.SetDisplay("Offset (pips)", "Target and stop distance in pips", "Risk")
			
			.SetOptimize(5m, 30m, 5m);

		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 4)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_rsi = null;
		_previousRsi = null;
		_lastSignalTime = null;
		_targetPrice = null;
		_stopPrice = null;
		_isLongPosition = false;
		_candlesSinceTrade = SignalCooldownCandles;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_previousRsi = null;
		_lastSignalTime = null;
		_targetPrice = null;
		_stopPrice = null;
		_isLongPosition = false;
		_candlesSinceTrade = SignalCooldownCandles;

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiLength
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(_rsi, ProcessCandle)
			.Start();

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

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

		if (!_rsi.IsFormed)
			return;

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		UpdateActivePositionTargets(candle);

		var previousRsi = _previousRsi;
		_previousRsi = rsiValue;

		if (previousRsi is null)
			return;

		var candleTime = candle.OpenTime;

		if (_lastSignalTime.HasValue && _lastSignalTime.Value == candleTime)
			return;

		if (_candlesSinceTrade >= SignalCooldownCandles && TryEnterShort(candle, rsiValue, previousRsi.Value))
		{
			_lastSignalTime = candleTime;
			_candlesSinceTrade = 0;
			return;
		}

		if (_candlesSinceTrade >= SignalCooldownCandles && TryEnterLong(candle, rsiValue, previousRsi.Value))
		{
			_lastSignalTime = candleTime;
			_candlesSinceTrade = 0;
		}
	}

	private void UpdateActivePositionTargets(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (!_isLongPosition)
			{
				_targetPrice = null;
				_stopPrice = null;
				return;
			}

			var shouldExit = (_targetPrice.HasValue && candle.HighPrice >= _targetPrice.Value)
				|| (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value);

			if (shouldExit)
			{
				SellMarket(Math.Abs(Position));
				_targetPrice = null;
				_stopPrice = null;
			}
		}
		else if (Position < 0)
		{
			if (_isLongPosition)
			{
				_targetPrice = null;
				_stopPrice = null;
				return;
			}

			var shouldExit = (_targetPrice.HasValue && candle.LowPrice <= _targetPrice.Value)
				|| (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value);

			if (shouldExit)
			{
				BuyMarket(Math.Abs(Position));
				_targetPrice = null;
				_stopPrice = null;
			}
		}
		else
		{
			_targetPrice = null;
			_stopPrice = null;
			_isLongPosition = false;
		}
	}

	private bool TryEnterShort(ICandleMessage candle, decimal currentRsi, decimal previousRsi)
	{
		if (!(currentRsi > UpperLevel && previousRsi <= UpperLevel))
			return false;

		var volume = TradeVolume;
		if (volume <= 0m)
			return false;

		if (Position > 0)
		{
			volume += Math.Abs(Position);
		}

		SellMarket(volume);

		var offset = CalculateOffset();
		if (offset > 0m)
		{
			var entryPrice = candle.ClosePrice;
			_targetPrice = entryPrice - offset;
			_stopPrice = entryPrice + offset;
			_isLongPosition = false;
		}
		else
		{
			_targetPrice = null;
			_stopPrice = null;
			_isLongPosition = false;
		}

		return true;
	}

	private bool TryEnterLong(ICandleMessage candle, decimal currentRsi, decimal previousRsi)
	{
		if (!(currentRsi < LowerLevel && previousRsi >= LowerLevel))
			return false;

		var volume = TradeVolume;
		if (volume <= 0m)
			return false;

		if (Position < 0)
		{
			volume += Math.Abs(Position);
		}

		BuyMarket(volume);

		var offset = CalculateOffset();
		if (offset > 0m)
		{
			var entryPrice = candle.ClosePrice;
			_targetPrice = entryPrice + offset;
			_stopPrice = entryPrice - offset;
			_isLongPosition = true;
		}
		else
		{
			_targetPrice = null;
			_stopPrice = null;
			_isLongPosition = true;
		}

		return true;
	}

	private decimal CalculateOffset()
	{
		var priceStep = Security?.PriceStep ?? 0.01m;
		if (priceStep <= 0m)
			priceStep = 0.01m;

		var decimals = Security?.Decimals ?? 0;
		var factor = decimals is 3 or 5 ? 10m : 1m;

		return OffsetInPips * priceStep * factor;
	}
}