Ver en GitHub

Estrategia Two MA RSI

Descripción general

La estrategia Two MA RSI es una conversión del asesor experto original de MetaTrader "2MA_RSI". Utiliza un cruce de media móvil exponencial (EMA) rápida y lenta confirmado por un filtro de Índice de Fuerza Relativa (RSI). Las órdenes se dimensionan con un bloque de gestión de dinero estilo martingala que aumenta el volumen de la siguiente orden después de una pérdida. La versión StockSharp funciona completamente en velas terminadas y reproduce el comportamiento original de take-profit y stop-loss en puntos de precio.

Datos e indicadores

  • La estrategia se suscribe a una única serie de velas definida por CandleType (velas de 5 minutos por defecto).
  • Se calculan tres indicadores en cada barra completada:
    • EMA de FastLength (aplicada al cierre de la vela).
    • EMA de SlowLength.
    • RSI con longitud RsiLength.
  • Los valores históricos de los indicadores se almacenan internamente para detectar cruces de EMA sin extraer datos de los buffers de indicadores.

Lógica de entrada

  1. La vela anterior debe estar terminada para evitar la re-evaluación intrabarra.
  2. No se permite ninguna posición activa (Position == 0).
  3. Entrada larga:
    • La EMA rápida cruza por encima de la EMA lenta (la EMA rápida en la barra actual es mayor que la EMA lenta, mientras que en la barra anterior EMA rápida < EMA lenta).
    • El valor del RSI está por debajo de RsiOversold, confirmando un mercado sobrevendido.
  4. Entrada corta:
    • La EMA rápida cruza por debajo de la EMA lenta con la condición análoga (EMA rápida ahora por debajo de EMA lenta, anteriormente por encima).
    • El RSI está por encima de RsiOverbought, indicando un mercado sobrecomprado.
  5. Cuando se satisfacen todas las condiciones, la estrategia envía una orden a mercado dimensionada según el módulo de martingala.

Lógica de salida

  • Un stop loss de protección y un take profit se calculan inmediatamente después de cada entrada. Las distancias se definen en "puntos" y se convierten a través del PriceStep del instrumento:
    • Largo:
      • Stop loss = precio de entrada - StopLossPoints * PriceStep.
      • Take profit = precio de entrada + TakeProfitPoints * PriceStep.
    • Corto:
      • Stop loss = precio de entrada + StopLossPoints * PriceStep.
      • Take profit = precio de entrada - TakeProfitPoints * PriceStep.
  • Solo estos niveles de protección cierran una operación. La estrategia espera la siguiente vela para confirmar si el mínimo/máximo tocó el objetivo o el stop y envía una orden ClosePosition() a mercado en consecuencia.
  • La prioridad de salida coincide con el comportamiento conservador del robot original: un stop loss se evalúa antes que un take profit si ambos niveles caen dentro del mismo rango de vela.

Dimensionamiento de posición y martingala

  1. El volumen base se calcula en cada entrada como floor(balance / BalanceDivider) * VolumeStep. El valor siempre se mantiene en o por encima de un paso de volumen y usa CurrentValue del portafolio (recurriendo a BeginValue cuando sea necesario).
  2. Después de cada salida perdedora, la etapa de martingala aumenta en uno hasta MaxDoublings. El siguiente volumen de orden se multiplica por 2^stage.
  3. Cualquier operación ganadora o alcanzar el número máximo de duplicaciones restablece la etapa a cero, volviendo al volumen base.
  4. Si MaxDoublings es cero o negativo, el tamaño nunca aumenta e iguala el volumen base.

Comportamiento adicional

  • La estrategia lleva un registro de los valores previos de EMA internamente y no solicita valores históricos de indicadores.
  • Las órdenes se ejecutan solo cuando la estrategia está en línea, los indicadores están formados y el trading está permitido.
  • La salida de gráfico dibuja velas de precios, operaciones propias y los tres indicadores para análisis visual.

Parámetros

