Стратегия разворота по полосам Боллинджера
Ценовые экстремумы за пределами полос Боллинджера часто возвращаются к средней линии. Эта стратегия торгует против таких расширений: покупает провалы ниже нижней полосы при закрытии зелёной свечи и продаёт росты выше верхней полосы после красной свечи.
Тестирование показывает среднегодичную доходность около 94%. Стратегию лучше запускать на фондовом рынке.
Алгоритм вычисляет полосы Боллинджера на каждом баре и проверяет, пробило ли закрытие внешнюю полосу. Если бычья свеча закрывается ниже нижней полосы, открывается лонг; если медвежья свеча закрывается выше верхней полосы, открывается шорт. Стоп основан на множителе ATR, а выход происходит при возврате цены к средней полосе.
Сделки на возврат к среднему обычно длятся всего несколько баров, что делает стратегию подходящей для краткосрочных сокращений волатильности.
Детали
- Условия входа: Закрытие ниже нижней полосы с бычьей свечой или закрытие выше верхней полосы с медвежьей свечой.
- Направление: Лонг и шорт.
- Условия выхода: Пересечение средней полосы ценой или стоп‑лосс.
- Стопы: Да, на основе ATR.
- Значения по умолчанию:
BollingerPeriod= 20BollingerDeviation= 2.0AtrMultiplier= 2.0CandleType= 5 минут
- Фильтры:
- Категория: Среднее восстановление
- Направление: Оба
- Индикаторы: Полосы Боллинджера, ATR
- Стопы: Да
- Сложность: Базовая
- Таймфрейм: Внутридневной
- Сезонность: Нет
- Нейронные сети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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>
/// Fades an outside Bollinger close only when the candle turns against the extension.
/// Exits at the middle band or native actual-fill protection with signal-bar ATR distance.
/// </summary>
public class BollingerBandReversalStrategy : Strategy
{
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private Order _pendingOrder;
private Unit _stopDistance;
private bool _protectionStarted;
public int BollingerPeriod { get => _bollingerPeriod.Value; set => _bollingerPeriod.Value = value; }
public decimal BollingerDeviation { get => _bollingerDeviation.Value; set => _bollingerDeviation.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public BollingerBandReversalStrategy()
{
_bollingerPeriod = Param(nameof(BollingerPeriod), 20).SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Close SMA and population deviation length", "Indicators");
_bollingerDeviation = Param(nameof(BollingerDeviation), 2m).SetNotNegative()
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier", "Indicators");
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Wilder ATR length", "Protection");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Multiplier", "Frozen signal ATR distance; zero disables the stop", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Bollinger and ATR candle timeframe", "General");
OrderRegistering += order => _pendingOrder = order;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
_pendingOrder = null;
_stopDistance = null;
_protectionStarted = false;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pendingOrder = null;
_stopDistance = null;
_protectionStarted = false;
var bands = new BollingerBands { Length = BollingerPeriod, Width = BollingerDeviation };
var atr = new AverageTrueRange { Length = AtrPeriod };
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var candles = SubscribeCandles(CandleType);
candles.BindEx(bands, atr, ProcessCandle, false).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candles);
DrawIndicator(area, bands);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection evaluates executable quotes between finished signal candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bandsValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished || !bandsValue.IsFormed ||
!atrValue.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
var bands = (IBollingerBandsValue)bandsValue;
if (bands.UpBand is not decimal upper || bands.LowBand is not decimal lower ||
bands.MovingAverage is not decimal middle)
return;
var close = candle.ClosePrice;
if (Position > 0m && close >= middle)
SellMarket(Position);
else if (Position < 0m && close <= middle)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && close < lower && close > candle.OpenPrice)
Enter(Sides.Buy, atrValue.GetValue<decimal>());
else if (Position == 0m && close > upper && close < candle.OpenPrice)
Enter(Sides.Sell, atrValue.GetValue<decimal>());
}
private void Enter(Sides side, decimal atr)
{
var distance = atr * AtrMultiplier;
_stopDistance ??= new Unit(distance);
// Cached native controllers retain this Unit across flat-to-position cycles.
_stopDistance.Value = distance;
if (!_protectionStarted && distance > 0m)
{
StartProtection(new Unit(), _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = Volume,
Comment = "Bollinger reversal entry",
});
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, OrderStates, OrderTypes, Sides, Level1Fields
from StockSharp.BusinessEntities import Subscription, Order
from StockSharp.Algo.Indicators import BollingerBands, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class bollinger_band_reversal_strategy(Strategy):
"""Fade an outside Bollinger close with an opposing candle and frozen ATR protection."""
def __init__(self):
super(bollinger_band_reversal_strategy, self).__init__()
self._bollinger_period = self.Param("BollingerPeriod", 20).SetGreaterThanZero().SetDisplay("Bollinger Period", "Close SMA and population deviation length", "Indicators")
self._bollinger_deviation = self.Param("BollingerDeviation", 2.0).SetNotNegative().SetDisplay("Bollinger Deviation", "Standard deviation multiplier", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Wilder ATR length", "Protection")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative().SetDisplay("ATR Multiplier", "Frozen signal ATR distance; zero disables the stop", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Bollinger and ATR candle timeframe", "General")
self._pending_order = None
self._stop_distance = None
self._protection_started = False
self.OrderRegistering += self._track_pending
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def OnReseted(self):
super(bollinger_band_reversal_strategy, self).OnReseted()
self._pending_order = None
self._stop_distance = None
self._protection_started = False
def OnStarted2(self, time):
super(bollinger_band_reversal_strategy, self).OnStarted2(time)
self._pending_order = None
self._stop_distance = None
self._protection_started = False
bands = BollingerBands()
bands.Length = self._bollinger_period.Value
bands.Width = Decimal(self._bollinger_deviation.Value)
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
candles = self.SubscribeCandles(self.candle_type)
candles.BindEx(bands, atr, self._process_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawIndicator(area, bands)
self.DrawIndicator(area, atr)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection evaluates executable quotes between finished signal candles.
pass
def _process_candle(self, candle, bands_value, atr_value):
if (candle.State != CandleStates.Finished or not bands_value.IsFormed or
not atr_value.IsFormed or not self.IsFormedAndOnlineAndAllowTrading()):
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
upper = bands_value.UpBand
lower = bands_value.LowBand
middle = bands_value.MovingAverage
if upper is None or lower is None or middle is None:
return
close = candle.ClosePrice
if self.Position > 0 and close >= middle:
self.SellMarket(self.Position)
elif self.Position < 0 and close <= middle:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and close < lower and close > candle.OpenPrice:
self._enter(Sides.Buy, atr_value.GetValue[Decimal](None))
elif self.Position == 0 and close > upper and close < candle.OpenPrice:
self._enter(Sides.Sell, atr_value.GetValue[Decimal](None))
def _enter(self, side, atr):
distance = atr * Decimal(self._atr_multiplier.Value)
if self._stop_distance is None:
self._stop_distance = Unit(distance)
# Cached native controllers retain this Unit across flat-to-position cycles.
self._stop_distance.Value = distance
if not self._protection_started and distance > 0:
self.StartProtection(Unit(), self._stop_distance, useMarketOrders=True, isLocalStop=True)
self._protection_started = True
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = self.Volume
order.Comment = "Bollinger reversal entry"
self.RegisterOrder(order)
def CreateClone(self):
return bollinger_band_reversal_strategy()