Стратегия Grid Bot Backtesting
Стратегия строит сетку ценовых уровней и покупает, когда цена опускается к ближайшей линии, закрывая позицию на следующей. Границы сетки можно задавать вручную или рассчитывать автоматически по недавним данным.
Детали
- Условия входа:
- Long: цена пересекает линию сетки снизу и на ней нет активной позиции
- Long/Short: Только long
- Условия выхода:
- цена пересекает следующую линию сетки сверху
- Стопы: Нет
- Значения по умолчанию:
AutoBounds= trueBoundSource= "Hi & Low"BoundLookback= 250BoundDeviation= 0.10UpperBound= 0.285LowerBound= 0.225GridLines= 30
- Фильтры:
- Категория: Торговля в диапазоне
- Направление: Long
- Индикаторы: Highest, Lowest, SimpleMovingAverage
- Стопы: Нет
- Сложность: Средняя
- Таймфрейм: Любой
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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()