Gaps — это прямой порт советника MetaTrader 4 gaps.mq4. Стратегия отслеживает 15-минутные свечи и ищет открытия, кот
орые происходят за пределами диапазона предыдущей свечи. При появлении такого гэпа система немедленно входит в рынок в расчет
е на возврат цены к диапазону.
Версия для StockSharp следует исходной логике и использует высокоуровневый API подписки на свечи. Управление сделками произ
водится рыночными ордерами без постановки фиксированных защитных заявок, что соответствует поведению оригинального кода MQL.
Торговые правила
Подписаться на 15-минутные свечи (тип регулируется параметром CandleType).
Сохранить максимум и минимум последней завершённой свечи.
Если цена открытия вышеpreviousHigh + gapBuffer, открыть короткую позицию.
Если цена открытия нижеpreviousLow - gapBuffer, открыть длинную позицию.
На каждой свече допускается только одна сделка. После отправки ордера стратегия ждёт следующую свечу, прежде чем генериров
ать новый сигнал.
Компонент спреда использует текущие котировки bid/ask, когда они доступны. При отсутствии данных стратегия применяет один ша
г цены в качестве консервативного буфера.
Параметры
Параметр
Значение по умолчанию
Описание
MinGapSize
1
Минимальный размер гэпа в шагах цены, необходимый для открытия позиции.
GapVolume
0.1
Объём рыночного ордера, отправляемого при срабатывании сигнала.
CandleType
15m TimeFrame
Тип свечей, используемых для расчётов (по умолчанию 15 минут).
Все параметры зарегистрированы через StrategyParam<T> и поддерживают оптимизацию в StockSharp Designer и других инструментах.
Особенности реализации
Используется SubscribeCandles с Bind, чтобы обрабатывать только завершённые свечи.
Диапазон предыдущей свечи сохраняется во внутренних переменных, что исключает необходимость дополнительных коллекций.
За счёт хранения времени открытия свечи, вызвавшей сделку, предотвращаются повторные ордера в рамках того же бара.
На график выводятся свечи и сделки стратегии для оперативной визуальной проверки.
Отличия от версии MQL
В оригинальном советнике значения для стоп-лосса и тейк-профита передавались в неверные параметры, поэтому сделки выполнялись
без защитных заявок. Перенос сохранён в таком же виде и не добавляет стопов.
Оценка спреда теперь использует актуальные котировки bid/ask и только при их отсутствии возвращается к минимальному шагу цены.
Требования
Доступ к свечным данным выбранного инструмента в среде StockSharp.
Поток котировок Level1 не обязателен, но повышает точность вычисления буфера по спреду.
Время сигнала при воспроизведении: гэп проверяется при первом полученном обновлении свечи (в состоянии Active, если оно доступно). Если источник выдаёт только Finished, гэп открытия выявляется лишь на закрытии свечи; это не подтверждает исполнение по цене открытия.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using StockSharp.Algo;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Enters against gaps between a new candle's open and the previous completed range.
/// No EMA condition or automatic protection is part of the original gap rules.
/// </summary>
public class GapReversionStrategy : Strategy
{
private readonly StrategyParam<decimal> _minGapSize;
private readonly StrategyParam<decimal> _gapVolume;
private readonly StrategyParam<DataType> _candleType;
private decimal? _previousHigh;
private decimal? _previousLow;
private DateTimeOffset? _observedBar;
private decimal? _bid;
private decimal? _ask;
public decimal MinGapSize { get => _minGapSize.Value; set => _minGapSize.Value = value; }
public decimal GapVolume { get => _gapVolume.Value; set => _gapVolume.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public GapReversionStrategy()
{
_minGapSize = Param(nameof(MinGapSize), 1m)
.SetGreaterThanZero()
.SetDisplay("Minimum Gap", "Minimum open gap in price steps", "Signals");
_gapVolume = Param(nameof(GapVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Gap Volume", "Volume of each market gap entry", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_previousHigh = null;
_previousLow = null;
_observedBar = null;
_bid = null;
_ask = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearState();
Volume = GapVolume;
// Quote subscriptions are optional: one price step is used as spread
// if the replay or provider does not expose both best prices.
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ProcessQuote).Start();
}
var candles = SubscribeCandles(CandleType);
candles.Bind(ProcessCandle).Start();
}
private void ProcessQuote(Level1ChangeMessage quote)
{
if (quote.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid)
_bid = bid;
if (quote.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask)
_ask = ask;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State is not (CandleStates.Active or CandleStates.Finished))
return;
// Process a bar just once, at its first update (Active if available).
// Finished-only feeds can only evaluate the opening gap retrospectively.
if (_observedBar != candle.OpenTime)
{
_observedBar = candle.OpenTime;
if (_previousHigh is decimal high && _previousLow is decimal low)
{
var step = Security?.PriceStep ?? 0m;
if (step > 0m)
{
var spread = _bid is decimal bid && _ask is decimal ask && ask > bid
? ask - bid
: step;
var buffer = MinGapSize * step + spread;
if (candle.OpenPrice > high + buffer)
SellMarket(GapVolume);
else if (candle.OpenPrice < low - buffer)
BuyMarket(GapVolume);
}
}
}
if (candle.State == CandleStates.Finished)
{
_previousHigh = candle.HighPrice;
_previousLow = candle.LowPrice;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Strategies import Strategy
class gap_reversion_strategy(Strategy):
"""Trades against opening gaps beyond the previous candle's high/low and spread."""
def __init__(self):
super(gap_reversion_strategy, self).__init__()
self._min_gap_size = self.Param("MinGapSize", 1.0).SetGreaterThanZero().SetDisplay(
"Minimum Gap", "Minimum open gap in price steps", "Signals")
self._gap_volume = self.Param("GapVolume", 0.1).SetGreaterThanZero().SetDisplay(
"Gap Volume", "Volume of each market gap entry", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay(
"Candle Type", "Candle timeframe", "General")
self._clear_state()
@property
def min_gap_size(self):
return self._min_gap_size.Value
@property
def gap_volume(self):
return self._gap_volume.Value
@property
def candle_type(self):
return self._candle_type.Value
def _clear_state(self):
self._previous_high = None
self._previous_low = None
self._observed_bar = None
self._best_bid = None
self._best_ask = None
def OnReseted(self):
super(gap_reversion_strategy, self).OnReseted()
self._clear_state()
def OnStarted2(self, time):
super(gap_reversion_strategy, self).OnStarted2(time)
self._clear_state()
self.Volume = Decimal(self.gap_volume)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._process_quote).Start()
self.SubscribeCandles(self.candle_type).Bind(self._process_candle).Start()
def _process_quote(self, quote):
bid = quote.Changes[Level1Fields.BestBidPrice] if quote.Changes.ContainsKey(Level1Fields.BestBidPrice) else None
ask = quote.Changes[Level1Fields.BestAskPrice] if quote.Changes.ContainsKey(Level1Fields.BestAskPrice) else None
if bid is not None:
self._best_bid = float(bid)
if ask is not None:
self._best_ask = float(ask)
def _process_candle(self, candle):
if candle.State != CandleStates.Active and candle.State != CandleStates.Finished:
return
# Active updates allow execution at the open. Finished-only feeds necessarily
# discover the opening gap at close; this is not equivalent to open execution.
if self._observed_bar != candle.OpenTime:
self._observed_bar = candle.OpenTime
if self._previous_high is not None and self._previous_low is not None:
step = float(self.Security.PriceStep)
if step > 0.0:
spread = (self._best_ask - self._best_bid
if self._best_ask is not None and self._best_bid is not None
and self._best_ask > self._best_bid else step)
buffer = float(self.min_gap_size) * step + spread
op = float(candle.OpenPrice)
if op > self._previous_high + buffer:
self.SellMarket(Decimal(self.gap_volume))
elif op < self._previous_low - buffer:
self.BuyMarket(Decimal(self.gap_volume))
if candle.State == CandleStates.Finished:
self._previous_high = float(candle.HighPrice)
self._previous_low = float(candle.LowPrice)
def CreateClone(self):
return gap_reversion_strategy()