Ver en GitHub

Estrategia Altarius RSI Stochastic

Descripción general

La Estrategia Altarius RSI Stochastic es una conversión directa del asesor experto de MetaTrader 5 "Altarius RSI Stohastic" a la API de alto nivel de StockSharp. El sistema sincroniza dos osciladores Stochastic con un RSI rápido de 3 períodos para capturar reversiones de corta duración que ocurren cuando el momentum se comprime y luego se expande nuevamente. La implementación en StockSharp preserva la lógica original de entrada y salida, añadiendo comodidades modernas como parámetros de estrategia, gestión automática del riesgo y dimensionamiento adaptativo de posición.

Cómo funciona

  • Stochastic primario (15/8/8): Actúa como filtro de tendencia. Las posiciones largas requieren que la línea %K esté por debajo de 50 y cruce por encima de la línea %D, señalando momentum alcista en una zona neutral a sobrevendida. Las posiciones cortas requieren la condición espejo por encima de 55.
  • Stochastic secundario (10/3/3): Mide con qué fuerza se desvía %K de %D. Se requiere un diferencial absoluto mínimo de 5 puntos para validar el momentum antes de entrar a una posición.
  • RSI (Período 3): Controla las salidas. Las posiciones largas se cierran cuando el RSI supera 60 y el %D primario gira hacia abajo desde un nivel superior a 70. Las posiciones cortas se cierran cuando el RSI cae por debajo de 40 y el %D primario gira hacia arriba desde un nivel inferior a 30.
  • Guarda de Drawdown: Si el PnL flotante cae por debajo del múltiplo de riesgo configurable del patrimonio de la cuenta, la estrategia liquida inmediatamente la posición abierta, de forma similar al stop de emergencia del código original.
  • Dimensionamiento adaptativo: El volumen inicial se deriva del patrimonio del portafolio multiplicado por el factor MaximumRisk y dividido por 1000, siguiendo el enfoque de MT5. Las operaciones perdedoras consecutivas reducen el tamaño de posición según el DecreaseFactor, respetando un volumen mínimo negociable.

Parámetros

Nombre Descripción Predeterminado
CandleType Marco temporal utilizado para las suscripciones de velas. Marco temporal de 5 minutos
BaseVolume Volumen de reserva usado cuando la información del portafolio no está disponible. 0.1
MinimumVolume Volumen mínimo permitido tras todos los cálculos. 0.1
MaximumRisk Multiplicador de riesgo aplicado al valor del portafolio para el dimensionamiento y la salida por drawdown. 0.1
DecreaseFactor Divisor que reduce el volumen tras operaciones perdedoras consecutivas. 3
PrimaryStochasticLength Período de lookback para la línea %K del Stochastic primario. 15
PrimaryStochasticKPeriod Suavizado para la línea %K primaria. 8
PrimaryStochasticDPeriod Período para la línea de señal %D primaria. 8
SecondaryStochasticLength Período de lookback para el Stochastic de confirmación. 10
SecondaryStochasticKPeriod Suavizado para la línea %K secundaria. 3
SecondaryStochasticDPeriod Período para la línea %D secundaria. 3
DifferenceThreshold Diferencial mínimo entre %K y %D secundarios para permitir entradas. 5
PrimaryBuyLimit Valor máximo de %K primario permitido antes de abrir un largo. 50
PrimarySellLimit Valor mínimo de %K primario permitido antes de abrir un corto. 55
PrimaryExitUpper Umbral de %D primario que debe superarse antes de cerrar largos. 70
PrimaryExitLower Umbral de %D primario que debe caerse por debajo antes de cerrar cortos. 30
RsiPeriod Longitud de lookback del RSI. 3
LongExitRsi Nivel de RSI que confirma las salidas de largos. 60
ShortExitRsi Nivel de RSI que confirma las salidas de cortos. 40

Reglas de trading

  1. Criterios de entrada
    • Largo: %K primario > %D primario, %K primario < PrimaryBuyLimit, y |%K secundario − %D secundario| > DifferenceThreshold mientras la estrategia está plana.
    • Corto: %K primario < %D primario, %K primario > PrimarySellLimit, y |%K secundario − %D secundario| > DifferenceThreshold mientras la estrategia está plana.
  2. Criterios de salida
    • Salida largo: RSI > LongExitRsi, %D primario > PrimaryExitUpper, y el valor actual de %D es inferior al de la vela anterior.
    • Salida corto: RSI < ShortExitRsi, %D primario < PrimaryExitLower, y el valor actual de %D es superior al de la vela anterior.
    • Salida por riesgo: Cuando la pérdida flotante supera MaximumRisk × Portfolio.CurrentValue.

