在 GitHub 上查看

Polish Layer Expert Advisor System Efficient

概述

该策略完整移植自 MQL4 指标交易系统 “Polish Layer Expert Advisor System Efficient”。原版建议运行在 5 或 15 分钟周期的图表上,因此这里同样只在单一时间框上运作,并且同一时间仅持有一张仓位。趋势方向由价格的快慢均线以及两条经 SMA 平滑处理的 RSI 均线共同决定;当趋势条件满足后,还需要同时通过随机指标、DeMarker 与 Williams %R 的极值反转信号确认,才会执行进出场。

交易逻辑

  1. 趋势过滤。 收盘价的 9 周期简单均线必须位于 45 周期线性加权均线之上才能做多,反之才能做空。与此同时,RSI 值的 9 周期 SMA 也必须高于(做多)或低于(做空)45 周期 SMA。只要任一过滤条件不同步,新的交易就会被禁止。
  2. 随机指标触发。 在满足多头趋势的前提下,%K 需要从下方突破超卖阈值(默认 19)并向上穿越 %D 才能触发买入。空头信号则要求 %K 从上方跌破超买阈值(默认 81)并且跌破 %D。所有平滑参数都继承自原始脚本。
  3. 动量确认。 多头入场还需满足 DeMarker 从下方突破 0.35,且 Williams %R 从下方突破 −81;空头则要求 DeMarker 向下突破 0.63,Williams %R 向下突破 −19。所有阈值判断均基于上一根完整 K 线与当前 K 线之间的变化。
  4. 仓位管理。 仅使用市价单开仓,策略在持仓期间不会加仓或反手。若设置了固定的止损/止盈,则会按照品种的最小报价步长把 “点” 转换为实际价格距离;若无法获得步长,则保护功能会被关闭。

风险管理

  • 止损 / 止盈。 参数以点数表示,并通过 Security.PriceStep 转换为价格差(默认约定 1 点 = 1 个最小报价步长),在入场后立即生效。将其设为 0 可关闭对应的保护。
  • 单一仓位。 与原版 EA 一样,不会在已有仓位时开立新单。

参数

参数 默认值 说明
Volume 0.1 下单手数。请根据账户合约规模自行调整。
CandleType TimeSpan.FromMinutes(15).TimeFrame() 用于计算指标的 K 线类型。建议使用 5 或 15 分钟周期以贴近原版。
RsiPeriod 14 RSI 基础计算周期。
ShortPricePeriod 9 趋势过滤中使用的价格快线 SMA 周期。
LongPricePeriod 45 趋势过滤中使用的价格慢线 LWMA 周期。
ShortRsiPeriod 9 应用于 RSI 数值的快速 SMA 周期。
LongRsiPeriod 45 应用于 RSI 数值的慢速 SMA 周期。
StochasticKPeriod 5 随机指标 %K 基础周期。
StochasticDPeriod 3 %D 平滑周期。
StochasticSlowing 3 %K 额外放缓系数。
DemarkerPeriod 14 DeMarker 指标的平滑周期。
WilliamsPeriod 14 Williams %R 的回溯周期。
StochasticOversoldLevel 19 %K 必须向上突破的超卖阈值。
StochasticOverboughtLevel 81 %K 必须向下跌破的超买阈值。
DemarkerBuyLevel 0.35 多头信号所需的 DeMarker 向上突破水平。
DemarkerSellLevel 0.63 空头信号所需的 DeMarker 向下突破水平。
WilliamsBuyLevel -81 Williams %R 向上突破的确认水平(做多)。
WilliamsSellLevel -19 Williams %R 向下突破的确认水平(做空)。
StopLossPips 7777 固定止损点数。默认的超大数值相当于关闭止损。
TakeProfitPips 17 固定止盈点数。设为 0 可禁用止盈。

注意事项

  • 请确保 Security.PriceStepSecurity.MinVolumeSecurity.VolumeStep 已正确配置,否则点数到价格的转换会失真。
  • 所有信号都依赖相邻两根完整 K 线之间的交叉状况,回测时务必保证数据时间对齐与原策略一致。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Polish Layer Expert Advisor System Efficient strategy converted from MQL4.
