Ver no GitHub

Estratégia de Risk Reward Ratio

Visão geral

A Estratégia de Risk Reward Ratio é um port de alto nível do StockSharp do especialista MetaTrader "Risk Reward Ratio". A estratégia combina vários filtros de confirmação de momentum e tendência com um módulo disciplinado de gestão de risco. As entradas são geradas a partir da confluência de osciladores estocásticos, um cruzamento de média móvel ponderada linear (LWMA), um filtro RSI de 14 períodos e uma verificação de tendência MACD. O controle de risco é alcançado através de um stop-loss baseado em pips, um take-profit de ratio de recompensa automático, stops de seguimento e lógica de break-even opcionais, e um interruptor de saída de emergência que liquida imediatamente a posição.

A conversão mantém o espírito original do especialista MetaTrader usando as subscrições de velas e APIs de binding de indicadores do StockSharp. Todo o processamento de indicadores ocorre em velas concluídas e evita o acesso direto aos buffers de indicadores, preservando o paradigma de streaming do motor.

Lógica de trading

  1. Confluência estocástica
    • Um estocástico rápido (5, 2, 2) fornece o sinal primário de momentum usando a linha %K.
    • Um estocástico lento (21, 10, 4) fornece o viés direcional através da sua linha %D suavizada.
    • Setups longos requerem que o %K rápido esteja acima do %D lento, enquanto setups curtos requerem o oposto.
  2. Confirmação RSI
    • Um RSI de 14 períodos deve estar acima de 50 para negociações longas e abaixo de 50 para curtas, garantindo que o mercado esteja alinhado com a direção proposta.
  3. Filtro de tendência via LWMAs
    • Duas médias móveis ponderadas linealmente (comprimentos 6 e 85) devem confirmar a direção: LWMA rápida acima da lenta para comprados e abaixo para vendidos.
  4. Qualificador de tendência MACD
    • O histograma MACD (12, 26, 9) deve estar de acordo com a direção do sinal. A linha principal deve liderar a linha de sinal permanecendo no lado apropriado do zero.
  5. Filtro de desvio de momentum
    • Um indicador de Momentum de 14 períodos mede a distância a partir de 100. Pelo menos uma das últimas três leituras de momentum deve exceder o limiar configurável para provar que o preço está acelerando o suficiente para justificar uma negociação.
  6. Limites de posição
    • A exposição líquida é limitada por MaxPositions * TradeVolume, de modo que a estratégia não possa piramidizar além da restrição original do EA.

As ordens são enviadas como execuções de mercado usando BuyMarket e SellMarket. A estratégia ignora velas não concluídas e mantém todo o estado dentro de campos de classe para respeitar a arquitetura orientada a eventos do StockSharp.

Gestão de risco

  • Stop-loss em pips – Cada entrada instala um stop de proteção a StopLossPips * PriceStep do preço de preenchimento.
  • Take-profit com ratio de recompensa – A distância do take-profit é igual à distância do stop multiplicada por RewardRatio para manter uma relação fixa de recompensa-risco.
  • Trailing stop – Quando habilitado, o stop se move atrás do preço por TrailingStopPips assim que o mercado avança pelo menos essa distância da entrada.
  • Ajuste de break-even – Após BreakEvenTriggerPips de movimento favorável, o stop é movido para a entrada mais uma almofada adicional de BreakEvenOffsetPips (ou menos para vendidos), assegurando ganhos.
  • Interruptor de saída – Definir ExitSwitch como true achata a posição atual na próxima vela concluída e desabilita o processamento até que o sinalizador seja desativado.

Parâmetros

Nome Padrão Descrição
TradeVolume 0.1 Volume de cada ordem de mercado.
CandleType Período de 15m Série de velas principal.
FastMaPeriod 6 Período da LWMA rápida.
SlowMaPeriod 85 Período da LWMA lenta.
MomentumThreshold 0.3 Distância absoluta mínima do indicador de Momentum a partir de 100 necessária para permitir entradas.
RewardRatio 2 Múltiplo de take-profit relativo ao stop-loss.
StopLossPips 20 Distância do stop-loss em pips (múltiplos de PriceStep).
MaxPositions 10 Número máximo de unidades de volume (TradeVolume) permitidas simultaneamente.
EnableTrailing true Habilita atualizações de trailing stop baseadas em pips.
TrailingStopPips 40 Distância do trailing stop em pips.
EnableBreakEven true Ativa o gerenciamento de stop de break-even.
BreakEvenTriggerPips 30 Lucro (em pips) necessário antes de mover o stop para break-even.
BreakEvenOffsetPips 30 Deslocamento adicional em pips quando o stop se move para break-even.
ExitSwitch false Força a estratégia a fechar toda a exposição na próxima vela concluída.

Fluxo de trabalho

  1. Configure o instrumento e a série de velas desejados, depois defina os parâmetros de risco.
  2. Inicie a estratégia. Ela se subscreve às velas, vincula indicadores e começa a processar barras concluídas.
  3. Quando as condições de entrada se alinham, o motor envia uma ordem de mercado e armazena os níveis de stop/alvo.
  4. Em cada vela concluída, o bloco de risco avalia as regras de trailing, break-even e saída de emergência.
  5. As saídas são acionadas ao atingir níveis de stop/take-profit, atualizações de trailing, ajustes de break-even ou o interruptor de emergência.

Notas

  • A conversão aproveita o binding de indicadores do StockSharp em vez do acesso manual a buffers, garantindo que cada indicador receba dados sincronizados.
  • Todos os cálculos dependem do PriceStep do instrumento. Se o passo for zero ou estiver ausente, as distâncias de risco permanecem desabilitadas para evitar níveis de preço inválidos.
  • A estratégia não modifica ordens pendentes; simplesmente envia ordens de mercado para abrir/fechar posições, espelhando a forma como o EA original achatava a exposição quando os limiares eram atingidos.
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; }
	}
}