GitHub で見る

ボリンジャーバンドNポジション戦略

概要

この戦略はMetaTraderエキスパートアドバイザーBollinger Bands N positionsのStockSharpポートです。ボリンジャーバンドエンベロープに対する終値を監視し、市場がチャネル外でバーを終了するたびにポジションに入ります。ポジション管理は総エクスポージャーの上限を設定し、固定のストップロスとテイクプロフィットオフセットを配置し、取引が十分に利益が出たらトレーリングストップを有効にすることで元のエキスパートを再現します。

トレードロジック

  1. 設定されたロウソク足タイプを購読し、選択した期間と幅でボリンジャーバンドを計算します。
  2. 完成した各ロウソク足で、戦略はまず既存のポジションをクローズする必要があるかどうかを確認します:
    • ロングポジションは固定ストップロス、固定テイクプロフィット、またはトレーリングストップレベルが突破されたときに終了します。
    • ショートポジションは対称的なロジックを適用します。
  3. 取引が許可されていて、現在のバーで終了が発生していない場合、エントリーシグナルが評価されます:
    • 終値が上限バンドを上回っている場合、戦略はショートエクスポージャーをフラット化し、ポジション上限内であれば要求されたボリュームで新しいロングポジションをオープンします。
    • 終値が下限バンドを下回っている場合、ロングエクスポージャーをフラット化し、同様にショートポジションをオープンします。
  4. トレーリングストップは、取引がトレーリング距離とトレーリングステップを合わせた量だけ前進したら、トレーリングステップパラメーターで定義されたインクリメントで動きます。トレーリングレベルはトレーリング距離だけ価格の後ろに留まり、利益が少なくとも1つのトレーリングステップ増加したときのみ前進します。

ポジション管理

  • Max PositionsMaxPositions × 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実装のトレーリングロジックと一致しますが、終了は次の完成したロウソク足で発生することを意味します。
  • トレーリング設定は起動時に検証されます。ゼロのトレーリングステップでトレーリングストップを有効にすると、元の初期化チェックを模倣するために例外がスローされます。

使用上の注記

  1. インストゥルメントの契約サイズとティック値に合わせてVolumeMaxPositions、リスクパラメーターを設定します。
  2. 証券が有効なPriceStepを公開していることを確認してください。ステップがゼロまたは欠落している場合、戦略はフォールバックとして1を使用しますが、すべての市場に適しているわけではありません。
  3. 不完全なデータで行動しないよう、インジケーターウォームアップ期間(ボリンジャー期間)が完了した後にのみ戦略を開始してください。
  4. リスク設定をカスタマイズする際はトレーリングステップ検証エラーのログを監視してください。
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;
	}
}