Ver no GitHub

Estratégia Two MA RSI

Visão geral

A estratégia Two MA RSI é uma conversão do assessor especialista original do MetaTrader "2MA_RSI". Usa um cruzamento de média móvel exponencial (EMA) rápida e lenta confirmado por um filtro de Índice de Força Relativa (RSI). As ordens são dimensionadas com um bloco de gestão de dinheiro estilo martingala que aumenta o volume da próxima ordem após uma perda. A versão StockSharp funciona completamente em velas terminadas e reproduz o comportamento original de take-profit e stop-loss em pontos de preço.

Dados e indicadores

  • A estratégia assina uma única série de velas definida por CandleType (velas de 5 minutos por padrão).
  • Três indicadores são calculados em cada barra completa:
    • EMA de FastLength (aplicada ao fechamento da vela).
    • EMA de SlowLength.
    • RSI com comprimento RsiLength.
  • Os valores históricos dos indicadores são armazenados internamente para detectar cruzamentos de EMA sem extrair dados dos buffers de indicadores.

Lógica de entrada

  1. A vela anterior deve estar terminada para evitar a reavaliação intrabarra.
  2. Nenhuma posição ativa é permitida (Position == 0).
  3. Entrada comprada:
    • A EMA rápida cruza acima da EMA lenta (a EMA rápida na barra atual é maior que a EMA lenta, enquanto a barra anterior tinha EMA rápida < EMA lenta).
    • O valor do RSI está abaixo de RsiOversold, confirmando um mercado sobrevendido.
  4. Entrada vendida:
    • A EMA rápida cruza abaixo da EMA lenta com a condição análoga (EMA rápida agora abaixo da EMA lenta, anteriormente acima).
    • O RSI está acima de RsiOverbought, sinalizando um mercado sobrecomprado.
  5. Quando todas as condições são satisfeitas, a estratégia envia uma ordem a mercado dimensionada de acordo com o módulo de martingala.

Lógica de saída

  • Um stop-loss de proteção e um take-profit são calculados imediatamente após cada entrada. As distâncias são definidas em "pontos" e convertidas através do PriceStep do instrumento:
    • Comprado:
      • Stop loss = preço de entrada - StopLossPoints * PriceStep.
      • Take profit = preço de entrada + TakeProfitPoints * PriceStep.
    • Vendido:
      • Stop loss = preço de entrada + StopLossPoints * PriceStep.
      • Take profit = preço de entrada - TakeProfitPoints * PriceStep.
  • Apenas esses níveis de proteção fecham uma operação. A estratégia aguarda a próxima vela para confirmar se a mínima/máxima tocou o alvo ou o stop e envia uma ordem ClosePosition() a mercado de acordo.
  • A prioridade de saída corresponde ao comportamento conservador do robô original: um stop loss é avaliado antes de um take profit se ambos os níveis caírem dentro do mesmo intervalo de vela.

Dimensionamento de posição e martingala

  1. O volume base é calculado em cada entrada como floor(balance / BalanceDivider) * VolumeStep. O valor sempre fica em ou acima de um passo de volume e usa CurrentValue do portfólio (recorrendo a BeginValue quando necessário).
  2. Após cada saída perdedora, o estágio de martingala aumenta em um até MaxDoublings. O próximo volume de ordem é multiplicado por 2^stage.
  3. Qualquer operação vencedora ou atingir o número máximo de duplicações redefine o estágio para zero, retornando ao volume base.
  4. Se MaxDoublings for zero ou negativo, o tamanho nunca aumenta e iguala o volume base.

Comportamento adicional

  • A estratégia mantém o registro dos valores anteriores de EMA internamente e não solicita valores históricos de indicadores.
  • As ordens são executadas apenas quando a estratégia está online, os indicadores estão formados e a negociação é permitida.
  • A saída do gráfico desenha velas de preço, operações próprias e os três indicadores para análise visual.

Parâmetros

Parâmetro Descrição Padrão
FastLength Comprimento da EMA rápida. 5
SlowLength Comprimento da EMA lenta. 20
RsiLength Número de barras usadas no cálculo do RSI. 14
RsiOverbought Nível RSI que bloqueia novos comprados e permite vendidos. 70
RsiOversold Nível RSI que permite comprados. 30
StopLossPoints Distância do stop-loss expressa em passos de preço. 500
TakeProfitPoints Distância do take-profit em passos de preço. 1500
BalanceDivider Divide o valor do portfólio para obter o tamanho base da ordem. 1000
MaxDoublings Número máximo de duplicações de martingala após perdas consecutivas. 1
CandleType Série de velas utilizada pela estratégia. Período de 5 minutos

Notas de uso

  • Fornecer um portfólio e instrumento com metadados válidos de PriceStep e VolumeStep para que a gestão de risco baseada em pontos e o dimensionamento de posição permaneçam consistentes.
  • Como ordens a mercado são usadas para saídas, derrapagem e spreads ainda são possíveis em comparação com as ordens limite da versão MetaTrader, mas a lógica de avaliação de stop/take é preservada.
  • A estratégia não cria uma versão Python; apenas a implementação C# é fornecida conforme 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;
		}
	}
}