在 GitHub 上查看

Universal MA Cross 策略

概述

Universal MA Cross Strategy 将原始的 MQL5 专家顾问 “UniversalMACrossEA” 移植到 StockSharp 的高级策略框架。该策略比较一条快速与一条慢速移动平均线,并允许为每条均线分别设置周期、平滑方法和价格类型。附加参数可控制信号确认方式、是否立即反向、风险管理以及允许交易的时间窗口。

交易逻辑

指标计算

  • 在所选的 K 线序列上计算两条移动平均线。每条均线都可以拥有自己的周期、平滑方法(SMA、EMA、SMMA 或 LWMA)以及价格类型(收盘价、开盘价、最高价、最低价、中价、典型价或加权价)。
  • MinCrossDistance 要求在产生交叉的那个 K 线上,两条均线之间的距离至少达到指定的价差。
  • 启用 ConfirmedOnEntry 时,交叉信号使用前两个已经完成的 K 线进行验证(对应原始 EA 中的索引 2 与 1)。关闭该选项时,当前完成的 K 线与上一根 K 线比较,以模拟 MQL 中的“实时”模式。
  • ReverseCondition 会交换做多与做空的规则,不需要修改任何指标设置。

入场规则

  1. 当快速均线向上穿越慢速均线,且差值不少于 MinCrossDistance 时开多;向下穿越且差值足够时开空。
  2. 若启用了 StopAndReverse,在收到相反信号时会先平掉当前仓位,再考虑新的订单。
  3. OneEntryPerBartrue 时,策略会记录最近一次入场的 K 线时间,在同一根 K 线内拒绝再次开仓。
  4. 每笔交易的下单数量由 Volume 参数决定。

仓位管理

  • 止损与止盈以绝对价格距离表示,在 PureSar 模式下会被忽略,这与原专家中的 “Pure SAR” 设置一致。
  • 当价格相对入场价运行 TrailingStop + TrailingStep 之后启动移动止损;之后每当价格额外前进至少 TrailingStep,止损就会向盈利方向收紧 TrailingStop 的距离。在 PureSar 模式下不会启用移动止损。
  • 每根已完成的 K 线都会检查保护水平。如果该 K 线的高低区间触及止损或止盈,仓位会以市价单平仓。

交易时段过滤

  • UseHourTrade 启用时,只在 K 线开盘时间位于 StartHourEndHour(包含边界)之间时才允许开仓。即使在时段外,移动止损仍会更新,但不会触发新的入场或“止损反手”。

参数说明

参数 说明
FastMaPeriod, SlowMaPeriod 快速与慢速移动平均线的周期。
FastMaType, SlowMaType 均线类型:简单、指数、平滑(RMA)或线性加权。
FastPriceType, SlowPriceType 输入到均线的价格类型。
StopLoss, TakeProfit 以价格单位表示的止损与止盈,设为 0 表示关闭。
TrailingStop, TrailingStep 移动止损的偏移量,以及再次移动前所需的额外行程。
MinCrossDistance 交叉时两条均线之间的最小距离。
ReverseCondition 交换多空条件。
ConfirmedOnEntry 仅使用已完成的 K 线确认信号。
OneEntryPerBar 每根 K 线最多只允许一次入场。
StopAndReverse 在相反信号出现时先平仓再反向开仓。
PureSar 关闭止损、止盈与移动止损逻辑。
UseHourTrade, StartHour, EndHour 交易时间过滤(0–23 小时制)。
Volume 每次下单的数量。
CandleType 订阅并用于计算的 K 线类型。

