Bollinger Bands N ポジション v2 戦略
概要
この戦略は Vladimir Karputov による「Bollinger Bands N positions v2」エキスパートアドバイザーを再現しています。完成したローソク足で動作し、Bollinger Bands エンベロープに対する価格ブレイクアウトを探します。StockSharp ポートは元のピラミッディング動作、リスク管理、トレーリングロジックを維持しながら、プラットフォームのネッティングモデルに注文管理を適応させています。
トレードロジック
- Bollinger Bands 指標(期間と偏差は設定可能)が選択されたローソク足シリーズで計算されます。
- ローソク足の終値が上部バンドを超えて終了すると、戦略はアクティブなショートエクスポージャーを閉じ、追加のロングポジションを開きます(設定された最大スタックエントリー数まで)。
- ローソク足の終値が下部バンドを下回って終了すると、戦略はアクティブなロングエクスポージャーを閉じ、追加のショートポジションを開きます(最大エントリーパラメーターによっても制限されます)。
- ポジションサイズは同じ方向にピラミッディングする際に固定のインクリメント(Volume パラメーター)で増加します。
- スタックされたポジションの平均エントリー価格が追跡され、ストップロス、テイクプロフィット、トレーリングストップレベルを一貫して管理します。
リスク管理
- ストップロスとテイクプロフィットの距離はピップで入力されます。銘柄の価格ステップを掛けることで絶対価格オフセットに変換されます。3 桁または 5 桁の小数点を持つ銘柄は、MetaTrader のピップサイズ調整を模倣するために自動的にステップを 10 倍します。
- トレーリングストップオフセットとトレーリングステップもピップで設定されます。トレーリングメカニズムは、トレードが現在の平均エントリーから
TrailingStop + TrailingStepピップ動いた後にのみストップ価格を更新します。各更新はトレーリングオフセットでストップをシフトし、過度な修正を避けるために追加のステップバッファを尊重します。 - 保護的なエグジット注文は戦略内でシミュレートされます:完成したローソク足がストップまたはターゲットレベルを越えると、成行注文を使用してポジション全体が閉じられます。
パラメーター
| パラメーター | 説明 |
|---|---|
| Bollinger Period | Bollinger Bands 移動平均のルックバック期間。 |
| Bollinger Deviation | Bollinger エンベロープの標準偏差乗数。 |
| Max Positions | 方向ごとに許可される最大スタックエントリー数。 |
| Volume | 各個別エントリーの注文ボリューム。 |
| Stop Loss (pips) | ピップでのストップロス距離(0 はストップを無効にします)。 |
| Take Profit (pips) | ピップでのテイクプロフィット距離(0 はターゲットを無効にします)。 |
| Trailing Stop (pips) | ピップでのトレーリングストップ距離(0 はトレーリングを無効にします)。 |
| Trailing Step (pips) | トレーリングストップを再び動かす前に必要な追加ピップ利益。トレーリングが有効の場合は正でなければなりません。 |
| Candle Type | 戦略によって処理されるローソク足シリーズ。 |
実装上の注意
- 戦略は StockSharp のガイドラインに従って、指標バインディング付きの高レベルローソク足サブスクリプションを使用します。
- MetaTrader の元の「新しいバー」ロジックを反映するために、完成したローソク足のみが処理されます。
- StockSharp はネッティングモードで動作するため、変換は他の方向に新しいピラミッドレイヤーを開く前に反対のエクスポージャーを閉じます。
- トレーリングストップがアクティブなときはトレーリングステップがゼロより大きくなければならず、元のエキスパートアドバイザーの安全確認と一致します。
- 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()