The Gap Reversion Strategy is a direct port of the MetaTrader 4 expert advisor gaps.mq4. The system monitors 15-minute candles and loo
ks for opening gaps that occur outside the previous candle's high/low range. When such a gap appears, the strategy immediately e
nters the market in the direction of the expected mean reversion move.
The StockSharp version follows the original logic while relying on the high-level candle subscription API. All trade management is
done with market orders and no fixed protective orders are placed, mirroring the behaviour found in the MQL code.
Trading Rules
Subscribe to 15-minute candles (configurable through the CandleType parameter).
Keep the previous completed candle's high and low.
When a new candle starts:
Calculate the gap buffer: (MinGapSize + spreadInSteps) * pointValue.
If the open price is abovepreviousHigh + gapBuffer, open a short position.
If the open price is belowpreviousLow - gapBuffer, open a long position.
Only one trade per candle is allowed. Once an order is placed, the strategy waits for the next candle before generating a new s
ignal.
The spread component uses the current best bid/ask if available. When no quote data is provided, the strategy falls back to a sin
gle price step as a conservative buffer.
Parameters
Parameter
Default
Description
MinGapSize
1
Minimum gap size in price steps that must be exceeded before sending an order.
GapVolume
0.1
Order volume for market entries triggered by gaps.
CandleType
15m TimeFrame
Candle type used for calculations (defaults to 15-minute candles).
All parameters are registered as StrategyParam<T> and support optimisation inside StockSharp Designer or other tools.
Implementation Notes
Uses SubscribeCandles with Bind to process the first observable candle update and to store the final high/low of completed candles.
Remembers the previous candle's range to avoid recalculating data series.
Blocks duplicate orders on the same candle by tracking the open time of the bar that triggered the trade.
Chart output draws the subscribed candles and the strategy trades for quick visual inspection.
Differences from the MQL Version
Take-profit and stop-loss levels were not set correctly in the original EA (the MQL code passed values to the wrong parameters)
. The StockSharp port therefore keeps the behaviour of running without protective orders.
Spread handling now checks real-time bid/ask quotes when available, providing a more adaptive buffer.
Requirements
StockSharp API with access to candle data for the selected instrument.
Level1 quotes are optional but improve spread detection.
Replay timing: The gap check runs on the first observed update of each candle (the Active update, when supplied). When the data source only emits Finished candles, the opening gap is detected at the candle close; this does not verify an entry at the candle-open price.
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()