Bollinger Bands N Positions v2 Strategy
Overview
This strategy replicates the "Bollinger Bands N positions v2" expert advisor by Vladimir Karputov. It operates on completed candles and looks for price breakouts relative to the Bollinger Bands envelope. The StockSharp port keeps the original pyramiding behaviour, risk controls, and trailing logic while adapting order management to the netting model of the platform.
Trading Logic
- A Bollinger Bands indicator (period and deviation configurable) is calculated on the selected candle series.
- When the candle close finishes above the upper band, the strategy exits any active short exposure and opens an additional long position (up to the configured maximum number of stacked entries).
- When the candle close finishes below the lower band, the strategy exits any active long exposure and opens an additional short position (also limited by the maximum entries parameter).
- Position size is increased in fixed increments (the Volume parameter) when pyramiding into the same direction.
- The average entry price of the stacked position is tracked to manage stop loss, take profit, and trailing stop levels consistently.
Risk Management
- Stop loss and take profit distances are entered in pips. They are converted into absolute price offsets by multiplying with the instrument price step. Instruments quoted with 3 or 5 decimal places automatically multiply the step by 10 to emulate MetaTrader's pip size adjustment.
- Trailing stop offset and trailing step are also configured in pips. The trailing mechanism updates the stop price only after the trade moves by
TrailingStop + TrailingSteppips from the current average entry. Each update shifts the stop by the trailing offset while respecting the extra step buffer to avoid excessive modifications. - Protective exit orders are simulated within the strategy: whenever a finished candle crosses the stop or target level, the entire position is closed using market orders.
Parameters
| Parameter | Description |
|---|---|
| Bollinger Period | Lookback period for the Bollinger Bands moving average. |
| Bollinger Deviation | Standard deviation multiplier for the Bollinger envelope. |
| Max Positions | Maximum number of stacked entries allowed per direction. |
| Volume | Order volume for each individual entry. |
| Stop Loss (pips) | Stop loss distance in pips (0 disables the stop). |
| Take Profit (pips) | Take profit distance in pips (0 disables the target). |
| Trailing Stop (pips) | Trailing stop distance in pips (0 disables trailing). |
| Trailing Step (pips) | Additional profit in pips required before moving the trailing stop again. Must be positive when trailing is enabled. |
| Candle Type | Candle series processed by the strategy. |
Implementation Notes
- The strategy uses high-level candle subscriptions with indicator binding, following the StockSharp guidelines.
- Only finished candles are processed to mirror the original "new bar" logic from MetaTrader.
- Because StockSharp operates in a netting mode, the conversion closes the opposite exposure before opening a new pyramid layer in the other direction.
- Trailing step must remain greater than zero whenever the trailing stop is active, matching the safety check of the original expert advisor.
- Python implementation is not included in this release.
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()