Стратегия Bollinger Bands N Positions v2
Обзор
Стратегия повторяет эксперта "Bollinger Bands N positions v2" Владимира Карпутова. Алгоритм работает только по завершённым свечам и оценивает положение цены относительно границ полос Боллинджера. Перенос на StockSharp сохраняет пирамидирование, параметры управления рисками и траление, адаптируя при этом регистрацию сделок к неттинговой модели платформы.
Торговая логика
- На выбранной серии свечей рассчитываются полосы Боллинджера с настраиваемыми периодом и отклонением.
- Если свеча закрывается выше верхней полосы, стратегия закрывает активные короткие позиции и добавляет новую длинную позицию (до достижения максимального количества усреднений).
- Если свеча закрывается ниже нижней полосы, стратегия закрывает активные длинные позиции и добавляет новую короткую позицию (также ограничено максимальным числом входов).
- Объём каждой добавки фиксирован и задаётся параметром Volume, благодаря чему наращивание позиции происходит дискретными ступенями.
- Средняя цена набранной позиции пересчитывается при каждом усреднении и используется для корректной работы стоп-лосса, тейк-профита и трейлинг-стопа.
Управление рисками
- Стоп-лосс и тейк-профит задаются в пунктах. Значение автоматически переводится в абсолютное смещение цены умножением на шаг цены инструмента. Для инструментов с 3 или 5 знаками после запятой шаг дополнительно умножается на 10, чтобы имитировать поправку MetaTrader на "пятизнак".
- Параметры трейлинг-стопа (расстояние и шаг) также задаются в пунктах. Трейлинг активируется, когда цена уходит от средней цены позиции более чем на
TrailingStop + TrailingStepпунктов; при каждом обновлении стоп переносится на расстояние трейлинга, соблюдая дополнительный буфер шага. - Защитные заявки эмулируются внутри стратегии: если завершившаяся свеча пересекает уровень стопа или цели, вся позиция закрывается рыночным ордером.
Параметры
| Параметр | Описание |
|---|---|
| Bollinger Period | Период скользящей средней полос Боллинджера. |
| Bollinger Deviation | Множитель стандартного отклонения для расчёта полос. |
| Max Positions | Максимальное количество наращиваний в одну сторону. |
| Volume | Объём ордера для каждого входа. |
| Stop Loss (pips) | Расстояние стоп-лосса в пунктах (0 — отключён). |
| Take Profit (pips) | Расстояние тейк-профита в пунктах (0 — отключён). |
| Trailing Stop (pips) | Расстояние трейлинг-стопа в пунктах (0 — отключён). |
| Trailing Step (pips) | Дополнительное смещение в пунктах перед очередным переносом трейлинг-стопа; должно быть положительным при включённом трейлинге. |
| Candle Type | Тип свечей, по которым работает стратегия. |
Особенности реализации
- Используются высокоуровневые подписки на свечи с привязкой индикатора через
BindEx, что соответствует рекомендациям StockSharp. - Обработка ведётся только по закрытым свечам, как и в оригинальном советнике.
- Из-за неттинговой модели StockSharp при появлении сигнала в противоположную сторону стратегия сначала закрывает существующую позицию и только затем открывает новую ступень.
- Проверка параметров гарантирует, что при активном трейлинге шаг переноса больше нуля, как это реализовано в MetaTrader-версии.
- В этом релизе отсутствует Python-версия стратегии.
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 that can pyramid entries and applies pip-based risk management.
/// </summary>
public class BollingerBandsNPositionsV2Strategy : Strategy
{
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<DataType> _candleType;
private decimal _pipValue;
private decimal _stopLossDistance;
private decimal _takeProfitDistance;
private decimal _trailingStopDistance;
private decimal _trailingStepDistance;
private decimal _longEntryPrice;
private decimal _shortEntryPrice;
private int _longEntryCount;
private int _shortEntryCount;
private BollingerBands _bollinger = null!;
private decimal? _longStopPrice;
private decimal? _longTakeProfitPrice;
private decimal? _shortStopPrice;
private decimal? _shortTakeProfitPrice;
/// <summary>
/// Bollinger Bands period.
/// </summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>
/// Bollinger Bands standard deviation multiplier.
/// </summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>
/// Maximum stacked entries per direction.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Additional profit in pips required before trailing stop is moved again.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="BollingerBandsNPositionsV2Strategy"/>.
/// </summary>
public BollingerBandsNPositionsV2Strategy()
{
_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Period used for Bollinger Bands.", "Indicators")
;
_bollingerDeviation = Param(nameof(BollingerDeviation), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands.", "Indicators")
;
_maxPositions = Param(nameof(MaxPositions), 1)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum number of stacked entries per direction.", "Trading");
_stopLossPips = Param(nameof(StopLossPips), 30m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips.", "Risk")
;
_takeProfitPips = Param(nameof(TakeProfitPips), 60m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Profit target distance in pips.", "Risk")
;
_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop offset in pips.", "Risk")
;
_trailingStepPips = Param(nameof(TrailingStepPips), 1m)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Extra profit in pips before trailing stop is adjusted.", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for Bollinger analysis.", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pipValue = 0m;
_stopLossDistance = 0m;
_takeProfitDistance = 0m;
_trailingStopDistance = 0m;
_trailingStepDistance = 0m;
ResetLongState();
ResetShortState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
throw new InvalidOperationException("Trailing step must be positive when trailing stop is enabled.");
_pipValue = CalculatePipValue();
UpdateRiskDistances();
_bollinger = new BollingerBands
{
Length = BollingerPeriod,
Width = BollingerDeviation
};
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;
IIndicatorValue indicatorValue;
try
{
indicatorValue = _bollinger.Process(candle);
}
catch (IndexOutOfRangeException)
{
return;
}
if (indicatorValue.IsEmpty || !_bollinger.IsFormed)
return;
UpdateRiskDistances();
var value = (BollingerBandsValue)indicatorValue;
if (value.UpBand is not decimal upper || value.LowBand is not decimal lower)
return;
HandleRiskManagement(candle);
if (candle.ClosePrice > upper)
{
TryEnterLong(candle);
return;
}
if (candle.ClosePrice < lower)
{
TryEnterShort(candle);
}
}
private void HandleRiskManagement(ICandleMessage candle)
{
if (_longEntryCount > 0 && Position > 0)
{
if (_longTakeProfitPrice is decimal takeProfit && candle.HighPrice >= takeProfit)
{
SellMarket(Position);
ResetLongState();
return;
}
if (_longStopPrice is decimal stopLoss && candle.LowPrice <= stopLoss)
{
SellMarket(Position);
ResetLongState();
return;
}
UpdateLongTrailing(candle);
}
else if (Position <= 0)
{
ResetLongState();
}
if (_shortEntryCount > 0 && Position < 0)
{
var positionVolume = Math.Abs(Position);
if (_shortTakeProfitPrice is decimal takeProfit && candle.LowPrice <= takeProfit)
{
BuyMarket(positionVolume);
ResetShortState();
return;
}
if (_shortStopPrice is decimal stopLoss && candle.HighPrice >= stopLoss)
{
BuyMarket(positionVolume);
ResetShortState();
return;
}
UpdateShortTrailing(candle);
}
else if (Position >= 0)
{
ResetShortState();
}
}
private void TryEnterLong(ICandleMessage candle)
{
if (_longEntryCount >= MaxPositions)
return;
if (Position < 0)
{
var closeVolume = Math.Abs(Position);
if (closeVolume > 0)
{
BuyMarket(closeVolume);
ResetShortState();
}
}
var tradeVolume = Volume;
if (tradeVolume <= 0)
return;
var existingVolume = _longEntryCount * tradeVolume;
BuyMarket(tradeVolume);
var entryPrice = candle.ClosePrice;
var newVolume = existingVolume + tradeVolume;
_longEntryPrice = existingVolume <= 0 ? entryPrice : ((_longEntryPrice * existingVolume) + entryPrice * tradeVolume) / newVolume;
_longEntryCount++;
_longStopPrice = StopLossPips > 0m ? _longEntryPrice - _stopLossDistance : null;
_longTakeProfitPrice = TakeProfitPips > 0m ? _longEntryPrice + _takeProfitDistance : null;
}
private void TryEnterShort(ICandleMessage candle)
{
if (_shortEntryCount >= MaxPositions)
return;
if (Position > 0)
{
var closeVolume = Position;
if (closeVolume > 0)
{
SellMarket(closeVolume);
ResetLongState();
}
}
var tradeVolume = Volume;
if (tradeVolume <= 0)
return;
var existingVolume = _shortEntryCount * tradeVolume;
SellMarket(tradeVolume);
var entryPrice = candle.ClosePrice;
var newVolume = existingVolume + tradeVolume;
_shortEntryPrice = existingVolume <= 0 ? entryPrice : ((_shortEntryPrice * existingVolume) + entryPrice * tradeVolume) / newVolume;
_shortEntryCount++;
_shortStopPrice = StopLossPips > 0m ? _shortEntryPrice + _stopLossDistance : null;
_shortTakeProfitPrice = TakeProfitPips > 0m ? _shortEntryPrice - _takeProfitDistance : null;
}
private void UpdateLongTrailing(ICandleMessage candle)
{
if (_trailingStopDistance <= 0m)
return;
var moveFromEntry = candle.ClosePrice - _longEntryPrice;
if (moveFromEntry <= _trailingStopDistance + _trailingStepDistance)
return;
var newStop = candle.ClosePrice - _trailingStopDistance;
if (_longStopPrice is not decimal currentStop || newStop > currentStop + _trailingStepDistance)
_longStopPrice = newStop;
}
private void UpdateShortTrailing(ICandleMessage candle)
{
if (_trailingStopDistance <= 0m)
return;
var moveFromEntry = _shortEntryPrice - candle.ClosePrice;
if (moveFromEntry <= _trailingStopDistance + _trailingStepDistance)
return;
var newStop = candle.ClosePrice + _trailingStopDistance;
if (_shortStopPrice is not decimal currentStop || newStop < currentStop - _trailingStepDistance)
_shortStopPrice = newStop;
}
private void ResetLongState()
{
_longEntryPrice = 0m;
_longEntryCount = 0;
_longStopPrice = null;
_longTakeProfitPrice = null;
}
private void ResetShortState()
{
_shortEntryPrice = 0m;
_shortEntryCount = 0;
_shortStopPrice = null;
_shortTakeProfitPrice = null;
}
private void UpdateRiskDistances()
{
_stopLossDistance = StopLossPips > 0m ? StopLossPips * _pipValue : 0m;
_takeProfitDistance = TakeProfitPips > 0m ? TakeProfitPips * _pipValue : 0m;
_trailingStopDistance = TrailingStopPips > 0m ? TrailingStopPips * _pipValue : 0m;
_trailingStepDistance = TrailingStepPips > 0m ? TrailingStepPips * _pipValue : 0m;
}
private decimal CalculatePipValue()
{
var security = Security;
if (security == null)
return 1m;
var step = security.PriceStep ?? 0m;
if (step <= 0m)
return 1m;
var decimals = CountDecimals(step);
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
private static int CountDecimals(decimal value)
{
value = Math.Abs(value);
var decimals = 0;
while (value != Math.Truncate(value) && decimals < 10)
{
value *= 10m;
decimals++;
}
return decimals;
}
}
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, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class bollinger_bands_n_positions_v2_strategy(Strategy):
def __init__(self):
super(bollinger_bands_n_positions_v2_strategy, self).__init__()
self._bollinger_period = self.Param("BollingerPeriod", 20) \
.SetDisplay("Bollinger Period", "Period used for Bollinger Bands.", "Indicators")
self._bollinger_deviation = self.Param("BollingerDeviation", 1.5) \
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands.", "Indicators")
self._max_positions = self.Param("MaxPositions", 1) \
.SetDisplay("Max Positions", "Maximum number of stacked entries per direction.", "Trading")
self._stop_loss_pips = self.Param("StopLossPips", 30.0) \
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips.", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 60.0) \
.SetDisplay("Take Profit (pips)", "Profit target distance in pips.", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 5.0) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop offset in pips.", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 1.0) \
.SetDisplay("Trailing Step (pips)", "Extra profit in pips before trailing stop is adjusted.", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe used for Bollinger analysis.", "General")
self._pip_value = 0.0
self._stop_loss_distance = 0.0
self._take_profit_distance = 0.0
self._trailing_stop_distance = 0.0
self._trailing_step_distance = 0.0
self._long_entry_price = 0.0
self._short_entry_price = 0.0
self._long_entry_count = 0
self._short_entry_count = 0
self._long_stop_price = None
self._long_take_profit_price = None
self._short_stop_price = None
self._short_take_profit_price = None
self._bollinger = None
@property
def bollinger_period(self):
return self._bollinger_period.Value
@property
def bollinger_deviation(self):
return self._bollinger_deviation.Value
@property
def max_positions(self):
return self._max_positions.Value
@property
def stop_loss_pips(self):
return self._stop_loss_pips.Value
@property
def take_profit_pips(self):
return self._take_profit_pips.Value
@property
def trailing_stop_pips(self):
return self._trailing_stop_pips.Value
@property
def trailing_step_pips(self):
return self._trailing_step_pips.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(bollinger_bands_n_positions_v2_strategy, self).OnReseted()
self._pip_value = 0.0
self._stop_loss_distance = 0.0
self._take_profit_distance = 0.0
self._trailing_stop_distance = 0.0
self._trailing_step_distance = 0.0
self._reset_long_state()
self._reset_short_state()
def OnStarted2(self, time):
super(bollinger_bands_n_positions_v2_strategy, self).OnStarted2(time)
self._pip_value = self._calculate_pip_value()
self._update_risk_distances()
self._bollinger = BollingerBands()
self._bollinger.Length = self.bollinger_period
self._bollinger.Width = self.bollinger_deviation
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
civ = CandleIndicatorValue(self._bollinger, candle)
civ.IsFinal = True
indicator_value = self._bollinger.Process(civ)
if indicator_value.IsEmpty or not self._bollinger.IsFormed:
return
self._update_risk_distances()
upper = indicator_value.UpBand
lower = indicator_value.LowBand
if upper is None or lower is None:
return
upper = float(upper)
lower = float(lower)
self._handle_risk_management(candle)
if float(candle.ClosePrice) > upper:
self._try_enter_long(candle)
return
if float(candle.ClosePrice) < lower:
self._try_enter_short(candle)
def _handle_risk_management(self, candle):
if self._long_entry_count > 0 and self.Position > 0:
if self._long_take_profit_price is not None and float(candle.HighPrice) >= self._long_take_profit_price:
self.SellMarket()
self._reset_long_state()
return
if self._long_stop_price is not None and float(candle.LowPrice) <= self._long_stop_price:
self.SellMarket()
self._reset_long_state()
return
self._update_long_trailing(candle)
elif self.Position <= 0:
self._reset_long_state()
if self._short_entry_count > 0 and self.Position < 0:
if self._short_take_profit_price is not None and float(candle.LowPrice) <= self._short_take_profit_price:
self.BuyMarket()
self._reset_short_state()
return
if self._short_stop_price is not None and float(candle.HighPrice) >= self._short_stop_price:
self.BuyMarket()
self._reset_short_state()
return
self._update_short_trailing(candle)
elif self.Position >= 0:
self._reset_short_state()
def _try_enter_long(self, candle):
if self._long_entry_count >= self.max_positions:
return
if self.Position < 0:
self.BuyMarket()
self._reset_short_state()
self.BuyMarket()
entry_price = float(candle.ClosePrice)
self._long_entry_count += 1
self._long_entry_price = entry_price
if self.stop_loss_pips > 0:
self._long_stop_price = self._long_entry_price - self._stop_loss_distance
if self.take_profit_pips > 0:
self._long_take_profit_price = self._long_entry_price + self._take_profit_distance
def _try_enter_short(self, candle):
if self._short_entry_count >= self.max_positions:
return
if self.Position > 0:
self.SellMarket()
self._reset_long_state()
self.SellMarket()
entry_price = float(candle.ClosePrice)
self._short_entry_count += 1
self._short_entry_price = entry_price
if self.stop_loss_pips > 0:
self._short_stop_price = self._short_entry_price + self._stop_loss_distance
if self.take_profit_pips > 0:
self._short_take_profit_price = self._short_entry_price - self._take_profit_distance
def _update_long_trailing(self, candle):
if self._trailing_stop_distance <= 0:
return
move_from_entry = float(candle.ClosePrice) - self._long_entry_price
if move_from_entry <= self._trailing_stop_distance + self._trailing_step_distance:
return
new_stop = float(candle.ClosePrice) - self._trailing_stop_distance
if self._long_stop_price is None or new_stop > self._long_stop_price + self._trailing_step_distance:
self._long_stop_price = new_stop
def _update_short_trailing(self, candle):
if self._trailing_stop_distance <= 0:
return
move_from_entry = self._short_entry_price - float(candle.ClosePrice)
if move_from_entry <= self._trailing_stop_distance + self._trailing_step_distance:
return
new_stop = float(candle.ClosePrice) + self._trailing_stop_distance
if self._short_stop_price is None or new_stop < self._short_stop_price - self._trailing_step_distance:
self._short_stop_price = new_stop
def _reset_long_state(self):
self._long_entry_price = 0.0
self._long_entry_count = 0
self._long_stop_price = None
self._long_take_profit_price = None
def _reset_short_state(self):
self._short_entry_price = 0.0
self._short_entry_count = 0
self._short_stop_price = None
self._short_take_profit_price = None
def _update_risk_distances(self):
self._stop_loss_distance = self.stop_loss_pips * self._pip_value if self.stop_loss_pips > 0 else 0.0
self._take_profit_distance = self.take_profit_pips * self._pip_value if self.take_profit_pips > 0 else 0.0
self._trailing_stop_distance = self.trailing_stop_pips * self._pip_value if self.trailing_stop_pips > 0 else 0.0
self._trailing_step_distance = self.trailing_step_pips * self._pip_value if self.trailing_step_pips > 0 else 0.0
def _calculate_pip_value(self):
security = self.Security
if security is None:
return 1.0
step = security.PriceStep
if step is None or float(step) <= 0:
return 1.0
step_val = float(step)
decimals = self._count_decimals(step_val)
if decimals == 3 or decimals == 5:
return step_val * 10.0
return step_val
@staticmethod
def _count_decimals(value):
value = abs(value)
decimals = 0
while value != int(value) and decimals < 10:
value *= 10
decimals += 1
return decimals
def CreateClone(self):
return bollinger_bands_n_positions_v2_strategy()