GitHub で見る

Bollinger ブレイクアウト DC2008

Sergey Pavlov (DC2008) のMetaTrader Bollinger ブレイクアウトエキスパートアドバイザーをStockSharp高レベル戦略APIに再実装したものです。戦略は完成したローソク足を監視し、選択した価格ソースでのBollinger Bandsのブレイクアウトを評価し、現在の取引が損失していない場合にのみポジションを開くか反転します。

概要

  • 設定されたタイムフレームと適用価格(終値、始値、高値、安値、中央値、典型値、加重値、または平均値)でBollinger Bandsエンベロープを計算します。
  • ローソク足の安値が下限バンドを下回りながら高値が中央バンドを下回る場合にロングセットアップを生成します(反転すべき強い下降延伸)。
  • ローソク足の高値が上限バンドを超えながら安値が中央バンドを上回る場合にショートセットアップを生成します(反転が期待される強い上昇延伸)。
  • オリジナルのMQLエキスパートはティックで取引していました。このポートでは安定性とインジケーターの整合性のために完成したローソク足ごとにシグナルが処理されます。
  • 既存のポジションが非負の未実現利益を示す場合にのみポジションを開くか反転し、オリジナルのリスクフィルターを再現します。

トレーディングロジック

インジケーターパイプライン

  1. 選択したCandleTypeのローソク足をサブスクライブします(デフォルト: 1時間タイムフレーム)。
  2. 選択した適用価格をBollinger Bandsインジケーターに入力します(Length = BandsPeriodWidth = BandsDeviation)。
  3. インジケーターが有効な上限、中央、下限値を生成するまでローソク足を無視します。

エントリー条件

  • 買い: Low < LowerBand かつ High < MiddleBand。ローソク足全体が下限バンドを突破した後、中央線より下で取引されたことを示します。
  • 売り: High > UpperBand かつ Low > MiddleBand。ローソク足全体が上限バンドを突破した後、中央線より上で取引されたことを示します。

ポジションフィルターと管理

  • ポジションがない場合、シグナルが現れたときに設定されたVolumeでマーケットオーダーを開きます。
  • ポジションがすでに存在する場合:
    • シグナルが現在の方向と反対の場合、ローソク足の終値を使用してPosition * (Close - PositionPrice)として未実現利益を計算します。
    • 未実現利益がの場合、このローソク足のすべてのアクションをスキップします(オリジナルの早期returnと同様)。
    • 未実現利益が非負でシグナルが反対の場合、現在のポジションを解消しシグナル方向に新しいポジションを確立するためにVolume + |Position|サイズの反転マーケットオーダーを送信します。
    • 現在の方向と一致するシグナルはポジションに追加しません(MQLバージョンと同じ)。
  • 明示的なストップロスやテイクプロフィットの注文はありません。取引の出口は利益フィルターを満たす反対シグナルを通じてのみ発生します。

パラメーター

名前 デフォルト値 説明
BandsPeriod 80 Bollinger移動平均と偏差の計算に使用するローソク足の数。正でなければならず、最適化可能です。
BandsDeviation 3.0 Bollinger Bandsの幅に適用される標準偏差乗数。正、最適化可能。
AppliedPrice Close インジケーターの価格ソース: Close、Open、High、Low、Median、Typical、Weighted、またはAverage (OHLC/4)。MetaTraderのENUM_APPLIED_PRICEを反映。
CandleType 1時間タイムフレーム 分析と注文に使用するローソク足タイプ(タイムフレーム)。StockSharpでサポートされている他のデータタイプに切り替え可能。
Volume(継承) ブローカー依存 新規エントリーの注文サイズ。反転時は既存のポジション絶対サイズを自動的に追加します。

オリジナルMQLエキスパートとの違い

  • MetaTrader EAは各ティックで条件を評価していましたが、このC#ポートは不完全なデータへの対応を避けるため完成したローソク足を待ちます。
  • インジケーターシフトはソースEAでゼロに固定されており、ここでも暗黙のままです。追加のシフトは公開されていません。
  • MetaTraderは浮動利益を直接報告していました。このポートはローソク足の終値とPositionPriceを通じて近似しており、フィルターが使用する符号比較には十分です。
  • MQLバージョンからの取引管理、文字列メッセージ、注文コメントは省略され、シグナル生成のみに焦点を当てています。

