Ver no GitHub

Estratégia de teste Rsi

Visão geral

RsiTestStrategy converte o MetaTrader 4 consultor especialista RSI_Test em StockSharp de alto nível API. A estratégia combina um filtro de impulso rápido RSI com confirmação simples de velas e dimensionamento de posição consciente do risco. Ele negocia um único instrumento definido pela estratégia hospedeira e usa apenas velas concluídas, refletindo a lógica tick-to-close do código original.

Regras de negociação

  1. Calcule o Índice de Força Relativa com o configurável RsiPeriod.
  2. Opere comprado quando o RSI estiver subindo de uma região de sobrevenda (BuyLevel) e a vela atual abrir acima da anterior.
  3. Opere vendido quando o RSI estiver caindo de uma região de sobrecompra (SellLevel) e a vela atual abrir abaixo da anterior.
  4. Respeite o limite de MaxOpenPositions. Um valor de 0 desativa o limite; caso contrário, a exposição líquida não poderá exceder MaxOpenPositions * Volume.
  5. Gerencie as saídas por meio de um trailing stop em estilo de escada que é ativado quando o preço avança TrailingDistanceSteps ticks além do preço médio de entrada.
  6. Nenhum take-profit explícito é usado. As posições saem quando o trailing stop é acionado ou quando a sessão de negociação encerra a estratégia.

Dimensionamento de posição e risco

  • A estratégia deriva um tamanho de pedido provisório de RiskPercentage do valor atual do portfólio. Quando o instrumento fornece dados de margem (Security.MarginBuy/Security.MarginSell) o capital exigido por lote é honrado; caso contrário, o valor será dividido pelo último preço de fechamento como uma alternativa conservadora.
  • Os volumes são arredondados para Security.VolumeStep (ou duas casas decimais se a etapa for desconhecida) e limitados dentro do intervalo Security.MinVolume/Security.MaxVolume.
  • Defina RiskPercentage como zero para desativar o dimensionamento dinâmico e sempre negociar o Volume configurado.

Comportamento de parada móvel

  • TrailingDistanceSteps expressa a distância em etapas de preço (Security.PriceStep). Se o instrumento não expor um passo, a distância é tratada como uma compensação direta de preço.
  • Uma vez que o fechamento ou a máxima intrabarra cruza o nível de ativação (entry + distance para posições compradas, entry - distance para posições vendidas), a estratégia arma o trailing stop no mesmo deslocamento além do preço de entrada.
  • O stop de proteção é aplicado apenas uma vez por posição, exatamente como o EA original que move o stop do ponto de equilíbrio para o primeiro degrau e o mantém lá.

Parâmetros

Nome Descrição Padrão
RsiPeriod RSI período de retrospectiva. 14
BuyLevel Limite de sobrevenda que prepara uma configuração longa. 12
SellLevel Limite de sobrecompra que prepara uma configuração curta. 88
RiskPercentage Participação do portfólio usada para dimensionamento de posição. Defina 0 para ignorar. 10
TrailingDistanceSteps Distância (em etapas de preço) necessária para armar o trailing stop. 50
MaxOpenPositions Máximas posições simultâneas; 0 remove o limite. 1
CandleType Prazo principal para cálculos. 15 minutos
Volume Volume base quando o dimensionamento do risco não pode ser resolvido. 1

