在 GitHub 上查看

EMA Cross 2 策略

概述

该策略是 MetaTrader 4 专家顾问 “EMA_CROSS_2” 的 StockSharp 移植版本。原始脚本监控两条指数移动平均线(EMA),当两条均线互换位置时就会开仓。移植版保留了原策略的反向特点——当长周期 EMA 高于短周期 EMA 时买入,当短周期 EMA 高于长周期 EMA 时卖出——并将全部逻辑封装在 StockSharp 的高级策略框架中。

策略基于可配置的 K 线数据类型,只在收盘后对完整 K 线进行分析,以避免在同一根 K 线内重复触发信号。风险管理模块沿用 MetaTrader 的做法,使用以“点(point)”为单位的止盈、止损和移动止损距离。

交易逻辑

  1. 指标计算
    • 在每根完成的 K 线上计算短周期与长周期 EMA。
    • 按照原始 EA 中的 first_time 标志,第一次计算被忽略,不会产生交易。
    • 之后只要长、短 EMA 的相对位置发生翻转,就视为新的方向变化。
  2. 信号解释
    • 当长周期 EMA 上穿短周期 EMA 时视为做多信号。移植版保留这种逆势做法,即便它与常见的均线交叉策略相反。
    • 当短周期 EMA 收盘价高于长周期 EMA 时发出做空信号。
    • 只有在账户没有持仓时才允许开新仓,对应原策略的 OrdersTotal() < 1 限制。
  3. 下单执行
    • 信号触发后按固定的可配置手数发送市价单。
    • 成交时根据参数记录止盈与止损价格,距离以点数换算。
  4. 风险管理
    • 每根完成的 K 线都会检查价格是否触及止盈或止损。一旦突破即通过市价单平仓。
    • 当浮盈超过设置的移动止损距离时,启动移动止损。多头仓位上移保护价,空头仓位下移保护价。
    • 仓位归零后会清除所有保护性价格。

参数

名称 说明 默认值
CandleType 用于指标计算和信号检测的 K 线类型。 15 分钟周期
OrderVolume 每次市价单的成交量(手数/合约数)。 2
TakeProfitPoints 止盈距离,单位为交易商点(price step)。设为 0 表示不启用止盈。 20
StopLossPoints 止损距离,单位为交易商点。设为 0 表示不启用止损。 30
TrailingStopPoints 移动止损距离,单位为交易商点。0 表示关闭移动止损。 50
ShortEmaPeriod 短周期 EMA 的长度。 5
LongEmaPeriod 长周期 EMA 的长度。 60

实现细节

  • 使用 SubscribeCandles().Bind(shortEma, longEma, ProcessCandle) 将 K 线数据与 EMA 指标绑定,完全依赖 StockSharp 的高级 API,无需手动维护缓冲区。
  • 在回调函数中直接获得经过解包的指标数值,无需调用 GetValue()
  • 防护距离通过乘以品种的 PriceStep 将 MetaTrader 点值转换成实际价格。如果品种采用 3 位或 5 位小数报价,辅助函数会自动返回合适的“pip”大小。
  • 止盈、止损与移动止损通过内部的市价平仓实现,因为 StockSharp 中没有 MetaTrader 4 OrderModify 的等价函数。逻辑与原版一致:每根 K 线检查一次,一旦突破立即退出。
  • 首次均线比较被刻意忽略,以复刻原脚本中避免首根信号误触发的机制。

与 MetaTrader 版本的差异

  • 资金管理:原 EA 始终按照 Lots 参数交易。移植版通过 OrderVolume 参数暴露同一概念,并同步到策略的 Volume 属性,方便在设计器和优化器中使用。
  • 下单方式:MetaTrader 在 OrderSend 中直接设置止盈止损。StockSharp 版本改为在策略内部追踪这些价位,并在触发后以市价单平仓。
  • 移动止损精度:原脚本基于即时的 Bid/Ask 逐点移动。移植版在 K 线收盘时更新移动止损,这是示例项目中可用的最细粒度。触发条件和距离保持不变。
  • 日志处理:省略了 MQL 中的错误码输出,改用 StockSharp 自带的日志系统记录事件。

使用建议

  • CandleType 设为与回测或实盘时段一致的周期,以保持 EMA 行为一致。
  • 对于带有小数点后 3 位或 5 位报价的外汇品种,请确认参数中的“点”对应期望的 pip 数(例如 EURUSD 中 10 点约等于 1 pip)。
  • 根据交易平台的要求设置 OrderVolume。策略不会自动调整手数。
  • 可以在设计器中启用参数的优化开关,像在 MetaTrader 中那样组合测试不同的 EMA 周期和风控距离。

