GitHub で見る

Polish Layer Strategy

Overview

The Polish Layer Strategy is a conversion of the MetaTrader expert advisor from MQL/17484 into the StockSharp high-level API. It targets short-term trend continuation on forex pairs using 5-minute or 15-minute candles. Trend direction is defined by the relationship between fast and slow exponential moving averages and the recent momentum of the Relative Strength Index (RSI). Entry confirmation requires synchronized signals from Stochastic Oscillator, DeMarker, and Williams %R oscillators.

Indicators

  • Exponential Moving Average (EMA) – fast (ShortEmaPeriod) and slow (LongEmaPeriod) trend filters.
  • Relative Strength Index (RSI) – momentum slope filter derived from prior candle values.
  • Stochastic Oscillator – detects oversold/overbought reversals via %K threshold crosses.
  • DeMarker – confirms accumulation/distribution phases.
  • Williams %R – validates momentum reversals at extreme levels.

Parameters

Parameter Default Description
ShortEmaPeriod 9 Length of the fast EMA trend filter.
LongEmaPeriod 45 Length of the slow EMA trend filter.
RsiPeriod 14 RSI lookback used for momentum slope comparison.
StochasticKPeriod 5 Lookback of the %K line.
StochasticDPeriod 3 Smoothing period for %D.
StochasticSlowing 3 Final slowing factor applied to %K.
WilliamsRPeriod 14 Williams %R lookback window.
DeMarkerPeriod 14 DeMarker lookback window.
TakeProfitPoints 17 Distance to the profit target in price points (uses Security.PriceStep).
StopLossPoints 77 Distance to the protective stop in price points.
CandleType 5-minute Candle data type processed by the strategy.
Volume 1 Trade size used for market entries.

Trading Logic

  1. Trend filter – the previous candle must show the fast EMA above the slow EMA and RSI rising (previous RSI > RSI from two bars ago) for long scenarios. The inverse configuration defines short scenarios.
  2. Oscillator confirmation – entries are only considered when the strategy is flat and all of the following conditions are met:
    • Stochastic %K crosses above 19 for longs or below 81 for shorts.
    • DeMarker crosses above 0.35 for longs or below 0.63 for shorts.
    • Williams %R crosses above -81 for longs or below -19 for shorts.
  3. Order execution – the strategy submits market orders using BuyMarket(Volume) or SellMarket(Volume) and relies on StartProtection to attach stop-loss and take-profit offsets automatically.

Risk Management

  • Protective orders are created via StartProtection, transforming TakeProfitPoints and StopLossPoints into absolute price distances based on the instrument PriceStep.
  • The algorithm remains out of the market until existing positions are closed by the protective orders, mirroring the behaviour of the original expert advisor.

Usage Notes

  • Works best on liquid forex pairs with 5-minute or 15-minute candles.
  • Ensure the security metadata contains a valid PriceStep; otherwise, adjust TakeProfitPoints and StopLossPoints to match the instrument tick size.
  • Consider forward-testing before live deployment because the confirmation sequence is sensitive to indicator smoothing and broker pricing increments.
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;
	}
}