Gap DM — это контртрендовая стратегия торговли гэпов. Она сравнивает цену закрытия предыдущей свечи с ценой открытия текущей и, если фиксирует значимый разрыв, немедленно открывает позицию в противоположную сторону, рассчитывая на закрытие гэпа. Реализация основана на оригинальном советнике MetaTrader 5 «Gap DM» от cmillion и адаптирована под высокоуровневый API StockSharp. Все сигналы строятся на завершённых свечах выбранного таймфрейма, что гарантирует воспроизводимость в тестах и на реальных данных.
Логика сигналов
Подписаться на серию свечей, заданную параметром CandleType.
Обрабатывать только завершённые свечи (CandleStates.Finished).
Сохранить закрытие предыдущей свечи и сравнить его с ценой открытия текущей.
Перевести разницу в пункты (pips) через PriceStep. Для инструментов с трёх- и пятизнаковыми котировками автоматически используется коэффициент 10, как в MT5.
Если текущее открытие ниже предыдущего закрытия минимум на Minimum Gap (pips), фиксируется отрицательный гэп и открывается длинная позиция.
Если текущее открытие выше предыдущего закрытия минимум на Minimum Gap (pips), фиксируется положительный гэп и открывается короткая позиция.
Если стратегия не готова торговать (нет подключения, идёт прогрев), новые сделки не открываются.
Объём и ограничения
Order Volume задаёт объём заявки в лотах. При реверсе стратегия автоматически добавляет объём, чтобы сначала закрыть встречную позицию и лишь затем открыть новую — это соответствует неттинговой модели StockSharp.
Max Positions ограничивает суммарную позицию в одну сторону. При достижении лимита новые сигналы игнорируются.
При смене направления (лонг ↔ шорт) дополнительный объём учитывает текущее нетто-плечо, чтобы итоговая позиция не превышала допустимый максимум.
Управление рисками
Stop Loss (pips) задаёт защитный стоп относительно цены входа. Каждый закрытый бар проверяет, прошла ли цена через уровень стопа, и при необходимости закрывает позицию рыночной заявкой.
Take Profit (pips) симметрично стопу фиксирует прибыль на заранее заданном расстоянии. Значение 0 отключает цель.
Трейлинг-стоп в данной реализации отсутствует, что соответствует логике исходного советника.
Параметры
Параметр
Описание
Значение по умолчанию
Order Volume
Объём каждой сделки в лотах.
1
Stop Loss (pips)
Размер защитного стопа в пунктах. 0 — без стопа.
0
Take Profit (pips)
Размер целевого профита в пунктах. 0 — без цели.
0
Minimum Gap (pips)
Минимальная величина гэпа для генерации сигнала.
1
Max Positions
Максимально допустимая суммарная позиция в одну сторону (в лотах).
15
Candle Type
Тип свечей, по которым измеряется гэп.
1 час
Последовательность работы
При запуске сбросить внутреннее состояние: размеры гэпов, стопов, прошлое закрытие.
Подписаться на свечи, подготовить отображение свечей и собственных сделок на графике (если доступна область).
Для каждой завершённой свечи:
Обновить активные уровни стопа и тейк-профита в соответствии с текущей позицией.
Проверить условия появления гэпа и при необходимости отправить рыночную заявку.
Повторно проверить защитные уровни, чтобы срабатывания внутри текущей свечи исполнялись без задержки.
Сохранить цену закрытия для анализа следующей свечи.
Отличия от версии MT5
В StockSharp используется неттинговый учёт, поэтому стратегия масштабирует суммарную позицию, а не создаёт отдельные заявки для каждого входа. Алгоритм поведения при этом остаётся максимально близким к оригиналу.
Все комментарии в коде написаны на английском языке согласно требованиям репозитория.
Режим управления риском через процент депозита (risk) не реализован. Вместо него используется фиксированный объём Order Volume.
Требования
Инструмент должен предоставлять валидный PriceStep.
Стратегия подходит для любых свечей (временных, объёмных, диапазонных), если понятие гэпа имеет смысл для выбранного рынка.
Необходима среда StockSharp с поддержкой рыночных заявок и отслеживания собственных сделок.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
public class GapDMStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
private int _cooldown;
public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public GapDMStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
protected override void OnReseted()
{
base.OnReseted();
_fast = null; _slow = null;
_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished) return;
if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }
_prevFast = fastValue; _prevSlow = slowValue;
}
}
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 gap_dm_strategy(Strategy):
def __init__(self):
super(gap_dm_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 14) \
.SetDisplay("Fast Period", "Fast MA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 50) \
.SetDisplay("Slow Period", "Slow MA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit in price steps", "Risk")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(gap_dm_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(gap_dm_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._slow.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 100
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return gap_dm_strategy()