Grid Bot Backtesting Strategy
Implements a grid trading bot that accumulates long positions when price falls to grid levels and closes them when price moves to the next line. Bounds may be set manually or calculated from recent data.
Details
- Entry Criteria:
- Long: price crosses below a grid line with no active order
- Long/Short: Long only
- Exit Criteria:
- price crosses above the next grid line
- Stops: None
- Default Values:
AutoBounds= trueBoundSource= "Hi & Low"BoundLookback= 250BoundDeviation= 0.10UpperBound= 0.285LowerBound= 0.225GridLines= 30
- Filters:
- Category: Range trading
- Direction: Long
- Indicators: Highest, Lowest, SimpleMovingAverage
- Stops: No
- Complexity: Medium
- Timeframe: Any
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Grid bot backtesting strategy.
/// GridLines lines are spread evenly from the lower to the upper bound. With AutoBounds the bounds are the BoundLookback highest high
/// and lowest low ("Hi & Low") or the BoundLookback SMA of the close ("Average"), widened by BoundDeviation; otherwise UpperBound and
/// LowerBound are used. A close crossing below a line that holds no order buys one Volume for that line, and a close crossing above the
/// next line up sells it again. Only longs are opened.
/// </summary>
public class GridBotBacktestingStrategy : Strategy
{
private const string _hiLowSource = "Hi & Low";
private readonly StrategyParam<bool> _autoBounds;
private readonly StrategyParam<string> _boundSource;
private readonly StrategyParam<int> _boundLookback;
private readonly StrategyParam<decimal> _boundDeviation;
private readonly StrategyParam<decimal> _upperBound;
private readonly StrategyParam<decimal> _lowerBound;
private readonly StrategyParam<int> _gridLines;
private readonly StrategyParam<DataType> _candleType;
private bool[] _filled = [];
private decimal? _prevClose;
/// <summary>
/// Calculate the bounds from recent data.
/// </summary>
public bool AutoBounds
{
get => _autoBounds.Value;
set => _autoBounds.Value = value;
}
/// <summary>
/// Source of the automatic bounds: "Hi & Low" or "Average".
/// </summary>
public string BoundSource
{
get => _boundSource.Value;
set => _boundSource.Value = value;
}
/// <summary>
/// Candles used for the automatic bounds.
/// </summary>
public int BoundLookback
{
get => _boundLookback.Value;
set => _boundLookback.Value = value;
}
/// <summary>
/// Fraction the automatic bounds are widened by.
/// </summary>
public decimal BoundDeviation
{
get => _boundDeviation.Value;
set => _boundDeviation.Value = value;
}
/// <summary>
/// Manual upper bound.
/// </summary>
public decimal UpperBound
{
get => _upperBound.Value;
set => _upperBound.Value = value;
}
/// <summary>
/// Manual lower bound.
/// </summary>
public decimal LowerBound
{
get => _lowerBound.Value;
set => _lowerBound.Value = value;
}
/// <summary>
/// Number of grid lines.
/// </summary>
public int GridLines
{
get => _gridLines.Value;
set => _gridLines.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public GridBotBacktestingStrategy()
{
_autoBounds = Param(nameof(AutoBounds), true)
.SetDisplay("Auto Bounds", "Calculate the bounds from recent data", "Grid");
_boundSource = Param(nameof(BoundSource), _hiLowSource)
.SetDisplay("Bound Source", "Source of the automatic bounds: Hi & Low or Average", "Grid");
_boundLookback = Param(nameof(BoundLookback), 250)
.SetGreaterThanZero()
.SetDisplay("Bound Lookback", "Candles used for the automatic bounds", "Grid");
_boundDeviation = Param(nameof(BoundDeviation), 0.10m)
.SetNotNegative()
.SetDisplay("Bound Deviation", "Fraction the automatic bounds are widened by", "Grid");
_upperBound = Param(nameof(UpperBound), 0.285m)
.SetGreaterThanZero()
.SetDisplay("Upper Bound", "Manual upper bound", "Grid");
_lowerBound = Param(nameof(LowerBound), 0.225m)
.SetGreaterThanZero()
.SetDisplay("Lower Bound", "Manual lower bound", "Grid");
_gridLines = Param(nameof(GridLines), 30)
.SetRange(2, 1000)
.SetDisplay("Grid Lines", "Number of grid lines", "Grid");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_filled = [];
_prevClose = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_filled = new bool[GridLines];
_prevClose = null;
var highest = new Highest { Length = BoundLookback };
var lowest = new Lowest { Length = BoundLookback };
var average = new SimpleMovingAverage { Length = BoundLookback };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(highest, lowest, average, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal highest, decimal lowest, decimal average)
{
if (candle.State != CandleStates.Finished)
return;
var prevClose = _prevClose;
_prevClose = candle.ClosePrice;
if (!IsFormedAndOnlineAndAllowTrading() || prevClose is not decimal pc)
return;
decimal upper, lower;
if (AutoBounds)
{
var hiLow = BoundSource == _hiLowSource;
upper = (hiLow ? highest : average) * (1 + BoundDeviation);
lower = (hiLow ? lowest : average) * (1 - BoundDeviation);
}
else
{
upper = UpperBound;
lower = LowerBound;
}
if (upper <= lower)
return;
var close = candle.ClosePrice;
var count = _filled.Length;
var step = (upper - lower) / (count - 1);
for (var i = 0; i < count; i++)
{
var line = lower + step * i;
if (!_filled[i] && pc > line && close <= line)
{
BuyMarket(Volume);
_filled[i] = true;
}
else if (_filled[i] && i + 1 < count)
{
var next = line + step;
if (pc < next && close >= next)
{
SellMarket(Volume);
_filled[i] = false;
}
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import Highest, Lowest, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class grid_bot_backtesting_strategy(Strategy):
"""
Grid bot backtesting strategy.
GridLines lines are spread evenly from the lower to the upper bound. With AutoBounds the bounds are the BoundLookback highest high
and lowest low ("Hi & Low") or the BoundLookback SMA of the close ("Average"), widened by BoundDeviation; otherwise UpperBound and
LowerBound are used. A close crossing below a line that holds no order buys one Volume for that line, and a close crossing above the
next line up sells it again. Only longs are opened.
"""
HI_LOW_SOURCE = "Hi & Low"
def __init__(self):
super(grid_bot_backtesting_strategy, self).__init__()
self._auto_bounds = self.Param("AutoBounds", True).SetDisplay("Auto Bounds", "Calculate the bounds from recent data", "Grid")
self._bound_source = self.Param("BoundSource", self.HI_LOW_SOURCE).SetDisplay("Bound Source", "Source of the automatic bounds: Hi & Low or Average", "Grid")
self._bound_lookback = self.Param("BoundLookback", 250).SetGreaterThanZero().SetDisplay("Bound Lookback", "Candles used for the automatic bounds", "Grid")
self._bound_deviation = self.Param("BoundDeviation", 0.10).SetNotNegative().SetDisplay("Bound Deviation", "Fraction the automatic bounds are widened by", "Grid")
self._upper_bound = self.Param("UpperBound", 0.285).SetGreaterThanZero().SetDisplay("Upper Bound", "Manual upper bound", "Grid")
self._lower_bound = self.Param("LowerBound", 0.225).SetGreaterThanZero().SetDisplay("Lower Bound", "Manual lower bound", "Grid")
self._grid_lines = self.Param("GridLines", 30).SetRange(2, 1000).SetDisplay("Grid Lines", "Number of grid lines", "Grid")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._filled = []
self._prev_close = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(grid_bot_backtesting_strategy, self).OnReseted()
self._filled = []
self._prev_close = None
def OnStarted2(self, time):
super(grid_bot_backtesting_strategy, self).OnStarted2(time)
self._filled = [False] * self._grid_lines.Value
self._prev_close = None
highest = Highest()
highest.Length = self._bound_lookback.Value
lowest = Lowest()
lowest.Length = self._bound_lookback.Value
average = SimpleMovingAverage()
average.Length = self._bound_lookback.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(highest, lowest, average, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, highest, lowest, average):
if candle.State != CandleStates.Finished:
return
prev_close = self._prev_close
self._prev_close = candle.ClosePrice
if not self.IsFormedAndOnlineAndAllowTrading() or prev_close is None:
return
if self._auto_bounds.Value:
deviation = Decimal(self._bound_deviation.Value)
hi_low = self._bound_source.Value == self.HI_LOW_SOURCE
upper = (highest if hi_low else average) * (Decimal(1) + deviation)
lower = (lowest if hi_low else average) * (Decimal(1) - deviation)
else:
upper = Decimal(self._upper_bound.Value)
lower = Decimal(self._lower_bound.Value)
if upper <= lower:
return
close = candle.ClosePrice
count = len(self._filled)
step = (upper - lower) / Decimal(count - 1)
for i in range(count):
line = lower + step * Decimal(i)
if not self._filled[i] and prev_close > line and close <= line:
self.BuyMarket(self.Volume)
self._filled[i] = True
elif self._filled[i] and i + 1 < count:
next_line = line + step
if prev_close < next_line and close >= next_line:
self.SellMarket(self.Volume)
self._filled[i] = False
def CreateClone(self):
return grid_bot_backtesting_strategy()