在 GitHub 上查看

EMA 交叉移动止损策略

概览

该策略是 MQL5 专家顾问 “Intersection 2 iMA” 的 StockSharp 版本。策略在选定的 K 线序列上计算两条指数移动平均线(EMA),仅在完全收盘的 K 线上检测交叉信号。原始 EA 支持根据资金动态计算手数,本移植版本改为通过参数控制下单量,同时完整保留均线交叉与阶梯式移动止损的核心逻辑。

交易规则

  1. 信号判定
    • 在订阅的 K 线上分别计算快、慢 EMA。
    • 若上一根已完成 K 线上快 EMA 低于或等于慢 EMA,而当前数值出现快 EMA 上穿慢 EMA,则生成做多信号。
    • 若上一根已完成 K 线上快 EMA 高于或等于慢 EMA,而当前数值出现快 EMA 下穿慢 EMA,则生成做空信号。
  2. 下单执行
    • 出现做多信号且当前没有多头仓位时,发送市价买单。
    • 出现做空信号且当前没有空头仓位时,发送市价卖单。
    • 如果存在反向仓位,会在下单量中加入当前持仓的绝对值,从而先平掉旧仓再建立新仓,与原始 EA 先平仓再开仓的行为一致。
  3. 移动止损
    • 通过一个可配置的“点数”与品种 PriceStep 相乘,得到止损与价格之间的固定距离。
    • 只有当价格向盈利方向推进超过设定的“阶梯步长”时,才会更新止损位置,避免频繁修改。
    • 当价格触及移动止损时,使用市价单立即平仓。

参数说明

参数 含义 默认值
FastPeriod 快速 EMA 的周期长度。 4
SlowPeriod 慢速 EMA 的周期长度。 18
TrailingStopPoints 移动止损与当前价格的距离,单位为价格步长(点)。设为 0 表示关闭移动止损。 20
TrailingStepPoints 更新移动止损前,价格至少需要向有利方向推进的点数。 5
CandleType 用于计算的 K 线类型或时间框架。 15 分钟
TradeVolume 每次下单的固定手数。 1

实现要点

  • 使用高层 API SubscribeCandles().Bind(...) 将 K 线与 EMA 指标绑定,无需手动管理指标缓存。
  • 移植时保留了原始 EA 中对 3/5 位数品种的“点值调整”思路:通过品种的 PriceStep 将点数转换为真实价格距离。
  • StockSharp 不提供 PositionModify 的直接等价方法,因此移动止损以市价平仓实现;行为与原策略一致,只是实现方式不同。
  • 所有关键参数均通过 StrategyParam<T> 暴露,方便在 Designer 中优化或在界面上调整。

使用建议

  • 根据实际交易或回测的时间周期设置 CandleType,确保 EMA 计算一致。
  • 对于 tick 很小的品种,请适当调整 TrailingStopPointsTrailingStepPoints,实际价格距离等于 “点数 × PriceStep”。
  • 通过 TradeVolume 参数设定合适的交易手数;策略会在反向信号出现时自动扩大下单量以一次性反手。

与原 EA 的差异

  • 原策略使用 MoneyFixedMargin 计算动态手数,移植版本提供固定下单量参数,复杂的资金管理可以在外部完成。
  • 源代码中的 InpCloseHalf 输入没有被实际使用,因此在移植中被省略。
  • 移植后移动止损改为内部逻辑控制市价平仓,不再尝试修改已有止损单,便于在 StockSharp 环境中运行。
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>
/// EMA crossover strategy with trailing stop logic converted from the MQL5 "Intersection 2 iMA" expert advisor.
/// The strategy opens trades on moving average crossovers and maintains a stepped trailing stop.
/// </summary>
public class EmaCrossoverTrailingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private ExponentialMovingAverage _fastEma = null!;
	private ExponentialMovingAverage _slowEma = null!;

	private decimal? _previousFastValue;
	private decimal? _previousSlowValue;

	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;

	private decimal _stopDistance;
	private decimal _stepDistance;

	/// <summary>
	/// Initializes <see cref="EmaCrossoverTrailingStrategy"/>.
	/// </summary>
	public EmaCrossoverTrailingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 4)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Period of the fast exponential moving average", "Moving Averages")
			
			.SetOptimize(2, 20, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 18)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Period of the slow exponential moving average", "Moving Averages")
			
			.SetOptimize(10, 60, 2);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 20m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Distance from price to trailing stop expressed in price steps", "Risk Management")
			
			.SetOptimize(5m, 40m, 5m);

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Minimum price advancement before the trailing stop is moved", "Risk Management")
			
			.SetOptimize(1m, 10m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume used for entries", "General");
	}

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Minimum move required before shifting the trailing stop.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Volume used for 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();

		Volume = TradeVolume;
		_previousFastValue = null;
		_previousSlowValue = null;
		_longStopPrice = null;
		_shortStopPrice = null;
		_fastEma = null!;
		_slowEma = null!;
		_stopDistance = 0m;
		_stepDistance = 0m;
	}

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

		Volume = TradeVolume;

		_fastEma = new EMA { Length = FastPeriod };
		_slowEma = new EMA { Length = SlowPeriod };

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

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

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

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

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

		UpdateTrailingStops(candle);

		if (!_fastEma.IsFormed || !_slowEma.IsFormed)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		// indicators bound via .Bind()

		if (_previousFastValue is null || _previousSlowValue is null)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		var fastPrev = _previousFastValue.Value;
		var slowPrev = _previousSlowValue.Value;

		var crossedUp = fastPrev <= slowPrev && fastValue > slowValue;
		var crossedDown = fastPrev >= slowPrev && fastValue < slowValue;

		if (crossedUp && Position <= 0)
		{
			var volumeToBuy = TradeVolume;

			if (Position < 0)
				volumeToBuy += Math.Abs(Position);

			if (volumeToBuy > 0)
			{
				BuyMarket();
				InitializeLongTrailing(candle.ClosePrice);
			}
		}
		else if (crossedDown && Position >= 0)
		{
			var volumeToSell = TradeVolume;

			if (Position > 0)
				volumeToSell += Math.Abs(Position);

			if (volumeToSell > 0)
			{
				SellMarket();
				InitializeShortTrailing(candle.ClosePrice);
			}
		}

		_previousFastValue = fastValue;
		_previousSlowValue = slowValue;
	}

	private decimal CalculateDistance(decimal points)
	{
		if (points <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m)
			priceStep = 1m;

		return points * priceStep;
	}

	private void InitializeLongTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			return;
		}

		_longStopPrice = price - _stopDistance;
		_shortStopPrice = null;
	}

	private void InitializeShortTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_shortStopPrice = null;
			return;
		}

		_shortStopPrice = price + _stopDistance;
		_longStopPrice = null;
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			_shortStopPrice = null;
			return;
		}

		if (Position > 0)
		{
			if (_longStopPrice is null)
			{
				InitializeLongTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice - _stopDistance;

				if (newStop - _longStopPrice.Value >= _stepDistance)
					_longStopPrice = newStop;

				if (candle.LowPrice <= _longStopPrice.Value)
				{
					SellMarket();
					_longStopPrice = null;
				}
			}
		}
		else if (Position < 0)
		{
			if (_shortStopPrice is null)
			{
				InitializeShortTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice + _stopDistance;

				if (_shortStopPrice.Value - newStop >= _stepDistance)
					_shortStopPrice = newStop;

				if (candle.HighPrice >= _shortStopPrice.Value)
				{
					BuyMarket();
					_shortStopPrice = null;
				}
			}
		}
		else
		{
			_longStopPrice = null;
			_shortStopPrice = null;
		}
	}
}