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()