在 GitHub 上查看

Envelopes EA 策略

概述

该策略复刻自 MetaTrader 4 专家顾问“EnvelopesEA”。策略在选定周期的K线之上计算指数移动平均线(EMA)包络,当价格显著偏离包络时逆势建仓,一旦价格重新回到包络内即平仓。原始 EA 在 2019 年的 EUR/USD 上进行过回测,StockSharp 版本保留了原有逻辑,并将核心输入暴露为可优化参数。

交易逻辑

  1. 在所选 K 线序列上计算长度为 EnvelopePeriod 的 EMA。
  2. 使用 UpperDeviationPercentLowerDeviationPercent 将 EMA 向上/向下扩展,得到上下包络。
  3. 根据 EntryOffsetPoints(乘以品种的价格步长)设置额外的进场缓冲,避免过早入场。
  4. 当没有持仓时:
    • 若收盘价低于下包络减去缓冲距离,则市价买入做多。
    • 若收盘价高于上包络加上缓冲距离,则市价卖出做空。
  5. 当存在持仓时:
    • 多头持仓在收盘价重新突破上包络时全部平仓。
    • 空头持仓在收盘价重新跌破下包络时全部平仓。

策略始终只持有一个方向的仓位,进出场均使用市价单。

资金管理

下单手数通过 Volume 参数直接指定(手)。策略不包含马丁或加仓模块,与原始 MQ4 程序默认关闭扩仓的行为保持一致。

参数

参数 说明 默认值
Volume 下单手数。 0.2
EnvelopePeriod 构成包络基础的 EMA 长度。 50
UpperDeviationPercent 上包络相对 EMA 的百分比偏移。 0.5
LowerDeviationPercent 下包络相对 EMA 的百分比偏移。 0.5
EntryOffsetPoints 价格必须在包络外进一步移动的点数(乘以价格步长)。 100
CandleType 用于计算的 K 线周期。 30 分钟

CandleType 外,其余数值型参数均已开放优化,以便复现原有的参数搜索流程。

说明

  • 相比早期版本使用的 SMA,本实现采用 EMA 作为包络基线,对价格变化反应更快,更契合最新 MQ4 脚本的逻辑。
  • 进场缓冲会乘以品种的 PriceStep。请确保标的证券的元数据提供有效的最小变动价位,否则策略会回退到保守的 0.0001 步长。
  • 图表展示包含价格K线、EMA包络以及策略成交,方便对照原专家顾问验证信号行为。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

public class EnvelopesEaStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public EnvelopesEaStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}