Открыть на GitHub

4218 RSI MA Strategy

Общее описание

Стратегия представляет собой порт оригинального эксперта MetaTrader из каталога MQL/9925. Индикатор RSI_MA рассчитывается путём умножения классического RSI на наклон экспоненциальной скользящей средней по взвешенной цене (High + Low + 2 * Close) / 4. Обработка ведётся только по закрытым свечам, что позволяет максимально точно воспроизвести поведение исходного алгоритма.

Базовая конфигурация рассчитана на дневные свечи EURUSD (таймфрейм D1) и допускает только одну открытую позицию. При необходимости стратегию можно адаптировать под другой инструмент, изменив тип свечей и пороговые уровни.

Логика работы

  1. Расчёт индикатора
    • RSI с настраиваемым периодом считается по ценам закрытия.
    • EMA с тем же периодом рассчитывается по взвешенной цене.
    • Итоговое значение индикатора = RSI * (EMA[тек] - EMA[пред]) / размер_пункта, ограниченное диапазоном [1, 99].
  2. Условия входа в лонг
    • Предыдущее значение индикатора ниже экстремального уровня перепроданности (по умолчанию 5).
    • Текущее значение выше уровня активации (по умолчанию 20).
    • Открытых позиций нет либо имеется короткая позиция (при необходимости она закрывается и открывается длинная).
  3. Условия входа в шорт
    • Предыдущее значение выше экстремального уровня перекупленности (по умолчанию 95).
  • Текущее значение ниже уровня активации (по умолчанию 80).
  • Открытых позиций нет либо имеется длинная позиция (при необходимости она закрывается и открывается короткая).
  1. Выход по индикатору
    • Лонг закрывается при переходе индикатора из зоны перекупленности ниже уровня активации (95 → 80).
    • Шорт закрывается при переходе индикатора из зоны перепроданности выше уровня активации (5 → 20).
  2. Защитные выходы
    • Стоп-лосс, тейк-профит и трейлинг выражаются в пунктах, которые автоматически переводятся в цену через PriceStep (резервное значение 0.0001).
    • Трейлинг активируется только после продвижения цены на расстояние, превышающее заданное значение, и никогда не отодвигает стоп назад.

Параметры

Параметр Значение
RsiPeriod Период RSI и EMA.
OversoldActivationLevel Уровень подтверждения входа в лонг после экстремума перепроданности.
OversoldExtremeLevel Экстремальное значение, которое должно быть достигнуто перед открытием лонга.
OverboughtActivationLevel Уровень подтверждения входа в шорт после экстремума перекупленности.
OverboughtExtremeLevel Экстремальное значение, которое должно быть достигнуто перед открытием шорта.
StopLossPips Дистанция стоп-лосса в пунктах (включается через UseStopLoss).
TakeProfitPips Дистанция тейк-профита в пунктах (включается через UseTakeProfit).
TrailingStopPips Дистанция трейлинг-стопа (включается через UseTrailingStop).
UseStopLoss Включение/отключение стоп-лосса.
UseTakeProfit Включение/отключение тейк-профита.
UseTrailingStop Включение/отключение трейлинг-стопа.
UseMoneyManagement Активация мани-менеджмента по проценту риска.
RiskPercent Процент капитала, который разрешается рисковать в сделке.
TradeVolume Фиксированный объём при отключённом мани-менеджменте.
CandleType Тип свечей для расчётов (по умолчанию дневные).

Рекомендации по применению

  • Для полного соответствия оригинальному советнику используйте символ EURUSD с дневными свечами. При смене инструмента скорректируйте тип свечей и пороги.
  • Стратегия всегда держит одну позицию: противоположное направление закрывается перед открытием новой сделки.
  • При отсутствии данных о портфеле или если рассчитанный объём ≤ 0 используется фиксированное значение TradeVolume.
  • Убедитесь, что PriceStep инструмента соответствует пункту (для большинства валютных пар 0.0001). При другом шаге пересмотрите значения стопов.

