Открыть на GitHub

Стратегия возврата гэпов

Обзор

Gaps — это прямой порт советника MetaTrader 4 gaps.mq4. Стратегия отслеживает 15-минутные свечи и ищет открытия, кот орые происходят за пределами диапазона предыдущей свечи. При появлении такого гэпа система немедленно входит в рынок в расчет е на возврат цены к диапазону.

Версия для StockSharp следует исходной логике и использует высокоуровневый API подписки на свечи. Управление сделками произ водится рыночными ордерами без постановки фиксированных защитных заявок, что соответствует поведению оригинального кода MQL.

Торговые правила

  1. Подписаться на 15-минутные свечи (тип регулируется параметром CandleType).
  2. Сохранить максимум и минимум последней завершённой свечи.
  3. Когда формируется новая свеча:
    • Рассчитать буфер гэпа: (MinGapSize + spreadInSteps) * pointValue.
    • Если цена открытия выше previousHigh + gapBuffer, открыть короткую позицию.
    • Если цена открытия ниже previousLow - gapBuffer, открыть длинную позицию.
  4. На каждой свече допускается только одна сделка. После отправки ордера стратегия ждёт следующую свечу, прежде чем генериров ать новый сигнал.

Компонент спреда использует текущие котировки 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;
        }
    }
}