Стратегия Bands
Обзор
Стратегия переносит эксперт MetaTrader 5 Bands.mq5 на высокоуровневый API StockSharp. Алгоритм ждёт закрытую свечу, которая п робивает полосу Боллинджера снаружи обратно в канал, и открывает сделку только тогда, когда наклон канала Дончиана в течение зад анных баров остаётся неизменным. Множители среднего истинного диапазона (ATR) воссоздают исходные уровни стоп-лосса и тейк-профи та, а диагностический блок выводит коэффициент детерминации (R-squared) по кривой капитала после каждых 100 сделок — так же, как это делал оригинальный советник.
Торговая логика
- Подписаться на поток свечей и вычислить полосы Боллинджера, канал Дончиана и ATR с теми же периодами, что и в MQL-версии.
- При отсутствии позиции анализировать предыдущую закрытую свечу:
- Открывать длинную позицию, если она открылась ниже нижней полосы Боллинджера и закрылась внутри канала, а нижняя граница До
нчиана не снижалась дольше, чем
ConfirmationPeriodбаров. - Открывать короткую позицию, если свеча открылась выше верхней полосы и закрылась внутри, а верхняя граница Дончиана не росла
дольше, чем
ConfirmationPeriodбаров.
- Открывать длинную позицию, если она открылась ниже нижней полосы Боллинджера и закрылась внутри канала, а нижняя граница До
нчиана не снижалась дольше, чем
- При наличии позиции закрывать её, если предыдущая свеча пересекла актуальную границу Дончиана, либо если текущая свеча наруши ла ATR-защитные уровни по максимуму или минимуму.
- После каждой собственной сделки сохранять значение капитала портфеля и каждые 100 сделок выводить R-squared. Отрицательный уг ол наклона приводит к отрицательному значению коэффициента, как и в оригинале.
Управление рисками
- Все входы выполняются рыночными ордерами объёмом
TradeVolume. - Стоп и тейк рассчитываются в коде без выставления отложенных заявок: сравниваются экстремумы свечи с ATR-множителями.
- При срабатывании защиты позиция полностью закрывается рыночным ордером, а рассчитанные уровни сбрасываются.
Параметры
| Параметр | Описание |
|---|---|
TradeVolume |
Чистый объём (в лотах) каждой рыночной сделки. |
CandleType |
Тип/таймфрейм свечей, используемый индикаторами. |
BollingerPeriod |
Период расчёта полос Боллинджера. |
BollingerDeviation |
Множитель стандартного отклонения полос. |
DonchianPeriod |
Длина канала Дончиана, применяемая как фильтр тренда. |
ConfirmationPeriod |
Минимальное количество последовательных баров, поддерживающих наклон канала. |
AtrPeriod |
Период индикатора ATR. |
StopAtrMultiplier |
Количество ATR для стоп-лосса. |
TakeAtrMultiplier |
Количество ATR для тейк-профита. |
Примечания
- Проверка наклона канала Дончиана реализована через счётчики, что избавляет от копирования буферов индикатора и соответствует т ребованиям проекта.
- Все комментарии и логи написаны на английском языке согласно правилам репозитория.
- Функции проверки маржи и расчёта объёма из MQL-версии не реализуются; размер позиции задаётся параметром
TradeVolume.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Bollinger Bands breakout strategy confirmed by Donchian channel slope and ATR-based risk management.
/// </summary>
public class BandsStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<int> _donchianPeriod;
private readonly StrategyParam<int> _confirmationPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _stopAtrMultiplier;
private readonly StrategyParam<decimal> _takeAtrMultiplier;
private decimal? _prevOpen;
private decimal? _prevClose;
private decimal? _prevLowerBand;
private decimal? _prevUpperBand;
private decimal? _prevDonchLower;
private decimal? _prevDonchUpper;
private decimal? _prevAtr;
private int _lowerTrendLength;
private int _upperTrendLength;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private int _equitySamples;
private decimal _sumIndices;
private decimal _sumEquity;
private decimal _sumIndexEquity;
private decimal _sumIndexSquared;
private decimal _sumEquitySquared;
/// <summary>
/// Initializes a new instance of <see cref="BandsStrategy"/>.
/// </summary>
public BandsStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Net volume in lots sent with every order", "Trading")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for indicator calculations", "Market Data");
_bollingerPeriod = Param(nameof(BollingerPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Number of candles used for the Bollinger Bands", "Indicators")
;
_bollingerDeviation = Param(nameof(BollingerDeviation), 1m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for the Bollinger Bands", "Indicators")
;
_donchianPeriod = Param(nameof(DonchianPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Donchian Period", "Donchian Channel length used as trend filter", "Indicators")
;
_confirmationPeriod = Param(nameof(ConfirmationPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Slope Confirmation", "Minimum number of bars that must keep the Donchian slope intact", "Indicators")
;
_atrPeriod = Param(nameof(AtrPeriod), 21)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Length of the Average True Range used for stops", "Indicators")
;
_stopAtrMultiplier = Param(nameof(StopAtrMultiplier), 4m)
.SetGreaterThanZero()
.SetDisplay("Stop ATR Multiplier", "How many ATRs below/above the entry to place the stop", "Risk")
;
_takeAtrMultiplier = Param(nameof(TakeAtrMultiplier), 4m)
.SetGreaterThanZero()
.SetDisplay("Take ATR Multiplier", "How many ATRs below/above the entry to place the target", "Risk")
;
}
/// <summary>
/// Trade volume in lots.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Candle type used for analysis.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Period of the Bollinger Bands.
/// </summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>
/// Deviation multiplier of the Bollinger Bands.
/// </summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>
/// Period of the Donchian Channel.
/// </summary>
public int DonchianPeriod
{
get => _donchianPeriod.Value;
set => _donchianPeriod.Value = value;
}
/// <summary>
/// Number of consecutive bars required to confirm the Donchian slope.
/// </summary>
public int ConfirmationPeriod
{
get => _confirmationPeriod.Value;
set => _confirmationPeriod.Value = value;
}
/// <summary>
/// Period of the Average True Range indicator.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in ATR multiples.
/// </summary>
public decimal StopAtrMultiplier
{
get => _stopAtrMultiplier.Value;
set => _stopAtrMultiplier.Value = value;
}
/// <summary>
/// Take-profit distance expressed in ATR multiples.
/// </summary>
public decimal TakeAtrMultiplier
{
get => _takeAtrMultiplier.Value;
set => _takeAtrMultiplier.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevOpen = null;
_prevClose = null;
_prevLowerBand = null;
_prevUpperBand = null;
_prevDonchLower = null;
_prevDonchUpper = null;
_prevAtr = null;
_lowerTrendLength = 0;
_upperTrendLength = 0;
_stopLossPrice = null;
_takeProfitPrice = null;
_equitySamples = 0;
_sumIndices = 0m;
_sumEquity = 0m;
_sumIndexEquity = 0m;
_sumIndexSquared = 0m;
_sumEquitySquared = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var bollinger = new BollingerBands
{
Length = BollingerPeriod,
Width = BollingerDeviation
};
var atr = new AverageTrueRange
{
Length = AtrPeriod
};
var donchian = new DonchianChannels
{
Length = DonchianPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(bollinger, atr, donchian, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bollingerVal, IIndicatorValue atrVal, IIndicatorValue donchianVal)
{
if (candle.State != CandleStates.Finished)
return;
if (!bollingerVal.IsFormed || !atrVal.IsFormed || !donchianVal.IsFormed)
return;
var bollingerComplex = (ComplexIndicatorValue<BollingerBands>)bollingerVal;
var middle = bollingerComplex.InnerValues.ElementAt(0).Value.GetValue<decimal>();
var upper = bollingerComplex.InnerValues.ElementAt(1).Value.GetValue<decimal>();
var lower = bollingerComplex.InnerValues.ElementAt(2).Value.GetValue<decimal>();
var atrValue = atrVal.GetValue<decimal>();
var donchianComplex = (ComplexIndicatorValue<DonchianChannels>)donchianVal;
var donchUpper = donchianComplex.InnerValues.ElementAt(0).Value.GetValue<decimal>();
var donchLower = donchianComplex.InnerValues.ElementAt(1).Value.GetValue<decimal>();
var lowerTrendLength = CalculateLowerTrendLength(donchLower);
var upperTrendLength = CalculateUpperTrendLength(donchUpper);
if (!_prevOpen.HasValue)
{
CachePreviousValues(candle, lower, upper, donchLower, donchUpper, atrValue, lowerTrendLength, upperTrendLength);
return;
}
var previousOpen = _prevOpen.Value;
var previousClose = _prevClose!.Value;
var previousLowerBand = _prevLowerBand!.Value;
var previousUpperBand = _prevUpperBand!.Value;
var previousDonchLower = _prevDonchLower!.Value;
var previousDonchUpper = _prevDonchUpper!.Value;
var atrForStops = _prevAtr ?? atrValue;
if (Position == 0m)
{
if (previousOpen < previousLowerBand && previousClose > previousLowerBand && lowerTrendLength > ConfirmationPeriod)
{
OpenLong(candle.ClosePrice, atrForStops);
}
else if (previousOpen > previousUpperBand && previousClose < previousUpperBand && upperTrendLength > ConfirmationPeriod)
{
OpenShort(candle.ClosePrice, atrForStops);
}
}
else if (Position > 0m)
{
var exitVolume = Position;
var stopTriggered = _stopLossPrice is decimal stop && candle.LowPrice <= stop;
var takeTriggered = _takeProfitPrice is decimal take && candle.HighPrice >= take;
if (stopTriggered || takeTriggered || previousClose > previousDonchUpper || previousClose < previousDonchLower)
{
SellMarket(exitVolume);
ClearProtection();
}
}
else if (Position < 0m)
{
var exitVolume = Math.Abs(Position);
var stopTriggered = _stopLossPrice is decimal stop && candle.HighPrice >= stop;
var takeTriggered = _takeProfitPrice is decimal take && candle.LowPrice <= take;
if (stopTriggered || takeTriggered || previousClose < previousDonchLower || previousClose > previousDonchUpper)
{
BuyMarket(exitVolume);
ClearProtection();
}
}
CachePreviousValues(candle, lower, upper, donchLower, donchUpper, atrValue, lowerTrendLength, upperTrendLength);
}
private int CalculateLowerTrendLength(decimal currentLower)
{
if (_prevDonchLower is decimal prevLower)
{
return currentLower >= prevLower ? _lowerTrendLength + 1 : 1;
}
return 1;
}
private int CalculateUpperTrendLength(decimal currentUpper)
{
if (_prevDonchUpper is decimal prevUpper)
{
return currentUpper <= prevUpper ? _upperTrendLength + 1 : 1;
}
return 1;
}
private void CachePreviousValues(ICandleMessage candle, decimal lower, decimal upper, decimal donchLower, decimal donchUpper, decimal atrValue, int lowerTrendLength, int upperTrendLength)
{
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_prevLowerBand = lower;
_prevUpperBand = upper;
_prevDonchLower = donchLower;
_prevDonchUpper = donchUpper;
_prevAtr = atrValue;
_lowerTrendLength = lowerTrendLength;
_upperTrendLength = upperTrendLength;
}
private void OpenLong(decimal entryPrice, decimal atrValue)
{
var volume = TradeVolume;
if (volume <= 0m)
return;
BuyMarket(volume);
AssignProtection(entryPrice, atrValue, true);
}
private void OpenShort(decimal entryPrice, decimal atrValue)
{
var volume = TradeVolume;
if (volume <= 0m)
return;
SellMarket(volume);
AssignProtection(entryPrice, atrValue, false);
}
private void AssignProtection(decimal entryPrice, decimal atrValue, bool isLong)
{
if (atrValue <= 0m)
{
ClearProtection();
return;
}
var stopDistance = atrValue * StopAtrMultiplier;
var takeDistance = atrValue * TakeAtrMultiplier;
if (isLong)
{
_stopLossPrice = entryPrice - stopDistance;
_takeProfitPrice = entryPrice + takeDistance;
}
else
{
_stopLossPrice = entryPrice + stopDistance;
_takeProfitPrice = entryPrice - takeDistance;
}
}
private void ClearProtection()
{
_stopLossPrice = null;
_takeProfitPrice = null;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var portfolio = Portfolio;
if (portfolio == null)
return;
UpdateEquityStatistics(portfolio.CurrentValue ?? 0m);
}
private void UpdateEquityStatistics(decimal equity)
{
var index = (decimal)_equitySamples;
_sumIndices += index;
_sumEquity += equity;
_sumIndexEquity += index * equity;
_sumIndexSquared += index * index;
_sumEquitySquared += equity * equity;
_equitySamples++;
if (_equitySamples % 100 != 0)
return;
var n = (decimal)_equitySamples;
if (n <= 1m)
return;
var denominator = n * _sumIndexSquared - _sumIndices * _sumIndices;
if (denominator == 0m)
return;
var slope = (n * _sumIndexEquity - _sumIndices * _sumEquity) / denominator;
var mean = _sumEquity / n;
var ssTotal = _sumEquitySquared - n * mean * mean;
if (ssTotal == 0m)
{
LogInfo("Equity R-squared: 1.0000");
return;
}
var regressionComponent = slope * (_sumIndexEquity - (_sumIndices / n) * _sumEquity);
var rSquared = regressionComponent / ssTotal;
if (slope < 0m)
rSquared = -rSquared;
LogInfo($"Equity R-squared: {rSquared:F4}");
}
}
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 BollingerBands, AverageTrueRange, DonchianChannels
from StockSharp.Algo.Strategies import Strategy
class bands_strategy(Strategy):
"""Bollinger Bands breakout confirmed by Donchian channel slope and ATR-based stops."""
def __init__(self):
super(bands_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Volume", "Net volume in lots sent with every order", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Time frame used for indicator calculations", "Market Data")
self._bollinger_period = self.Param("BollingerPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Period", "Number of candles used for Bollinger Bands", "Indicators")
self._bollinger_deviation = self.Param("BollingerDeviation", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier", "Indicators")
self._donchian_period = self.Param("DonchianPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Donchian Period", "Donchian Channel length", "Indicators")
self._confirmation_period = self.Param("ConfirmationPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Slope Confirmation", "Min bars for Donchian slope confirmation", "Indicators")
self._atr_period = self.Param("AtrPeriod", 21) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "Length of Average True Range", "Indicators")
self._stop_atr_multiplier = self.Param("StopAtrMultiplier", 4.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop ATR Multiplier", "ATRs for stop placement", "Risk")
self._take_atr_multiplier = self.Param("TakeAtrMultiplier", 4.0) \
.SetGreaterThanZero() \
.SetDisplay("Take ATR Multiplier", "ATRs for target placement", "Risk")
self._prev_open = None
self._prev_close = None
self._prev_lower_band = None
self._prev_upper_band = None
self._prev_donch_lower = None
self._prev_donch_upper = None
self._prev_atr = None
self._lower_trend_length = 0
self._upper_trend_length = 0
self._stop_loss_price = None
self._take_profit_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def TradeVolume(self):
return self._trade_volume.Value
@property
def BollingerPeriod(self):
return self._bollinger_period.Value
@property
def BollingerDeviation(self):
return self._bollinger_deviation.Value
@property
def DonchianPeriod(self):
return self._donchian_period.Value
@property
def ConfirmationPeriod(self):
return self._confirmation_period.Value
@property
def AtrPeriod(self):
return self._atr_period.Value
@property
def StopAtrMultiplier(self):
return self._stop_atr_multiplier.Value
@property
def TakeAtrMultiplier(self):
return self._take_atr_multiplier.Value
def OnReseted(self):
super(bands_strategy, self).OnReseted()
self._prev_open = None
self._prev_close = None
self._prev_lower_band = None
self._prev_upper_band = None
self._prev_donch_lower = None
self._prev_donch_upper = None
self._prev_atr = None
self._lower_trend_length = 0
self._upper_trend_length = 0
self._stop_loss_price = None
self._take_profit_price = None
def OnStarted2(self, time):
super(bands_strategy, self).OnStarted2(time)
bollinger = BollingerBands()
bollinger.Length = self.BollingerPeriod
bollinger.Width = self.BollingerDeviation
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
donchian = DonchianChannels()
donchian.Length = self.DonchianPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(bollinger, atr, donchian, self._process_candle).Start()
def _process_candle(self, candle, bb_val, atr_val, donch_val):
if candle.State != CandleStates.Finished:
return
if not bb_val.IsFormed or not atr_val.IsFormed or not donch_val.IsFormed:
return
upper_bb = bb_val.UpBand
lower_bb = bb_val.LowBand
atr_v = float(atr_val)
donch_upper = donch_val.UpperBand
donch_lower = donch_val.LowerBand
if upper_bb is None or lower_bb is None or donch_upper is None or donch_lower is None:
return
upper = float(upper_bb)
lower = float(lower_bb)
d_upper = float(donch_upper)
d_lower = float(donch_lower)
lower_trend = self._calc_lower_trend(d_lower)
upper_trend = self._calc_upper_trend(d_upper)
if self._prev_open is None:
self._cache_values(candle, lower, upper, d_lower, d_upper, atr_v, lower_trend, upper_trend)
return
prev_open = self._prev_open
prev_close = self._prev_close
prev_lower_band = self._prev_lower_band
prev_upper_band = self._prev_upper_band
prev_donch_lower = self._prev_donch_lower
prev_donch_upper = self._prev_donch_upper
atr_for_stops = self._prev_atr if self._prev_atr is not None else atr_v
if self.Position == 0:
if prev_open < prev_lower_band and prev_close > prev_lower_band and lower_trend > self.ConfirmationPeriod:
self._open_long(float(candle.ClosePrice), atr_for_stops)
elif prev_open > prev_upper_band and prev_close < prev_upper_band and upper_trend > self.ConfirmationPeriod:
self._open_short(float(candle.ClosePrice), atr_for_stops)
elif self.Position > 0:
stop_hit = self._stop_loss_price is not None and float(candle.LowPrice) <= self._stop_loss_price
take_hit = self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price
if stop_hit or take_hit or prev_close > prev_donch_upper or prev_close < prev_donch_lower:
self.SellMarket(self.Position)
self._clear_protection()
elif self.Position < 0:
stop_hit = self._stop_loss_price is not None and float(candle.HighPrice) >= self._stop_loss_price
take_hit = self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price
if stop_hit or take_hit or prev_close < prev_donch_lower or prev_close > prev_donch_upper:
self.BuyMarket(abs(self.Position))
self._clear_protection()
self._cache_values(candle, lower, upper, d_lower, d_upper, atr_v, lower_trend, upper_trend)
def _calc_lower_trend(self, current_lower):
if self._prev_donch_lower is not None:
return self._lower_trend_length + 1 if current_lower >= self._prev_donch_lower else 1
return 1
def _calc_upper_trend(self, current_upper):
if self._prev_donch_upper is not None:
return self._upper_trend_length + 1 if current_upper <= self._prev_donch_upper else 1
return 1
def _cache_values(self, candle, lower, upper, d_lower, d_upper, atr_v, lower_trend, upper_trend):
self._prev_open = float(candle.OpenPrice)
self._prev_close = float(candle.ClosePrice)
self._prev_lower_band = lower
self._prev_upper_band = upper
self._prev_donch_lower = d_lower
self._prev_donch_upper = d_upper
self._prev_atr = atr_v
self._lower_trend_length = lower_trend
self._upper_trend_length = upper_trend
def _open_long(self, entry_price, atr_v):
vol = float(self.TradeVolume)
if vol <= 0:
return
self.BuyMarket(vol)
self._assign_protection(entry_price, atr_v, True)
def _open_short(self, entry_price, atr_v):
vol = float(self.TradeVolume)
if vol <= 0:
return
self.SellMarket(vol)
self._assign_protection(entry_price, atr_v, False)
def _assign_protection(self, entry_price, atr_v, is_long):
if atr_v <= 0:
self._clear_protection()
return
stop_dist = atr_v * float(self.StopAtrMultiplier)
take_dist = atr_v * float(self.TakeAtrMultiplier)
if is_long:
self._stop_loss_price = entry_price - stop_dist
self._take_profit_price = entry_price + take_dist
else:
self._stop_loss_price = entry_price + stop_dist
self._take_profit_price = entry_price - take_dist
def _clear_protection(self):
self._stop_loss_price = None
self._take_profit_price = None
def CreateClone(self):
return bands_strategy()