Управление рисками

  • Проверка стоп-уровней выполняется по диапазону High/Low закрывшейся свечи.
  • Трейлинг-стоп подтягивается только при наличии прибыли, не снижая защитный уровень.
  • Даже при отключении защитных ордеров индикаторные условия продолжают закрывать позиции, что повторяет логику MQL-версии.
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;

using StockSharp.Algo.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// RSI and EMA based strategy converted from the original MQL implementation.
/// Combines a custom RSI*EMA momentum oscillator with basic risk management.
/// </summary>
public class RsiMaStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _oversoldActivationLevel;
	private readonly StrategyParam<decimal> _oversoldExtremeLevel;
	private readonly StrategyParam<decimal> _overboughtActivationLevel;
	private readonly StrategyParam<decimal> _overboughtExtremeLevel;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<bool> _useStopLoss;
	private readonly StrategyParam<bool> _useTakeProfit;
	private readonly StrategyParam<bool> _useTrailingStop;
	private readonly StrategyParam<bool> _useMoneyManagement;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;
	
	private RelativeStrengthIndex _rsi;
	private ExponentialMovingAverage _ema;
	
	private decimal? _previousIndicatorValue;
	
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _entryPrice;
	
	public RsiMaStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 14)
		.SetDisplay("RSI Period", string.Empty, "Oscillator")
		;
		
		_oversoldActivationLevel = Param(nameof(OversoldActivationLevel), 40m)
		.SetDisplay("Oversold Activation", string.Empty, "Oscillator")
		;
		
		_oversoldExtremeLevel = Param(nameof(OversoldExtremeLevel), 30m)
		.SetDisplay("Oversold Extreme", string.Empty, "Oscillator");
		
		_overboughtActivationLevel = Param(nameof(OverboughtActivationLevel), 60m)
		.SetDisplay("Overbought Activation", string.Empty, "Oscillator")
		;
		
		_overboughtExtremeLevel = Param(nameof(OverboughtExtremeLevel), 70m)
		.SetDisplay("Overbought Extreme", string.Empty, "Oscillator");
		
		_stopLossPips = Param(nameof(StopLossPips), 399m)
		.SetDisplay("Stop Loss (pips)", string.Empty, "Risk");
		
		_takeProfitPips = Param(nameof(TakeProfitPips), 999m)
		.SetDisplay("Take Profit (pips)", string.Empty, "Risk");
		
		_trailingStopPips = Param(nameof(TrailingStopPips), 299m)
		.SetDisplay("Trailing Stop (pips)", string.Empty, "Risk");
		
		_useStopLoss = Param(nameof(UseStopLoss), true)
		.SetDisplay("Use Stop Loss", string.Empty, "Risk");
		
		_useTakeProfit = Param(nameof(UseTakeProfit), true)
		.SetDisplay("Use Take Profit", string.Empty, "Risk");
		
		_useTrailingStop = Param(nameof(UseTrailingStop), true)
		.SetDisplay("Use Trailing Stop", string.Empty, "Risk");
		
		_useMoneyManagement = Param(nameof(UseMoneyManagement), false)
		.SetDisplay("Use Risk Percent Position Sizing", string.Empty, "Position");
		
		_riskPercent = Param(nameof(RiskPercent), 10m)
		.SetDisplay("Risk Percent", string.Empty, "Position");
		
		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
		.SetDisplay("Fixed Volume", string.Empty, "Position");
		
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
		.SetDisplay("Candle TimeFrame", string.Empty, "General");
	}
	
	/// <summary>
	/// RSI calculation period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}
	
	/// <summary>
	/// Activation threshold after an oversold extreme.
	/// </summary>
	public decimal OversoldActivationLevel
	{
		get => _oversoldActivationLevel.Value;
		set => _oversoldActivationLevel.Value = value;
	}
	
	/// <summary>
	/// Oversold extreme required before a long setup becomes valid.
	/// </summary>
	public decimal OversoldExtremeLevel
	{
		get => _oversoldExtremeLevel.Value;
		set => _oversoldExtremeLevel.Value = value;
	}
	
	/// <summary>
	/// Activation threshold after an overbought extreme.
	/// </summary>
	public decimal OverboughtActivationLevel
	{
		get => _overboughtActivationLevel.Value;
		set => _overboughtActivationLevel.Value = value;
	}
	
	/// <summary>
	/// Overbought extreme required before a short setup becomes valid.
	/// </summary>
	public decimal OverboughtExtremeLevel
	{
		get => _overboughtExtremeLevel.Value;
		set => _overboughtExtremeLevel.Value = value;
	}
	
	/// <summary>
	/// Stop-loss distance measured in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}
	
	/// <summary>
	/// Take-profit distance measured in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}
	
	/// <summary>
	/// Trailing-stop distance measured in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}
	
	/// <summary>
	/// Enable or disable stop-loss management.
	/// </summary>
	public bool UseStopLoss
	{
		get => _useStopLoss.Value;
		set => _useStopLoss.Value = value;
	}
	
	/// <summary>
	/// Enable or disable take-profit management.
	/// </summary>
	public bool UseTakeProfit
	{
		get => _useTakeProfit.Value;
		set => _useTakeProfit.Value = value;
	}
	
	/// <summary>
	/// Enable or disable trailing stop adjustments.
	/// </summary>
	public bool UseTrailingStop
	{
		get => _useTrailingStop.Value;
		set => _useTrailingStop.Value = value;
	}
	
	/// <summary>
	/// Enable or disable percent based position sizing.
	/// </summary>
	public bool UseMoneyManagement
	{
		get => _useMoneyManagement.Value;
		set => _useMoneyManagement.Value = value;
	}
	
	/// <summary>
	/// Portfolio risk percentage when money management is enabled.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}
	
	/// <summary>
	/// Fixed volume used when money management is disabled.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}
	
	/// <summary>
	/// Candle type used for signal generation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}
	
	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, CandleType);
	}
	
	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_rsi = null;
		_ema = null;
		_previousIndicatorValue = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_entryPrice = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		
		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};
		
		_ema = new ExponentialMovingAverage
		{
			Length = RsiPeriod
		};
		
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_rsi, ProcessCandle)
			.Start();
		
		StartProtection(null, null);
	}
	
	private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_rsi.IsFormed)
			return;

		var indicatorValue = rsiValue;

		if (_previousIndicatorValue is decimal previousValue)
		{
			ManageOpenPosition(candle, previousValue, indicatorValue);
			EvaluateEntries(candle, previousValue, indicatorValue);
		}

		_previousIndicatorValue = indicatorValue;
	}
	
	private void ManageOpenPosition(ICandleMessage candle, decimal previousValue, decimal currentValue)
	{
		if (Position > 0)
		{
			var exitSignal = previousValue > OverboughtExtremeLevel && currentValue < OverboughtActivationLevel;
			if (exitSignal)
			{
				SellMarket();
				ResetRiskLevels();
				return;
			}

			UpdateTrailingStopForLong(candle);

			if (ShouldCloseLong(candle))
			{
				SellMarket();
				ResetRiskLevels();
			}
		}
		else if (Position < 0)
		{
			var exitSignal = previousValue < OversoldExtremeLevel && currentValue > OversoldActivationLevel;
			if (exitSignal)
			{
				BuyMarket();
				ResetRiskLevels();
				return;
			}

			UpdateTrailingStopForShort(candle);

			if (ShouldCloseShort(candle))
			{
				BuyMarket();
				ResetRiskLevels();
			}
		}
	}
	
	private void EvaluateEntries(ICandleMessage candle, decimal previousValue, decimal currentValue)
	{
		var price = candle.ClosePrice;
		if (price <= 0m)
			return;
		
		if (previousValue < OversoldExtremeLevel && currentValue > OversoldActivationLevel && Position <= 0)
		{
			_entryPrice = price;
			InitializeRiskLevelsForLong(price);
			BuyMarket();
		}
		else if (previousValue > OverboughtExtremeLevel && currentValue < OverboughtActivationLevel && Position >= 0)
		{
			_entryPrice = price;
			InitializeRiskLevelsForShort(price);
			SellMarket();
		}
	}
	
	private void InitializeRiskLevelsForLong(decimal price)
	{
		var pipDistance = GetPipSize();
		
		if (UseStopLoss && StopLossPips > 0m)
			_stopLossPrice = price - pipDistance * StopLossPips;
		else
			_stopLossPrice = null;
		
		if (UseTakeProfit && TakeProfitPips > 0m)
			_takeProfitPrice = price + pipDistance * TakeProfitPips;
		else
			_takeProfitPrice = null;
	}
	
	private void InitializeRiskLevelsForShort(decimal price)
	{
		var pipDistance = GetPipSize();
		
		if (UseStopLoss && StopLossPips > 0m)
			_stopLossPrice = price + pipDistance * StopLossPips;
		else
			_stopLossPrice = null;
		
		if (UseTakeProfit && TakeProfitPips > 0m)
			_takeProfitPrice = price - pipDistance * TakeProfitPips;
		else
			_takeProfitPrice = null;
	}
	
	private void UpdateTrailingStopForLong(ICandleMessage candle)
	{
		if (!UseTrailingStop || TrailingStopPips <= 0m)
			return;
		
		var pipDistance = GetPipSize() * TrailingStopPips;
		if (pipDistance <= 0m)
			return;
		
		var profit = candle.ClosePrice - _entryPrice;
		if (profit <= pipDistance)
			return;
		
		var newStop = candle.ClosePrice - pipDistance;
		if (_stopLossPrice is null || newStop > _stopLossPrice)
			_stopLossPrice = newStop;
	}
	
	private void UpdateTrailingStopForShort(ICandleMessage candle)
	{
		if (!UseTrailingStop || TrailingStopPips <= 0m)
			return;
		
		var pipDistance = GetPipSize() * TrailingStopPips;
		if (pipDistance <= 0m)
			return;
		
		var profit = _entryPrice - candle.ClosePrice;
		if (profit <= pipDistance)
			return;
		
		var newStop = candle.ClosePrice + pipDistance;
		if (_stopLossPrice is null || newStop < _stopLossPrice)
			_stopLossPrice = newStop;
	}
	
	private bool ShouldCloseLong(ICandleMessage candle)
	{
		var stopHit = UseStopLoss && _stopLossPrice is decimal stop && candle.LowPrice <= stop;
		var takeProfitHit = UseTakeProfit && _takeProfitPrice is decimal takeProfit && candle.HighPrice >= takeProfit;
		return stopHit || takeProfitHit;
	}
	
	private bool ShouldCloseShort(ICandleMessage candle)
	{
		var stopHit = UseStopLoss && _stopLossPrice is decimal stop && candle.HighPrice >= stop;
		var takeProfitHit = UseTakeProfit && _takeProfitPrice is decimal takeProfit && candle.LowPrice <= takeProfit;
		return stopHit || takeProfitHit;
	}
	
	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_entryPrice = 0m;
	}
	
	private decimal GetPipSize()
	{
		var priceStep = Security?.PriceStep;
		if (priceStep is null || priceStep == 0m)
			return 0.0001m;
		
		return priceStep.Value;
	}
	
	private decimal GetOrderVolume(decimal price)
	{
		var volume = TradeVolume;

		if (!UseMoneyManagement || Portfolio is null || price <= 0m)
			return volume;

		var portfolioValue = Portfolio.CurrentValue ?? 0m;
		if (portfolioValue <= 0m)
			return volume;

		var riskAmount = portfolioValue * RiskPercent / 100m;
		if (riskAmount <= 0m)
			return volume;

		var estimatedVolume = riskAmount / price;

		var volumeStep = Security?.VolumeStep ?? 0m;
		if (volumeStep > 0m)
		{
			estimatedVolume = Math.Floor(estimatedVolume / volumeStep) * volumeStep;
		}

		if (estimatedVolume <= 0m)
			estimatedVolume = volume;

		return estimatedVolume;
	}
}