Ver en GitHub

Estrategia Multi Stochastic

Descripción general

La estrategia Multi Stochastic es una implementación de alto nivel de StockSharp del asesor experto de MetaTrader 5 "Multi Stochastic (barabashkakvn's edition)". Monitorea hasta cuatro pares de divisas simultáneamente y se basa en señales sincronizadas de las lecturas del Oscilador Stochastic (5, 3, 3). La estrategia abre una única posición de mercado por símbolo cuando ocurre un cruce desde zona de sobreventa o sobrecompra y cierra las operaciones mediante objetivos fijos de stop-loss y take-profit basados en pips.

Lógica de trading

  • Cada símbolo configurado recibe su propio Oscilador Stochastic (longitud 5, suavizado %K 3, suavizado %D 3).
  • Una señal larga se produce cuando el %K actual está por debajo del OversoldLevel (predeterminado 20), la barra anterior tenía %K por debajo de %D, y la barra actual cierra con %K cruzando hacia arriba %D.
  • Una señal corta se produce cuando el %K actual está por encima del OverboughtLevel (predeterminado 80), la barra anterior tenía %K por encima de %D, y la barra actual cierra con %K cruzando hacia abajo %D.
  • Solo se permite una posición abierta por instrumento. Las señales adicionales se ignoran hasta que la posición existente esté cerrada.

Gestión de riesgo

  • Los valores de stop-loss y take-profit se expresan en pips. La estrategia convierte automáticamente pips a distancias de precio absolutas multiplicando por el paso de precio del instrumento y ajustando para cotizaciones forex de 3 o 5 dígitos (pip = paso × 10 para esos instrumentos).
  • Las posiciones largas se cierran cuando el mínimo de la vela toca el nivel de stop-loss o el máximo de la vela alcanza el nivel de take-profit.
  • Las posiciones cortas se cierran cuando el máximo de la vela toca el nivel de stop-loss o el mínimo de la vela alcanza el nivel de take-profit.

Parámetros

  • CandleType – marco temporal usado para todas las velas suscritas (predeterminado: 1 hora).
  • StochasticLength – longitud base del Oscilador Stochastic (predeterminado: 5).
  • StochasticKPeriod – período de suavizado para %K (predeterminado: 3).
  • StochasticDPeriod – período de suavizado para %D (predeterminado: 3).
  • OversoldLevel – umbral usado para detectar condiciones de sobreventa (predeterminado: 20).
  • OverboughtLevel – umbral usado para detectar condiciones de sobrecompra (predeterminado: 80).
  • StopLossPips – distancia al stop protector en pips (predeterminado: 50).
  • TakeProfitPips – distancia al objetivo de beneficio en pips (predeterminado: 10).
  • UseSymbol1UseSymbol4 – habilita el trading para el slot de símbolo respectivo (predeterminado: true).
  • Symbol1Symbol4 – instrumentos negociados por cada slot. Symbol 1 utiliza el instrumento principal de la estrategia cuando no se especifica.

Notas de implementación

  • Cada suscripción de símbolo es independiente. Cada una usa SubscribeCandles con BindEx para recibir actualizaciones de StochasticOscillatorValue junto con los datos de velas.
  • Los valores anteriores de %K y %D se almacenan en caché por símbolo para emular la lógica de detección de cruce de MT5.
  • Los parámetros de riesgo se recalculan para cada entrada, y los niveles de stop/take se reinician después de que una posición se cierra o cuando no existe ninguna posición.
  • Las órdenes se envían con BuyMarket/SellMarket usando la propiedad Volume compartida, cumpliendo la restricción de posición única del experto original.

Diferencias con la versión MT5

  • La versión StockSharp aprovecha suscripciones de alto nivel en lugar de llamadas manuales de actualización de tasas.
  • La detección del tamaño del pip se basa en Security.PriceStep y Security.Decimals. Si los metadatos no están disponibles, los stops y objetivos permanecen deshabilitados para evitar cálculos de riesgo incorrectos.
  • Los hooks de registro y dibujo de gráficos están listos para su extensión pero no son necesarios para el comportamiento principal.

