Ver en GitHub

Estrategia MA + RSI Wizard

Descripción general

Esta estrategia es el port de StockSharp del experto de MetaTrader 5 "MQL5 Wizard MA RSI" de la carpeta MQL/17489. El robot original combina un filtro de media móvil con un filtro RSI y abre operaciones cuando la suma ponderada de los filtros cruza umbrales configurables. La versión en C# mantiene la misma estructura expresando la lógica con la API de alto nivel de StockSharp y los ayudantes modernos de gestión de riesgo.

El bot funciona en cualquier instrumento que proporcione velas OHLCV. Evalúa una media móvil que puede retrasarse por un número de barras definido por el usuario y un RSI que puede alimentarse con diferentes fuentes de precio. Ambos indicadores contribuyen a una puntuación compuesta. Una posición se abre una vez que la puntuación supera el umbral de apertura y se cierra cuando la puntuación contraria alcanza el umbral de cierre. Las configuraciones opcionales de distancia, stop loss y take profit replican los parámetros de gestión de dinero del Asesor Experto original.

Indicadores y puntuación

  • Media Móvil – período configurable, método (simple, exponencial, suavizado, linealmente ponderado), fuente de precio y desplazamiento hacia adelante. Cuando el precio de cierre está por encima de la media desplazada, la puntuación MA es igual a 100, de lo contrario es 0.
  • Índice de Fuerza Relativa (RSI) – período y fuente de precio configurables. La contribución RSI crece linealmente de 0 cuando RSI = 50 a 100 cuando RSI = 100 para señales largas, y refleja el mismo comportamiento para señales cortas.
  • Puntuación compuesta – las puntuaciones MA y RSI se ponderan por MaWeight y RsiWeight. El valor final es la media ponderada score = (maScore * MaWeight + rsiScore * RsiWeight) / (MaWeight + RsiWeight) que permanece dentro del intervalo [0;100] como en la versión MetaTrader.
  • Filtro de distancia de precioPriceLevelPoints define la distancia mínima entre el cierre de la vela y la media móvil desplazada (convertida a precio usando el paso del instrumento). Las señales más cercanas que el umbral se ignoran.

Reglas de operación

  1. Cada vela terminada actualiza los indicadores y puntuaciones.
  2. Si la puntuación opuesta supera ThresholdClose, la posición actual se cierra a mercado.
  3. Entrada larga – permitida cuando no hay exposición larga, la puntuación larga es al menos ThresholdOpen, el tiempo de espera (ExpirationBars) ha pasado, y se satisface el filtro de distancia de precio. El tamaño de la orden es Volume + |Position|, lo que automáticamente cambia una posición corta si es necesario.
  4. Entrada corta – simétrica a la lógica larga.
  5. StartProtection opcional aplica stop loss y take profit usando puntos absolutos de precio.

Gestión de riesgo

La estrategia activa StartProtection una vez que comienza. Las distancias se definen en puntos de precio (StopLevelPoints, TakeLevelPoints) y se traducen con el Security.PriceStep actual. Ambos valores pueden establecerse en cero para deshabilitar la protección correspondiente. El parámetro de tiempo de espera evita re-entradas inmediatas en la misma dirección, emulando la configuración de vencimiento de órdenes pendientes del EA original.

Parámetros

Parámetro Descripción Valor predeterminado
CandleType Serie de datos usada para el análisis. Marco temporal de 15 minutos
ThresholdOpen Puntuación ponderada mínima para abrir una posición. 55
ThresholdClose Puntuación opuesta mínima para cerrar una posición. 100
PriceLevelPoints Distancia requerida entre precio y MA desplazada (en puntos). 0
StopLevelPoints Distancia del stop loss (puntos). 50
TakeLevelPoints Distancia del take profit (puntos). 50
ExpirationBars Tiempo de espera en barras antes de re-entrar en la misma dirección. 4
MaPeriod Período de la media móvil. 20
MaShift Desplazamiento hacia adelante aplicado a la salida de la MA (barras). 3
MaMethods Método de media móvil (Simple, Exponencial, Suavizado, LinearWeighted). Simple
MaAppliedPrice Fuente de precio para la MA. Close
MaWeight Peso asignado a la puntuación MA. 0.8
RsiPeriod Período RSI. 3
RsiAppliedPrice Fuente de precio para el RSI. Close
RsiWeight Peso asignado a la puntuación RSI. 0.5

Notas

  • La estrategia funciona estrictamente en velas terminadas e ignora actualizaciones parciales.
  • Establecer ambos pesos de indicador en cero deshabilita el trading porque la puntuación combinada ya no puede alcanzar los umbrales.
  • El tiempo de espera (ExpirationBars) igual a cero permite múltiples entradas en la misma dirección sin esperar.
  • Porque StockSharp ejecuta órdenes de mercado por defecto, la expiración de órdenes pendientes del EA original se representa por el mecanismo de tiempo de espera en lugar de la cancelación real de órdenes.
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
		};
	}
}