Speed MA — это перенос советника MetaTrader 4 ytg_Speed_MA_ea на платформу StockSharp. В оригинальном коде измеряется изменение простой скользящей средней между соседними барами: если наклон превышает заданный порог, эксперт открывает позицию в соответствующем направлении. Реализация на C# воспроизводит эту логику с использованием высокоуровневого API StockSharp: подписка на свечи, расчёт смещённой простой скользящей средней и открытие сделок при достаточно большом различии между соседними значениями. Объём ордера, тейк-профит и стоп-лосс задаются в тех же MetaTrader-поинтах, что и в исходном MQL4.
Логика работы
Подписаться на выбранный тип свечей (по умолчанию минутный) и создать простую скользящую среднюю длиной MovingAveragePeriod.
Для каждой завершённой свечи сохранять очередное значение скользящей средней. В буфере хранятся только необходимые элементы, чтобы сравнивать значения на смещениях Shift и Shift + 1.
Рассчитать наклон как разность между значением скользящей средней Shift баров назад и значением на бар раньше (Shift + 1). Это эквивалентно вызовам iMA(..., shift) и iMA(..., shift + 1) в MetaTrader.
Преобразовать наклон в абсолютное изменение цены и сравнить его с порогом SlopeThresholdPoints, выраженным в поинтах. Превышение положительного порога даёт сигнал на покупку, превышение отрицательного — на продажу.
Включение ReverseSignals инвертирует направление сигналов, позволяя торговать в противоположную сторону.
Новая позиция открывается только при отсутствии текущей. В MQL4 версия проверяла OrdersTotal() < 1; в StockSharp аналогичное поведение реализовано проверкой Position == 0.
Защитные заявки управляются методом StartProtection. Параметры TakeProfitPoints и StopLossPoints задаются в поинтах и автоматически переводятся в абсолютные ценовые смещения по количеству знаков инструмента.
Управление рисками
Стоп-лосс — StopLossPoints определяет расстояние от входа до защитного стопа в поинтах. Значение 0 отключает стоп.
Тейк-профит — TakeProfitPoints задаёт целевую прибыль в поинтах. Значение 0 отключает тейк.
Стратегия не использует трейлинг и частичные фиксации, полностью повторяя исходный алгоритм с фиксированными стопом и тейком.
Одновременно может быть открыта только одна позиция, что соответствует изначальному советнику с фиксированным объёмом 0.1 лота.
Параметры
Параметр
Описание
Значение по умолчанию
OrderVolume
Объём сделки, аналог 0.1 лота в MQL4.
0.1
MovingAveragePeriod
Период простой скользящей средней.
13
Shift
Количество завершённых баров для смещённого значения (сравниваются shift и shift + 1).
1
SlopeThresholdPoints
Минимальное изменение между смещёнными значениями, выраженное в поинтах.
10
ReverseSignals
Инвертировать направление сигналов.
false
TakeProfitPoints
Расстояние до тейк-профита в поинтах (конвертируется в цену).
500
StopLossPoints
Расстояние до стоп-лосса в поинтах (конвертируется в цену).
490
CandleType
Тип свечей для расчётов (по умолчанию 1 минута).
Таймфрейм 1 минута
Особенности реализации
Значение MetaTrader-поинта восстанавливается по количеству знаков инструмента (Decimals), что даёт верный шаг цены для инструментов с 5 и 3 знаками.
История скользящей средней обрезается до минимально необходимого размера, поэтому память не растёт бесконтрольно, а индексы совпадают с оригиналом.
StartProtection преобразует значения в поинтах в объекты Unit с абсолютным смещением цены, обеспечивая эквивалентные уровни стопа и тейка.
Используется связка SubscribeCandles().Bind(...), поэтому расчёты выполняются только на завершённых свечах и не требуют прямого обращения к GetValue().
В коде добавлены комментарии на английском языке, поясняющие ключевые решения при миграции.
В каталоге присутствует только реализация на C#, Python-версия намеренно не создавалась.
Рекомендации по использованию
Уменьшение SlopeThresholdPoints увеличивает число сделок, поскольку даже небольшие изменения скользящей средней дадут сигнал. Увеличение порога, наоборот, фильтрует шум и требует более сильного движения.
Параметр Shift определяет, насколько "глубоко" измеряется наклон. Значение 0 сравнивает текущий завершённый бар с предыдущим, а большие значения анализируют более ранние отрезки.
При необходимости можно подключать дополнительные рисковые модули StockSharp или ограничения на уровне портфеля.
Важно, чтобы выбранный CandleType совпадал с таймфреймом, использованным при оптимизации MQL4-советника, иначе масштаб наклона изменится.
Отличия от оригинального советника
Для входа и выхода используются методы StockSharp (BuyMarket, SellMarket и StartProtection) вместо OrderSend, однако итоговая схема (один рыночный ордер с фиксированными SL/TP) полностью совпадает.
В MetaTrader контроль позиций ведётся через количество ордеров, а в StockSharp — через суммарную позицию. Проверка на отсутствие позиции воспроизводит условие OrdersTotal() < 1.
Благодаря инфраструктуре StockSharp доступны расширенные журналы, визуализация и работа с единицами измерения, не влияющие на логику торговли.
Файлы
CS/SpeedMAStrategy.cs — реализация стратегии.
README.md, README_zh.md, README_ru.md — подробное описание на английском, китайском и русском языках.
Python-папка и скрипты отсутствуют согласно требованиям задачи.
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Speed MA: EMA slope breakout with ATR stops.
/// </summary>
public class SpeedMAStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<int> _atrLength;
private decimal _prevEma;
private decimal _prevPrevEma;
private decimal _entryPrice;
public SpeedMAStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_emaLength = Param(nameof(EmaLength), 13)
.SetDisplay("EMA Length", "Moving average period.", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
}
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int EmaLength { get => _emaLength.Value; set => _emaLength.Value = value; }
public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevEma = 0; _prevPrevEma = 0; _entryPrice = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevEma = 0; _prevPrevEma = 0; _entryPrice = 0;
var ema = new ExponentialMovingAverage { Length = EmaLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ema, atr, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, ema); DrawOwnTrades(area); }
}
private void ProcessCandle(ICandleMessage candle, decimal emaVal, decimal atrVal)
{
if (candle.State != CandleStates.Finished) return;
if (_prevEma == 0 || atrVal <= 0) { _prevPrevEma = _prevEma; _prevEma = emaVal; return; }
if (_prevPrevEma == 0) { _prevPrevEma = _prevEma; _prevEma = emaVal; return; }
var close = candle.ClosePrice;
var slope = emaVal - _prevEma;
var prevSlope = _prevEma - _prevPrevEma;
if (Position > 0)
{
if (close >= _entryPrice + atrVal * 2.5m || close <= _entryPrice - atrVal * 1.5m || slope < 0) { SellMarket(); _entryPrice = 0; }
}
else if (Position < 0)
{
if (close <= _entryPrice - atrVal * 2.5m || close >= _entryPrice + atrVal * 1.5m || slope > 0) { BuyMarket(); _entryPrice = 0; }
}
if (Position == 0)
{
if (slope > 0 && prevSlope <= 0) { _entryPrice = close; BuyMarket(); }
else if (slope < 0 && prevSlope >= 0) { _entryPrice = close; SellMarket(); }
}
_prevPrevEma = _prevEma; _prevEma = emaVal;
}
}
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.Strategies import Strategy
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
class speed_ma_strategy(Strategy):
def __init__(self):
super(speed_ma_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(8))) \
.SetDisplay("Candle Type", "Timeframe.", "General")
self._ema_length = self.Param("EmaLength", 13) \
.SetDisplay("EMA Length", "Moving average period.", "Indicators")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period.", "Indicators")
self._prev_ema = 0.0
self._prev_prev_ema = 0.0
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def EmaLength(self):
return self._ema_length.Value
@property
def AtrLength(self):
return self._atr_length.Value
def OnStarted2(self, time):
super(speed_ma_strategy, self).OnStarted2(time)
self._prev_ema = 0.0
self._prev_prev_ema = 0.0
self._entry_price = 0.0
self._ema = ExponentialMovingAverage()
self._ema.Length = self.EmaLength
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._ema, self._atr, self.ProcessCandle).Start()
def ProcessCandle(self, candle, ema_val, atr_val):
if candle.State != CandleStates.Finished:
return
ev = float(ema_val)
av = float(atr_val)
if self._prev_ema == 0 or av <= 0:
self._prev_prev_ema = self._prev_ema
self._prev_ema = ev
return
if self._prev_prev_ema == 0:
self._prev_prev_ema = self._prev_ema
self._prev_ema = ev
return
close = float(candle.ClosePrice)
slope = ev - self._prev_ema
prev_slope = self._prev_ema - self._prev_prev_ema
if self.Position > 0:
if close >= self._entry_price + av * 2.5 or close <= self._entry_price - av * 1.5 or slope < 0:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if close <= self._entry_price - av * 2.5 or close >= self._entry_price + av * 1.5 or slope > 0:
self.BuyMarket()
self._entry_price = 0.0
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_prev_ema = self._prev_ema
self._prev_ema = ev
return
if self.Position == 0:
if slope > 0 and prev_slope <= 0:
self._entry_price = close
self.BuyMarket()
elif slope < 0 and prev_slope >= 0:
self._entry_price = close
self.SellMarket()
self._prev_prev_ema = self._prev_ema
self._prev_ema = ev
def OnReseted(self):
super(speed_ma_strategy, self).OnReseted()
self._prev_ema = 0.0
self._prev_prev_ema = 0.0
self._entry_price = 0.0
def CreateClone(self):
return speed_ma_strategy()