转换说明

  • 由于 StockSharp 的高级策略基于完成的 K 线运行,保护性订单通过检测 K 线的最高价与最低价来模拟,从而在不使用低级 API 的情况下再现原始 EA 的行为。
  • 移动止损的调整与 MQL 实现一致:只有在价格运行了 TrailingStop + TrailingStep 之后才会移动止损。
  • 按照要求,此次转换未提供 Python 版本。
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>
/// Universal moving average crossover strategy converted from the original MQL version.
/// The strategy trades based on a fast and a slow moving average with optional signal confirmation,
/// stop-and-reverse behaviour, trailing stop management and time filtering.
/// </summary>
public class UniversalMaCrossStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation methods supported by the strategy.
	/// </summary>
	public enum MovingAverageMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	/// <summary>
	/// Price sources that can feed the moving averages.
	/// </summary>
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<MovingAverageMethods> _fastMaType;
	private readonly StrategyParam<MovingAverageMethods> _slowMaType;
	private readonly StrategyParam<AppliedPrices> _fastPriceType;
	private readonly StrategyParam<AppliedPrices> _slowPriceType;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _trailingStep;
	private readonly StrategyParam<decimal> _minCrossDistance;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<bool> _confirmedOnEntry;
	private readonly StrategyParam<bool> _oneEntryPerBar;
	private readonly StrategyParam<bool> _stopAndReverse;
	private readonly StrategyParam<bool> _pureSar;
	private readonly StrategyParam<bool> _useHourTrade;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<DataType> _candleType;

	private IIndicator _fastMa;
	private IIndicator _slowMa;

	private decimal? _fastPrev;
	private decimal? _fastPrevPrev;
	private decimal? _slowPrev;
	private decimal? _slowPrevPrev;

	private DateTimeOffset? _lastEntryBar;
	private TradeDirections _lastTrade = TradeDirections.None;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Fast moving average method.
	/// </summary>
	public MovingAverageMethods FastMaType
	{
		get => _fastMaType.Value;
		set => _fastMaType.Value = value;
	}

	/// <summary>
	/// Slow moving average method.
	/// </summary>
	public MovingAverageMethods SlowMaType
	{
		get => _slowMaType.Value;
		set => _slowMaType.Value = value;
	}

	/// <summary>
	/// Price type used for the fast moving average.
	/// </summary>
	public AppliedPrices FastPriceType
	{
		get => _fastPriceType.Value;
		set => _fastPriceType.Value = value;
	}

	/// <summary>
	/// Price type used for the slow moving average.
	/// </summary>
	public AppliedPrices SlowPriceType
	{
		get => _slowPriceType.Value;
		set => _slowPriceType.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price units.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Additional move required before shifting the trailing stop.
	/// </summary>
	public decimal TrailingStep
	{
		get => _trailingStep.Value;
		set => _trailingStep.Value = value;
	}

	/// <summary>
	/// Minimum distance between the averages to validate a crossover.
	/// </summary>
	public decimal MinCrossDistance
	{
		get => _minCrossDistance.Value;
		set => _minCrossDistance.Value = value;
	}

	/// <summary>
	/// Reverse buy and sell conditions.
	/// </summary>
	public bool ReverseCondition
	{
		get => _reverseCondition.Value;
		set => _reverseCondition.Value = value;
	}

	/// <summary>
	/// Confirm signals on closed candles only.
	/// </summary>
	public bool ConfirmedOnEntry
	{
		get => _confirmedOnEntry.Value;
		set => _confirmedOnEntry.Value = value;
	}

	/// <summary>
	/// Limit the strategy to a single entry per bar.
	/// </summary>
	public bool OneEntryPerBar
	{
		get => _oneEntryPerBar.Value;
		set => _oneEntryPerBar.Value = value;
	}

	/// <summary>
	/// Close the current trade and reverse when an opposite signal appears.
	/// </summary>
	public bool StopAndReverse
	{
		get => _stopAndReverse.Value;
		set => _stopAndReverse.Value = value;
	}

	/// <summary>
	/// Disable protective orders and rely purely on signal reversals.
	/// </summary>
	public bool PureSar
	{
		get => _pureSar.Value;
		set => _pureSar.Value = value;
	}

	/// <summary>
	/// Enable trading only within the selected hours.
	/// </summary>
	public bool UseHourTrade
	{
		get => _useHourTrade.Value;
		set => _useHourTrade.Value = value;
	}

	/// <summary>
	/// Hour when trading can start (0-23).
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour when trading must end (0-23).
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}


	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="UniversalMaCrossStrategy"/>.
	/// </summary>
	public UniversalMaCrossStrategy()
	{
		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Fast moving average length", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Slow moving average length", "Indicators")
			
			.SetOptimize(30, 200, 5);

		_fastMaType = Param(nameof(FastMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Fast MA Type", "Method for fast average", "Indicators");

		_slowMaType = Param(nameof(SlowMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Slow MA Type", "Method for slow average", "Indicators");

		_fastPriceType = Param(nameof(FastPriceType), AppliedPrices.Close)
			.SetDisplay("Fast Price Type", "Price source for fast MA", "Indicators");

		_slowPriceType = Param(nameof(SlowPriceType), AppliedPrices.Close)
			.SetDisplay("Slow Price Type", "Price source for slow MA", "Indicators");

		_stopLoss = Param(nameof(StopLoss), 0m)
			.SetDisplay("Stop Loss", "Stop-loss distance in price", "Risk");

		_takeProfit = Param(nameof(TakeProfit), 0m)
			.SetDisplay("Take Profit", "Take-profit distance in price", "Risk");

		_trailingStop = Param(nameof(TrailingStop), 0m)
			.SetDisplay("Trailing Stop", "Trailing stop distance", "Risk");

		_trailingStep = Param(nameof(TrailingStep), 0m)
			.SetDisplay("Trailing Step", "Additional move before trailing", "Risk");

		_minCrossDistance = Param(nameof(MinCrossDistance), 0m)
			.SetDisplay("Min Cross Distance", "Minimum distance between averages", "Filters");

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Signals", "Swap long and short conditions", "General");

		_confirmedOnEntry = Param(nameof(ConfirmedOnEntry), true)
			.SetDisplay("Confirmed On Entry", "Use closed candles for signals", "General");

		_oneEntryPerBar = Param(nameof(OneEntryPerBar), true)
			.SetDisplay("One Entry Per Bar", "Allow only one entry per candle", "General");

		_stopAndReverse = Param(nameof(StopAndReverse), true)
			.SetDisplay("Stop And Reverse", "Close and reverse on opposite signal", "Risk");

		_pureSar = Param(nameof(PureSar), false)
			.SetDisplay("Pure SAR", "Disable stop-loss, take-profit and trailing", "Risk");

		_useHourTrade = Param(nameof(UseHourTrade), false)
			.SetDisplay("Use Hour Filter", "Limit trading by session hours", "Session");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Trading window start hour", "Session");

		_endHour = Param(nameof(EndHour), 23)
			.SetDisplay("End Hour", "Trading window end hour", "Session");


		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle subscription", "General");
	}

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

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

		_fastMa = null;
		_slowMa = null;
		_fastPrev = null;
		_fastPrevPrev = null;
		_slowPrev = null;
		_slowPrevPrev = null;
		_lastEntryBar = null;
		_lastTrade = TradeDirections.None;
		ResetProtection();
	}

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

		_fastMa = CreateMovingAverage(FastMaType, FastMaPeriod);
		_slowMa = CreateMovingAverage(SlowMaType, SlowMaPeriod);

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}

	}

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

		ManageExistingPosition(candle);

		if (_fastMa is null || _slowMa is null)
			return;

		var fastPrice = GetPrice(candle, FastPriceType);
		var slowPrice = GetPrice(candle, SlowPriceType);

		var fastResult = _fastMa.Process(new DecimalIndicatorValue(_fastMa, fastPrice, candle.OpenTime) { IsFinal = true });
		var slowResult = _slowMa.Process(new DecimalIndicatorValue(_slowMa, slowPrice, candle.OpenTime) { IsFinal = true });

		if (fastResult.IsEmpty || slowResult.IsEmpty)
			return;

		var fastValue = fastResult.ToDecimal();
		var slowValue = slowResult.ToDecimal();

		var prevFast = _fastPrev;
		var prevSlow = _slowPrev;
		var prevFastPrev = _fastPrevPrev;
		var prevSlowPrev = _slowPrevPrev;

		_fastPrevPrev = prevFast;
		_slowPrevPrev = prevSlow;
		_fastPrev = fastValue;
		_slowPrev = slowValue;


		bool crossUp = false;
		bool crossDown = false;

		if (ConfirmedOnEntry)
		{
			if (prevFast.HasValue && prevSlow.HasValue && prevFastPrev.HasValue && prevSlowPrev.HasValue)
			{
				var fastPrevPrevValue = prevFastPrev.Value;
				var slowPrevPrevValue = prevSlowPrev.Value;
				var fastPrevValue = prevFast.Value;
				var slowPrevValue = prevSlow.Value;
				var diff = fastPrevValue - slowPrevValue;

				crossUp = fastPrevPrevValue < slowPrevPrevValue && fastPrevValue > slowPrevValue && diff >= MinCrossDistance;
				crossDown = fastPrevPrevValue > slowPrevPrevValue && fastPrevValue < slowPrevValue && -diff >= MinCrossDistance;
			}
		}
		else
		{
			if (prevFast.HasValue && prevSlow.HasValue)
			{
				var fastPrevValue = prevFast.Value;
				var slowPrevValue = prevSlow.Value;
				var diff = fastValue - slowValue;

				crossUp = fastPrevValue < slowPrevValue && fastValue > slowValue && diff >= MinCrossDistance;
				crossDown = fastPrevValue > slowPrevValue && fastValue < slowValue && -diff >= MinCrossDistance;
			}
		}

		bool buySignal;
		bool sellSignal;

		if (!ReverseCondition)
		{
			buySignal = crossUp;
			sellSignal = crossDown;
		}
		else
		{
			buySignal = crossDown;
			sellSignal = crossUp;
		}

		var canTrade = IsWithinTradingHours(candle);

		if (!canTrade)
			return;

		if (StopAndReverse && Position != 0)
		{
			if ((_lastTrade == TradeDirections.Long && sellSignal) || (_lastTrade == TradeDirections.Short && buySignal))
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}

		if (Position != 0)
			return;

		var entryAllowed = !OneEntryPerBar || _lastEntryBar != candle.OpenTime;

		if (!entryAllowed)
			return;

		if (buySignal)
		{
			BuyMarket(Volume);
			SetProtectionLevels(candle.ClosePrice, true);
			_lastTrade = TradeDirections.Long;
			_lastEntryBar = candle.OpenTime;
		}
		else if (sellSignal)
		{
			SellMarket(Volume);
			SetProtectionLevels(candle.ClosePrice, false);
			_lastTrade = TradeDirections.Short;
			_lastEntryBar = candle.OpenTime;
		}
	}

	private void ManageExistingPosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetProtection();
			return;
		}

		UpdateTrailingStop(candle);

		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (PureSar || TrailingStop <= 0m || !_entryPrice.HasValue)
			return;

		var activationDistance = TrailingStop + TrailingStep;

		if (Position > 0)
		{
			if (candle.ClosePrice - _entryPrice.Value > activationDistance)
			{
				var activationLevel = candle.ClosePrice - activationDistance;
				if (!_stopPrice.HasValue || _stopPrice.Value < activationLevel)
				{
					var newStop = candle.ClosePrice - TrailingStop;
					_stopPrice = _stopPrice.HasValue ? Math.Max(_stopPrice.Value, newStop) : newStop;
				}
			}
		}
		else if (Position < 0)
		{
			if (_entryPrice.Value - candle.ClosePrice > activationDistance)
			{
				var activationLevel = candle.ClosePrice + activationDistance;
				if (!_stopPrice.HasValue || _stopPrice.Value > activationLevel)
				{
					var newStop = candle.ClosePrice + TrailingStop;
					_stopPrice = _stopPrice.HasValue ? Math.Min(_stopPrice.Value, newStop) : newStop;
				}
			}
		}
	}

	private bool IsWithinTradingHours(ICandleMessage candle)
	{
		if (!UseHourTrade)
			return true;

		var hour = candle.OpenTime.Hour;
		var start = StartHour;
		var end = EndHour;

		if (start <= end)
			return hour >= start && hour <= end;

		return hour >= start || hour <= end;
	}

	private static IIndicator CreateMovingAverage(MovingAverageMethods method, int length)
	{
		return method switch
		{
			MovingAverageMethods.Simple => new SimpleMovingAverage { Length = length },
			MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = length },
			MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageMethods.LinearWeighted => new WeightedMovingAverage { Length = length },
			_ => new SimpleMovingAverage { Length = length }
		};
	}

	private static decimal GetPrice(ICandleMessage candle, AppliedPrices priceType)
	{
		return priceType switch
		{
			AppliedPrices.Open => candle.OpenPrice,
			AppliedPrices.High => candle.HighPrice,
			AppliedPrices.Low => candle.LowPrice,
			AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}

	private void SetProtectionLevels(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;

		if (PureSar)
		{
			_stopPrice = null;
			_takeProfitPrice = null;
			return;
		}

		var stop = StopLoss;
		var take = TakeProfit;

		_stopPrice = stop > 0m ? (isLong ? entryPrice - stop : entryPrice + stop) : null;
		_takeProfitPrice = take > 0m ? (isLong ? entryPrice + take : entryPrice - take) : null;
	}

	private void ResetProtection()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private enum TradeDirections
	{
		None,
		Long,
		Short
	}
}