Grid Bot Strategy
The grid bot divides a predefined price range into equal levels and trades the oscillations between them. When price drifts toward the lower half of the grid the strategy accumulates long positions, selling them as price returns to the upper half. This approach thrives in sideways markets with clear bounds.
No directional bias is assumed; the bot simply reacts to proximity to grid lines.
Details
- Entry Criteria:
- Long: price touches a level in the lower half while no long position
- Short: price touches a level in the upper half while no short position
- Long/Short: Both sides
- Exit Criteria:
- Opposite entry signal closes existing position
- Stops: None
- Default Values:
UpperLimit= 48000LowerLimit= 45000GridCount= 10
- Filters:
- Category: Range trading
- Direction: Both
- Indicators: Price levels
- Stops: No
- Complexity: Low
- Timeframe: Short-term
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Grid Bot Strategy.
/// Splits the predefined price range between <see cref="LowerLimit"/> and <see cref="UpperLimit"/>
/// into <see cref="GridCount"/> equal levels and trades the oscillations between them.
/// Touching a level in the lower half opens a long, touching a level in the upper half opens a short,
/// and every signal closes the opposite position first.
/// </summary>
public class GridBotStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleTypeParam;
private readonly StrategyParam<decimal> _upperLimit;
private readonly StrategyParam<decimal> _lowerLimit;
private readonly StrategyParam<int> _gridCount;
// Grid line the previous candle closed on, -1 before the first one is evaluated.
private int _prevLevel;
/// <summary>
/// Initializes a new instance of the <see cref="GridBotStrategy"/>.
/// </summary>
public GridBotStrategy()
{
_candleTypeParam = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle type", "Candle type for strategy calculation.", "General");
_upperLimit = Param(nameof(UpperLimit), 48000m)
.SetGreaterThanZero()
.SetDisplay("Upper Limit", "Top price of the grid range", "Grid Settings");
_lowerLimit = Param(nameof(LowerLimit), 45000m)
.SetGreaterThanZero()
.SetDisplay("Lower Limit", "Bottom price of the grid range", "Grid Settings");
_gridCount = Param(nameof(GridCount), 10)
.SetGreaterThanZero()
.SetDisplay("Grid Count", "Number of equal levels the range is split into", "Grid Settings");
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleTypeParam.Value;
set => _candleTypeParam.Value = value;
}
/// <summary>
/// Top price of the grid range.
/// </summary>
public decimal UpperLimit
{
get => _upperLimit.Value;
set => _upperLimit.Value = value;
}
/// <summary>
/// Bottom price of the grid range.
/// </summary>
public decimal LowerLimit
{
get => _lowerLimit.Value;
set => _lowerLimit.Value = value;
}
/// <summary>
/// Number of equal levels the range is split into.
/// </summary>
public int GridCount
{
get => _gridCount.Value;
set => _gridCount.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevLevel = -1;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (UpperLimit <= LowerLimit)
throw new InvalidOperationException($"{nameof(UpperLimit)} must be above {nameof(LowerLimit)}.");
_prevLevel = -1;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var level = GetLevel(candle.ClosePrice);
// A touch is the move onto another line; standing on the same one is not a new signal.
if (level == _prevLevel)
return;
_prevLevel = level;
// The middle line splits the range into halves and carries no bias of its own.
var middle = GridCount / 2m;
if (level < middle && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (level > middle && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
}
// Index of the grid line the price sits on, counted from LowerLimit up to GridCount.
private int GetLevel(decimal price)
{
var step = (UpperLimit - LowerLimit) / GridCount;
// A price outside the predefined range belongs to the outermost line of the grid.
var clamped = Math.Clamp(price, LowerLimit, UpperLimit);
return (int)Math.Floor((clamped - LowerLimit) / step + 0.5m);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class grid_bot_strategy(Strategy):
"""Grid Bot Strategy. Fixed grid of equal levels between LowerLimit and UpperLimit."""
def __init__(self):
super(grid_bot_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle type", "Candle type for strategy calculation.", "General")
self._upper_limit = self.Param("UpperLimit", 48000.0) \
.SetDisplay("Upper Limit", "Top price of the grid range", "Grid Settings")
self._lower_limit = self.Param("LowerLimit", 45000.0) \
.SetDisplay("Lower Limit", "Bottom price of the grid range", "Grid Settings")
self._grid_count = self.Param("GridCount", 10) \
.SetDisplay("Grid Count", "Number of equal levels the range is split into", "Grid Settings")
# Grid line the previous candle closed on, -1 before the first one is evaluated.
self._prev_level = -1
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(grid_bot_strategy, self).OnReseted()
self._prev_level = -1
def OnStarted2(self, time):
super(grid_bot_strategy, self).OnStarted2(time)
if float(self._upper_limit.Value) <= float(self._lower_limit.Value):
raise Exception("UpperLimit must be above LowerLimit.")
self._prev_level = -1
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
level = self._get_level(float(candle.ClosePrice))
# A touch is the move onto another line; standing on the same one is not a new signal.
if level == self._prev_level:
return
self._prev_level = level
# The middle line splits the range into halves and carries no bias of its own.
middle = int(self._grid_count.Value) / 2.0
if level < middle and self.Position <= 0:
self.BuyMarket(self.Volume + Math.Abs(self.Position))
elif level > middle and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
def _get_level(self, price):
"""Index of the grid line the price sits on, counted from LowerLimit up to GridCount."""
upper = float(self._upper_limit.Value)
lower = float(self._lower_limit.Value)
count = int(self._grid_count.Value)
step = (upper - lower) / count
# A price outside the predefined range belongs to the outermost line of the grid.
clamped = min(max(price, lower), upper)
return int(Math.Floor((clamped - lower) / step + 0.5))
def CreateClone(self):
return grid_bot_strategy()