EMA Cross 2 策略
概述
该策略是 MetaTrader 4 专家顾问 “EMA_CROSS_2” 的 StockSharp 移植版本。原始脚本监控两条指数移动平均线(EMA),当两条均线互换位置时就会开仓。移植版保留了原策略的反向特点——当长周期 EMA 高于短周期 EMA 时买入,当短周期 EMA 高于长周期 EMA 时卖出——并将全部逻辑封装在 StockSharp 的高级策略框架中。
策略基于可配置的 K 线数据类型,只在收盘后对完整 K 线进行分析,以避免在同一根 K 线内重复触发信号。风险管理模块沿用 MetaTrader 的做法,使用以“点(point)”为单位的止盈、止损和移动止损距离。
交易逻辑
- 指标计算
- 在每根完成的 K 线上计算短周期与长周期 EMA。
- 按照原始 EA 中的
first_time标志,第一次计算被忽略,不会产生交易。 - 之后只要长、短 EMA 的相对位置发生翻转,就视为新的方向变化。
- 信号解释
- 当长周期 EMA 上穿短周期 EMA 时视为做多信号。移植版保留这种逆势做法,即便它与常见的均线交叉策略相反。
- 当短周期 EMA 收盘价高于长周期 EMA 时发出做空信号。
- 只有在账户没有持仓时才允许开新仓,对应原策略的
OrdersTotal() < 1限制。
- 下单执行
- 信号触发后按固定的可配置手数发送市价单。
- 成交时根据参数记录止盈与止损价格,距离以点数换算。
- 风险管理
- 每根完成的 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class ema_cross_2_strategy(Strategy):
"""
Counter-trend EMA crossover strategy (EMA_CROSS_2 MetaTrader port).
Buys when long EMA rises above short EMA, sells on opposite.
Point-based stop-loss, take-profit, and trailing stop management.
"""
def __init__(self):
super(ema_cross_2_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Time frame for EMA calculations", "General")
self._take_profit_points = self.Param("TakeProfitPoints", 500.0) \
.SetDisplay("Take Profit (points)", "Distance from entry to take-profit", "Risk")
self._stop_loss_points = self.Param("StopLossPoints", 500.0) \
.SetDisplay("Stop Loss (points)", "Distance from entry to stop-loss", "Risk")
self._trailing_stop_points = self.Param("TrailingStopPoints", 500.0) \
.SetDisplay("Trailing Stop (points)", "Trailing distance after entry", "Risk")
self._short_ema_period = self.Param("ShortEmaPeriod", 5) \
.SetDisplay("Short EMA", "Length of the fast EMA", "Indicators")
self._long_ema_period = self.Param("LongEmaPeriod", 60) \
.SetDisplay("Long EMA", "Length of the slow EMA", "Indicators")
self._skip_first_signal = True
self._last_direction = 0
self._stop_loss_price = None
self._take_profit_price = None
self._point_size = 0.0
self._entry_price = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(ema_cross_2_strategy, self).OnReseted()
self._skip_first_signal = True
self._last_direction = 0
self._stop_loss_price = None
self._take_profit_price = None
self._point_size = 0.0
self._entry_price = 0.0
def OnStarted2(self, time):
super(ema_cross_2_strategy, self).OnStarted2(time)
self._point_size = self._calculate_point_size()
short_ema = ExponentialMovingAverage()
short_ema.Length = self._short_ema_period.Value
long_ema = ExponentialMovingAverage()
long_ema.Length = self._long_ema_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(short_ema, long_ema, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, short_ema)
self.DrawIndicator(area, long_ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, short_val, long_val):
if candle.State != CandleStates.Finished:
return
short_val = float(short_val)
long_val = float(long_val)
if self._point_size <= 0.0:
self._point_size = self._calculate_point_size()
if self._check_risk(candle):
return
if self.Position != 0:
self._update_trailing_stop(candle)
elif self._stop_loss_price is not None or self._take_profit_price is not None:
self._reset_risk_levels()
signal = self._evaluate_cross(long_val, short_val)
if signal == 0:
return
if self.Position != 0:
return
if signal == 1:
self.BuyMarket()
self._set_risk_levels(float(candle.ClosePrice), True)
elif signal == 2:
self.SellMarket()
self._set_risk_levels(float(candle.ClosePrice), False)
def _evaluate_cross(self, long_val, short_val):
current_direction = 0
if long_val > short_val:
current_direction = 1
elif long_val < short_val:
current_direction = 2
if self._skip_first_signal:
self._skip_first_signal = False
return 0
if current_direction != 0 and current_direction != self._last_direction:
self._last_direction = current_direction
return self._last_direction
return 0
def _check_risk(self, candle):
if self.Position > 0:
if self._stop_loss_price is not None and float(candle.LowPrice) <= self._stop_loss_price:
self.SellMarket()
self._reset_risk_levels()
return True
if self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket()
self._reset_risk_levels()
return True
elif self.Position < 0:
if self._stop_loss_price is not None and float(candle.HighPrice) >= self._stop_loss_price:
self.BuyMarket()
self._reset_risk_levels()
return True
if self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket()
self._reset_risk_levels()
return True
elif self._stop_loss_price is not None or self._take_profit_price is not None:
self._reset_risk_levels()
return False
def _update_trailing_stop(self, candle):
trail_pts = self._trailing_stop_points.Value
if trail_pts <= 0 or self._point_size <= 0:
return
distance = trail_pts * self._point_size
if distance <= 0:
return
entry = self._entry_price if self._entry_price > 0 else float(candle.ClosePrice)
close = float(candle.ClosePrice)
if self.Position > 0:
profit = close - entry
if profit > distance:
candidate = close - distance
if self._stop_loss_price is None or self._stop_loss_price < candidate:
self._stop_loss_price = candidate
elif self.Position < 0:
profit = entry - close
if profit > distance:
candidate = close + distance
if self._stop_loss_price is None or self._stop_loss_price > candidate:
self._stop_loss_price = candidate
def _set_risk_levels(self, price, is_long):
if self._point_size <= 0:
self._reset_risk_levels()
return
self._entry_price = price
sl = self._stop_loss_points.Value
tp = self._take_profit_points.Value
direction = 1.0 if is_long else -1.0
self._stop_loss_price = price - direction * sl * self._point_size if sl > 0 else None
self._take_profit_price = price + direction * tp * self._point_size if tp > 0 else None
def _reset_risk_levels(self):
self._stop_loss_price = None
self._take_profit_price = None
def _calculate_point_size(self):
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
return step if step > 0 else 1.0
def CreateClone(self):
return ema_cross_2_strategy()