在 GitHub 上查看

平滑均线突破策略

概述

平滑均线突破策略再现了原始 MQL5 专家顾问 Smoothing Average (barabashkakvn's edition) 的逻辑。策略将可配置的移动平均线与按点数衡量的距离过滤器结合使用。当价格偏离均线达到设定的点数时,系统会顺势开仓(若启用反向模式,则反向开仓)。当价格穿越扩大后的均线通道时,仓位被平仓。

交易逻辑

标准模式(ReverseSignals = false

  • 做多开仓: 收盘价高于 MA - Entry Delta (pips)
  • 做空开仓: 收盘价低于 MA + Entry Delta (pips)
  • 做空平仓: 收盘价高于 MA + Entry Delta (pips) × Close Delta Coefficient
  • 做多平仓: 收盘价低于 MA - Entry Delta (pips) × Close Delta Coefficient

反向模式(ReverseSignals = true

  • 做多开仓: 收盘价低于 MA + Entry Delta (pips)
  • 做空开仓: 收盘价高于 MA - Entry Delta (pips)
  • 做多平仓: 收盘价低于 MA - Entry Delta (pips) × Close Delta Coefficient
  • 做空平仓: 收盘价高于 MA + Entry Delta (pips) × Close Delta Coefficient

移动平均线可以向前平移若干根 K 线。策略通过保存最近的指标值并取 MaShift 根之前的数值来模拟这一效果,与 MetaTrader 中指标绘制的平移线一致。

参数

  • Candle Type – 参与计算的 K 线类型。
  • MA Length – 平滑均线的周期长度。
  • MA Shift – 均线向前平移的 K 线数量。
  • MA Type – 均线类型(简单、指数、平滑、线性加权)。
  • Price Source – 输入到均线中的价格(默认使用典型价)。
  • Entry Delta (pips) – 触发开仓所需的点数距离,按合约的最小变动价位转换为价格。
  • Close Delta Coefficient – 计算平仓通道时对入场点数的倍数。
  • Reverse Signals – 是否反转多空条件。
  • Trade Volume – 每次下单的固定手数。

风险管理

  • 所有订单均采用 Trade Volume 指定的固定手数,不在持仓期间加仓或减仓。
  • 平仓完全依赖规则,不会主动提交止损或止盈,但会调用 StartProtection() 以启用平台的安全保护。
  • 反向模式允许在不修改其他参数的情况下进行逆势交易。

实现细节

  • 点值来自 Security.PriceStep。对于三位或五位报价的外汇品种,点值会按 MQL5 版本的方式乘以 10。
  • 均线使用 Price Source 参数,可匹配原始 EA 中对不同价格的选择。
  • 条件判断使用 K 线收盘价,作为原始程序中 bid/ask 检查的稳定替代。
  • C# 源码中的注释全部采用英文,以符合转换指引的要求。
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>
/// Smoothing Average strategy converted from MQL5.
/// Opens trades when price moves away from the moving average by a configurable delta.
/// Supports reversing the signals and shifting the moving average output.
/// </summary>
public class SmoothingAverageCrossoverStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MovingAverageKinds> _maType;
	private readonly StrategyParam<CandlePrices> _priceSource;
	private readonly StrategyParam<decimal> _entryDeltaPips;
	private readonly StrategyParam<decimal> _closeDeltaCoefficient;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<decimal> _tradeVolume;

	private readonly Queue<decimal> _maShiftBuffer = new();

	private decimal _entryDelta;
	private decimal _closeDelta;

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
        public SmoothingAverageCrossoverStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "General");

		_maLength = Param(nameof(MaLength), 60)
			.SetGreaterThanZero()
			.SetDisplay("MA Length", "Period of the smoothing average", "Moving Average");

		_maShift = Param(nameof(MaShift), 3)
			.SetNotNegative()
			.SetDisplay("MA Shift", "Horizontal shift applied to the average", "Moving Average");

		_maType = Param(nameof(MaType), MovingAverageKinds.Simple)
			.SetDisplay("MA Type", "Type of smoothing applied", "Moving Average");

		_priceSource = Param(nameof(PriceSource), CandlePrices.Typical)
			.SetDisplay("Price Source", "Price used for the moving average", "Moving Average");

		_entryDeltaPips = Param(nameof(EntryDeltaPips), 60m)
			.SetNotNegative()
			.SetDisplay("Entry Delta (pips)", "Distance from MA to trigger entries", "Trading Rules");

		_closeDeltaCoefficient = Param(nameof(CloseDeltaCoefficient), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Close Delta Coefficient", "Multiplier applied to entry delta for exits", "Trading Rules");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert long and short logic", "Trading Rules");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Order volume for each entry", "Risk");
	}

	/// <summary>
	/// Primary candle series used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Moving average period.
	/// </summary>
	public int MaLength
	{
		get => _maLength.Value;
		set => _maLength.Value = value;
	}

	/// <summary>
	/// Number of candles used to shift the moving average output.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average type.
	/// </summary>
	public MovingAverageKinds MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Candle price source for the moving average.
	/// </summary>
	public CandlePrices PriceSource
	{
		get => _priceSource.Value;
		set => _priceSource.Value = value;
	}

	/// <summary>
	/// Delta in pip units used to open new positions.
	/// </summary>
	public decimal EntryDeltaPips
	{
		get => _entryDeltaPips.Value;
		set => _entryDeltaPips.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the entry delta when evaluating exits.
	/// </summary>
	public decimal CloseDeltaCoefficient
	{
		get => _closeDeltaCoefficient.Value;
		set => _closeDeltaCoefficient.Value = value;
	}

	/// <summary>
	/// If true, swaps long and short signals.
	/// </summary>
	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	/// <summary>
	/// Volume sent with market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

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

		_maShiftBuffer.Clear();
		_entryDelta = 0m;
		_closeDelta = 0m;
	}

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

		// Sync the base strategy volume with the parameter value.
		Volume = TradeVolume;

		// Calculate pip-based offsets once at the start to avoid repeated computations.
		_entryDelta = CalculateEntryDelta();
		_closeDelta = _entryDelta * CloseDeltaCoefficient;

		var movingAverage = CreateMovingAverage(MaType, MaLength);
		//movingAverage.CandlePrice = PriceSource;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(movingAverage, ProcessCandle)
			.Start();

		// Enable built-in protection helpers (no additional parameters required).
		StartProtection(null, null);
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var shiftedMa = ApplyShift(maValue);

		// Use candle close as a proxy for bid/ask checks from the original Expert Advisor.
		var askPrice = candle.ClosePrice;
		var bidPrice = candle.ClosePrice;

		var entryUpper = shiftedMa + _entryDelta;
		var entryLower = shiftedMa - _entryDelta;
		var closeUpper = shiftedMa + _closeDelta;
		var closeLower = shiftedMa - _closeDelta;

		if (Position == 0m)
		{
			if (!ReverseSignals)
			{
				if (askPrice > entryUpper)
				{
					OpenLong();
					return;
				}

				if (bidPrice < entryLower)
				{
					OpenShort();
					return;
				}
			}
			else
			{
				if (askPrice > entryUpper)
				{
					OpenShort();
					return;
				}

				if (bidPrice < entryLower)
				{
					OpenLong();
					return;
				}
			}
		}
		else
		{
			if (!ReverseSignals)
			{
				if (Position < 0m && bidPrice > closeUpper)
					CloseShort();

				if (Position > 0m && askPrice < closeLower)
					CloseLong();
			}
			else
			{
				if (Position > 0m && askPrice < closeLower)
					CloseLong();

				if (Position < 0m && bidPrice > closeUpper)
					CloseShort();
			}
		}
	}

	private decimal ApplyShift(decimal currentValue)
	{
		if (MaShift <= 0)
			return currentValue;

		var shifted = _maShiftBuffer.Count < MaShift ? currentValue : _maShiftBuffer.Peek();

		_maShiftBuffer.Enqueue(currentValue);

		if (_maShiftBuffer.Count > MaShift)
			_maShiftBuffer.Dequeue();

		return shifted;
	}

	private decimal CalculateEntryDelta()
	{
		var pip = CalculatePipSize();
		return pip * EntryDeltaPips;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 0.0001m;

		var digits = (int)Math.Round(Math.Log10((double)(1m / step)));
		return digits == 3 || digits == 5 ? step * 10m : step;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MovingAverageKinds type, int length)
	{
		return type switch
		{
			MovingAverageKinds.Simple => new SMA { Length = length },
			MovingAverageKinds.Exponential => new EMA { Length = length },
			MovingAverageKinds.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageKinds.LinearWeighted => new WeightedMovingAverage { Length = length },
			_ => new SMA { Length = length }
		};
	}

	private void OpenLong()
	{
		var volume = TradeVolume + Math.Max(0m, -Position);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
	}

	private void OpenShort()
	{
		var volume = TradeVolume + Math.Max(0m, Position);
		if (volume <= 0m)
			return;

		SellMarket(volume);
	}

	private void CloseLong()
	{
		if (Position <= 0m)
			return;

		SellMarket(Position);
	}

	private void CloseShort()
	{
		if (Position >= 0m)
			return;

		BuyMarket(Math.Abs(Position));
	}

	/// <summary>
	/// Supported moving average types replicating the MQL5 enumeration.
	/// </summary>
	public enum MovingAverageKinds
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	public enum CandlePrices
	{
		Open,
		Close,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
}