ボリンジャーバンドNポジション戦略
概要
この戦略はMetaTraderエキスパートアドバイザーBollinger Bands N positionsのStockSharpポートです。ボリンジャーバンドエンベロープに対する終値を監視し、市場がチャネル外でバーを終了するたびにポジションに入ります。ポジション管理は総エクスポージャーの上限を設定し、固定のストップロスとテイクプロフィットオフセットを配置し、取引が十分に利益が出たらトレーリングストップを有効にすることで元のエキスパートを再現します。
トレードロジック
- 設定されたロウソク足タイプを購読し、選択した期間と幅でボリンジャーバンドを計算します。
- 完成した各ロウソク足で、戦略はまず既存のポジションをクローズする必要があるかどうかを確認します:
- ロングポジションは固定ストップロス、固定テイクプロフィット、またはトレーリングストップレベルが突破されたときに終了します。
- ショートポジションは対称的なロジックを適用します。
- 取引が許可されていて、現在のバーで終了が発生していない場合、エントリーシグナルが評価されます:
- 終値が上限バンドを上回っている場合、戦略はショートエクスポージャーをフラット化し、ポジション上限内であれば要求されたボリュームで新しいロングポジションをオープンします。
- 終値が下限バンドを下回っている場合、ロングエクスポージャーをフラット化し、同様にショートポジションをオープンします。
- トレーリングストップは、取引がトレーリング距離とトレーリングステップを合わせた量だけ前進したら、トレーリングステップパラメーターで定義されたインクリメントで動きます。トレーリングレベルはトレーリング距離だけ価格の後ろに留まり、利益が少なくとも1つのトレーリングステップ増加したときのみ前進します。
ポジション管理
- Max Positionsは
MaxPositions × Volumeとして測定される最大ネットエクスポージャーを定義します。StockSharpはネッティングモードで動作するため、戦略は一度に1つのネットポジションのみ保持できます。パラメーターは、現在の絶対ポジションが設定された制限に既に達しているときに戦略が再エントリーするのを防ぐ安全上限として機能します。 - ストップロスとテイクプロフィットの距離はpipsで指定されます。戦略は証券の
PriceStepを使用してそれらを価格に変換します。インストゥルメントが小数pipの価格設定を使用している場合は、それに応じて値を調整する必要があるかもしれません。 - トレーリングストップは距離とステップの両方が正である必要があります。トレーリングストップ距離をゼロに設定すると、トレーリングモジュールが無効になります。
パラメーター
| パラメーター | 説明 | デフォルト |
|---|---|---|
Volume |
各エントリーに使用するロット単位の注文サイズ。 | 0.1 |
MaxPositions |
Volumeの倍数で表されたネットポジション上限。 |
9 |
BollingerPeriod |
ボリンジャー移動平均のルックバック期間。 | 20 |
BollingerWidth |
ボリンジャーバンドの標準偏差乗数。 | 2 |
StopLossPips |
pips単位のストップロス距離。 | 50 |
TakeProfitPips |
pips単位のテイクプロフィット距離。 | 50 |
TrailingStopPips |
pips単位のトレーリングストップ距離。無効にするには0に設定。 |
5 |
TrailingStepPips |
トレーリングストップが前進する前に必要な最小利益インクリメント。 | 5 |
CandleType |
ボリンジャーバンド構築に使用する時間軸またはカスタムロウソク足タイプ。 | 1分時間軸 |
MQL5エキスパートとの違い
- 元のエキスパートはMetaTraderのヘッジングモードで動作し、同時にロングとショートポジションを保持できます。StockSharpの戦略はネッティングされているため、このポートは新しい取引に入る前に反対のエクスポージャーをフラット化します。したがって
MaxPositionsパラメーターは独立したチケット数の代わりにネットポジションの絶対サイズを制限します。 - 注文ストップは添付されたストップ注文として送信される代わりに戦略内でシミュレートされます。これはMQL実装のトレーリングロジックと一致しますが、終了は次の完成したロウソク足で発生することを意味します。
- トレーリング設定は起動時に検証されます。ゼロのトレーリングステップでトレーリングストップを有効にすると、元の初期化チェックを模倣するために例外がスローされます。
使用上の注記
- インストゥルメントの契約サイズとティック値に合わせて
Volume、MaxPositions、リスクパラメーターを設定します。 - 証券が有効な
PriceStepを公開していることを確認してください。ステップがゼロまたは欠落している場合、戦略はフォールバックとして1を使用しますが、すべての市場に適しているわけではありません。 - 不完全なデータで行動しないよう、インジケーターウォームアップ期間(ボリンジャー期間)が完了した後にのみ戦略を開始してください。
- リスク設定をカスタマイズする際はトレーリングステップ検証エラーのログを監視してください。
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()