GitHub で見る

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;
	}
}