/// Combines price and RSI moving averages with Stochastic, DeMarker, and Williams %R confirmations.
/// </summary>
public class PolishLayerExpertAdvisorSystemEfficientStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<int> _shortPricePeriod;
private readonly StrategyParam<int> _longPricePeriod;
private readonly StrategyParam<int> _shortRsiPeriod;
private readonly StrategyParam<int> _longRsiPeriod;
private readonly StrategyParam<int> _stochasticKPeriod;
private readonly StrategyParam<int> _stochasticDPeriod;
private readonly StrategyParam<int> _stochasticSlowing;
private readonly StrategyParam<int> _demarkerPeriod;
private readonly StrategyParam<int> _williamsPeriod;
private readonly StrategyParam<decimal> _stochasticOversoldLevel;
private readonly StrategyParam<decimal> _stochasticOverboughtLevel;
private readonly StrategyParam<decimal> _demarkerBuyLevel;
private readonly StrategyParam<decimal> _demarkerSellLevel;
private readonly StrategyParam<decimal> _williamsBuyLevel;
private readonly StrategyParam<decimal> _williamsSellLevel;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;

private SimpleMovingAverage _shortPriceMa = null!;
private WeightedMovingAverage _longPriceMa = null!;
private RelativeStrengthIndex _rsi = null!;
private SimpleMovingAverage _shortRsiAverage = null!;
private SimpleMovingAverage _longRsiAverage = null!;

private StochasticOscillator _stochastic = null!;
private DeMarker _deMarker = null!;
private WilliamsR _williams = null!;

private decimal? _previousStochasticMain;
private decimal? _previousStochasticSignal;
private decimal? _previousDeMarker;
private decimal? _previousWilliams;

private decimal? _longStopPrice;
private decimal? _longTakePrice;
private decimal? _shortStopPrice;
private decimal? _shortTakePrice;

private decimal _priceStep;


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

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

/// <summary>
/// Period for the fast price moving average.
/// </summary>
public int ShortPricePeriod
{
get => _shortPricePeriod.Value;
set => _shortPricePeriod.Value = value;
}

/// <summary>
/// Period for the slow price moving average (LWMA).
/// </summary>
public int LongPricePeriod
{
get => _longPricePeriod.Value;
set => _longPricePeriod.Value = value;
}

/// <summary>
/// Length of the short RSI smoothing average.
/// </summary>
public int ShortRsiPeriod
{
get => _shortRsiPeriod.Value;
set => _shortRsiPeriod.Value = value;
}