Parámetro Descripción Valor predeterminado
FastLength Longitud de la EMA rápida. 5
SlowLength Longitud de la EMA lenta. 20
RsiLength Número de barras usadas en el cálculo del RSI. 14
RsiOverbought Nivel RSI que bloquea nuevos largos y permite cortos. 70
RsiOversold Nivel RSI que permite largos. 30
StopLossPoints Distancia del stop loss expresada en pasos de precio. 500
TakeProfitPoints Distancia del take profit en pasos de precio. 1500
BalanceDivider Divide el valor del portafolio para obtener el tamaño base de la orden. 1000
MaxDoublings Número máximo de duplicaciones de martingala después de pérdidas consecutivas. 1
CandleType Serie de velas utilizada por la estrategia. Marco temporal de 5 minutos

Notas de uso

  • Proporcionar un portafolio e instrumento con metadatos válidos de PriceStep y VolumeStep para que la gestión de riesgo basada en puntos y el dimensionamiento de posición permanezcan consistentes.
  • Dado que se usan órdenes a mercado para las salidas, el deslizamiento y los spreads son posibles en comparación con las órdenes límite de la versión MetaTrader, pero la lógica de evaluación de stop/take se preserva.
  • La estrategia no crea una versión Python; solo se suministra la implementación C# según lo solicitado.
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 crossover strategy with RSI confirmation and martingale sizing.
/// </summary>
public class TwoMaRsiStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<decimal> _rsiOverbought;
	private readonly StrategyParam<decimal> _rsiOversold;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _balanceDivider;
	private readonly StrategyParam<int> _maxDoublings;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _slowEma;
	private RelativeStrengthIndex _rsi;

	private decimal? _previousFast;
	private decimal? _previousSlow;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;
	private int _martingaleStage;
	private bool _isClosing;

	/// <summary>
	/// Initializes a new instance of the <see cref="TwoMaRsiStrategy"/> class.
	/// </summary>
	public TwoMaRsiStrategy()
	{
		_fastLength = Param(nameof(FastLength), 5)
			.SetDisplay("Fast EMA Length", "Length of the fast exponential moving average", "Indicators")
			
			.SetOptimize(2, 20, 1);

		_slowLength = Param(nameof(SlowLength), 20)
			.SetDisplay("Slow EMA Length", "Length of the slow exponential moving average", "Indicators")
			
			.SetOptimize(10, 60, 5);

		_rsiLength = Param(nameof(RsiLength), 14)
			.SetDisplay("RSI Length", "Number of bars for the RSI calculation", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_rsiOverbought = Param(nameof(RsiOverbought), 50m)
			.SetDisplay("RSI Overbought", "Upper RSI threshold for short entries", "Signals");

		_rsiOversold = Param(nameof(RsiOversold), 50m)
			.SetDisplay("RSI Oversold", "Lower RSI threshold for long entries", "Signals");

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetDisplay("Stop Loss (points)", "Stop loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 1500m)
			.SetDisplay("Take Profit (points)", "Take profit distance in price steps", "Risk");

		_balanceDivider = Param(nameof(BalanceDivider), 1000m)
			.SetDisplay("Balance Divider", "Divides portfolio value to estimate base order volume", "Money Management");

		_maxDoublings = Param(nameof(MaxDoublings), 1)
			.SetDisplay("Max Doublings", "Maximum number of martingale doublings", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series for the strategy", "General");
	}

	/// <summary>
	/// Fast EMA length.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

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

	/// <summary>
	/// Overbought threshold for RSI.
	/// </summary>
	public decimal RsiOverbought
	{
		get => _rsiOverbought.Value;
		set => _rsiOverbought.Value = value;
	}

	/// <summary>
	/// Oversold threshold for RSI.
	/// </summary>
	public decimal RsiOversold
	{
		get => _rsiOversold.Value;
		set => _rsiOversold.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Divider applied to the portfolio value to calculate the base order volume.
	/// </summary>
	public decimal BalanceDivider
	{
		get => _balanceDivider.Value;
		set => _balanceDivider.Value = value;
	}

	/// <summary>
	/// Maximum number of martingale doublings.
	/// </summary>
	public int MaxDoublings
	{
		get => _maxDoublings.Value;
		set => _maxDoublings.Value = value;
	}

	/// <summary>
	/// Candle data type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

		_fastEma = null;
		_slowEma = null;
		_rsi = null;
		_previousFast = null;
		_previousSlow = null;
		_entryPrice = default;
		_stopPrice = default;
		_takeProfitPrice = default;
		_martingaleStage = 0;
		_isClosing = false;
	}

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

		_fastEma = new ExponentialMovingAverage
		{
			Length = FastLength
		};

		_slowEma = new ExponentialMovingAverage
		{
			Length = SlowLength
		};

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiLength
		};

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

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

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

		if (Position == 0 && _isClosing)
		{
			_isClosing = false;
			_entryPrice = default;
			_stopPrice = default;
			_takeProfitPrice = default;
		}

		var fastResult = _fastEma.Process(candle);
		var slowResult = _slowEma.Process(candle);
		var rsiResult = _rsi.Process(candle);

		if (fastResult.IsEmpty || slowResult.IsEmpty || rsiResult.IsEmpty)
		{
			return;
		}

		if (!_fastEma.IsFormed || !_slowEma.IsFormed || !_rsi.IsFormed)
		{
			_previousFast = fastResult.GetValue<decimal>();
			_previousSlow = slowResult.GetValue<decimal>();
			return;
		}

		var fast = fastResult.GetValue<decimal>();
		var slow = slowResult.GetValue<decimal>();
		var rsi = rsiResult.GetValue<decimal>();
		var point = GetPoint();

		if (Position > 0)
		{
			var stopHit = candle.LowPrice <= _stopPrice;
			var takeHit = candle.HighPrice >= _takeProfitPrice;

			if (!_isClosing && stopHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterLoss();
			}
			else if (!_isClosing && takeHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterWin();
			}
		}
		else if (Position < 0)
		{
			var stopHit = candle.HighPrice >= _stopPrice;
			var takeHit = candle.LowPrice <= _takeProfitPrice;

			if (!_isClosing && stopHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterLoss();
			}
			else if (!_isClosing && takeHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterWin();
			}
		}
		else if (!_isClosing)
		{
			if (_previousFast is null || _previousSlow is null)
			{
				_previousFast = fast;
				_previousSlow = slow;
				return;
			}

			var prevFast = _previousFast.Value;
			var prevSlow = _previousSlow.Value;

			var crossUp = prevFast < prevSlow && fast > slow && rsi < RsiOversold;
			var crossDown = prevFast > prevSlow && fast < slow && rsi > RsiOverbought;

			if (crossUp)
			{
				var volume = CalculateOrderVolume();
				if (volume > 0m)
				{
					BuyMarket(volume);
					_entryPrice = candle.ClosePrice;
					_stopPrice = _entryPrice - StopLossPoints * point;
					_takeProfitPrice = _entryPrice + TakeProfitPoints * point;
				}
			}
			else if (crossDown)
			{
				var volume = CalculateOrderVolume();
				if (volume > 0m)
				{
					SellMarket(volume);
					_entryPrice = candle.ClosePrice;
					_stopPrice = _entryPrice + StopLossPoints * point;
					_takeProfitPrice = _entryPrice - TakeProfitPoints * point;
				}
			}
		}

		_previousFast = fast;
		_previousSlow = slow;
	}

	private decimal GetPoint()
	{
		var step = Security?.PriceStep ?? 1m;
		return step > 0m ? step : 1m;
	}

	private decimal CalculateOrderVolume()
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var baseVolume = step;
		var divider = BalanceDivider;
		var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (divider > 0m && balance > 0m)
		{
			var count = Math.Floor((double)(balance / divider));
			baseVolume = (decimal)count * step;
			if (baseVolume < step)
				baseVolume = step;
		}

		var multiplier = CalculateMartingaleMultiplier();
		var volume = baseVolume * multiplier;

		if (volume < step)
			volume = step;

		var ratio = volume / step;
		volume = Math.Ceiling(ratio) * step;

		return volume;
	}

	private decimal CalculateMartingaleMultiplier()
	{
		if (MaxDoublings <= 0 || _martingaleStage <= 0)
			return 1m;

		var stage = Math.Min(_martingaleStage, MaxDoublings);
		return (decimal)Math.Pow(2d, stage);
	}

	private void RegisterWin()
	{
		_martingaleStage = 0;
	}

	private void ClosePosition()
	{
		if (Position > 0)
			SellMarket(Position);
		else if (Position < 0)
			BuyMarket(-Position);
	}

	private void RegisterLoss()
	{
		if (MaxDoublings <= 0)
		{
			_martingaleStage = 0;
			return;
		}

		if (_martingaleStage < MaxDoublings)
		{
			_martingaleStage++;
		}
		else
		{
			_martingaleStage = 0;
		}
	}
}