文件列表

  • CS/EmaCross2Strategy.cs —— 策略源码。
  • README.md —— 英文文档。
  • README_zh.md —— 中文文档(本文件)。
  • README_ru.md —— 俄文文档。
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>
/// Counter-trend EMA crossover strategy converted from the MetaTrader 4 expert "EMA_CROSS_2".
/// Buys when the long EMA rises above the short EMA, and sells when the short EMA climbs above the long EMA.
/// Incorporates MetaTrader-style risk management with point-based stop-loss, take-profit, and trailing stop levels.
/// </summary>
public class EmaCross2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<int> _shortEmaPeriod;
	private readonly StrategyParam<int> _longEmaPeriod;

	private ExponentialMovingAverage _shortEma;
	private ExponentialMovingAverage _longEma;

	private bool _skipFirstSignal = true;
	private int _lastDirection;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _pointSize;
	private decimal _entryPrice;

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

	/// <summary>
	/// Order volume applied to new market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in broker points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in broker points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

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

	/// <summary>
	/// Period of the short EMA.
	/// </summary>
	public int ShortEmaPeriod
	{
		get => _shortEmaPeriod.Value;
		set => _shortEmaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the long EMA.
	/// </summary>
	public int LongEmaPeriod
	{
		get => _longEmaPeriod.Value;
		set => _longEmaPeriod.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public EmaCross2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for EMA calculations", "General");

		_orderVolume = Param(nameof(OrderVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume of each market order", "Trading")
		
		.SetOptimize(0.1m, 5m, 0.1m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Distance from entry to take-profit in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (points)", "Distance from entry to stop-loss in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (points)", "Trailing distance maintained after entry", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_shortEmaPeriod = Param(nameof(ShortEmaPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Short EMA", "Length of the fast EMA", "Indicators")
		
		.SetOptimize(2, 40, 1);

		_longEmaPeriod = Param(nameof(LongEmaPeriod), 60)
		.SetGreaterThanZero()
		.SetDisplay("Long EMA", "Length of the slow EMA", "Indicators")
		
		.SetOptimize(10, 200, 5);
	}

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

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

		Volume = OrderVolume;
		_shortEma = null;
		_longEma = null;
		_skipFirstSignal = true;
		_lastDirection = 0;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_pointSize = 0m;
		_entryPrice = 0m;
	}

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

		Volume = OrderVolume;
		_pointSize = CalculatePointSize();

		_shortEma = new EMA { Length = ShortEmaPeriod };
		_longEma = new EMA { Length = LongEmaPeriod };

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

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

	private void ProcessCandle(ICandleMessage candle, decimal shortEmaValue, decimal longEmaValue)
	{
		// Work only with finished candles to avoid repeated signals inside the same bar.
		if (candle.State != CandleStates.Finished)
		return;

		if (_pointSize <= 0m)
		_pointSize = CalculatePointSize();

		if (CheckRisk(candle))
		return;

		if (Position != 0)
		UpdateTrailingStop(candle);
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		ResetRiskLevels();

		var signal = EvaluateCross(longEmaValue, shortEmaValue);

		if (signal == 0)
		return;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (Position != 0)
		return;

		var volume = OrderVolume;
		if (volume <= 0m)
		volume = 1m;

		if (signal == 1)
		{
			BuyMarket(volume);
			SetRiskLevels(candle.ClosePrice, true);
		}
		else if (signal == 2)
		{
			SellMarket(volume);
			SetRiskLevels(candle.ClosePrice, false);
		}
	}

	private int EvaluateCross(decimal longValue, decimal shortValue)
	{
		var currentDirection = 0;

		if (longValue > shortValue)
		currentDirection = 1;
		else if (longValue < shortValue)
		currentDirection = 2;

		if (_skipFirstSignal)
		{
			_skipFirstSignal = false;
			return 0;
		}

		if (currentDirection != 0 && currentDirection != _lastDirection)
		{
			_lastDirection = currentDirection;
			return _lastDirection;
		}

		return 0;
	}

	private bool CheckRisk(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (Position < 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		{
			ResetRiskLevels();
		}

		return false;
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m || _pointSize <= 0m)
		return;

		var distance = TrailingStopPoints * _pointSize;
		if (distance <= 0m)
		return;

		var entryPrice = _entryPrice > 0 ? _entryPrice : candle.ClosePrice;

		if (Position > 0)
		{
			var profit = candle.ClosePrice - entryPrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice - distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value < candidate)
				_stopLossPrice = candidate;
			}
		}
		else if (Position < 0)
		{
			var profit = entryPrice - candle.ClosePrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice + distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value > candidate)
				_stopLossPrice = candidate;
			}
		}
	}

	private void SetRiskLevels(decimal executionPrice, bool isLong)
	{
		if (_pointSize <= 0m)
		{
			ResetRiskLevels();
			return;
		}

		_stopLossPrice = StopLossPoints > 0m
		? executionPrice + (isLong ? -1m : 1m) * StopLossPoints * _pointSize
		: null;

		_takeProfitPrice = TakeProfitPoints > 0m
		? executionPrice + (isLong ? 1m : -1m) * TakeProfitPoints * _pointSize
		: null;
	}

	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private decimal CalculatePointSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}