缺口回补策略
概述
Gaps 策略 是 MetaTrader 4 专家顾问 gaps.mq4 的直接移植版本。系统监控 15 分钟蜡烛图,并寻找在上一根蜡烛高低区间之外开盘的缺口。一旦出现缺口,策略立即建立仓位,期待价格向缺口方向回补。
StockSharp 版本遵循原始逻辑,完全基于高级蜡烛订阅 API 实现。和 MQL 代码一样,所有交易都通过市价单执行,不会自动放置固定的止损或止盈订单。
交易规则
- 订阅 15 分钟蜡烛(可通过
CandleType参数调整)。 - 保存上一根已完成蜡烛的最高价和最低价。
- 当新蜡烛开始时:
- 计算缺口缓冲区:
(MinGapSize + spreadInSteps) * pointValue。 - 如果开盘价 高于
previousHigh + gapBuffer,则开立 空单。 - 如果开盘价 低于
previousLow - gapBuffer,则开立 多单。
- 计算缺口缓冲区:
- 每根蜡烛最多只允许一次交易。下单后必须等待下一根蜡烛才能生成新的信号。
当可用时,策略使用实时买价/卖价计算点差;若没有报价数据,则退回到单个价格步长作为保守的缓冲设置。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
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;
}
}
}
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()