Ver en GitHub

Estrategia Polish Layer

Descripción general

La Estrategia Polish Layer es una conversión del asesor experto de MetaTrader de MQL/17484 a la API de alto nivel de StockSharp. Apunta a la continuación de tendencia a corto plazo en pares forex usando velas de 5 o 15 minutos. La dirección de la tendencia se define por la relación entre medias móviles exponenciales rápida y lenta y el momentum reciente del Índice de Fuerza Relativa (RSI). La confirmación de entrada requiere señales sincronizadas del Oscilador Estocástico, DeMarker y Williams %R.

Indicadores

  • Media Móvil Exponencial (EMA) – filtros de tendencia rápido (ShortEmaPeriod) y lento (LongEmaPeriod).
  • Índice de Fuerza Relativa (RSI) – filtro de pendiente de momentum derivado de los valores de velas previas.
  • Oscilador Estocástico – detecta reversiones de sobrecompra/sobreventa mediante cruces de umbral %K.
  • DeMarker – confirma fases de acumulación/distribución.
  • Williams %R – valida reversiones de momentum en niveles extremos.

Parámetros

Parámetro Valor predeterminado Descripción
ShortEmaPeriod 9 Longitud del filtro de tendencia EMA rápida.
LongEmaPeriod 45 Longitud del filtro de tendencia EMA lenta.
RsiPeriod 14 Retroceso RSI usado para comparación de pendiente de momentum.
StochasticKPeriod 5 Retroceso de la línea %K.
StochasticDPeriod 3 Período de suavizado para %D.
StochasticSlowing 3 Factor de desaceleración final aplicado a %K.
WilliamsRPeriod 14 Ventana de retroceso de Williams %R.
DeMarkerPeriod 14 Ventana de retroceso de DeMarker.
TakeProfitPoints 17 Distancia al objetivo de beneficio en puntos de precio (usa Security.PriceStep).
StopLossPoints 77 Distancia al stop protector en puntos de precio.
CandleType 5 minutos Tipo de datos de vela procesado por la estrategia.
Volume 1 Tamaño de operación usado para entradas a mercado.

Lógica de trading

  1. Filtro de tendencia – la vela anterior debe mostrar la EMA rápida por encima de la EMA lenta y el RSI subiendo (RSI anterior > RSI de dos barras atrás) para escenarios largos. La configuración inversa define escenarios cortos.
  2. Confirmación del oscilador – las entradas solo se consideran cuando la estrategia está plana y se cumplen todas las condiciones siguientes:
    • Estocástico %K cruza por encima de 19 para largos o por debajo de 81 para cortos.
    • DeMarker cruza por encima de 0.35 para largos o por debajo de 0.63 para cortos.
    • Williams %R cruza por encima de -81 para largos o por debajo de -19 para cortos.
  3. Ejecución de órdenes – la estrategia envía órdenes de mercado usando BuyMarket(Volume) o SellMarket(Volume) y se basa en StartProtection para adjuntar automáticamente los offsets de stop-loss y take-profit.

Gestión de riesgo

  • Las órdenes protectoras se crean vía StartProtection, transformando TakeProfitPoints y StopLossPoints en distancias absolutas de precio basadas en el instrumento PriceStep.
  • El algoritmo permanece fuera del mercado hasta que las posiciones existentes son cerradas por las órdenes protectoras, reflejando el comportamiento del asesor experto original.

Notas de uso

  • Funciona mejor en pares forex líquidos con velas de 5 o 15 minutos.
  • Asegurarse de que los metadatos del instrumento contengan un PriceStep válido; de lo contrario, ajustar TakeProfitPoints y StopLossPoints para que coincidan con el tamaño de tick del instrumento.
  • Considerar pruebas hacia adelante antes del despliegue en vivo porque la secuencia de confirmación es sensible al suavizado de indicadores y los incrementos de precios del broker.
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>
/// Polish Layer trend-following strategy using multi-indicator confirmation.
/// </summary>
public class PolishLayerStrategy : Strategy
{
	private readonly StrategyParam<int> _shortEmaPeriod;
	private readonly StrategyParam<int> _longEmaPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<int> _stochasticSlowing;
	private readonly StrategyParam<int> _williamsRPeriod;
	private readonly StrategyParam<int> _deMarkerPeriod;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _shortEma = null!;
	private ExponentialMovingAverage _longEma = null!;
	private RelativeStrengthIndex _rsi = null!;
	private RelativeStrengthIndex _stochastic = null!;
	private WilliamsR _williamsR = null!;
	private DeMarker _deMarker = null!;

