在 GitHub 上查看

Polish Layer 策略

概述

Polish Layer 策略源自 MQL/17484 的 MetaTrader 智能交易系统,现已移植到 StockSharp 的高层 API。策略关注外汇市场的短期趋势延续,默认使用 5 分钟或 15 分钟 K 线。趋势方向由快、慢指数移动平均线(EMA)的相对位置和 RSI 的动量斜率决定,入场需要 Stochastic、DeMarker 与 Williams %R 三个振荡指标同时给出突破信号。

指标

  • 指数移动平均线 (EMA) —— ShortEmaPeriodLongEmaPeriod 组成的快慢趋势过滤器。
  • 相对强弱指数 (RSI) —— 根据前两根 K 线的值评估动量变化。
  • 随机振荡指标 (Stochastic Oscillator) —— 通过 %K 线突破水平判断超买/超卖反转。
  • DeMarker 指标 —— 辨别市场的吸筹与派发阶段。
  • Williams %R —— 在极值区域确认动量反转。

参数

参数 默认值 说明
ShortEmaPeriod 9 快速 EMA 的周期。
LongEmaPeriod 45 慢速 EMA 的周期。
RsiPeriod 14 RSI 的计算周期。
StochasticKPeriod 5 %K 线的周期。
StochasticDPeriod 3 %D 线的平滑周期。
StochasticSlowing 3 %K 的最终平滑系数。
WilliamsRPeriod 14 Williams %R 的回看窗口。
DeMarkerPeriod 14 DeMarker 指标的回看窗口。
TakeProfitPoints 17 止盈距离(按 Security.PriceStep 转换为价格)。
StopLossPoints 77 止损距离(按价格步长计算)。
CandleType 5 分钟 策略使用的 K 线类型。
Volume 1 每次下单的交易量。

交易逻辑

  1. 趋势过滤:上一根 K 线的快速 EMA 必须高于慢速 EMA,同时上一根 RSI 要高于两根之前的 RSI 才允许做多;做空信号则相反。
  2. 振荡指标确认:仅在没有持仓时检查以下条件:
    • Stochastic %K 上穿 19 触发做多,下穿 81 触发做空。
    • DeMarker 上穿 0.35(多头)或下破 0.63(空头)。
    • Williams %R 上穿 -81(多头)或下破 -19(空头)。
  3. 下单执行:满足条件后调用 BuyMarket(Volume)SellMarket(Volume),并通过 StartProtection 自动附加止盈止损。

风险控制

  • StartProtection 会根据 PriceStepTakeProfitPointsStopLossPoints 换算为绝对价格差,并自动维护保护单。
  • 只有在已有仓位通过止盈或止损离场后,策略才会寻找新的交易机会,从而与原始 EA 的行为保持一致。

使用建议

  • 适用于流动性高的外汇品种,推荐 5 分钟或 15 分钟周期。
  • 请确认交易品种已正确设置 PriceStep,必要时调整止盈、止损参数以匹配最小报价单位。
  • 由于多重指标需要同步确认,建议在真实交易前进行前向测试以验证滑点与数据差异的影响。
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;
	}
}