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.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Grid Bot Strategy.
/// Creates a dynamic grid around a moving average and trades grid crossings.
/// Buys when price crosses below a grid line, sells when price crosses above.
/// </summary>
public class GridBotStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleTypeParam;
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<int> _gridCount;
private readonly StrategyParam<decimal> _gridMultiplier;
private readonly StrategyParam<int> _cooldownBars;
private ExponentialMovingAverage _ma;
private AverageTrueRange _atr;
private decimal _prevClose;
private int _cooldownRemaining;
public GridBotStrategy()
{
_candleTypeParam = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle type", "Candle type for strategy calculation.", "General");
_maLength = Param(nameof(MALength), 50)
.SetGreaterThanZero()
.SetDisplay("MA Length", "Moving average for grid center", "Grid Settings");
_atrLength = Param(nameof(ATRLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period for grid spacing", "Grid Settings");
_gridCount = Param(nameof(GridCount), 3)
.SetDisplay("Grid Count", "Number of grid levels each side", "Grid Settings");
_gridMultiplier = Param(nameof(GridMultiplier), 0.5m)
.SetDisplay("Grid Multiplier", "ATR multiplier for grid spacing", "Grid Settings");
_cooldownBars = Param(nameof(CooldownBars), 20)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk");
}
public DataType CandleType
{
get => _candleTypeParam.Value;
set => _candleTypeParam.Value = value;
}
public int MALength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
public int ATRLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
public int GridCount
{
get => _gridCount.Value;
set => _gridCount.Value = value;
}
public decimal GridMultiplier
{
get => _gridMultiplier.Value;
set => _gridMultiplier.Value = value;
}
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ma = null;
_atr = null;
_prevClose = 0;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ma = new ExponentialMovingAverage { Length = MALength };
_atr = new AverageTrueRange { Length = ATRLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ma, _atr, OnProcess)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ma);
DrawOwnTrades(area);
}
}
private void OnProcess(ICandleMessage candle, decimal maValue, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_ma.IsFormed || !_atr.IsFormed)
{
_prevClose = candle.ClosePrice;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevClose = candle.ClosePrice;
return;
}
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
_prevClose = candle.ClosePrice;
return;
}
if (_prevClose == 0 || atrValue <= 0)
{
_prevClose = candle.ClosePrice;
return;
}
var close = candle.ClosePrice;
var gridSpacing = atrValue * GridMultiplier;
// Check grid crossings
for (var i = 1; i <= GridCount; i++)
{
var lowerGrid = maValue - gridSpacing * i;
var upperGrid = maValue + gridSpacing * i;
// Price crossed below a lower grid line - buy
if (_prevClose > lowerGrid && close <= lowerGrid && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
_cooldownRemaining = CooldownBars;
_prevClose = close;
return;
}
// Price crossed above an upper grid line - sell
if (_prevClose < upperGrid && close >= upperGrid && Position >= 0)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(Volume);
_cooldownRemaining = CooldownBars;
_prevClose = close;
return;
}
}
// Mean reversion exits at MA
if (Position > 0 && _prevClose < maValue && close >= maValue)
{
SellMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
else if (Position < 0 && _prevClose > maValue && close <= maValue)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
_prevClose = close;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class grid_bot_strategy(Strategy):
"""Grid Bot Strategy. Dynamic grid around EMA with ATR spacing."""
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._ma_length = self.Param("MALength", 50) \
.SetDisplay("MA Length", "Moving average for grid center", "Grid Settings")
self._atr_length = self.Param("ATRLength", 14) \
.SetDisplay("ATR Length", "ATR period for grid spacing", "Grid Settings")
self._grid_count = self.Param("GridCount", 3) \
.SetDisplay("Grid Count", "Number of grid levels each side", "Grid Settings")
self._grid_multiplier = self.Param("GridMultiplier", 0.5) \
.SetDisplay("Grid Multiplier", "ATR multiplier for grid spacing", "Grid Settings")
self._cooldown_bars = self.Param("CooldownBars", 20) \
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk")
self._ma = None
self._atr = None
self._prev_close = 0.0
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(grid_bot_strategy, self).OnReseted()
self._ma = None
self._atr = None
self._prev_close = 0.0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(grid_bot_strategy, self).OnStarted2(time)
self._ma = ExponentialMovingAverage()
self._ma.Length = int(self._ma_length.Value)
self._atr = AverageTrueRange()
self._atr.Length = int(self._atr_length.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._ma, self._atr, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ma)
self.DrawOwnTrades(area)
def _on_process(self, candle, ma_val, atr_val):
if candle.State != CandleStates.Finished:
return
if not self._ma.IsFormed or not self._atr.IsFormed:
self._prev_close = float(candle.ClosePrice)
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_close = float(candle.ClosePrice)
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._prev_close = float(candle.ClosePrice)
return
if self._prev_close == 0.0 or float(atr_val) <= 0:
self._prev_close = float(candle.ClosePrice)
return
close = float(candle.ClosePrice)
ma = float(ma_val)
atr = float(atr_val)
grid_mult = float(self._grid_multiplier.Value)
grid_count = int(self._grid_count.Value)
cooldown = int(self._cooldown_bars.Value)
grid_spacing = atr * grid_mult
for i in range(1, grid_count + 1):
lower_grid = ma - grid_spacing * i
upper_grid = ma + grid_spacing * i
if self._prev_close > lower_grid and close <= lower_grid and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.BuyMarket(self.Volume)
self._cooldown_remaining = cooldown
self._prev_close = close
return
if self._prev_close < upper_grid and close >= upper_grid and self.Position >= 0:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.SellMarket(self.Volume)
self._cooldown_remaining = cooldown
self._prev_close = close
return
# Mean reversion exits at MA
if self.Position > 0 and self._prev_close < ma and close >= ma:
self.SellMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
elif self.Position < 0 and self._prev_close > ma and close <= ma:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
self._prev_close = close
def CreateClone(self):
return grid_bot_strategy()