在 GitHub 上查看

EMA(barabashkakvn 版本)策略

本策略由 MetaTrader 5 专家顾问 "EMA (barabashkakvn's edition)" 转换而来。系统监控两条以中位价计算的指数移动平均线,在确认均线交叉且价格对上一根 K 线极值出现小幅回撤后才开仓。止盈与止损均为“虚拟”距离,以点数(pip)形式表示。

核心思路

  1. 在指定周期上计算以 (High + Low) / 2 为基础的 5 与 10 周期 EMA。
  2. 当快线与慢线交叉时,仅记录信号标志,并不立即下单。
  3. 等待价格从上一根 K 线的高点或低点回撤 MoveBackPips 点,同时要求两条 EMA 的差值大于 2 * pipSize
  4. 满足条件后,按照交叉方向开仓。
  5. 开仓后根据点数距离维护虚拟止盈和止损,一旦价格触及便立即平仓。

这种流程完全复现了原 MQL 程序:原始 EA 使用变量 check 来标记交叉,然后检查 EMA 差值与回撤条件,最后通过虚拟报价判断是否触及止盈/止损。

指标与数据

  • 以中位价计算的 EMA(5)。
  • 以中位价计算的 EMA(10)。
  • 前一根已完成 K 线的最高价与最低价,用于判断回撤。
  • 所有计算均基于配置的 CandleType 订阅并仅处理收盘完毕的 K 线。

参数

参数 默认值 说明
OrderVolume 0.1 每次开仓的合约/手数。
VirtualProfitPips 5 入场价到虚拟止盈的点数距离。
MoveBackPips 3 交叉后所需的回撤点数,从上一根 K 线极值开始计算。
StopLossPips 20 入场价到虚拟止损的点数距离。
PipSize 0.0001 单个点对应的价格变动,需要根据交易品种调整。
FastLength 5 快速 EMA 的周期。
SlowLength 10 慢速 EMA 的周期。
CandleType TimeFrame(1m) 用于计算的 K 线类型。

所有与点数相关的参数都会乘以 PipSize 转换成价格差。如果该值为 0 或负数,策略会尝试使用 Security.PriceStep(若交易所提供)。

交易逻辑

入场规则

  • 信号记录:每次 EMA 发生交叉都会设置内部标志,但此时不下单。
  • 做空条件
    • 信号标志已设置;
    • SlowEMA - FastEMA > 2 * pipSize
    • 当前 K 线最高价 ≥ 上一根 K 线最低价 + MoveBackPips * pipSize(价格自前低向上回撤)。
  • 做多条件
    • 信号标志已设置;
    • FastEMA - SlowEMA > 2 * pipSize
    • 当前 K 线最低价 ≤ 上一根 K 线最高价 - MoveBackPips * pipSize(价格自前高向下回撤)。

开仓后会清除信号标志,避免重复进场。

离场规则

策略通过比较 K 线极值与虚拟距离来模拟原 EA 的 Bid/Ask 判断:

  • 多单
    • 若最高价 ≥ 入场价 + VirtualProfitPips * pipSize 则止盈;
    • 若最低价 ≤ 入场价 - StopLossPips * pipSize 则止损。
  • 空单
    • 若最低价 ≤ 入场价 - VirtualProfitPips * pipSize 则止盈;
    • 若最高价 ≥ 入场价 + StopLossPips * pipSize 则止损。

平仓后虚拟目标会被重置,等待下一次交叉。

实现细节

  • 采用高层 API 的蜡烛订阅 (SubscribeCandles),并可在图表上绘制 EMA 与成交点。
  • 中位价直接由高低价计算,完全对齐 MetaTrader 的 PRICE_MEDIAN 设定。
  • _hasCrossSignal 与原脚本中的 check 作用一致,确保只有交叉+回撤同时满足才会下单。
  • OnStarted 中调用 StartProtection(),即使退出逻辑为手动也可启用平台的风控机制。
  • 代码注释全部为英文,并且未直接访问任何指标缓冲区。