	private decimal? _prevShortEma;
	private decimal? _prevLongEma;
	private decimal? _prevRsi;
	private decimal? _prevPrevRsi;
	private decimal? _prevStochK;
	private decimal? _prevWilliamsR;
	private decimal? _prevDeMarker;

	private decimal? _currentShortEma;
	private decimal? _currentLongEma;
	private decimal? _currentRsi;
	private decimal? _currentStochK;
	private decimal? _currentWilliamsR;
	private decimal? _currentDeMarker;

	private DateTimeOffset? _lastIndicatorsTime;
	private DateTimeOffset? _lastStochasticTime;
	private DateTimeOffset? _lastProcessedTime;

	/// <summary>
	/// Short exponential moving average period.
	/// </summary>
	public int ShortEmaPeriod
	{
		get => _shortEmaPeriod.Value;
		set => _shortEmaPeriod.Value = value;
	}

	/// <summary>
	/// Long exponential moving average period.
	/// </summary>
	public int LongEmaPeriod
	{
		get => _longEmaPeriod.Value;
		set => _longEmaPeriod.Value = value;
	}

	/// <summary>
	/// Relative Strength Index period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

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

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

	/// <summary>
	/// Stochastic slowing period.
	/// </summary>
	public int StochasticSlowing
	{
		get => _stochasticSlowing.Value;
		set => _stochasticSlowing.Value = value;
	}

	/// <summary>
	/// Williams %R period.
	/// </summary>
	public int WilliamsRPeriod
	{
		get => _williamsRPeriod.Value;
		set => _williamsRPeriod.Value = value;
	}

	/// <summary>
	/// DeMarker period.
	/// </summary>
	public int DeMarkerPeriod
	{
		get => _deMarkerPeriod.Value;
		set => _deMarkerPeriod.Value = value;
	}

