Ver no GitHub

Estratégia MA + RSI Wizard

Visão geral

Esta estratégia é o port do StockSharp do especialista do MetaTrader 5 "MQL5 Wizard MA RSI" da pasta MQL/17489. O robô original combina um filtro de média móvel com um filtro RSI e abre operações quando a soma ponderada dos filtros cruza limiares configuráveis. A versão em C# mantém a mesma estrutura expressando a lógica com a API de alto nível do StockSharp e auxiliares modernos de gestão de risco.

O bot funciona em qualquer instrumento que forneça velas OHLCV. Ele avalia uma média móvel que pode ser atrasada por um número de barras definido pelo usuário e um RSI que pode ser alimentado com diferentes fontes de preço. Ambos os indicadores contribuem para uma pontuação composta. Uma posição é aberta assim que a pontuação excede o limiar de abertura e fechada quando a pontuação contrária atinge o limiar de fechamento. As configurações opcionais de distância, stop loss e take profit replicam os parâmetros de gestão de dinheiro do Consultor Especialista original.

Indicadores e pontuação

  • Média Móvel – período configurável, método (simples, exponencial, suavizado, linearmente ponderado), fonte de preço e deslocamento para frente. Quando o preço de fechamento está acima da média deslocada, a pontuação MA é igual a 100, caso contrário é 0.
  • Índice de Força Relativa (RSI) – período e fonte de preço configuráveis. A contribuição RSI cresce linearmente de 0 quando RSI = 50 para 100 quando RSI = 100 para sinais comprados, e espelha o mesmo comportamento para sinais vendidos.
  • Pontuação composta – as pontuações MA e RSI são ponderadas por MaWeight e RsiWeight. O valor final é a média ponderada score = (maScore * MaWeight + rsiScore * RsiWeight) / (MaWeight + RsiWeight) que permanece dentro do intervalo [0;100] assim como na versão MetaTrader.
  • Filtro de distância de preçoPriceLevelPoints define a distância mínima entre o fechamento da vela e a média móvel deslocada (convertida para preço usando o passo do instrumento). Sinais mais próximos que o limiar são ignorados.

Regras de operação

  1. Cada vela finalizada atualiza os indicadores e pontuações.
  2. Se a pontuação oposta ultrapassar ThresholdClose, a posição atual é fechada a mercado.
  3. Entrada comprada – permitida quando não há exposição comprada, a pontuação comprada é pelo menos ThresholdOpen, o tempo de espera (ExpirationBars) passou, e o filtro de distância de preço é satisfeito. O tamanho da ordem é Volume + |Position|, o que automaticamente inverte uma posição vendida se necessário.
  4. Entrada vendida – simétrica à lógica comprada.
  5. StartProtection opcional aplica stop loss e take profit usando pontos absolutos de preço.

Gestão de risco

A estratégia ativa StartProtection uma vez que inicia. As distâncias são definidas em pontos de preço (StopLevelPoints, TakeLevelPoints) e são traduzidas com o Security.PriceStep atual. Ambos os valores podem ser definidos como zero para desabilitar a proteção correspondente. O parâmetro de tempo de espera evita re-entradas imediatas na mesma direção, emulando a configuração de vencimento de ordens pendentes do EA original.

Parâmetros

Parâmetro Descrição Valores padrão
CandleType Série de dados usada para análise. Período de 15 minutos
ThresholdOpen Pontuação ponderada mínima para abrir uma posição. 55
ThresholdClose Pontuação oposta mínima para fechar uma posição. 100
PriceLevelPoints Distância necessária entre preço e MA deslocada (em pontos). 0
StopLevelPoints Distância do stop loss (pontos). 50
TakeLevelPoints Distância do take profit (pontos). 50
ExpirationBars Tempo de espera em barras antes de re-entrar na mesma direção. 4
MaPeriod Período da média móvel. 20
MaShift Deslocamento para frente aplicado à saída da MA (barras). 3
MaMethods Método de média móvel (Simple, Exponential, Smoothed, LinearWeighted). Simple
MaAppliedPrice Fonte de preço para a MA. Close
MaWeight Peso atribuído à pontuação MA. 0.8
RsiPeriod Período RSI. 3
RsiAppliedPrice Fonte de preço para o RSI. Close
RsiWeight Peso atribuído à pontuação RSI. 0.5

