在 GitHub 上查看

5Mins Envelopes 策略

5Mins Envelopes 策略复刻自 MetaTrader 专家顾问,围绕线性加权移动平均线 (LWMA) 构建包络,在 5 分钟周期上捕捉价格的极端偏离并逆向进场。 策略检测价格是否远离包络带并期待回归均值,同时保留原策略中的点差过滤、固定止损、可选止盈以及移动止损管理。

交易逻辑

  • 指标:以中值价格 (最高价+最低价)/2 计算的线性加权移动平均线,周期为 3。
  • 包络宽度:LWMA 的上下偏移 0.05%。
  • 信号判定(基于上一根已完成 K 线与当前买价):
    • 做多:上一根 K 线最低价低于下轨超过 DistancePoints,且当前买价同样低于下轨超过该距离。
    • 做空:上一根 K 线最高价高于上轨超过 DistancePoints,且当前买价同样高于上轨超过该距离。
  • 过滤条件:
    • 同一时间只允许一笔持仓,持仓未平仓时不再开新单。
    • 若 MaxSpreadPoints 大于 0,新单提交前需要点差低于该阈值。

风险控制

  • 下单手数:TradeVolume 参数控制每次市价单的数量。
  • 止损:StopLossPoints 乘以品种最小跳动价差转换为绝对价格距离。
  • 止盈:可选的 TakeProfitPoints,设为 0 表示关闭。
  • 移动止损:可选的 TrailingStopPoints,设为 0 表示关闭。
  • 保护机制:StartProtection 辅助函数以市价单方式应用所有离场规则,与 MetaTrader 行为保持一致。

参数

  • TradeVolume = 1m
  • DistancePoints = 140
  • EnvelopePeriod = 3
  • EnvelopeDeviationPercent = 0.05m
  • StopLossPoints = 250
  • TakeProfitPoints = 0
  • TrailingStopPoints = 120
  • MaxSpreadPoints = 25
  • CandleType = TimeFrame(5 minutes)

标签

  • 分类:均值回归
  • 方向:双向
  • 指标:WeightedMovingAverage
  • 止损:是(固定 + 移动)
  • 周期:日内 (M5)
  • 复杂度:入门
  • 风险等级:中等
  • 季节性:无
  • 神经网络:无
  • 背离:无
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using Ecng.Common;

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

