在 GitHub 上查看

缺口回补策略

概述

Gaps 策略 是 MetaTrader 4 专家顾问 gaps.mq4 的直接移植版本。系统监控 15 分钟蜡烛图,并寻找在上一根蜡烛高低区间之外开盘的缺口。一旦出现缺口,策略立即建立仓位,期待价格向缺口方向回补。

StockSharp 版本遵循原始逻辑,完全基于高级蜡烛订阅 API 实现。和 MQL 代码一样,所有交易都通过市价单执行,不会自动放置固定的止损或止盈订单。

交易规则

  1. 订阅 15 分钟蜡烛(可通过 CandleType 参数调整)。
  2. 保存上一根已完成蜡烛的最高价和最低价。
  3. 当新蜡烛开始时:
    • 计算缺口缓冲区:(MinGapSize + spreadInSteps) * pointValue。
    • 如果开盘价 高于 previousHigh + gapBuffer,则开立 空单。
    • 如果开盘价 低于 previousLow - gapBuffer,则开立 多单。
  4. 每根蜡烛最多只允许一次交易。下单后必须等待下一根蜡烛才能生成新的信号。

当可用时,策略使用实时买价/卖价计算点差;若没有报价数据,则退回到单个价格步长作为保守的缓冲设置。

参数

参数 默认值 说明
MinGapSize 1 触发交易所需的最小缺口大小(以价格步长表示)。
GapVolume 0.1 缺口信号触发时下单的交易量。
CandleType 15m TimeFrame 用于计算的蜡烛类型(默认 15 分钟)。

所有参数都通过 StrategyParam<T> 注册,可在 StockSharp Designer 等工具中进行优化。

实现细节

  • 使用 SubscribeCandles + Bind 组合,仅处理已完成的蜡烛。
  • 持续保存上一根蜡烛的价格范围,避免在内存中维护额外数据序列。
  • 通过记录触发交易的蜡烛开盘时间,阻止同一根蜡烛出现重复下单。
  • 图表输出会绘制订阅的蜡烛以及策略交易,方便快速可视化检查。

与 MQL 版本的差异

  • 原始 EA 中的止盈/止损参数传递位置错误,因此实际运行时相当于没有保护单。移植版保持这一行为,不会自动设置保护订单。
  • 点差处理逻辑改为优先使用实时买卖报价,在无法获取报价时再退回到最小价格步长。

使用要求

  • 需要具备 StockSharp API 以及对应品种的蜡烛数据。
  • Level1 报价不是必须项,但会提升点差估计的准确度。

回放时机: 首次收到新K线更新时检查跳空(数据源提供时为 Active)。如果数据源只发送 Finished K线,开盘跳空只能在收盘后发现;这不能证明交易按开盘价执行。

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;
        }
    }
}