/// <summary>
/// Length of the long RSI smoothing average.
/// </summary>
public int LongRsiPeriod
{
get => _longRsiPeriod.Value;
set => _longRsiPeriod.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 factor.
/// </summary>
public int StochasticSlowing
{
get => _stochasticSlowing.Value;
set => _stochasticSlowing.Value = value;
}

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

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

/// <summary>
/// Stochastic oversold level.
/// </summary>
public decimal StochasticOversoldLevel
{
get => _stochasticOversoldLevel.Value;
set => _stochasticOversoldLevel.Value = value;
}

/// <summary>
/// Stochastic overbought level.
/// </summary>
public decimal StochasticOverboughtLevel
{
get => _stochasticOverboughtLevel.Value;
set => _stochasticOverboughtLevel.Value = value;
}

/// <summary>
/// DeMarker level for long setups.
/// </summary>
public decimal DemarkerBuyLevel
{
get => _demarkerBuyLevel.Value;
set => _demarkerBuyLevel.Value = value;
}

/// <summary>
/// DeMarker level for short setups.
/// </summary>
public decimal DemarkerSellLevel
{
get => _demarkerSellLevel.Value;
set => _demarkerSellLevel.Value = value;
}

/// <summary>
/// Williams %R threshold that confirms longs.
/// </summary>
public decimal WilliamsBuyLevel
{
get => _williamsBuyLevel.Value;
set => _williamsBuyLevel.Value = value;
}

/// <summary>
/// Williams %R threshold that confirms shorts.
/// </summary>
public decimal WilliamsSellLevel
{
get => _williamsSellLevel.Value;
set => _williamsSellLevel.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>
/// Initializes strategy parameters.
/// </summary>
public PolishLayerExpertAdvisorSystemEfficientStrategy()
{

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

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

_shortPricePeriod = Param(nameof(ShortPricePeriod), 9)
.SetGreaterThanZero()
.SetDisplay("Fast Price MA", "Length of the fast price moving average", "Trend")
;

_longPricePeriod = Param(nameof(LongPricePeriod), 45)
.SetGreaterThanZero()
.SetDisplay("Slow Price MA", "Length of the slow price moving average", "Trend")
;

_shortRsiPeriod = Param(nameof(ShortRsiPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("Fast RSI MA", "Length of the fast RSI moving average", "RSI")
;

_longRsiPeriod = Param(nameof(LongRsiPeriod), 45)
.SetGreaterThanZero()
.SetDisplay("Slow RSI MA", "Length of the slow RSI moving average", "RSI")
;

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

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

_stochasticSlowing = Param(nameof(StochasticSlowing), 3)
.SetGreaterThanZero()
.SetDisplay("Slowing", "Stochastic slowing factor", "Stochastic");

_demarkerPeriod = Param(nameof(DemarkerPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("DeMarker Period", "DeMarker averaging period", "DeMarker");

_williamsPeriod = Param(nameof(WilliamsPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("Williams %R Period", "Williams %R lookback", "Williams %R");

_stochasticOversoldLevel = Param(nameof(StochasticOversoldLevel), 19m)
.SetDisplay("%K Oversold", "Oversold level for %K", "Thresholds");

_stochasticOverboughtLevel = Param(nameof(StochasticOverboughtLevel), 81m)
.SetDisplay("%K Overbought", "Overbought level for %K", "Thresholds");

_demarkerBuyLevel = Param(nameof(DemarkerBuyLevel), 0.35m)
.SetDisplay("DeMarker Buy Level", "Minimum DeMarker value for longs", "Thresholds");

_demarkerSellLevel = Param(nameof(DemarkerSellLevel), 0.63m)
.SetDisplay("DeMarker Sell Level", "Maximum DeMarker value for shorts", "Thresholds");

_williamsBuyLevel = Param(nameof(WilliamsBuyLevel), -81m)
.SetDisplay("Williams Buy Level", "Williams %R level for longs", "Thresholds");

_williamsSellLevel = Param(nameof(WilliamsSellLevel), -19m)
.SetDisplay("Williams Sell Level", "Williams %R level for shorts", "Thresholds");

_stopLossPips = Param(nameof(StopLossPips), 7777m)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Risk");

_takeProfitPips = Param(nameof(TakeProfitPips), 17m)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit distance in pips", "Risk");
}

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

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

_previousStochasticMain = null;
_previousStochasticSignal = null;
_previousDeMarker = null;
_previousWilliams = null;

ResetProtectionLevels();
}

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

StartProtection(
	takeProfit: new Unit(2, UnitTypes.Percent),
	stopLoss: new Unit(1, UnitTypes.Percent));

_priceStep = Security?.PriceStep ?? 0m;

_shortPriceMa = new SimpleMovingAverage { Length = ShortPricePeriod };
_longPriceMa = new WeightedMovingAverage { Length = LongPricePeriod };
_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
_shortRsiAverage = new SimpleMovingAverage { Length = ShortRsiPeriod };
_longRsiAverage = new SimpleMovingAverage { Length = LongRsiPeriod };
_stochastic = new StochasticOscillator();
_stochastic.K.Length = StochasticKPeriod;
_stochastic.D.Length = StochasticDPeriod;
_deMarker = new DeMarker { Length = DemarkerPeriod };
_williams = new WilliamsR { Length = WilliamsPeriod };

Indicators.Add(_stochastic);
Indicators.Add(_deMarker);
Indicators.Add(_williams);

var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_shortPriceMa, _longPriceMa, _rsi, ProcessCandle)
.Start();

var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _shortPriceMa);
DrawIndicator(area, _longPriceMa);
DrawIndicator(area, _rsi);
DrawIndicator(area, _stochastic);
DrawIndicator(area, _deMarker);
DrawIndicator(area, _williams);
DrawOwnTrades(area);
}
}

private void ProcessCandle(
ICandleMessage candle,
decimal fastPrice,
decimal slowPrice,
decimal rsi)
{
if (candle.State != CandleStates.Finished)
return;

var fastRsi = _shortRsiAverage.Process(rsi, candle.OpenTime, true).ToDecimal();
var slowRsi = _longRsiAverage.Process(rsi, candle.OpenTime, true).ToDecimal();

var stochasticResult = _stochastic.Process(candle);
var demarkerResult = _deMarker.Process(candle);
var williamsResult = _williams.Process(candle);

if (!_stochastic.IsFormed || !_deMarker.IsFormed || !_williams.IsFormed)
return;

if (!_shortRsiAverage.IsFormed || !_longRsiAverage.IsFormed)
return;

var stochastic = (StochasticOscillatorValue)stochasticResult;
if (stochastic.K is not decimal currentStochasticMain || stochastic.D is not decimal currentStochasticSignal)
return;

var demarker = demarkerResult.ToDecimal();
var williams = williamsResult.ToDecimal();

if (_previousStochasticMain is null || _previousStochasticSignal is null || _previousDeMarker is null || _previousWilliams is null)
{
UpdatePreviousValues(currentStochasticMain, currentStochasticSignal, demarker, williams);
return;
}

if (Position != 0)
{
UpdatePreviousValues(currentStochasticMain, currentStochasticSignal, demarker, williams);
return;
}

var longTrend = fastPrice > slowPrice && fastRsi > slowRsi;
var shortTrend = fastPrice < slowPrice && fastRsi < slowRsi;

// Simplified: just require trend + stochastic cross (relax demarker/williams requirements)
var stochasticCrossUp = _previousStochasticMain < StochasticOversoldLevel && currentStochasticMain >= StochasticOversoldLevel;
var stochasticCrossDown = _previousStochasticMain > StochasticOverboughtLevel && currentStochasticMain <= StochasticOverboughtLevel;

var longSignal = longTrend && stochasticCrossUp;
var shortSignal = shortTrend && stochasticCrossDown;

if (longSignal)
{
BuyMarket();
}
else if (shortSignal)
{
SellMarket();
}

UpdatePreviousValues(currentStochasticMain, currentStochasticSignal, demarker, williams);
}

private void TryEnterPosition(Sides side, decimal entryPrice)
{
var volume = Volume;
if (volume <= 0m)
return;

if (side == Sides.Buy)
{
BuyMarket(volume);
SetProtectionLevels(entryPrice, true);
}
else
{
SellMarket(volume);
SetProtectionLevels(entryPrice, false);
}
}

private bool ManageOpenPosition(ICandleMessage candle)
{
if (Position > 0)
{
if (_longStopPrice is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Position);
ResetProtectionLevels();
return true;
}

if (_longTakePrice is decimal take && candle.HighPrice >= take)
{
SellMarket(Position);
ResetProtectionLevels();
return true;
}
}
else if (Position < 0)
{
var shortVolume = Math.Abs(Position);
if (_shortStopPrice is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(shortVolume);
ResetProtectionLevels();
return true;
}

if (_shortTakePrice is decimal take && candle.LowPrice <= take)
{
BuyMarket(shortVolume);
ResetProtectionLevels();
return true;
}
}

return false;
}

private void SetProtectionLevels(decimal entryPrice, bool isLong)
{
if (_priceStep <= 0m)
{
ResetProtectionLevels();
return;
}

var stopOffset = StopLossPips > 0m ? StopLossPips * _priceStep : 0m;
var takeOffset = TakeProfitPips > 0m ? TakeProfitPips * _priceStep : 0m;

if (isLong)
{
_longStopPrice = stopOffset > 0m ? entryPrice - stopOffset : null;
_longTakePrice = takeOffset > 0m ? entryPrice + takeOffset : null;
_shortStopPrice = null;
_shortTakePrice = null;
}
else
{
_shortStopPrice = stopOffset > 0m ? entryPrice + stopOffset : null;
_shortTakePrice = takeOffset > 0m ? entryPrice - takeOffset : null;
_longStopPrice = null;
_longTakePrice = null;
}
}

private void ResetProtectionLevels()
{
_longStopPrice = null;
_longTakePrice = null;
_shortStopPrice = null;
_shortTakePrice = null;
}

private void UpdatePreviousValues(decimal stochasticMain, decimal stochasticSignal, decimal demarker, decimal williams)
{
_previousStochasticMain = stochasticMain;
_previousStochasticSignal = stochasticSignal;
_previousDeMarker = demarker;
_previousWilliams = williams;
}
}