Consejos de uso

  1. Asigna los instrumentos deseados a los slots de símbolos y ajusta el marco temporal de velas para que coincida con tu horizonte de trading.
  2. Asegúrate de que las distancias de stop-loss y take-profit sean compatibles con el tamaño del tick del instrumento para evitar cierres inmediatos.
  3. Deshabilita los slots de símbolos no usados para reducir el consumo de recursos cuando se monitorean menos instrumentos.
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>
/// StockSharp port of the Multi Stochastic MT5 expert advisor.
/// </summary>
public class MultiStochasticStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _stochasticLength;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<decimal> _oversoldLevel;
	private readonly StrategyParam<decimal> _overboughtLevel;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;

	private readonly StrategyParam<bool> _useSymbol1;
	private readonly StrategyParam<bool> _useSymbol2;
	private readonly StrategyParam<bool> _useSymbol3;
	private readonly StrategyParam<bool> _useSymbol4;

	private readonly StrategyParam<Security> _symbol1;
	private readonly StrategyParam<Security> _symbol2;
	private readonly StrategyParam<Security> _symbol3;
	private readonly StrategyParam<Security> _symbol4;

	private Security _resolvedSymbol1;
	private Security _resolvedSymbol2;
	private Security _resolvedSymbol3;
	private Security _resolvedSymbol4;

	private StochasticOscillator _stochastic1;
	private StochasticOscillator _stochastic2;
	private StochasticOscillator _stochastic3;
	private StochasticOscillator _stochastic4;

	private decimal? _prevK1;
	private decimal? _prevD1;
	private decimal? _prevK2;
	private decimal? _prevD2;
	private decimal? _prevK3;
	private decimal? _prevD3;
	private decimal? _prevK4;
	private decimal? _prevD4;

	private decimal? _stopPrice1;
	private decimal? _takePrice1;
	private decimal? _stopPrice2;
	private decimal? _takePrice2;
	private decimal? _stopPrice3;
	private decimal? _takePrice3;
	private decimal? _stopPrice4;
	private decimal? _takePrice4;

	private decimal _pipValue1;
	private decimal _pipValue2;
	private decimal _pipValue3;
	private decimal _pipValue4;

	/// <summary>
	/// Candle series used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Base length for the Stochastic Oscillator.
	/// </summary>
	public int StochasticLength
	{
		get => _stochasticLength.Value;
		set => _stochasticLength.Value = value;
	}

	/// <summary>
	/// %K smoothing period.
	/// </summary>
	public int StochasticKPeriod
	{
		get => _stochasticKPeriod.Value;
		set => _stochasticKPeriod.Value = value;
	}

	/// <summary>
	/// %D smoothing period.
	/// </summary>
	public int StochasticDPeriod
	{
		get => _stochasticDPeriod.Value;
		set => _stochasticDPeriod.Value = value;
	}

	/// <summary>
	/// Oversold threshold for long signals.
	/// </summary>
	public decimal OversoldLevel
	{
		get => _oversoldLevel.Value;
		set => _oversoldLevel.Value = value;
	}

	/// <summary>
	/// Overbought threshold for short signals.
	/// </summary>
	public decimal OverboughtLevel
	{
		get => _overboughtLevel.Value;
		set => _overboughtLevel.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Enable trading for the first symbol slot.
	/// </summary>
	public bool UseSymbol1
	{
		get => _useSymbol1.Value;
		set => _useSymbol1.Value = value;
	}

	/// <summary>
	/// Enable trading for the second symbol slot.
	/// </summary>
	public bool UseSymbol2
	{
		get => _useSymbol2.Value;
		set => _useSymbol2.Value = value;
	}

	/// <summary>
	/// Enable trading for the third symbol slot.
	/// </summary>
	public bool UseSymbol3
	{
		get => _useSymbol3.Value;
		set => _useSymbol3.Value = value;
	}

	/// <summary>
	/// Enable trading for the fourth symbol slot.
	/// </summary>
	public bool UseSymbol4
	{
		get => _useSymbol4.Value;
		set => _useSymbol4.Value = value;
	}

	/// <summary>
	/// Security used in the first slot.
	/// </summary>
	public Security Symbol1
	{
		get => _symbol1.Value;
		set => _symbol1.Value = value;
	}

	/// <summary>
	/// Security used in the second slot.
	/// </summary>
	public Security Symbol2
	{
		get => _symbol2.Value;
		set => _symbol2.Value = value;
	}

	/// <summary>
	/// Security used in the third slot.
	/// </summary>
	public Security Symbol3
	{
		get => _symbol3.Value;
		set => _symbol3.Value = value;
	}

	/// <summary>
	/// Security used in the fourth slot.
	/// </summary>
	public Security Symbol4
	{
		get => _symbol4.Value;
		set => _symbol4.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="MultiStochasticStrategy"/>.
	/// </summary>
	public MultiStochasticStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame applied to every symbol", "Data");

		_stochasticLength = Param(nameof(StochasticLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Length", "Base period for Stochastic", "Indicators");

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("%K Period", "Smoothing period for %K", "Indicators");

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("%D Period", "Smoothing period for %D", "Indicators");

		_oversoldLevel = Param(nameof(OversoldLevel), 20m)
			.SetGreaterThanZero()
			.SetDisplay("Oversold Level", "Threshold for long entries", "Signals");

		_overboughtLevel = Param(nameof(OverboughtLevel), 80m)
			.SetGreaterThanZero()
			.SetDisplay("Overbought Level", "Threshold for short entries", "Signals");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Stop-loss distance expressed in pips", "Risk")
			
			.SetOptimize(10m, 200m, 10m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 10m)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Take-profit distance expressed in pips", "Risk")
			
			.SetOptimize(5m, 100m, 5m);

		_useSymbol1 = Param(nameof(UseSymbol1), true)
			.SetDisplay("Use symbol #1", "Enable trading for the first slot", "Symbols");

		_useSymbol2 = Param(nameof(UseSymbol2), false)
			.SetDisplay("Use symbol #2", "Enable trading for the second slot", "Symbols");

		_useSymbol3 = Param(nameof(UseSymbol3), false)
			.SetDisplay("Use symbol #3", "Enable trading for the third slot", "Symbols");

		_useSymbol4 = Param(nameof(UseSymbol4), false)
			.SetDisplay("Use symbol #4", "Enable trading for the fourth slot", "Symbols");

		_symbol1 = Param<Security>(nameof(Symbol1))
			.SetDisplay("Symbol #1", "Security assigned to the first slot", "Symbols");

		_symbol2 = Param<Security>(nameof(Symbol2))
			.SetDisplay("Symbol #2", "Security assigned to the second slot", "Symbols");

		_symbol3 = Param<Security>(nameof(Symbol3))
			.SetDisplay("Symbol #3", "Security assigned to the third slot", "Symbols");

		_symbol4 = Param<Security>(nameof(Symbol4))
			.SetDisplay("Symbol #4", "Security assigned to the fourth slot", "Symbols");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		var symbol1 = UseSymbol1 ? Symbol1 ?? Security : null;
		var symbol2 = UseSymbol2 ? Symbol2 : null;
		var symbol3 = UseSymbol3 ? Symbol3 : null;
		var symbol4 = UseSymbol4 ? Symbol4 : null;

		if (symbol1 != null)
			yield return (symbol1, CandleType);

		if (symbol2 != null)
			yield return (symbol2, CandleType);

		if (symbol3 != null)
			yield return (symbol3, CandleType);

		if (symbol4 != null)
			yield return (symbol4, CandleType);
	}

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

		_prevK1 = _prevD1 = null;
		_prevK2 = _prevD2 = null;
		_prevK3 = _prevD3 = null;
		_prevK4 = _prevD4 = null;

		_stopPrice1 = _takePrice1 = null;
		_stopPrice2 = _takePrice2 = null;
		_stopPrice3 = _takePrice3 = null;
		_stopPrice4 = _takePrice4 = null;

		_pipValue1 = _pipValue2 = _pipValue3 = _pipValue4 = 0m;
		_resolvedSymbol1 = _resolvedSymbol2 = _resolvedSymbol3 = _resolvedSymbol4 = null;
		_stochastic1 = _stochastic2 = _stochastic3 = _stochastic4 = null;
	}

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

		_resolvedSymbol1 = UseSymbol1 ? Symbol1 ?? Security : null;
		_resolvedSymbol2 = UseSymbol2 ? Symbol2 : null;
		_resolvedSymbol3 = UseSymbol3 ? Symbol3 : null;
		_resolvedSymbol4 = UseSymbol4 ? Symbol4 : null;

		StartForSymbol(_resolvedSymbol1, ref _stochastic1, ref _pipValue1, ProcessSymbol1);
		StartForSymbol(_resolvedSymbol2, ref _stochastic2, ref _pipValue2, ProcessSymbol2);
		StartForSymbol(_resolvedSymbol3, ref _stochastic3, ref _pipValue3, ProcessSymbol3);
		StartForSymbol(_resolvedSymbol4, ref _stochastic4, ref _pipValue4, ProcessSymbol4);
	}

	private void StartForSymbol(Security security, ref StochasticOscillator indicator, ref decimal pipValue, Action<ICandleMessage, IIndicatorValue> handler)
	{
		if (security == null)
			return;

		indicator = CreateStochastic();
		pipValue = CalculatePipValue(security);

		if (pipValue <= 0m)
			LogWarning($"Unable to detect pip size for {security.Id}. Protective levels will be disabled.");

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(indicator, handler)
			.Start();
	}

	private StochasticOscillator CreateStochastic()
	{
		return new StochasticOscillator
		{
			K = { Length = StochasticKPeriod },
			D = { Length = StochasticDPeriod }
		};
	}

	private void ProcessSymbol1(ICandleMessage candle, IIndicatorValue stochValue)
	{
		HandleSymbol(candle, stochValue, _resolvedSymbol1, ref _prevK1, ref _prevD1, ref _stopPrice1, ref _takePrice1, _pipValue1);
	}

	private void ProcessSymbol2(ICandleMessage candle, IIndicatorValue stochValue)
	{
		HandleSymbol(candle, stochValue, _resolvedSymbol2, ref _prevK2, ref _prevD2, ref _stopPrice2, ref _takePrice2, _pipValue2);
	}

	private void ProcessSymbol3(ICandleMessage candle, IIndicatorValue stochValue)
	{
		HandleSymbol(candle, stochValue, _resolvedSymbol3, ref _prevK3, ref _prevD3, ref _stopPrice3, ref _takePrice3, _pipValue3);
	}

	private void ProcessSymbol4(ICandleMessage candle, IIndicatorValue stochValue)
	{
		HandleSymbol(candle, stochValue, _resolvedSymbol4, ref _prevK4, ref _prevD4, ref _stopPrice4, ref _takePrice4, _pipValue4);
	}

	private void HandleSymbol(
		ICandleMessage candle,
		IIndicatorValue stochValue,
		Security security,
		ref decimal? prevK,
		ref decimal? prevD,
		ref decimal? stopPrice,
		ref decimal? takePrice,
		decimal pipValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochValue.IsFinal)
			return;

		if (security == null)
			return;

		var typed = (StochasticOscillatorValue)stochValue;
		var currentK = typed.K;
		var currentD = typed.D;

		var position = GetPositionVolume(security);

		if (ManageRisk(candle, security, ref stopPrice, ref takePrice, position))
		{
			prevK = currentK;
			prevD = currentD;
			return;
		}

		if (prevK is null || prevD is null)
		{
			prevK = currentK;
			prevD = currentD;
			return;
		}

		// Indicator readiness is checked via stochValue.IsFinal above.

		var longSignal = currentK < OversoldLevel && prevK.Value < prevD.Value && currentK > currentD;
		var shortSignal = currentK > OverboughtLevel && prevK.Value > prevD.Value && currentK < currentD;

		position = GetPositionVolume(security);

		if (position == 0m)
		{
			var volume = Volume;

			if (longSignal && volume > 0m)
			{
				// Enter long position after bullish crossover in oversold zone.
				BuyMarket();
				stopPrice = StopLossPips > 0m && pipValue > 0m ? candle.ClosePrice - StopLossPips * pipValue : null;
				takePrice = TakeProfitPips > 0m && pipValue > 0m ? candle.ClosePrice + TakeProfitPips * pipValue : null;
			}
			else if (shortSignal && volume > 0m)
			{
				// Enter short position after bearish crossover in overbought zone.
				SellMarket();
				stopPrice = StopLossPips > 0m && pipValue > 0m ? candle.ClosePrice + StopLossPips * pipValue : null;
				takePrice = TakeProfitPips > 0m && pipValue > 0m ? candle.ClosePrice - TakeProfitPips * pipValue : null;
			}
		}

		prevK = currentK;
		prevD = currentD;
	}

	private bool ManageRisk(ICandleMessage candle, Security security, ref decimal? stopPrice, ref decimal? takePrice, decimal position)
	{
		if (position > 0m)
		{
			// Close long positions on protective levels.
			if (stopPrice.HasValue && candle.LowPrice <= stopPrice.Value)
			{
				SellMarket();
				stopPrice = takePrice = null;
				return true;
			}

			if (takePrice.HasValue && candle.HighPrice >= takePrice.Value)
			{
				SellMarket();
				stopPrice = takePrice = null;
				return true;
			}
		}
		else if (position < 0m)
		{
			// Close short positions on protective levels.
			var volume = Math.Abs(position);

			if (stopPrice.HasValue && candle.HighPrice >= stopPrice.Value)
			{
				BuyMarket();
				stopPrice = takePrice = null;
				return true;
			}

			if (takePrice.HasValue && candle.LowPrice <= takePrice.Value)
			{
				BuyMarket();
				stopPrice = takePrice = null;
				return true;
			}
		}
		else
		{
			// Reset protective levels when no position is active.
			stopPrice = takePrice = null;
		}

		return false;
	}

	private decimal GetPositionVolume(Security security)
	{
		return Position;
	}

	private decimal CalculatePipValue(Security security)
	{
		var step = security.PriceStep ?? 0m;

		if (step <= 0m)
			return 0m;

		var decimals = security.Decimals ?? 0;
		var multiplier = (decimals == 3 || decimals == 5) ? 10m : 1m;

		return step * multiplier;
	}
}