Notas

  • A estratégia funciona estritamente em velas finalizadas e ignora atualizações parciais.
  • Definir ambos os pesos de indicador como zero desabilita o trading porque a pontuação combinada não pode mais atingir os limiares.
  • Tempo de espera (ExpirationBars) igual a zero permite múltiplas entradas na mesma direção sem esperar.
  • Como o StockSharp executa ordens a mercado por padrão, o vencimento de ordens pendentes do EA original é representado pelo mecanismo de tempo de espera em vez do cancelamento real de ordens.
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>
/// Moving average plus RSI strategy converted from the MQL5 Wizard template.
/// The strategy computes weighted scores from a shifted moving average and RSI momentum.
/// </summary>
public class MaRsiWizardStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation methods supported by the strategy.
	/// </summary>
	public enum MaMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	/// <summary>
	/// Price sources compatible with the indicators used in the strategy.
	/// </summary>
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _thresholdOpen;
	private readonly StrategyParam<int> _thresholdClose;
	private readonly StrategyParam<decimal> _priceLevelPoints;
	private readonly StrategyParam<int> _stopLevelPoints;
	private readonly StrategyParam<int> _takeLevelPoints;
	private readonly StrategyParam<int> _expirationBars;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MaMethods> _maMethod;
	private readonly StrategyParam<AppliedPrices> _maAppliedPrice;
	private readonly StrategyParam<decimal> _maWeight;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<AppliedPrices> _rsiAppliedPrice;
	private readonly StrategyParam<decimal> _rsiWeight;

	private DecimalLengthIndicator _ma = null!;
	private RelativeStrengthIndex _rsi = null!;
	private readonly Queue<decimal> _maShiftBuffer = new();

	private int _barIndex;
	private int? _lastLongEntryBar;
	private int? _lastShortEntryBar;

	/// <summary>
	/// Initializes a new instance of the <see cref="MaRsiWizardStrategy"/>.
	/// </summary>
	public MaRsiWizardStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for incoming candles", "General");

		_thresholdOpen = Param(nameof(ThresholdOpen), 75)
			.SetRange(0, 100)
			.SetDisplay("Open Threshold", "Weighted score required to open a position", "Signals")
			;

		_thresholdClose = Param(nameof(ThresholdClose), 100)
			.SetRange(0, 100)
			.SetDisplay("Close Threshold", "Weighted score required to exit an existing position", "Signals")
			;

		_priceLevelPoints = Param(nameof(PriceLevelPoints), 0m)
			.SetDisplay("Price Level (points)", "Minimum distance between price and moving average", "Signals")
			;

		_stopLevelPoints = Param(nameof(StopLevelPoints), 50)
			.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price points", "Risk")
			;

		_takeLevelPoints = Param(nameof(TakeLevelPoints), 50)
			.SetDisplay("Take Profit (points)", "Profit target distance expressed in price points", "Risk")
			;

		_expirationBars = Param(nameof(ExpirationBars), 24)
			.SetDisplay("Signal Cooldown (bars)", "Bars to wait before allowing a new trade in the same direction", "Signals")
			;

		_maPeriod = Param(nameof(MaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average period", "Moving Average")
			;

		_maShift = Param(nameof(MaShift), 3)
			.SetRange(0, 100)
			.SetDisplay("MA Shift", "Lag applied to the moving average output", "Moving Average")
			;

		_maMethod = Param(nameof(MaMethods), MaMethods.Simple)
			.SetDisplay("MA Method", "Moving average calculation method", "Moving Average");

		_maAppliedPrice = Param(nameof(MaAppliedPrice), AppliedPrices.Close)
			.SetDisplay("MA Source", "Price type used for the moving average", "Moving Average");

		_maWeight = Param(nameof(MaWeight), 0.8m)
			.SetDisplay("MA Weight", "Contribution of the moving average score", "Signals")
			.SetRange(0m, 1m)
			;

		_rsiPeriod = Param(nameof(RsiPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI calculation length", "RSI")
			;

		_rsiAppliedPrice = Param(nameof(RsiAppliedPrice), AppliedPrices.Close)
			.SetDisplay("RSI Source", "Price type used for RSI", "RSI");

		_rsiWeight = Param(nameof(RsiWeight), 0.5m)
			.SetDisplay("RSI Weight", "Contribution of the RSI score", "Signals")
			.SetRange(0m, 1m)
			;
	}

	/// <summary>
	/// Type of candles used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Weighted score required to open a new position.
	/// </summary>
	public int ThresholdOpen
	{
		get => _thresholdOpen.Value;
		set => _thresholdOpen.Value = value;
	}

	/// <summary>
	/// Weighted score required to close the current position.
	/// </summary>
	public int ThresholdClose
	{
		get => _thresholdClose.Value;
		set => _thresholdClose.Value = value;
	}

	/// <summary>
	/// Minimum price distance from the moving average expressed in points.
	/// </summary>
	public decimal PriceLevelPoints
	{
		get => _priceLevelPoints.Value;
		set => _priceLevelPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in points.
	/// </summary>
	public int StopLevelPoints
	{
		get => _stopLevelPoints.Value;
		set => _stopLevelPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in points.
	/// </summary>
	public int TakeLevelPoints
	{
		get => _takeLevelPoints.Value;
		set => _takeLevelPoints.Value = value;
	}

	/// <summary>
	/// Cooldown measured in bars before a new trade in the same direction is allowed.
	/// </summary>
	public int ExpirationBars
	{
		get => _expirationBars.Value;
		set => _expirationBars.Value = value;
	}

	/// <summary>
	/// Moving average length.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Number of bars used to lag the moving average output.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average calculation method.
	/// </summary>
	public MaMethods MaMethod
	{
		get => _maMethod.Value;
		set => _maMethod.Value = value;
	}

	/// <summary>
	/// Price source used for the moving average.
	/// </summary>
	public AppliedPrices MaAppliedPrice
	{
		get => _maAppliedPrice.Value;
		set => _maAppliedPrice.Value = value;
	}

	/// <summary>
	/// Contribution of the moving average score in the weighted decision.
	/// </summary>
	public decimal MaWeight
	{
		get => _maWeight.Value;
		set => _maWeight.Value = value;
	}

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

	/// <summary>
	/// Price source used for the RSI indicator.
	/// </summary>
	public AppliedPrices RsiAppliedPrice
	{
		get => _rsiAppliedPrice.Value;
		set => _rsiAppliedPrice.Value = value;
	}

	/// <summary>
	/// Contribution of the RSI score in the weighted decision.
	/// </summary>
	public decimal RsiWeight
	{
		get => _rsiWeight.Value;
		set => _rsiWeight.Value = value;
	}

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

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

		_maShiftBuffer.Clear();
		_barIndex = 0;
		_lastLongEntryBar = null;
		_lastShortEntryBar = null;
	}

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

		_maShiftBuffer.Clear();
		_barIndex = 0;
		_lastLongEntryBar = null;
		_lastShortEntryBar = null;

		_ma = CreateMovingAverage(MaMethod, MaPeriod);
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

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

		var step = Security.PriceStep ?? 1m;

		Unit takeProfit = TakeLevelPoints > 0
			? new Unit(TakeLevelPoints * step, UnitTypes.Absolute)
			: null;

		Unit stopLoss = StopLevelPoints > 0
			? new Unit(StopLevelPoints * step, UnitTypes.Absolute)
			: null;

		if (stopLoss != null || takeProfit != null)
			StartProtection(stopLoss ?? new Unit(), takeProfit ?? new Unit());

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

		var rsiArea = CreateChartArea();
		if (rsiArea != null)
		{
			rsiArea.Title = "RSI";
			DrawIndicator(rsiArea, _rsi);
		}
	}

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

		// removed IsFormedAndOnlineAndAllowTrading for backtesting

		_barIndex++;

		var maInput = SelectAppliedPrice(candle, MaAppliedPrice);
		var maValue = _ma.Process(new DecimalIndicatorValue(_ma, maInput, candle.OpenTime) { IsFinal = true });
		if (!maValue.IsFinal || maValue is not DecimalIndicatorValue maResult)
			return;

		var rsiInput = SelectAppliedPrice(candle, RsiAppliedPrice);
		var rsiValue = _rsi.Process(new DecimalIndicatorValue(_rsi, rsiInput, candle.OpenTime) { IsFinal = true });
		if (!rsiValue.IsFinal || rsiValue is not DecimalIndicatorValue rsiResult)
			return;

		var referenceMa = UpdateAndGetShiftedMa(maResult.Value);
		if (referenceMa == null)
			return;

		var currentPrice = candle.ClosePrice;
		var step = Security.PriceStep ?? 1m;
		var priceOffset = PriceLevelPoints * step;

		if (PriceLevelPoints > 0 && Math.Abs(currentPrice - referenceMa.Value) < priceOffset)
			return;

		var maLongSignal = currentPrice > referenceMa.Value ? 100m : 0m;
		var maShortSignal = currentPrice < referenceMa.Value ? 100m : 0m;

		var rsi = rsiResult.Value;
		var rsiLongSignal = rsi > 50m ? Math.Min(100m, (rsi - 50m) * 2m) : 0m;
		var rsiShortSignal = rsi < 50m ? Math.Min(100m, (50m - rsi) * 2m) : 0m;

		var weightSum = MaWeight + RsiWeight;
		if (weightSum <= 0m)
			return;

		var longScore = (MaWeight * maLongSignal + RsiWeight * rsiLongSignal) / weightSum;
		var shortScore = (MaWeight * maShortSignal + RsiWeight * rsiShortSignal) / weightSum;

		if (Position > 0 && shortScore >= ThresholdClose)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (Position < 0 && longScore >= ThresholdClose)
		{
			BuyMarket(Math.Abs(Position));
		}

		var allowLong = ExpirationBars <= 0 || _lastLongEntryBar == null || _barIndex - _lastLongEntryBar >= ExpirationBars;
		var allowShort = ExpirationBars <= 0 || _lastShortEntryBar == null || _barIndex - _lastShortEntryBar >= ExpirationBars;

		if (Position <= 0 && longScore >= ThresholdOpen && allowLong)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0)
			{
				BuyMarket(volume);
				_lastLongEntryBar = _barIndex;
			}
			return;
		}

		if (Position >= 0 && shortScore >= ThresholdOpen && allowShort)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0)
			{
				SellMarket(volume);
				_lastShortEntryBar = _barIndex;
			}
		}
	}

	private decimal? UpdateAndGetShiftedMa(decimal maValue)
	{
		var shift = Math.Max(0, MaShift);
		if (shift == 0)
		{
			return maValue;
		}

		_maShiftBuffer.Enqueue(maValue);

		if (_maShiftBuffer.Count <= shift)
			return null;

		if (_maShiftBuffer.Count > shift + 1)
			_maShiftBuffer.Dequeue();

		return _maShiftBuffer.Count == shift + 1 ? _maShiftBuffer.Peek() : (decimal?)null;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MaMethods method, int period)
	{
		return method switch
		{
			MaMethods.Simple => new SMA { Length = period },
			MaMethods.Exponential => new EMA { Length = period },
			MaMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MaMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
			_ => new SMA { Length = period }
		};
	}

	private static decimal SelectAppliedPrice(ICandleMessage candle, AppliedPrices price)
	{
		return price switch
		{
			AppliedPrices.Open => candle.OpenPrice,
			AppliedPrices.High => candle.HighPrice,
			AppliedPrices.Low => candle.LowPrice,
			AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}
}