Ver en GitHub

Estrategia de Risk Reward Ratio

Descripción general

La Estrategia de Risk Reward Ratio es un port de alto nivel en StockSharp del experto de MetaTrader "Risk Reward Ratio". La estrategia combina varios filtros de confirmación de impulso y tendencia con un módulo disciplinado de gestión del riesgo. Las entradas se generan a partir de la confluencia de osciladores estocásticos, un cruce de media móvil ponderada lineal (LWMA), un filtro RSI de 14 períodos y una verificación de tendencia con MACD. El control del riesgo se logra mediante un stop-loss basado en pips, un take-profit con ratio de recompensa automático, stops de seguimiento y lógica de break-even opcionales, y un interruptor de salida de emergencia que liquida inmediatamente la posición.

La conversión mantiene el espíritu original del experto de MetaTrader usando las suscripciones a velas e indicadores de StockSharp. Todo el procesamiento de indicadores ocurre en velas completadas y evita el acceso directo a los búferes de indicadores, preservando el paradigma de streaming del motor.

Lógica de trading

  1. Confluencia estocástica
    • Un estocástico rápido (5, 2, 2) proporciona la señal principal de impulso usando la línea %K.
    • Un estocástico lento (21, 10, 4) suministra el sesgo direccional a través de su línea %D suavizada.
    • Los setups largos requieren que el %K rápido esté por encima del %D lento, mientras que los cortos requieren lo contrario.
  2. Confirmación RSI
    • Un RSI de 14 períodos debe estar por encima de 50 para operaciones largas y por debajo de 50 para cortas, asegurando que el mercado esté alineado con la dirección propuesta.
  3. Filtro de tendencia con LWMAs
    • Dos medias móviles ponderadas linealmente (longitudes 6 y 85) deben confirmar la dirección: la LWMA rápida sobre la lenta para largos y por debajo para cortos.
  4. Calificador de tendencia MACD
    • El histograma MACD (12, 26, 9) debe estar de acuerdo con la dirección de la señal. La línea principal debe liderar a la línea de señal permaneciendo en el lado apropiado del cero.
  5. Filtro de desviación de impulso
    • Un indicador de Momentum de 14 períodos mide la distancia desde 100. Al menos una de las últimas tres lecturas de momentum debe superar el umbral configurable para demostrar que el precio está acelerando lo suficiente para justificar una operación.
  6. Límites de posición
    • La exposición neta está limitada por MaxPositions * TradeVolume, de modo que la estrategia no pueda piramidizar más allá de la restricción original del EA.

Las órdenes se envían como ejecuciones de mercado usando BuyMarket y SellMarket. La estrategia ignora las velas no finalizadas y mantiene todo el estado dentro de los campos de clase para respetar la arquitectura orientada a eventos de StockSharp.

Gestión del riesgo

  • Stop-loss en pips – Cada entrada instala un stop de protección a StopLossPips * PriceStep desde el precio de llenado.
  • Take-profit con ratio de recompensa – La distancia del take-profit es igual a la distancia del stop multiplicada por RewardRatio para mantener una relación fija de recompensa-riesgo.
  • Stop de seguimiento – Cuando está habilitado, el stop se mueve detrás del precio por TrailingStopPips una vez que el mercado avanza al menos esa distancia desde la entrada.
  • Ajuste de break-even – Después de BreakEvenTriggerPips de movimiento favorable, el stop se empuja a la entrada más un colchón adicional de BreakEvenOffsetPips (o menos para cortos), asegurando ganancias.
  • Interruptor de salida – Establecer ExitSwitch en true aplana la posición actual en la siguiente barra completada y deshabilita el procesamiento hasta que el indicador se desactive.

Parámetros

Nombre Predeterminado Descripción
TradeVolume 0.1 Volumen de cada orden de mercado.
CandleType Marco temporal de 15m Serie de velas principal.
FastMaPeriod 6 Período de la LWMA rápida.
SlowMaPeriod 85 Período de la LWMA lenta.
MomentumThreshold 0.3 Distancia absoluta mínima del indicador de Momentum desde 100 necesaria para permitir entradas.
RewardRatio 2 Múltiplo de take-profit relativo al stop-loss.
StopLossPips 20 Distancia del stop-loss en pips (múltiplos de PriceStep).
MaxPositions 10 Número máximo de unidades de volumen (TradeVolume) permitidas simultáneamente.
EnableTrailing true Habilita actualizaciones de stop de seguimiento basadas en pips.
TrailingStopPips 40 Distancia del stop de seguimiento en pips.
EnableBreakEven true Activa la gestión de stop de break-even.
BreakEvenTriggerPips 30 Ganancia (en pips) requerida antes de mover el stop al break-even.
BreakEvenOffsetPips 30 Desplazamiento adicional en pips cuando el stop se traslada al break-even.
ExitSwitch false Fuerza a la estrategia a liquidar toda la exposición en la siguiente vela completada.

