Стратегия представляет собой портирование советника MetaTrader 4 MQL/8606/EMA_CROSS_2.mq4 в инфраструктуру StockSharp. Она сохраняет исходную идею — отслеживать взаимное положение медленной и быстрой экспоненциальных скользящих средних и открывать единственную позицию при смене направления. Управление тейк-профитом, стоп-лоссом и трейлинг-стопом реализовано через высокоуровневый метод StartProtection, что позволяет повторить логику оригинала и одновременно следовать рекомендациям StockSharp.
Логика торговли
Формируются свечи заданного таймфрейма CandleType (по умолчанию 15 минут) и на их основе рассчитываются две EMA: медленная с периодом SlowEmaLength и быстрая с периодом FastEmaLength.
Ведётся учёт последнего направления (медленная EMA выше или ниже быстрой). Первая завершённая свеча после формирования индикаторов используется только для инициализации направления — аналог переменной first_time в MQL.
Когда медленная EMA поднимается выше быстрой (новое направление равно 1) и стратегия находится вне позиции, отправляется рыночная заявка на покупку. Когда медленная EMA опускается ниже быстрой (новое направление 2), отправляется рыночная заявка на продажу. Тем самым полностью воспроизводится поведение функции Crossed(LEma, SEma) из исходного кода.
Одновременно может существовать только одна позиция. Пока ордер на вход исполняется либо позиция открыта, все последующие сигналы игнорируются.
Управление сделкой и рисками
StartProtection задаёт параметры тейк-профита, стоп-лосса и трейлинг-стопа в абсолютных ценовых шагах, вычисленных из PriceStep инструмента. Значение TrailingStopPips = 0 отключает трейлинг.
Для входов используются BuyMarket/SellMarket, а закрытие происходит рыночными ордерами при достижении защитных уровней, что отражает логику циклов OrderSend и OrderModify в советнике.
Параметр OrderVolume задаёт базовый объём позиции. Перед отправкой заявки значение корректируется в соответствии с шагом объёма, минимальными и максимальными ограничениями инструмента.
Параметры
Параметр
Описание
TakeProfitPips
Расстояние в пунктах до тейк-профита. Значение по умолчанию: 20.
StopLossPips
Расстояние в пунктах до стоп-лосса. Значение по умолчанию: 30.
TrailingStopPips
Длина трейлинг-стопа в пунктах. Значение 0 выключает трейлинг. По умолчанию: 50.
OrderVolume
Объём входа (в лотах) до выравнивания по шагу. Значение по умолчанию: 2.
FastEmaLength
Период быстрой EMA по ценам закрытия. Значение по умолчанию: 5.
SlowEmaLength
Период медленной EMA по ценам закрытия. Значение по умолчанию: 60.
CandleType
Таймфрейм свечей. Значение по умолчанию: 15 минут.
Примечания
Проверка Bars < 100 из MQL заменена ожиданием формирования обеих EMA, что обеспечивает такую же устойчивость сигналов.
Трейлинг-стоп обслуживается встроенным модулем защиты. Благодаря этому нет необходимости вручную вызывать OrderModify, как это делалось в оригинальной версии.
Python-реализация не создавалась по запросу — в директории присутствует только C# версия.
using System;
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.
/// Buys when fast EMA crosses above slow EMA, sells on the opposite crossover.
/// </summary>
public class EmaCrossTrailingStrategy : Strategy
{
private readonly StrategyParam<int> _fastEmaLength;
private readonly StrategyParam<int> _slowEmaLength;
private readonly StrategyParam<DataType> _candleType;
private int _currentDirection;
private bool _hasInitialDirection;
public EmaCrossTrailingStrategy()
{
_fastEmaLength = Param(nameof(FastEmaLength), 5)
.SetDisplay("Fast EMA", "Length of the fast exponential moving average.", "Indicator");
_slowEmaLength = Param(nameof(SlowEmaLength), 60)
.SetDisplay("Slow EMA", "Length of the slow exponential moving average.", "Indicator");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Time frame used to build candles and EMAs.", "General");
}
public int FastEmaLength
{
get => _fastEmaLength.Value;
set => _fastEmaLength.Value = value;
}
public int SlowEmaLength
{
get => _slowEmaLength.Value;
set => _slowEmaLength.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_currentDirection = 0;
_hasInitialDirection = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
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;
// Determine direction: 1 = fast above slow (bullish), -1 = fast below slow (bearish)
var newDirection = fastValue > slowValue ? 1 : fastValue < slowValue ? -1 : 0;
if (newDirection == 0)
return;
if (!_hasInitialDirection)
{
_currentDirection = newDirection;
_hasInitialDirection = true;
return;
}
if (newDirection == _currentDirection)
return;
var prevDirection = _currentDirection;
_currentDirection = newDirection;
// Crossover detected
if (newDirection == 1 && prevDirection == -1)
{
// Bullish crossover
if (Position < 0)
BuyMarket(); // Close short
if (Position <= 0)
BuyMarket(); // Open long
}
else if (newDirection == -1 && prevDirection == 1)
{
// Bearish crossover
if (Position > 0)
SellMarket(); // Close long
if (Position >= 0)
SellMarket(); // Open short
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class ema_cross_trailing_strategy(Strategy):
"""
EMA crossover strategy with trailing stop.
Buys when fast EMA crosses above slow EMA, sells on opposite crossover.
"""
def __init__(self):
super(ema_cross_trailing_strategy, self).__init__()
self._fast_ema_length = self.Param("FastEmaLength", 5) \
.SetDisplay("Fast EMA", "Length of the fast EMA", "Indicator")
self._slow_ema_length = self.Param("SlowEmaLength", 60) \
.SetDisplay("Slow EMA", "Length of the slow EMA", "Indicator")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Time frame for candles and EMAs", "General")
self._current_direction = 0
self._has_initial_direction = False
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(ema_cross_trailing_strategy, self).OnReseted()
self._current_direction = 0
self._has_initial_direction = False
def OnStarted2(self, time):
super(ema_cross_trailing_strategy, self).OnStarted2(time)
fast_ema = ExponentialMovingAverage()
fast_ema.Length = self._fast_ema_length.Value
slow_ema = ExponentialMovingAverage()
slow_ema.Length = self._slow_ema_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(fast_ema, slow_ema, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, fast_ema)
self.DrawIndicator(area, slow_ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, fast_val, slow_val):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_val)
slow_val = float(slow_val)
if fast_val > slow_val:
new_direction = 1
elif fast_val < slow_val:
new_direction = -1
else:
return
if not self._has_initial_direction:
self._current_direction = new_direction
self._has_initial_direction = True
return
if new_direction == self._current_direction:
return
prev_direction = self._current_direction
self._current_direction = new_direction
if new_direction == 1 and prev_direction == -1:
if self.Position < 0:
self.BuyMarket()
if self.Position <= 0:
self.BuyMarket()
elif new_direction == -1 and prev_direction == 1:
if self.Position > 0:
self.SellMarket()
if self.Position >= 0:
self.SellMarket()
def CreateClone(self):
return ema_cross_trailing_strategy()