Notas de uso

  1. Anexe a estratégia a um título que exponha PriceStep, VolumeStep e metadados de margem precisos para a melhor correspondência com o comportamento MQL.
  2. O algoritmo verifica apenas velas concluídas (CandleStates.Finished), portanto, os backtests devem usar o mesmo período de produção.
  3. StartProtection() da classe base está habilitado em OnStarted, permitindo que o controle de risco integrado de StockSharp gerencie remanescentes de posição inesperados.
  4. Como o consultor especialista original lançou MetaTrader otimizações por meio de GlobalVariableGet, esse comportamento é omitido intencionalmente. Configure os parâmetros diretamente em StockSharp.
  5. Combine a estratégia com um portfólio que atualize Portfolio.CurrentValue para dimensionamento dinâmico de risco. Sem isso, a estratégia volta normalmente para a estática Volume.
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// RSI-based strategy with volume sizing and stair-like trailing stop.
/// </summary>
public class RsiTestStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _buyLevel;
	private readonly StrategyParam<decimal> _sellLevel;
	private readonly StrategyParam<decimal> _riskPercentage;
	private readonly StrategyParam<int> _trailingDistanceSteps;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private decimal? _previousRsi;
	private decimal? _previousOpen;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private bool _trailingArmed;
	private decimal _priceStep;

	/// <summary>
	/// Initialize <see cref="RsiTestStrategy"/>.
	/// </summary>
	public RsiTestStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 7)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Lookback period for RSI", "Indicators")

			.SetOptimize(7, 28, 1);

		_buyLevel = Param(nameof(BuyLevel), 40m)
			.SetDisplay("RSI Buy Level", "Oversold threshold for long entries", "Trading");

		_sellLevel = Param(nameof(SellLevel), 60m)
			.SetDisplay("RSI Sell Level", "Overbought threshold for short entries", "Trading");

		_riskPercentage = Param(nameof(RiskPercentage), 10m)
			.SetDisplay("Risk Percentage", "Portfolio percentage used for sizing", "Risk");

		_trailingDistanceSteps = Param(nameof(TrailingDistanceSteps), 50)
			.SetDisplay("Trailing Distance Steps", "Steps before activating trailing stop", "Risk");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
			.SetDisplay("Max Open Positions", "Maximum simultaneous positions. 0 disables the limit.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "Data");
	}

	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	public decimal BuyLevel
	{
		get => _buyLevel.Value;
		set => _buyLevel.Value = value;
	}

	public decimal SellLevel
	{
		get => _sellLevel.Value;
		set => _sellLevel.Value = value;
	}

	public decimal RiskPercentage
	{
		get => _riskPercentage.Value;
		set => _riskPercentage.Value = value;
	}

	public int TrailingDistanceSteps
	{
		get => _trailingDistanceSteps.Value;
		set => _trailingDistanceSteps.Value = value;
	}

	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_previousRsi = null;
		_previousOpen = null;
		_entryPrice = null;
		_stopPrice = null;
		_trailingArmed = false;
		_priceStep = 0m;
	}

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

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		_priceStep = Security?.PriceStep ?? 0m;

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

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

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
	{
		// Only react to fully formed candles to match the MQL implementation.
		if (candle.State != CandleStates.Finished)
		return;

		// Manage trailing logic and exits before attempting fresh entries.
		ManagePosition(candle);

		if (!_rsi.IsFormed)
		{
			_previousRsi = rsiValue;
			_previousOpen = candle.OpenPrice;
			return;
		}

		if (_previousRsi is null || _previousOpen is null)
		{
			_previousRsi = rsiValue;
			_previousOpen = candle.OpenPrice;
			return;
		}

		if (rsiValue < BuyLevel && Position <= 0)
		{
			TryEnterLong(candle);
		}
		else if (rsiValue > SellLevel && Position >= 0)
		{
			TryEnterShort(candle);
		}

		_previousRsi = rsiValue;
		_previousOpen = candle.OpenPrice;
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		// Close short position first if needed
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			ResetPositionState();
		}

		if (Position == 0)
		{
			BuyMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = null;
			_trailingArmed = false;
		}
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		// Close long position first if needed
		if (Position > 0)
		{
			SellMarket(Math.Abs(Position));
			ResetPositionState();
		}

		if (Position == 0)
		{
			SellMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = null;
			_trailingArmed = false;
		}
	}

	private void ManagePosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetPositionState();
			return;
		}

		var avgPrice = _entryPrice;
		if (avgPrice > 0m)
		_entryPrice = avgPrice;

		if (Position > 0)
		{
			UpdateTrailingForLong(candle);
			TryExitLong(candle);
		}
		else if (Position < 0)
		{
			UpdateTrailingForShort(candle);
			TryExitShort(candle);
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingDistanceSteps <= 0 || _entryPrice is null || _trailingArmed)
		return;

		var trailingDistance = GetPriceOffset(TrailingDistanceSteps);
		if (trailingDistance <= 0m)
		return;

		var activationPrice = _entryPrice.Value + trailingDistance;
		if (candle.HighPrice < activationPrice)
		return;

		_stopPrice = _entryPrice.Value + trailingDistance;
		_trailingArmed = true;
		LogInfo($"Activated long trailing stop at {_stopPrice:0.#####}.");
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingDistanceSteps <= 0 || _entryPrice is null || _trailingArmed)
		return;

		var trailingDistance = GetPriceOffset(TrailingDistanceSteps);
		if (trailingDistance <= 0m)
		return;

		var activationPrice = _entryPrice.Value - trailingDistance;
		if (candle.LowPrice > activationPrice)
		return;

		_stopPrice = _entryPrice.Value - trailingDistance;
		_trailingArmed = true;
		LogInfo($"Activated short trailing stop at {_stopPrice:0.#####}.");
	}

	private void TryExitLong(ICandleMessage candle)
	{
		if (_stopPrice is null)
		return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (candle.LowPrice > _stopPrice.Value)
		return;

		SellMarket(volume);
		ResetPositionState();
	}

	private void TryExitShort(ICandleMessage candle)
	{
		if (_stopPrice is null)
		return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (candle.HighPrice < _stopPrice.Value)
		return;

		BuyMarket(volume);
		ResetPositionState();
	}

	private decimal CalculateOrderVolume(decimal referencePrice)
	{
		var volume = Volume;

		if (RiskPercentage > 0m)
		{
			var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			var riskCapital = portfolioValue * RiskPercentage / 100m;

			if (riskCapital > 0m)
			{
				var margin = GetSecurityValue<decimal?>(Level1Fields.MarginBuy) ?? GetSecurityValue<decimal?>(Level1Fields.MarginSell) ?? 0m;

				if (margin > 0m)
				{
					volume = riskCapital / margin;
				}
				else if (referencePrice > 0m)
				{
					volume = riskCapital / referencePrice;
				}
			}
		}

		volume = RoundVolume(volume);

		// Ensure volume is at least the base Volume when calculation produces too small a value
		if (volume <= 0m)
			volume = Volume;

		var minVolume = Security?.MinVolume;
		if (minVolume != null && minVolume.Value > 0m && volume < minVolume.Value)
		{
			volume = minVolume.Value;
		}

		var maxVolume = Security?.MaxVolume;
		if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
		{
			volume = maxVolume.Value;
		}

		return volume;
	}

	private decimal RoundVolume(decimal volume)
	{
		if (volume <= 0m)
		{
			return 0m;
		}

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			if (steps <= 0m)
			{
				return step;
			}

			return steps * step;
		}

		return Math.Round(volume, 2, MidpointRounding.ToZero);
	}

	private bool HasCapacityForNewPosition(decimal volume)
	{
		if (MaxOpenPositions <= 0)
		{
			return true;
		}

		if (volume <= 0m)
		{
			return false;
		}

		var exposure = Math.Abs(Position);
		var maxExposure = MaxOpenPositions * volume;

		return exposure + volume <= maxExposure + volume * 0.0001m;
	}

	private decimal GetPriceOffset(int steps)
	{
		if (steps <= 0)
		{
			return 0m;
		}

		if (_priceStep > 0m)
		{
			return steps * _priceStep;
		}

		return steps;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_trailingArmed = false;
	}
}