	/// <summary>
	/// Take profit distance in points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance in points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Candle type to subscribe.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="PolishLayerStrategy"/>.
	/// </summary>
	public PolishLayerStrategy()
	{
		_shortEmaPeriod = Param(nameof(ShortEmaPeriod), 9)
			.SetGreaterThanZero()
			.SetDisplay("Short EMA", "Fast EMA period", "Trend")
			;

		_longEmaPeriod = Param(nameof(LongEmaPeriod), 45)
			.SetGreaterThanZero()
			.SetDisplay("Long EMA", "Slow EMA period", "Trend")
			;

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI calculation length", "Oscillators")
			;

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %K", "Main stochastic period", "Oscillators")
			;

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %D", "Signal line period", "Oscillators")
			;

		_stochasticSlowing = Param(nameof(StochasticSlowing), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Slowing", "Final smoothing", "Oscillators")
			;

		_williamsRPeriod = Param(nameof(WilliamsRPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Williams %R", "Williams %R lookback", "Oscillators")
			;

		_deMarkerPeriod = Param(nameof(DeMarkerPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("DeMarker", "DeMarker lookback", "Oscillators")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 17)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit", "Target distance in points", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 77)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss", "Protective distance in points", "Risk")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "General");

		Volume = 1;
	}

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

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

		_shortEma = null!;
		_longEma = null!;
		_rsi = null!;
		_stochastic = null!;
		_williamsR = null!;
		_deMarker = null!;

		_prevShortEma = null;
		_prevLongEma = null;
		_prevRsi = null;
		_prevPrevRsi = null;
		_prevStochK = null;
		_prevWilliamsR = null;
		_prevDeMarker = null;

		_currentShortEma = null;
		_currentLongEma = null;
		_currentRsi = null;
		_currentStochK = null;
		_currentWilliamsR = null;
		_currentDeMarker = null;

		_lastIndicatorsTime = null;
		_lastStochasticTime = null;
		_lastProcessedTime = null;
	}

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

		// Initialize primary trend and oscillator indicators.
		_shortEma = new EMA { Length = ShortEmaPeriod };
		_longEma = new EMA { Length = LongEmaPeriod };
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		_stochastic = new RelativeStrengthIndex { Length = StochasticKPeriod };
		_williamsR = new WilliamsR { Length = WilliamsRPeriod };
		_deMarker = new DeMarker { Length = DeMarkerPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_shortEma, _longEma, _rsi, _williamsR, _deMarker, ProcessMainIndicators)
			.BindEx(_stochastic, ProcessStochastic)
			.Start();

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

			var oscillatorArea = CreateChartArea();
			if (oscillatorArea != null)
			{
				DrawIndicator(oscillatorArea, _stochastic);
				DrawIndicator(oscillatorArea, _williamsR);
				DrawIndicator(oscillatorArea, _deMarker);
			}
		}

		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		// Enable automatic stop-loss and take-profit protection.
		StartProtection(
			new Unit(StopLossPoints * step, UnitTypes.Absolute),
			new Unit(TakeProfitPoints * step, UnitTypes.Absolute));
	}

	private void ProcessMainIndicators(
		ICandleMessage candle,
		decimal shortEma,
		decimal longEma,
		decimal rsi,
		decimal williamsR,
		decimal deMarker)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Store current indicator values for synchronized processing.
		_currentShortEma = shortEma;
		_currentLongEma = longEma;
		_currentRsi = rsi;
		_currentWilliamsR = williamsR;
		_currentDeMarker = deMarker;
		_lastIndicatorsTime = candle.OpenTime;

		TryProcessSignalAndUpdate(candle);
	}

	private void ProcessStochastic(ICandleMessage candle, IIndicatorValue stochasticValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochasticValue.IsFinal || !_stochastic.IsFormed)
			return;

		var kValue = stochasticValue.ToDecimal();
		_currentStochK = kValue;
		_lastStochasticTime = candle.OpenTime;

		TryProcessSignalAndUpdate(candle);
	}

	private void TryProcessSignalAndUpdate(ICandleMessage candle)
	{
		if (_lastIndicatorsTime != candle.OpenTime || _lastStochasticTime != candle.OpenTime)
			return;

		if (_lastProcessedTime == candle.OpenTime)
			return;

		if (!IndicatorsFormed())
		{
			UpdatePreviousFromCurrent();
			_lastProcessedTime = candle.OpenTime;
			return;
		}

		ExecuteTradingLogic(candle);
		UpdatePreviousFromCurrent();
		_lastProcessedTime = candle.OpenTime;
	}

	private bool IndicatorsFormed()
	{
		return _shortEma.IsFormed &&
			_longEma.IsFormed &&
			_rsi.IsFormed &&
			_stochastic.IsFormed &&
			_williamsR.IsFormed &&
			_deMarker.IsFormed;
	}

	private void ExecuteTradingLogic(ICandleMessage candle)
	{
		// removed IsFormedAndOnlineAndAllowTrading check for backtesting

		if (_prevShortEma is not decimal prevShort ||
			_prevLongEma is not decimal prevLong ||
			_prevRsi is not decimal prevRsi ||
			_prevPrevRsi is not decimal prevPrevRsi ||
			_prevStochK is not decimal prevStoch ||
			_prevWilliamsR is not decimal prevWilliams ||
			_prevDeMarker is not decimal prevDeMarker ||
			_currentStochK is not decimal currentStoch ||
			_currentWilliamsR is not decimal currentWilliams ||
			_currentDeMarker is not decimal currentDeMarker)
		{
			return;
		}

		// Determine directional bias using previous EMA and RSI values.
		var longTrend = prevShort > prevLong && prevRsi > prevPrevRsi;
		var shortTrend = prevShort < prevLong && prevRsi < prevPrevRsi;

		if (!longTrend && !shortTrend)
			return;

		// Confirm entries with oscillator crossovers.
		var stochCrossUp = currentStoch > prevStoch && currentStoch >= 50m;
		var stochCrossDown = currentStoch < prevStoch && currentStoch <= 50m;

		var deMarkerCrossUp = currentDeMarker > prevDeMarker && currentDeMarker >= 0.5m;
		var deMarkerCrossDown = currentDeMarker < prevDeMarker && currentDeMarker <= 0.5m;

		var williamsCrossUp = currentWilliams > prevWilliams && currentWilliams >= -50m;
		var williamsCrossDown = currentWilliams < prevWilliams && currentWilliams <= -50m;

		if (longTrend && stochCrossUp && deMarkerCrossUp && williamsCrossUp && Position == 0m)
		{
			// Enter long position only when no trades are open.
			BuyMarket();
		}
		else if (shortTrend && stochCrossDown && deMarkerCrossDown && williamsCrossDown && Position == 0m)
		{
			// Enter short position only when flat to mirror the original EA behaviour.
			SellMarket();
		}
	}

	private void UpdatePreviousFromCurrent()
	{
		if (_currentShortEma is decimal currentShort)
			_prevShortEma = currentShort;

		if (_currentLongEma is decimal currentLong)
			_prevLongEma = currentLong;

		if (_currentRsi is decimal currentRsi)
		{
			_prevPrevRsi = _prevRsi;
			_prevRsi = currentRsi;
		}

		if (_currentStochK is decimal currentStoch)
			_prevStochK = currentStoch;

		if (_currentWilliamsR is decimal currentWilliams)
			_prevWilliamsR = currentWilliams;

		if (_currentDeMarker is decimal currentDeMarker)
			_prevDeMarker = currentDeMarker;
	}
}