Gestión del riesgo

  • La estrategia llama automáticamente a StartProtection() para activar los servicios de protección de posición integrados de StockSharp.
  • El tamaño de la posición se reduce cuando _lossStreak supera una operación perdedora consecutiva, imitando la lógica DecreaseFactor de MT5.
  • MinimumVolume evita que el tamaño de posición caiga por debajo de los requisitos de tamaño de tick del mercado.

Notas

  • La estrategia asume un portafolio con capacidad de cobertura, exactamente como el EA original.
  • Personaliza el parámetro CandleType para que coincida con el marco temporal que habrías utilizado en MetaTrader (M1, M5, etc.).
  • Combina este módulo con StockSharp Designer o el proyecto Backtester en este repositorio para validar el rendimiento con tus propios datos.
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>
/// Altarius RSI Stochastic strategy converted from the original MQL implementation.
/// Combines two Stochastic oscillators with RSI exits and adaptive position sizing.
/// </summary>
public class AltariusRsiStochasticStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _minimumVolume;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<int> _primaryStochasticLength;
	private readonly StrategyParam<int> _primaryStochasticKPeriod;
	private readonly StrategyParam<int> _primaryStochasticDPeriod;
	private readonly StrategyParam<int> _secondaryStochasticLength;
	private readonly StrategyParam<int> _secondaryStochasticKPeriod;
	private readonly StrategyParam<int> _secondaryStochasticDPeriod;
	private readonly StrategyParam<decimal> _differenceThreshold;
	private readonly StrategyParam<decimal> _primaryBuyLimit;
	private readonly StrategyParam<decimal> _primarySellLimit;
	private readonly StrategyParam<decimal> _primaryExitUpper;
	private readonly StrategyParam<decimal> _primaryExitLower;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _longExitRsi;
	private readonly StrategyParam<decimal> _shortExitRsi;

	private decimal _prevPrimarySignal;
	private bool _hasPrevSignal;
	private decimal _entryPrice;
	private int _positionDirection;
	private int _lossStreak;

	/// <summary>
	/// Type of candles used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Base volume used when account information is not available.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Minimum allowed trade volume.
	/// </summary>
	public decimal MinimumVolume
	{
		get => _minimumVolume.Value;
		set => _minimumVolume.Value = value;
	}

	/// <summary>
	/// Risk multiplier used for volume sizing and drawdown exit.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Factor that reduces volume after consecutive losses.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// Period for the primary Stochastic oscillator.
	/// </summary>
	public int PrimaryStochasticLength
	{
		get => _primaryStochasticLength.Value;
		set => _primaryStochasticLength.Value = value;
	}

	/// <summary>
	/// %K smoothing period for the primary Stochastic oscillator.
	/// </summary>
	public int PrimaryStochasticKPeriod
	{
		get => _primaryStochasticKPeriod.Value;
		set => _primaryStochasticKPeriod.Value = value;
	}

	/// <summary>
	/// %D period for the primary Stochastic oscillator.
	/// </summary>
	public int PrimaryStochasticDPeriod
	{
		get => _primaryStochasticDPeriod.Value;
		set => _primaryStochasticDPeriod.Value = value;
	}

	/// <summary>
	/// Period for the secondary Stochastic oscillator.
	/// </summary>
	public int SecondaryStochasticLength
	{
		get => _secondaryStochasticLength.Value;
		set => _secondaryStochasticLength.Value = value;
	}

	/// <summary>
	/// %K smoothing period for the secondary Stochastic oscillator.
	/// </summary>
	public int SecondaryStochasticKPeriod
	{
		get => _secondaryStochasticKPeriod.Value;
		set => _secondaryStochasticKPeriod.Value = value;
	}

	/// <summary>
	/// %D period for the secondary Stochastic oscillator.
	/// </summary>
	public int SecondaryStochasticDPeriod
	{
		get => _secondaryStochasticDPeriod.Value;
		set => _secondaryStochasticDPeriod.Value = value;
	}

	/// <summary>
	/// Minimum gap between %K and %D on the secondary Stochastic to confirm momentum.
	/// </summary>
	public decimal DifferenceThreshold
	{
		get => _differenceThreshold.Value;
		set => _differenceThreshold.Value = value;
	}

	/// <summary>
	/// Upper bound for primary %K during long entries.
	/// </summary>
	public decimal PrimaryBuyLimit
	{
		get => _primaryBuyLimit.Value;
		set => _primaryBuyLimit.Value = value;
	}

	/// <summary>
	/// Lower bound for primary %K during short entries.
	/// </summary>
	public decimal PrimarySellLimit
	{
		get => _primarySellLimit.Value;
		set => _primarySellLimit.Value = value;
	}

	/// <summary>
	/// Minimum primary %D level to trigger long exits.
	/// </summary>
	public decimal PrimaryExitUpper
	{
		get => _primaryExitUpper.Value;
		set => _primaryExitUpper.Value = value;
	}

	/// <summary>
	/// Maximum primary %D level to trigger short exits.
	/// </summary>
	public decimal PrimaryExitLower
	{
		get => _primaryExitLower.Value;
		set => _primaryExitLower.Value = value;
	}

	/// <summary>
	/// RSI lookback period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// RSI threshold that closes long positions.
	/// </summary>
	public decimal LongExitRsi
	{
		get => _longExitRsi.Value;
		set => _longExitRsi.Value = value;
	}

	/// <summary>
	/// RSI threshold that closes short positions.
	/// </summary>
	public decimal ShortExitRsi
	{
		get => _shortExitRsi.Value;
		set => _shortExitRsi.Value = value;
	}

	/// <summary>
	/// Initialize parameters for the strategy.
	/// </summary>
	public AltariusRsiStochasticStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for calculations", "General");

		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Fallback volume when portfolio data is unavailable", "Position Sizing");

		_minimumVolume = Param(nameof(MinimumVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Volume", "Smallest volume allowed for orders", "Position Sizing");

		_maximumRisk = Param(nameof(MaximumRisk), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Risk Factor", "Risk multiplier used for sizing and drawdown control", "Risk");

		_decreaseFactor = Param(nameof(DecreaseFactor), 3m)
			.SetGreaterThanZero()
			.SetDisplay("Decrease Factor", "Divider applied after losing trades", "Risk");

		_primaryStochasticLength = Param(nameof(PrimaryStochasticLength), 15)
			.SetGreaterThanZero()
			.SetDisplay("Primary %K Length", "Lookback for primary Stochastic", "Primary Stochastic");

		_primaryStochasticKPeriod = Param(nameof(PrimaryStochasticKPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Primary %K Smoothing", "Smoothing for primary %K", "Primary Stochastic");

		_primaryStochasticDPeriod = Param(nameof(PrimaryStochasticDPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Primary %D Period", "Signal period for primary Stochastic", "Primary Stochastic");

		_secondaryStochasticLength = Param(nameof(SecondaryStochasticLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Secondary %K Length", "Lookback for secondary Stochastic", "Secondary Stochastic");

		_secondaryStochasticKPeriod = Param(nameof(SecondaryStochasticKPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Secondary %K Smoothing", "Smoothing for secondary %K", "Secondary Stochastic");

		_secondaryStochasticDPeriod = Param(nameof(SecondaryStochasticDPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Secondary %D Period", "Signal period for secondary Stochastic", "Secondary Stochastic");

		_differenceThreshold = Param(nameof(DifferenceThreshold), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Signal Gap", "Minimum gap between %K and %D on the fast Stochastic", "Entries");

		_primaryBuyLimit = Param(nameof(PrimaryBuyLimit), 50m)
			.SetDisplay("Primary Buy Cap", "Primary %K must stay below this level for longs", "Entries");

		_primarySellLimit = Param(nameof(PrimarySellLimit), 55m)
			.SetDisplay("Primary Sell Floor", "Primary %K must stay above this level for shorts", "Entries");

		_primaryExitUpper = Param(nameof(PrimaryExitUpper), 70m)
			.SetDisplay("Long Exit %D", "Primary %D threshold that ends long trades", "Exits");

		_primaryExitLower = Param(nameof(PrimaryExitLower), 30m)
			.SetDisplay("Short Exit %D", "Primary %D threshold that ends short trades", "Exits");

		_rsiPeriod = Param(nameof(RsiPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Lookback for RSI filter", "Indicators");

		_longExitRsi = Param(nameof(LongExitRsi), 60m)
			.SetDisplay("RSI Exit Long", "RSI value that closes long positions", "Exits");

		_shortExitRsi = Param(nameof(ShortExitRsi), 40m)
			.SetDisplay("RSI Exit Short", "RSI value that closes short positions", "Exits");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevPrimarySignal = 0m;
		_hasPrevSignal = false;
		_entryPrice = 0m;
		_positionDirection = 0;
		_lossStreak = 0;
	}

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

		var primaryStochastic = new StochasticOscillator
		{
			K = { Length = PrimaryStochasticLength },
			D = { Length = PrimaryStochasticDPeriod },
		};

		var secondaryStochastic = new StochasticOscillator
		{
			K = { Length = SecondaryStochasticLength },
			D = { Length = SecondaryStochasticDPeriod },
		};

		var rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod,
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(primaryStochastic, secondaryStochastic, rsi, ProcessCandle)
			.Start();

		// No protection (TP/SL handled internally).
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue primaryValue, IIndicatorValue secondaryValue, IIndicatorValue rsiValue)
	{
		// Trade only on finished candles to avoid intrabar noise.
		if (candle.State != CandleStates.Finished)
			return;

		if (!primaryValue.IsFinal || !secondaryValue.IsFinal || !rsiValue.IsFinal)
			return;

		var primary = (IStochasticOscillatorValue)primaryValue;
		var secondary = (IStochasticOscillatorValue)secondaryValue;

		if (primary.K is not decimal primaryMain || primary.D is not decimal primarySignal)
			return;

		if (secondary.K is not decimal secondaryMain || secondary.D is not decimal secondarySignal)
			return;

		var rsi = rsiValue.GetValue<decimal>();
		var difference = Math.Abs(secondaryMain - secondarySignal);

		// Emergency drawdown exit replicates the account-level risk guard from MQL.
		if (Position != 0)
		{
			var accountValue = Portfolio?.CurrentValue ?? 0m;
			var riskLimit = accountValue * MaximumRisk;
			if (PnL < 0m && riskLimit > 0m && Math.Abs(PnL) >= riskLimit)
			{
				ClosePosition(candle.ClosePrice);
				UpdatePrimarySignal(primarySignal);
				return;
			}
		}

	var canTrade = IsFormedAndOnlineAndAllowTrading();

	if (Position == 0)
	{
		if (!canTrade)
		{
			UpdatePrimarySignal(primarySignal);
			return;
		}

		var bullishSetup = primaryMain > primarySignal && primaryMain < PrimaryBuyLimit && difference > DifferenceThreshold;
		var bearishSetup = primaryMain < primarySignal && primaryMain > PrimarySellLimit && difference > DifferenceThreshold;

		if (bullishSetup)
		{
			var volume = CalculateTradeVolume();
			if (volume > 0m)
			{
				BuyMarket(volume);
				_entryPrice = candle.ClosePrice;
				_positionDirection = 1;
			}
		}
		else if (bearishSetup)
		{
			var volume = CalculateTradeVolume();
			if (volume > 0m)
			{
				SellMarket(volume);
				_entryPrice = candle.ClosePrice;
				_positionDirection = -1;
			}
		}
	}
	else if (canTrade)
	{
		if (Position > 0)
		{
			var exitSignal = rsi > LongExitRsi && _hasPrevSignal && primarySignal < _prevPrimarySignal && primarySignal > PrimaryExitUpper;
			if (exitSignal)
				ClosePosition(candle.ClosePrice);
		}
		else if (Position < 0)
		{
			var exitSignal = rsi < ShortExitRsi && _hasPrevSignal && primarySignal > _prevPrimarySignal && primarySignal < PrimaryExitLower;
			if (exitSignal)
				ClosePosition(candle.ClosePrice);
		}
	}

	UpdatePrimarySignal(primarySignal);
	}

	private decimal CalculateTradeVolume()
	{
		var volume = BaseVolume;
		var accountValue = Portfolio?.CurrentValue;

		// Derive lot size from account equity similar to the original MQL logic.
		if (accountValue is decimal value && value > 0m)
		{
			var riskVolume = Math.Round(value * MaximumRisk / 1000m, 2, MidpointRounding.AwayFromZero);
			if (riskVolume > 0m)
				volume = riskVolume;
		}

		if (DecreaseFactor > 0m && _lossStreak > 1)
		{
			var reduction = volume * _lossStreak / DecreaseFactor;
			volume = Math.Max(volume - reduction, MinimumVolume);
		}

		if (volume < MinimumVolume)
			volume = MinimumVolume;

		return volume;
	}

	private void ClosePosition(decimal exitPrice)
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
		{
			_positionDirection = 0;
			_entryPrice = 0m;
			return;
		}

		var direction = _positionDirection;
		var entryPrice = _entryPrice;

		if (Position > 0)
			SellMarket(volume);
		else
			BuyMarket(volume);

		if (entryPrice > 0m)
		{
			if (direction > 0)
			{
				var profit = exitPrice - entryPrice;
				if (profit < 0m)
					_lossStreak++;
				else if (profit > 0m)
					_lossStreak = 0;
			}
			else if (direction < 0)
			{
				var profit = entryPrice - exitPrice;
				if (profit < 0m)
					_lossStreak++;
				else if (profit > 0m)
					_lossStreak = 0;
			}
		}

		_entryPrice = 0m;
		_positionDirection = 0;
	}

	private void UpdatePrimarySignal(decimal signal)
	{
		_prevPrimarySignal = signal;
		_hasPrevSignal = true;
	}
}