使用建议

  • 根据交易品种调整 PipSize(如日元货币对、指数、加密货币等报价制式差异)。
  • 由于平仓判断依赖 K 线的最高/最低价,使用 1~5 分钟等较短周期更接近原 EA 的逐笔行为。
  • 可通过优化器调整 EMA 周期、回撤点数和虚拟止盈止损距离以适配不同市场。
  • 策略一次只持有一个方向的仓位,外部手动交易可能干扰虚拟止盈止损的跟踪。

风险提示

  • 基于 K 线的极值判断可能遗漏盘中瞬间触价,必要时请使用更高分辨率的数据。
  • 虚拟止损不会发送真实保护性委托,断线或滑点可能导致亏损超出预期。
  • 均线交叉策略在震荡行情表现不佳,可根据需要叠加趋势或波动率过滤条件。
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 virtual take profit and stop loss distances.
/// Converted from the MQL5 expert "EMA (barabashkakvn's edition)".
/// </summary>
public class EmaBarabashkakvnEditionStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _virtualProfitPips;
	private readonly StrategyParam<int> _moveBackPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<decimal> _pipSize;
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _slowEma;
	private bool _hasCrossSignal;
	private decimal? _prevFast;
	private decimal? _prevSlow;
	private decimal? _prevHigh;
	private decimal? _prevLow;
	private decimal? _entryPrice;
	private decimal? _virtualTarget;
	private decimal? _virtualStop;

	/// <summary>
	/// Order volume in lots.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Virtual take profit distance in pips.
	/// </summary>
	public int VirtualProfitPips
	{
		get => _virtualProfitPips.Value;
		set => _virtualProfitPips.Value = value;
	}

	/// <summary>
	/// Retracement distance after a crossover in pips.
	/// </summary>
	public int MoveBackPips
	{
		get => _moveBackPips.Value;
		set => _moveBackPips.Value = value;
	}

	/// <summary>
	/// Virtual stop loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Pip size in price units.
	/// </summary>
	public decimal PipSize
	{
		get => _pipSize.Value;
		set => _pipSize.Value = value;
	}

	/// <summary>
	/// Fast EMA length applied to median price.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length applied to median price.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="EmaBarabashkakvnEditionStrategy"/>.
	/// </summary>
	public EmaBarabashkakvnEditionStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume in lots", "Trading")
			
			.SetOptimize(0.05m, 1m, 0.05m);

		_virtualProfitPips = Param(nameof(VirtualProfitPips), 5)
			.SetGreaterThanZero()
			.SetDisplay("Virtual Profit", "Take profit distance in pips", "Risk")
			
			.SetOptimize(2, 20, 1);

		_moveBackPips = Param(nameof(MoveBackPips), 3)
			.SetGreaterThanZero()
			.SetDisplay("Move Back", "Retracement after crossover in pips", "Entries")
			
			.SetOptimize(1, 10, 1);

		_stopLossPips = Param(nameof(StopLossPips), 20)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss", "Virtual stop loss distance in pips", "Risk")
			
			.SetOptimize(10, 60, 2);

		_pipSize = Param(nameof(PipSize), 0.0001m)
			.SetGreaterThanZero()
			.SetDisplay("Pip Size", "Instrument pip size in price units", "General");

		_fastLength = Param(nameof(FastLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast EMA length on median price", "Indicators")
			
			.SetOptimize(3, 15, 1);

		_slowLength = Param(nameof(SlowLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow EMA length on median price", "Indicators")
			
			.SetOptimize(8, 40, 1);

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

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

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

		_fastEma = default;
		_slowEma = default;
		_hasCrossSignal = false;
		_prevFast = default;
		_prevSlow = default;
		_prevHigh = default;
		_prevLow = default;
		_entryPrice = default;
		_virtualTarget = default;
		_virtualStop = default;
	}

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

		_fastEma = new EMA { Length = FastLength };
		_slowEma = new EMA { Length = SlowLength };

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

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

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

		// Calculate median price as in the original expert (PRICE_MEDIAN).
		var medianPrice = (candle.HighPrice + candle.LowPrice) / 2m;

		// Update EMA values using the median price.
		var fastValue = _fastEma.Process(new DecimalIndicatorValue(_fastEma, medianPrice, candle.OpenTime) { IsFinal = true });
		var slowValue = _slowEma.Process(new DecimalIndicatorValue(_slowEma, medianPrice, candle.OpenTime) { IsFinal = true });

		if (!_fastEma.IsFormed || !_slowEma.IsFormed)
		{
			_prevHigh = candle.HighPrice;
			_prevLow = candle.LowPrice;
			_prevFast = fastValue.ToDecimal();
			_prevSlow = slowValue.ToDecimal();
			return;
		}

		var fast = fastValue.ToDecimal();
		var slow = slowValue.ToDecimal();

		if (_prevFast is decimal prevFast && _prevSlow is decimal prevSlow)
		{
			var bullishCross = prevFast <= prevSlow && fast > slow;
			var bearishCross = prevFast >= prevSlow && fast < slow;

			if (bullishCross || bearishCross)
				_hasCrossSignal = true;
		}

		_prevFast = fast;
		_prevSlow = slow;

		var pipValue = PipSize;
		if (pipValue <= 0m)
			pipValue = Security?.PriceStep ?? 0.0001m;

		var moveBackPrice = MoveBackPips * pipValue;
		var profitDistance = VirtualProfitPips * pipValue;
		var stopDistance = StopLossPips * pipValue;

		if (Position == 0 && _hasCrossSignal && _prevHigh is decimal prevHigh && _prevLow is decimal prevLow)
		{
			var bearishSpread = slow - fast;
			var bullishSpread = fast - slow;

			var bearishReady = bearishSpread > 2m * pipValue && candle.HighPrice >= prevLow + moveBackPrice;
			var bullishReady = bullishSpread > 2m * pipValue && candle.LowPrice <= prevHigh - moveBackPrice;

			if (bearishReady)
			{
				// Enter short after bearish cross and retracement above the previous low.
				_entryPrice = candle.ClosePrice;
				_virtualTarget = _entryPrice - profitDistance;
				_virtualStop = _entryPrice + stopDistance;
				SellMarket();
				_hasCrossSignal = false;
			}
			else if (bullishReady)
			{
				// Enter long after bullish cross and retracement below the previous high.
				_entryPrice = candle.ClosePrice;
				_virtualTarget = _entryPrice + profitDistance;
				_virtualStop = _entryPrice - stopDistance;
				BuyMarket();
				_hasCrossSignal = false;
			}
		}
		else if (Position != 0 && _entryPrice is decimal && _virtualTarget is decimal target && _virtualStop is decimal stop)
		{
			if (Position > 0)
			{
				// Long position: use high for profit target and low for stop.
				var hitTarget = candle.HighPrice >= target;
				var hitStop = candle.LowPrice <= stop;

				if (hitTarget || hitStop)
				{
					SellMarket();
					_hasCrossSignal = false;
					_entryPrice = null;
					_virtualTarget = null;
					_virtualStop = null;
				}
			}
			else if (Position < 0)
			{
				// Short position: use low for profit target and high for stop.
				var hitTarget = candle.LowPrice <= target;
				var hitStop = candle.HighPrice >= stop;

				if (hitTarget || hitStop)
				{
					BuyMarket();
					_hasCrossSignal = false;
					_entryPrice = null;
					_virtualTarget = null;
					_virtualStop = null;
				}
			}
		}

		if (Position == 0)
		{
			_entryPrice = null;
			_virtualTarget = null;
			_virtualStop = null;
		}

		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
	}
}