GitHub で見る

バウンス数戦略

概要

バウンス数戦略は、MetaTrader インジケーター BounceNumber_V0.mq4 / BounceNumber_V1.mq4 の StockSharp ポートです。オリジナルのツールは、価格が対称チャネルから抜け出すまでに何回接触したかをカウントするビジュアルアナライザーでした。この C# 戦略は、高レベルの API を使用してバウンス カウンターを再作成し、結果を分布テーブルに保存し、完了したすべてのサイクルを戦略ログを通じて報告します。この実装は、MetaTrader ロジックに忠実でありながら、StockSharp のイベント駆動型パイプラインに適応させています。

元のインジケーターとは異なり、ポートは戦略コンポーネントとして実行されます。完成したローソク足をサブスクライブし、バンドのタッチを監視し、価格が半幅の 2 倍でチャネルから出る前に交互ヒットが何回発生するかを追跡します。収集された統計は、BounceDistribution プロパティまたは生成されたログ メッセージから利用できます。

仕組み

  1. ストラテジーが開始されると、機器がゼロ以外の PriceStep を公開しているかどうかが検証されます。ポイントベースの入力は、この値に基づいて MetaTrader の「ポイント」を小数の価格距離に変換します。
  2. CandleType から作成されたローソク足サブスクリプションは、完了したバーのみをバウンス アナライザーにフィードします。
  3. 最初に入ってくるローソク足がチャネルの中心 (終値) を定義します。半値幅が ChannelPoints * PriceStep に等しい対称バンドがその中心の周りに作成されます。
  4. 新しい完成したキャンドルはすべてサイクル カウンターをインクリメントし、次の 3 つのルールで評価されます。
    • ブレイクアウト検出: ローソク足の範囲が center ± 2 * halfWidth を超えると、現在のサイクルが終了し、そのバウンス数が記録されます。
    • 下位バンドタッチ: ローソク足が下位バンドにまたがっており、前のタッチも下位バンドタッチではなかった場合、バウンスカウンターが 1 つ増加し、方向が「下位」に切り替わります。
    • アッパーバンドタッチ: アッパーバンドの対称ルール。
  5. サイクルが MaxHistoryCandles よりも多くのローソク足で続いた場合(パラメーターが正の場合)、チャネルは強制的にリセットされ、価格が永久に横方向に変動した場合でもヒストグラムが更新されます。
  6. サイクルがリセットされるたびに、配布ディクショナリが更新され、元のインターフェイス カウンタの動作を反映する情報ログが生成されます。

この戦略では、意図的に注文を行うことはありません。これは、BounceDistribution 統計を使用する他のコンポーネント (ダッシュボード、UI、データ エクスポーター) と一緒にホストする必要があります。

パラメーター

名前 種類 デフォルト MetaTrader アナログ 説明
MaxHistoryCandles int 10000 maxbar 入力 強制リセット前の 1 サイクル内で許可されるキャンドルの最大数。安全リセットを無効にするには、0 に設定します。
ChannelPoints int 300 BPoints 入力 価格ポイントで表されるバウンス チャネルの半値幅 (PriceStep の倍数)。
CandleType DataType M1 の時間枠 TF 入力 バウンス計算に使用されるキャンドル シリーズ。

MetaTrader コードとの違い

  • ヒストグラムは、チャート上のテキスト オブジェクトではなく、辞書として保存されます。これにより、StockSharp ダッシュボードでの情報のエクスポートや視覚化が容易になります。
  • インジケーターからの UI 固有の入力 (色、フォント、ボタン) は表面的なものであり、分析ロジックに影響を与えないため、削除されます。
  • MaxHistoryCandles による強制リセットはオプションになり (0 は無効になります)、ライブ データ ストリームで機能しますが、MetaTrader は有限の履歴ブロックを処理しました。
  • すべての情報メッセージは AddInfoLog を通じて英語で書かれており、英語のみのコード コメント/ログの要件に一致します。

使い方のヒント

  • 選択したセキュリティが PriceStep を定義していることを確認してください。それ以外の場合、ポイントベースのオフセットを計算できないため、ストラテジーは開始時に例外をスローします。
  • この戦略を、BounceDistribution を読み取るカスタム UI ウィジェットまたはスクリプトと組み合わせて、MetaTrader グリッドのカウントを複製します。
  • 日中のノイズを分析する場合は、ChannelPoints に小さい値を使用し、より高い時間枠または変動性の高い商品の場合は大きい値を使用します。
  • MQL バージョンからの履歴スキャンをエミュレートするには、コネクタで HistoryBuildMode を有効にして戦略を開始し、要求された履歴範囲を処理させます。バックフィルされたキャンドルが配信されるとすぐに、ディストリビューションにデータが追加されます。
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>
/// Port of the "BounceNumber" MetaTrader indicator that counts how many times price bounces inside a channel before breaking it.
/// The strategy keeps track of the touch statistics and logs the distribution after each completed cycle.
/// </summary>
public class BounceNumberStrategy : Strategy
{
	private readonly StrategyParam<int> _maxHistoryCandles;
	private readonly StrategyParam<int> _channelPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Dictionary<int, int> _bounceDistribution = new();