実装メモ

  • ローソク足、インジケーター、取引コールはStockSharpの高レベルAPI(SubscribeCandles().Bind(...)BuyMarketSellMarket)に依存しています。
  • UIにチャートエリアが利用可能な場合、インジケーターは自動的に描画されます。取引もデバッグのために表示されます。
  • 戦略はすべての起動時にインジケーターをリセットして再構築するため、パラメーター変更は次回の実行で即座に有効になります。
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 breakout strategy inspired by DC2008 implementation.
/// </summary>
public class BollingerBreakoutDc2008Strategy : Strategy
{
	public enum AppliedPriceTypes
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Average
	}

	private readonly StrategyParam<int> _bandsPeriod;
	private readonly StrategyParam<decimal> _bandsDeviation;
	private readonly StrategyParam<AppliedPriceTypes> _appliedPrice;
	private readonly StrategyParam<DataType> _candleType;

	private BollingerBands _bollinger;
	private decimal _entryPrice;

	public int BandsPeriod
	{
		get => _bandsPeriod.Value;
		set => _bandsPeriod.Value = value;
	}

	public decimal BandsDeviation
	{
		get => _bandsDeviation.Value;
		set => _bandsDeviation.Value = value;
	}

	public AppliedPriceTypes AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public BollingerBreakoutDc2008Strategy()
	{
		_bandsPeriod = Param(nameof(BandsPeriod), 80)
			.SetDisplay("Bands Period", "Number of candles for Bollinger Bands", "Indicators")
			.SetGreaterThanZero()
			
			.SetOptimize(10, 200, 10);

		_bandsDeviation = Param(nameof(BandsDeviation), 3m)
			.SetDisplay("Deviation", "Standard deviation multiplier", "Indicators")
			.SetGreaterThanZero()
			
			.SetOptimize(1m, 5m, 0.5m);

		_appliedPrice = Param(nameof(AppliedPrice), AppliedPriceTypes.Close)
			.SetDisplay("Applied Price", "Candle price source for Bollinger Bands", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to analyze", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, CandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_bollinger = null;
		_entryPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Create Bollinger Bands indicator with the configured parameters.
		_bollinger = new BollingerBands
		{
			Length = BandsPeriod,
			Width = BandsDeviation
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _bollinger);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished || _bollinger is null)
			return;

		// Calculate Bollinger Bands for the selected price source.
		var indicatorValue = _bollinger.Process(new DecimalIndicatorValue(_bollinger, GetAppliedPrice(candle), candle.OpenTime) { IsFinal = true });

		if (!indicatorValue.IsFinal)
			return;

		if (indicatorValue is not BollingerBandsValue bands)
			return;

		if (bands.UpBand is not decimal upper || bands.LowBand is not decimal lower || bands.MovingAverage is not decimal middle)
			return;

		var high = candle.HighPrice;
		var low = candle.LowPrice;
		var close = candle.ClosePrice;

		// Determine breakout conditions based on Bollinger structure.
		var buySignal = low < lower && high < middle;
		var sellSignal = high > upper && low > middle;

		if (!buySignal && !sellSignal)
			return;

		// Compute unrealized profit to mimic original position filter.
		var unrealizedPnL = Position == 0 ? 0m : Position * (close - _entryPrice);

		if (buySignal)
		{
			if (Position == 0)
			{
				// No position open, start a new long.
				BuyMarket();
				_entryPrice = close;
			}
			else
			{
				if (unrealizedPnL < 0m)
					return;

				if (Position < 0)
				{
					// Reverse from short to long while preserving target volume.
					BuyMarket();
					_entryPrice = close;
				}
			}

			return;
		}

		if (sellSignal)
		{
			if (Position == 0)
			{
				// No position open, start a new short.
				SellMarket();
				_entryPrice = close;
			}
			else
			{
				if (unrealizedPnL < 0m)
					return;

				if (Position > 0)
				{
					// Reverse from long to short while preserving target volume.
					SellMarket();
					_entryPrice = close;
				}
			}
		}
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		return AppliedPrice switch
		{
			AppliedPriceTypes.Close => candle.ClosePrice,
			AppliedPriceTypes.Open => candle.OpenPrice,
			AppliedPriceTypes.High => candle.HighPrice,
			AppliedPriceTypes.Low => candle.LowPrice,
			AppliedPriceTypes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceTypes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPriceTypes.Weighted => (candle.HighPrice + candle.LowPrice + (2m * candle.ClosePrice)) / 4m,
			AppliedPriceTypes.Average => (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}
}