Flujo de trabajo

  1. Configure el instrumento y la serie de velas deseados, luego establezca los parámetros de riesgo.
  2. Inicie la estrategia. Se suscribe a las velas, vincula indicadores y comienza a procesar barras completadas.
  3. Cuando se alinean las condiciones de entrada, el motor envía una orden de mercado y almacena los niveles de stop/objetivo.
  4. En cada vela completada, el bloque de riesgo evalúa las reglas de seguimiento, break-even y salida de emergencia.
  5. Las salidas se activan al alcanzar niveles de stop/take-profit, actualizaciones de seguimiento, ajustes de break-even o el interruptor de emergencia.

Notas

  • La conversión aprovecha el binding de indicadores de StockSharp en lugar del acceso manual a búferes, asegurando que cada indicador reciba datos sincronizados.
  • Todos los cálculos se basan en el PriceStep del instrumento. Si el paso es cero o falta, las distancias de riesgo permanecen deshabilitadas para evitar niveles de precio no válidos.
  • La estrategia no modifica órdenes pendientes; simplemente envía órdenes de mercado para abrir/cerrar posiciones, replicando la forma en que el EA original aplanaba la exposición cuando se alcanzaban los umbrales.
using System;
using System.Collections.Generic;
using System.Linq;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Multi-filter risk/reward strategy.
/// </summary>
public class RiskRewardRatioStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<decimal> _momentumThreshold;
	private readonly StrategyParam<decimal> _rewardRatio;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<bool> _enableTrailing;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<bool> _enableBreakEven;
	private readonly StrategyParam<int> _breakEvenTriggerPips;
	private readonly StrategyParam<int> _breakEvenOffsetPips;
	private readonly StrategyParam<bool> _exitSwitch;

	private readonly List<Leg> _legs = [];
	private readonly List<decimal> _momentumDistances = [];

	private WeightedMovingAverage _fastLwma;
	private WeightedMovingAverage _slowLwma;
	private RelativeStrengthIndex _rsi;
	private MovingAverageConvergenceDivergenceSignal _macd;
	private RateOfChange _momentum;
	private StochasticK _fastStochasticK;
	private StochasticK _slowStochasticK;
	private SimpleMovingAverage _fastStochasticMain;
	private SimpleMovingAverage _slowStochasticMain;
	private SimpleMovingAverage _slowStochasticSignal;

	public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int FastMaPeriod { get => _fastMaPeriod.Value; set => _fastMaPeriod.Value = value; }
	public int SlowMaPeriod { get => _slowMaPeriod.Value; set => _slowMaPeriod.Value = value; }
	public decimal MomentumThreshold { get => _momentumThreshold.Value; set => _momentumThreshold.Value = value; }
	public decimal RewardRatio { get => _rewardRatio.Value; set => _rewardRatio.Value = value; }
	public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public int MaxPositions { get => _maxPositions.Value; set => _maxPositions.Value = value; }
	public bool EnableTrailing { get => _enableTrailing.Value; set => _enableTrailing.Value = value; }
	public int TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
	public bool EnableBreakEven { get => _enableBreakEven.Value; set => _enableBreakEven.Value = value; }
	public int BreakEvenTriggerPips { get => _breakEvenTriggerPips.Value; set => _breakEvenTriggerPips.Value = value; }
	public int BreakEvenOffsetPips { get => _breakEvenOffsetPips.Value; set => _breakEvenOffsetPips.Value = value; }
	public bool ExitSwitch { get => _exitSwitch.Value; set => _exitSwitch.Value = value; }

	public RiskRewardRatioStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 0.1m).SetGreaterThanZero();
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
		_fastMaPeriod = Param(nameof(FastMaPeriod), 6).SetGreaterThanZero();
		_slowMaPeriod = Param(nameof(SlowMaPeriod), 85).SetGreaterThanZero();
		_momentumThreshold = Param(nameof(MomentumThreshold), 0.3m).SetNotNegative();
		_rewardRatio = Param(nameof(RewardRatio), 2m).SetGreaterThanZero();
		_stopLossPips = Param(nameof(StopLossPips), 20).SetGreaterThanZero();
		_maxPositions = Param(nameof(MaxPositions), 10).SetGreaterThanZero();
		_enableTrailing = Param(nameof(EnableTrailing), true);
		_trailingStopPips = Param(nameof(TrailingStopPips), 40).SetNotNegative();
		_enableBreakEven = Param(nameof(EnableBreakEven), true);
		_breakEvenTriggerPips = Param(nameof(BreakEvenTriggerPips), 30).SetNotNegative();
		_breakEvenOffsetPips = Param(nameof(BreakEvenOffsetPips), 30).SetNotNegative();
		_exitSwitch = Param(nameof(ExitSwitch), false);
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	protected override void OnReseted()
	{
		base.OnReseted();
		_legs.Clear();
		_momentumDistances.Clear();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_fastLwma = new() { Length = FastMaPeriod };
		_slowLwma = new() { Length = SlowMaPeriod };
		_rsi = new() { Length = 14 };
		_macd = new()
		{
			Macd =
			{
				ShortMa = { Length = 12 },
				LongMa = { Length = 26 },
			},
			SignalMa = { Length = 9 },
		};
		_momentum = new() { Length = 14 };
		_fastStochasticK = new() { Length = 5 };
		_slowStochasticK = new() { Length = 21 };
		_fastStochasticMain = new() { Length = 2 };
		_slowStochasticMain = new() { Length = 4 };
		_slowStochasticSignal = new() { Length = 10 };

		SubscribeCandles(CandleType)
			.BindEx(_fastLwma, _slowLwma, _rsi, _macd, _momentum, _fastStochasticK, _slowStochasticK, ProcessCandle, true)
			.Start();
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade?.Order is null || trade.Trade is null)
			return;

		var leg = _legs.FirstOrDefault(l => l.Order == trade.Order);
		if (leg is not null)
			AddFill(leg, trade);
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue fastLwmaValue, IIndicatorValue slowLwmaValue, IIndicatorValue rsiValue,
		IIndicatorValue macdValue, IIndicatorValue momentumValue, IIndicatorValue fastKValue, IIndicatorValue slowKValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var fastMain = UpdateFastStochastic(fastKValue);
		var slowSignal = UpdateSlowStochastic(slowKValue);
		UpdateMomentum(momentumValue);

		if (ExitSwitch)
		{
			Flatten();
			return;
		}

		if (ApplyRisk(candle))
			return;

		if (fastMain is not decimal fastK || slowSignal is not decimal slowD)
			return;

		if (!_fastLwma.IsFormed || !_slowLwma.IsFormed || !_rsi.IsFormed || !_macd.IsFormed || !_momentum.IsFormed)
			return;

		var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
		if (macd.Macd is not decimal macdMain || macd.Signal is not decimal macdSignal)
			return;

		var rsi = rsiValue.ToDecimal();
		var fastLwma = fastLwmaValue.ToDecimal();
		var slowLwma = slowLwmaValue.ToDecimal();
		var momentumBurst = _momentumDistances.Count == 0 ? 0m : _momentumDistances.Max();

		var longSignal =
			fastK > slowD &&
			rsi > 50m &&
			fastLwma > slowLwma &&
			macdMain > macdSignal && macdMain > 0m &&
			momentumBurst >= MomentumThreshold;

		var shortSignal =
			fastK < slowD &&
			rsi < 50m &&
			fastLwma < slowLwma &&
			macdMain < macdSignal && macdMain < 0m &&
			momentumBurst >= MomentumThreshold;

		var maxExposure = MaxPositions * TradeVolume;

		if (longSignal && Position >= 0m && Position + TradeVolume <= maxExposure)
			Enter(Sides.Buy, candle.OpenTime);
		else if (shortSignal && Position <= 0m && Math.Abs(Position - TradeVolume) <= maxExposure)
			Enter(Sides.Sell, candle.OpenTime);
	}

	private decimal? UpdateFastStochastic(IIndicatorValue fastKValue)
	{
		if (!_fastStochasticK.IsFormed || fastKValue.IsEmpty)
			return null;

		var main = _fastStochasticMain.Process(fastKValue);
		return _fastStochasticMain.IsFormed && !main.IsEmpty ? main.ToDecimal() : null;
	}

	private decimal? UpdateSlowStochastic(IIndicatorValue slowKValue)
	{
		if (!_slowStochasticK.IsFormed || slowKValue.IsEmpty)
			return null;

		var main = _slowStochasticMain.Process(slowKValue);
		if (!_slowStochasticMain.IsFormed || main.IsEmpty)
			return null;

		var signal = _slowStochasticSignal.Process(main);
		return _slowStochasticSignal.IsFormed && !signal.IsEmpty ? signal.ToDecimal() : null;
	}

	private void UpdateMomentum(IIndicatorValue momentumValue)
	{
		if (!_momentum.IsFormed || momentumValue.IsEmpty)
			return;

		// |ROC| equals the momentum's distance from 100.
		_momentumDistances.Add(Math.Abs(momentumValue.ToDecimal()));

		if (_momentumDistances.Count > 3)
			_momentumDistances.RemoveAt(0);
	}

	private void Enter(Sides side, DateTime candleTime)
	{
		var order = side == Sides.Buy ? BuyMarket(TradeVolume) : SellMarket(TradeVolume);

		var leg = new Leg
		{
			Order = order,
			Side = side,
			EntryTime = candleTime,
		};

		_legs.Add(leg);

		// The emulator can fill a market order before BuyMarket or SellMarket returns it.
		foreach (var trade in MyTrades.Where(t => t.Order == order && t.Trade is not null).ToArray())
			AddFill(leg, trade);
	}

	private void AddFill(Leg leg, MyTrade trade)
	{
		leg.FilledVolume += trade.Trade.Volume;
		leg.FilledValue += trade.Trade.Price * trade.Trade.Volume;
		leg.FillPrice = leg.FilledValue / leg.FilledVolume;
		SetInitialLevels(leg, leg.FillPrice.Value);
	}

	private void SetInitialLevels(Leg leg, decimal fill)
	{
		leg.BestPrice = fill;

		if (GetPip() is not decimal pip)
		{
			leg.StopPrice = null;
			leg.TakePrice = null;
			return;
		}

		var stopDistance = StopLossPips * pip;
		var takeDistance = stopDistance * RewardRatio;

		if (leg.Side == Sides.Buy)
		{
			leg.StopPrice = fill - stopDistance;
			leg.TakePrice = fill + takeDistance;
		}
		else
		{
			leg.StopPrice = fill + stopDistance;
			leg.TakePrice = fill - takeDistance;
		}
	}

	private bool ApplyRisk(ICandleMessage candle)
	{
		var pip = GetPip();
		var exitSell = 0m;
		var exitBuy = 0m;

		for (var i = _legs.Count - 1; i >= 0; i--)
		{
			var leg = _legs[i];
			if (leg.FillPrice is not decimal fill || candle.OpenTime <= leg.EntryTime)
				continue;

			var isLong = leg.Side == Sides.Buy;
			leg.BestPrice = isLong ? Math.Max(leg.BestPrice, candle.HighPrice) : Math.Min(leg.BestPrice, candle.LowPrice);

			if (pip is decimal step)
				MoveStop(leg, fill, step);

			var hit = isLong
				? (leg.StopPrice is decimal stop && candle.LowPrice <= stop) || (leg.TakePrice is decimal take && candle.HighPrice >= take)
				: (leg.StopPrice is decimal shortStop && candle.HighPrice >= shortStop) || (leg.TakePrice is decimal shortTake && candle.LowPrice <= shortTake);

			if (!hit)
				continue;

			if (isLong)
				exitSell += leg.FilledVolume;
			else
				exitBuy += leg.FilledVolume;

			_legs.RemoveAt(i);
		}

		if (exitSell > 0m)
			SellMarket(exitSell);

		if (exitBuy > 0m)
			BuyMarket(exitBuy);

		return exitSell > 0m || exitBuy > 0m;
	}

	private void MoveStop(Leg leg, decimal fill, decimal pip)
	{
		var isLong = leg.Side == Sides.Buy;
		var advance = isLong ? leg.BestPrice - fill : fill - leg.BestPrice;

		if (EnableBreakEven && advance >= BreakEvenTriggerPips * pip)
		{
			var offset = BreakEvenOffsetPips * pip;
			TightenStop(leg, isLong ? fill + offset : fill - offset);
		}

		if (EnableTrailing && TrailingStopPips > 0)
		{
			var trail = TrailingStopPips * pip;

			if (advance >= trail)
				TightenStop(leg, isLong ? leg.BestPrice - trail : leg.BestPrice + trail);
		}
	}

	private static void TightenStop(Leg leg, decimal candidate)
	{
		if (leg.StopPrice is not decimal stop || (leg.Side == Sides.Buy ? candidate > stop : candidate < stop))
			leg.StopPrice = candidate;
	}

	private void Flatten()
	{
		if (Position > 0m)
			SellMarket(Math.Abs(Position));
		else if (Position < 0m)
			BuyMarket(Math.Abs(Position));

		_legs.Clear();
	}

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

	private sealed class Leg
	{
		public Order Order { get; init; }
		public Sides Side { get; init; }
		public DateTime EntryTime { get; init; }
		public decimal FilledVolume { get; set; }
		public decimal FilledValue { get; set; }
		public decimal? FillPrice { get; set; }
		public decimal BestPrice { get; set; }
		public decimal? StopPrice { get; set; }
		public decimal? TakePrice { get; set; }
	}
}