	private decimal? _channelCenter;
	private int _bounceCount;
	private int _lastTouchDirection;
	private int _candlesInCycle;

	/// <summary>
	/// Maximum number of candles allowed inside one channel cycle before it is forcefully reset.
	/// </summary>
	public int MaxHistoryCandles
	{
		get => _maxHistoryCandles.Value;
		set => _maxHistoryCandles.Value = value;
	}

	/// <summary>
	/// Half-width of the bounce channel expressed in price points.
	/// </summary>
	public int ChannelPoints
	{
		get => _channelPoints.Value;
		set => _channelPoints.Value = value;
	}

	/// <summary>
	/// Candle series that feeds the bounce counter.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Provides read-only access to the accumulated bounce distribution.
	/// </summary>
	public IReadOnlyDictionary<int, int> BounceDistribution => _bounceDistribution;

	/// <summary>
	/// Initializes a new instance of the <see cref="BounceNumberStrategy"/> class.
	/// </summary>
	public BounceNumberStrategy()
	{
		_maxHistoryCandles = Param(nameof(MaxHistoryCandles), 10000)
			.SetNotNegative()
			.SetDisplay("Max History Candles", "Maximum number of candles inspected inside a single channel cycle", "General")
			;

		_channelPoints = Param(nameof(ChannelPoints), 10)
			.SetRange(10, 5000)
			.SetDisplay("Channel Half-Width", "Half height of the bounce channel measured in price points", "General")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to perform the bounce analysis", "Data");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_bounceDistribution.Clear();
		_channelCenter = null;
		_bounceCount = 0;
		_lastTouchDirection = 0;
		_candlesInCycle = 0;
	}

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

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(OnProcessCandle)
			.Start();
	}

	private void OnProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var channelHalf = GetChannelHalfWidth();

		if (channelHalf <= 0m)
			return;

		if (_channelCenter is null)
		{
			ResetChannel(candle.ClosePrice, channelHalf);
			return;
		}

		_candlesInCycle++;

		var center = _channelCenter.Value;
		var upperBand = center + channelHalf;
		var lowerBand = center - channelHalf;
		var breakUpper = center + channelHalf * 2m;
		var breakLower = center - channelHalf * 2m;

		var candleHigh = candle.HighPrice;
		var candleLow = candle.LowPrice;

		var breakoutUp = candleHigh >= breakUpper;
		var breakoutDown = candleLow <= breakLower;

		if (breakoutUp || breakoutDown || (_candlesInCycle >= MaxHistoryCandles && MaxHistoryCandles > 0))
		{
			RegisterBounceResult();
			ResetChannel(candle.ClosePrice, channelHalf);
			return;
		}

		var touchedLower = candleLow <= lowerBand && candleHigh >= lowerBand;
		var touchedUpper = candleHigh >= upperBand && candleLow <= upperBand;

		if (touchedLower && _lastTouchDirection >= 0)
		{
			_bounceCount++;
			_lastTouchDirection = -1;

			if (Position <= 0)
			{
				if (Position < 0)
					BuyMarket();
				BuyMarket();
			}
		}
		else if (touchedUpper && _lastTouchDirection <= 0)
		{
			_bounceCount++;
			_lastTouchDirection = 1;

			if (Position >= 0)
			{
				if (Position > 0)
					SellMarket();
				SellMarket();
			}
		}

		if (breakoutUp && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
		}
		else if (breakoutDown && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
		}
	}

	private void RegisterBounceResult()
	{
		if (!_bounceDistribution.TryGetValue(_bounceCount, out var occurrences))
			occurrences = 0;

		_bounceDistribution[_bounceCount] = occurrences + 1;

		LogInfo($"Channel cycle finished with {_bounceCount} bounce(s). Total occurrences for this count: {_bounceDistribution[_bounceCount]}.");
	}

	private void ResetChannel(decimal center, decimal channelHalf)
	{
		_channelCenter = center;
		_bounceCount = 0;
		_lastTouchDirection = 0;
		_candlesInCycle = 0;

		LogInfo($"Channel reset around price {center} with half-width {channelHalf}.");
	}

	private decimal GetChannelHalfWidth()
	{
		var priceStep = Security?.PriceStep;

		if (priceStep is null || priceStep.Value <= 0m)
			return ChannelPoints;

		return ChannelPoints * priceStep.Value;
	}
}