/// <summary>
/// Mean reversion from LWMA envelopes of median candle price, using the last finished candle and live bid.
/// </summary>
public class FiveMinsEnvelopesStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _distancePoints;
	private readonly StrategyParam<int> _envelopePeriod;
	private readonly StrategyParam<decimal> _envelopeDeviationPercent;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _maxSpreadPoints;

	private readonly List<decimal> _medianPrices = [];
	private decimal? _previousLow;
	private decimal? _previousHigh;
	private decimal? _previousLowerBand;
	private decimal? _previousUpperBand;
	private decimal? _bestBid;
	private decimal? _bestAsk;
	private decimal? _entryPrice;
	private decimal? _trailingStop;
	private bool _entryRequested;
	private bool _exitRequested;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
	public decimal DistancePoints { get => _distancePoints.Value; set => _distancePoints.Value = value; }
	public int EnvelopePeriod { get => _envelopePeriod.Value; set => _envelopePeriod.Value = value; }
	public decimal EnvelopeDeviationPercent { get => _envelopeDeviationPercent.Value; set => _envelopeDeviationPercent.Value = value; }
	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
	public decimal TrailingStopPoints { get => _trailingStopPoints.Value; set => _trailingStopPoints.Value = value; }
	public decimal MaxSpreadPoints { get => _maxSpreadPoints.Value; set => _maxSpreadPoints.Value = value; }

	public FiveMinsEnvelopesStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame());
		_tradeVolume = Param(nameof(TradeVolume), 1m).SetGreaterThanZero();
		_distancePoints = Param(nameof(DistancePoints), 140m).SetNotNegative();
		_envelopePeriod = Param(nameof(EnvelopePeriod), 3).SetGreaterThanZero();
		_envelopeDeviationPercent = Param(nameof(EnvelopeDeviationPercent), 0.05m).SetNotNegative();
		_stopLossPoints = Param(nameof(StopLossPoints), 250m).SetNotNegative();
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m).SetNotNegative();
		_trailingStopPoints = Param(nameof(TrailingStopPoints), 120m).SetNotNegative();
		_maxSpreadPoints = Param(nameof(MaxSpreadPoints), 25m).SetNotNegative();
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

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

	private void ResetRuntime()
	{
		_medianPrices.Clear();
		_previousLow = null;
		_previousHigh = null;
		_previousLowerBand = null;
		_previousUpperBand = null;
		_bestBid = null;
		_bestAsk = null;
		_entryPrice = null;
		_trailingStop = null;
		_entryRequested = false;
		_exitRequested = false;
	}

	private decimal Point()
	{
		var point = Security?.PriceStep ?? 0m;
		if (point <= 0m)
			throw new InvalidOperationException("FiveMinsEnvelopesStrategy needs positive Security.PriceStep.");
		return point;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ResetRuntime();
		var point = Point();
		var takeProfit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints * point, UnitTypes.Absolute) : new Unit();
		var stopLoss = StopLossPoints > 0m ? new Unit(StopLossPoints * point, UnitTypes.Absolute) : new Unit();
		StartProtection(takeProfit, stopLoss, useMarketOrders: true, isLocalStop: true);
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Bind(ProcessLevel1).Start();
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_medianPrices.Add((candle.HighPrice + candle.LowPrice) / 2m);
		while (_medianPrices.Count > EnvelopePeriod)
			_medianPrices.RemoveAt(0);
		if (_medianPrices.Count < EnvelopePeriod)
			return;

		var weighted = 0m;
		var totalWeights = EnvelopePeriod * (EnvelopePeriod + 1) / 2m;
		for (var i = 0; i < _medianPrices.Count; i++)
			weighted += _medianPrices[i] * (i + 1);
		var middle = weighted / totalWeights;
		_previousLow = candle.LowPrice;
		_previousHigh = candle.HighPrice;
		_previousLowerBand = middle * (1m - EnvelopeDeviationPercent / 100m);
		_previousUpperBand = middle * (1m + EnvelopeDeviationPercent / 100m);
	}

	private void ProcessLevel1(Level1ChangeMessage message)
	{
		if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid && bid > 0m)
			_bestBid = bid;
		if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask && ask > 0m)
			_bestAsk = ask;
		if (_bestBid is not decimal currentBid || _bestAsk is not decimal currentAsk || currentAsk < currentBid)
			return;

		if (Position != 0m)
		{
			ApplyTrailing();
			return;
		}

		if (_entryRequested || _exitRequested || _previousLow is null || _previousHigh is null ||
			_previousLowerBand is null || _previousUpperBand is null || TradeVolume <= 0m)
			return;

		var point = Point();
		if (MaxSpreadPoints > 0m && currentAsk - currentBid >= MaxSpreadPoints * point)
			return;

		var distance = DistancePoints * point;
		if (_previousLow.Value < _previousLowerBand.Value - distance &&
			currentBid < _previousLowerBand.Value - distance)
		{
			_entryRequested = true;
			BuyMarket(TradeVolume);
		}
		else if (_previousHigh.Value > _previousUpperBand.Value + distance &&
			currentBid > _previousUpperBand.Value + distance)
		{
			_entryRequested = true;
			SellMarket(TradeVolume);
		}
	}

	private void ApplyTrailing()
	{
		if (_exitRequested || _entryPrice is null || TrailingStopPoints <= 0m)
			return;
		var price = Position > 0m ? _bestBid : _bestAsk;
		if (price is null)
			return;
		var gap = TrailingStopPoints * Point();
		if (Position > 0m)
		{
			if (price.Value >= _entryPrice.Value + gap)
			{
				var candidate = price.Value - gap;
				if (_trailingStop is null || candidate > _trailingStop)
					_trailingStop = candidate;
			}
			if (_trailingStop is decimal stop && price.Value <= stop)
				RequestExit();
		}
		else
		{
			if (price.Value <= _entryPrice.Value - gap)
			{
				var candidate = price.Value + gap;
				if (_trailingStop is null || candidate < _trailingStop)
					_trailingStop = candidate;
			}
			if (_trailingStop is decimal stop && price.Value >= stop)
				RequestExit();
		}
	}

	private void RequestExit()
	{
		_exitRequested = true;
		ClosePosition();
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (Position == 0m)
		{
			_entryPrice = null;
			_trailingStop = null;
			_entryRequested = false;
			_exitRequested = false;
		}
		else if (_entryPrice is null)
		{
			_entryPrice = trade.Trade.Price;
		}
	}
}