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 translated from the MQL5 version with N-position control.
/// Opens positions when price closes outside the Bollinger envelope and manages exits via fixed and trailing stops.
/// </summary>
public class BollingerBandsNPositionsStrategy : Strategy
{
private readonly StrategyParam<decimal> _volumeTolerance;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerWidth;
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? _longEntryPrice;
private decimal? _shortEntryPrice;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
/// <summary>
/// Maximum allowed net position expressed as multiples of <see cref="Volume"/>.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Bollinger Bands period.
/// </summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>
/// Bollinger Bands width multiplier.
/// </summary>
public decimal BollingerWidth
{
get => _bollingerWidth.Value;
set => _bollingerWidth.Value = value;
}
/// <summary>
/// Stop-loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing-stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Trailing-step increment in pips.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Net position magnitude treated as flat.
/// </summary>
public decimal VolumeTolerance
{
get => _volumeTolerance.Value;
set => _volumeTolerance.Value = value;
}
/// <summary>
/// Candle type used for signal calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes <see cref="BollingerBandsNPositionsStrategy"/>.
/// </summary>
public BollingerBandsNPositionsStrategy()
{
_maxPositions = Param(nameof(MaxPositions), 9)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Net position limit in multiples of Volume", "Risk");
_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Moving average length", "Indicators");
_bollingerWidth = Param(nameof(BollingerWidth), 2m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Width", "Standard deviation multiplier", "Indicators");
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing-stop distance in pips", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Trailing adjustment increment", "Risk");
_volumeTolerance = Param(nameof(VolumeTolerance), 0.00000001m)
.SetNotNegative()
.SetDisplay("Volume Tolerance", "Minimum net position magnitude treated as flat", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Source candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetLongState();
ResetShortState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");
var bollinger = new BollingerBands
{
Length = BollingerPeriod,
Width = BollingerWidth
};
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(bollinger, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bbValue)
{
if (candle.State != CandleStates.Finished)
return;
var bb = bbValue as IBollingerBandsValue;
var upper = bb?.UpBand ?? 0m;
var lower = bb?.LowBand ?? 0m;
if (HandleActivePosition(candle))
return;
if (!IsFormed)
return;
if (TryEnterLong(candle, upper))
return;
TryEnterShort(candle, lower);
}
private bool HandleActivePosition(ICandleMessage candle)
{
if (Position > VolumeTolerance)
return ManageLong(candle);
if (Position < -VolumeTolerance)
return ManageShort(candle);
if (_longEntryPrice.HasValue || _shortEntryPrice.HasValue)
{
ResetLongState();
ResetShortState();
}
return false;
}
private bool ManageLong(ICandleMessage candle)
{
if (_longEntryPrice is null)
_longEntryPrice = candle.ClosePrice;
var entry = _longEntryPrice.Value;
var step = GetPriceStep();
if (StopLossPips > 0m)
{
var stopLevel = entry - StopLossPips * step;
if (candle.LowPrice <= stopLevel)
{
SellMarket();
ResetLongState();
return true;
}
}
if (TakeProfitPips > 0m)
{
var targetLevel = entry + TakeProfitPips * step;
if (candle.HighPrice >= targetLevel)
{
SellMarket();
ResetLongState();
return true;
}
}
if (TrailingStopPips > 0m && TrailingStepPips > 0m)
{
var trailingDistance = TrailingStopPips * step;
var trailingStep = TrailingStepPips * step;
var activationDistance = trailingDistance + trailingStep;
if (candle.ClosePrice - entry > activationDistance)
{
var candidate = candle.ClosePrice - trailingDistance;
if (_longTrailingStop is null || candidate - _longTrailingStop.Value > trailingStep)
_longTrailingStop = candidate;
}
if (_longTrailingStop is decimal trailing && candle.LowPrice <= trailing)
{
SellMarket();
ResetLongState();
return true;
}
}
return false;
}
private bool ManageShort(ICandleMessage candle)
{
if (_shortEntryPrice is null)
_shortEntryPrice = candle.ClosePrice;
var entry = _shortEntryPrice.Value;
var step = GetPriceStep();
if (StopLossPips > 0m)
{
var stopLevel = entry + StopLossPips * step;
if (candle.HighPrice >= stopLevel)
{
BuyMarket();
ResetShortState();
return true;
}
}
if (TakeProfitPips > 0m)
{
var targetLevel = entry - TakeProfitPips * step;
if (candle.LowPrice <= targetLevel)
{
BuyMarket();
ResetShortState();
return true;
}
}
if (TrailingStopPips > 0m && TrailingStepPips > 0m)
{
var trailingDistance = TrailingStopPips * step;
var trailingStep = TrailingStepPips * step;
var activationDistance = trailingDistance + trailingStep;
if (entry - candle.ClosePrice > activationDistance)
{
var candidate = candle.ClosePrice + trailingDistance;
if (_shortTrailingStop is null || _shortTrailingStop.Value - candidate > trailingStep)
_shortTrailingStop = candidate;
}
if (_shortTrailingStop is decimal trailing && candle.HighPrice >= trailing)
{
BuyMarket();
ResetShortState();
return true;
}
}
return false;
}
private bool TryEnterLong(ICandleMessage candle, decimal upper)
{
if (candle.ClosePrice <= upper)
return false;
if (!HasCapacity())
return false;
if (Position < -VolumeTolerance)
{
BuyMarket();
ResetShortState();
return true;
}
if (Position > VolumeTolerance)
{
SellMarket();
ResetLongState();
return true;
}
BuyMarket();
_longEntryPrice = candle.ClosePrice;
_longTrailingStop = null;
ResetShortState();
return true;
}
private bool TryEnterShort(ICandleMessage candle, decimal lower)
{
if (candle.ClosePrice >= lower)
return false;
if (!HasCapacity())
return false;
if (Position > VolumeTolerance)
{
SellMarket();
ResetLongState();
return true;
}
if (Position < -VolumeTolerance)
{
BuyMarket();
ResetShortState();
return true;
}
SellMarket();
_shortEntryPrice = candle.ClosePrice;
_shortTrailingStop = null;
ResetLongState();
return true;
}
private bool HasCapacity()
{
if (Volume <= 0m || MaxPositions <= 0)
return false;
var limitVolume = MaxPositions * Volume;
return Math.Abs(Position) < limitVolume - VolumeTolerance;
}
private decimal GetPriceStep()
{
var step = Security?.PriceStep ?? 0m;
return step <= 0m ? 1m : step;
}
private void ResetLongState()
{
_longEntryPrice = null;
_longTrailingStop = null;
}
private void ResetShortState()
{
_shortEntryPrice = null;
_shortTrailingStop = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import BollingerBands
class bollinger_bands_n_positions_strategy(Strategy):
"""BB breakout strategy with N-position control, SL/TP and trailing stop."""
def __init__(self):
super(bollinger_bands_n_positions_strategy, self).__init__()
self._max_positions = self.Param("MaxPositions", 9) \
.SetGreaterThanZero() \
.SetDisplay("Max Positions", "Net position limit in multiples of Volume", "Risk")
self._bb_period = self.Param("BollingerPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Period", "Moving average length", "Indicators")
self._bb_width = self.Param("BollingerWidth", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Width", "Standard deviation multiplier", "Indicators")
self._sl_pips = self.Param("StopLossPips", 50.0) \
.SetNotNegative() \
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk")
self._tp_pips = self.Param("TakeProfitPips", 50.0) \
.SetNotNegative() \
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk")
self._trail_pips = self.Param("TrailingStopPips", 5.0) \
.SetNotNegative() \
.SetDisplay("Trailing Stop (pips)", "Trailing-stop distance in pips", "Risk")
self._trail_step_pips = self.Param("TrailingStepPips", 5.0) \
.SetNotNegative() \
.SetDisplay("Trailing Step (pips)", "Trailing adjustment increment", "Risk")
self._vol_tol = self.Param("VolumeTolerance", 0.00000001) \
.SetNotNegative() \
.SetDisplay("Volume Tolerance", "Minimum net position magnitude treated as flat", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Source candles", "General")
self._long_entry = None
self._short_entry = None
self._long_trail = None
self._short_trail = None
@property
def MaxPositions(self):
return self._max_positions.Value
@property
def BollingerPeriod(self):
return self._bb_period.Value
@property
def BollingerWidth(self):
return self._bb_width.Value
@property
def StopLossPips(self):
return self._sl_pips.Value
@property
def TakeProfitPips(self):
return self._tp_pips.Value
@property
def TrailingStopPips(self):
return self._trail_pips.Value
@property
def TrailingStepPips(self):
return self._trail_step_pips.Value
@property
def VolumeTolerance(self):
return self._vol_tol.Value
@property
def CandleType(self):
return self._candle_type.Value
def _step(self):
sec = self.Security
s = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
return s
def OnStarted2(self, time):
super(bollinger_bands_n_positions_strategy, self).OnStarted2(time)
bb = BollingerBands()
bb.Length = self.BollingerPeriod
bb.Width = self.BollingerWidth
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(bb, self.process_candle).Start()
def process_candle(self, candle, bb_val):
if candle.State != CandleStates.Finished:
return
upper = float(bb_val.UpBand) if bb_val.UpBand is not None else 0.0
lower = float(bb_val.LowBand) if bb_val.LowBand is not None else 0.0
if self._handle_active(candle):
return
if not self.IsFormed:
return
self._try_long(candle, upper)
self._try_short(candle, lower)
def _handle_active(self, candle):
tol = float(self.VolumeTolerance)
if self.Position > tol:
return self._manage_long(candle)
if self.Position < -tol:
return self._manage_short(candle)
if self._long_entry is not None or self._short_entry is not None:
self._reset_long()
self._reset_short()
return False
def _manage_long(self, candle):
if self._long_entry is None:
self._long_entry = float(candle.ClosePrice)
entry = self._long_entry
step = self._step()
sl = float(self.StopLossPips)
tp = float(self.TakeProfitPips)
ts = float(self.TrailingStopPips)
tstp = float(self.TrailingStepPips)
if sl > 0:
if float(candle.LowPrice) <= entry - sl * step:
self.SellMarket()
self._reset_long()
return True
if tp > 0:
if float(candle.HighPrice) >= entry + tp * step:
self.SellMarket()
self._reset_long()
return True
if ts > 0 and tstp > 0:
td = ts * step
tstep = tstp * step
act = td + tstep
if float(candle.ClosePrice) - entry > act:
cand = float(candle.ClosePrice) - td
if self._long_trail is None or cand - self._long_trail > tstep:
self._long_trail = cand
if self._long_trail is not None and float(candle.LowPrice) <= self._long_trail:
self.SellMarket()
self._reset_long()
return True
return False
def _manage_short(self, candle):
if self._short_entry is None:
self._short_entry = float(candle.ClosePrice)
entry = self._short_entry
step = self._step()
sl = float(self.StopLossPips)
tp = float(self.TakeProfitPips)
ts = float(self.TrailingStopPips)
tstp = float(self.TrailingStepPips)
if sl > 0:
if float(candle.HighPrice) >= entry + sl * step:
self.BuyMarket()
self._reset_short()
return True
if tp > 0:
if float(candle.LowPrice) <= entry - tp * step:
self.BuyMarket()
self._reset_short()
return True
if ts > 0 and tstp > 0:
td = ts * step
tstep = tstp * step
act = td + tstep
if entry - float(candle.ClosePrice) > act:
cand = float(candle.ClosePrice) + td
if self._short_trail is None or self._short_trail - cand > tstep:
self._short_trail = cand
if self._short_trail is not None and float(candle.HighPrice) >= self._short_trail:
self.BuyMarket()
self._reset_short()
return True
return False
def _try_long(self, candle, upper):
if float(candle.ClosePrice) <= upper:
return
if not self._has_capacity():
return
tol = float(self.VolumeTolerance)
if self.Position < -tol:
self.BuyMarket()
self._reset_short()
return
if self.Position > tol:
self.SellMarket()
self._reset_long()
return
self.BuyMarket()
self._long_entry = float(candle.ClosePrice)
self._long_trail = None
self._reset_short()
def _try_short(self, candle, lower):
if float(candle.ClosePrice) >= lower:
return
if not self._has_capacity():
return
tol = float(self.VolumeTolerance)
if self.Position > tol:
self.SellMarket()
self._reset_long()
return
if self.Position < -tol:
self.BuyMarket()
self._reset_short()
return
self.SellMarket()
self._short_entry = float(candle.ClosePrice)
self._short_trail = None
self._reset_long()
def _has_capacity(self):
if self.Volume <= 0 or self.MaxPositions <= 0:
return False
limit = self.MaxPositions * self.Volume
return abs(self.Position) < limit - float(self.VolumeTolerance)
def _reset_long(self):
self._long_entry = None
self._long_trail = None
def _reset_short(self):
self._short_entry = None
self._short_trail = None
def OnReseted(self):
super(bollinger_bands_n_positions_strategy, self).OnReseted()
self._reset_long()
self._reset_short()
def CreateClone(self):
return bollinger_bands_n_positions_strategy()