GitHub で見る

Big Dog レンジブレイクアウト戦略

Big Dog戦略はロンドンの朝のセッション内の狭い統合ウィンドウを探し、そのボックスからのブレイクアウトを取引します。オリジナルのMQLエキスパートアドバイザーは、指定されたStartHourStopHourの間の価格レンジが設定可能なポイント数内に収まった場合にストップ注文を配置しました。StockSharpポートは同じアイデアを維持し、ブレイクアウト時に成行注文を使用し、統合の極値から導出された動的なストップロスとテイクプロフィットレベルを伴います。

トレーディングロジック

  1. StartHour(含む)からStopHour(デフォルトで除く)の間の完成したローソク足を収集して、日次レンジを構築します。
  2. セッションの高値と安値の差がMaxRangePoints(調整済みポイントサイズを使用して価格単位に変換)を超える場合、セッションを無視します。
  3. セッションが終了した後、現在の最良ask/bidとブレイクアウトレベルの間の距離を確認します。市場が高値から(ロングエントリーの場合)またはロー値から(ショートエントリーの場合)少なくともDistancePoints離れている場合にのみセットアップが有効化されます。
  4. 価格が後続のローソク足で準備済みの高値または安値を突破したら、OrderVolumeでサイジングした成行注文でエントリーします(反対のポジションを自動的にオフセット)。
  5. すぐに決済を設定します:
    • ロング取引は記録されたセッション安値にストップロスを、エントリーレベルのTakeProfitPoints上にテイクプロフィットを使用します。
    • ショート取引は記録されたセッション高値にストップロスを、エントリーレベルのTakeProfitPoints下にテイクプロフィットを使用します。
  6. 各完成したローソク足でストラテジーは高値/安値を監視してストップロスまたはテイクプロフィットに達したかどうかを判断し、それに応じてポジションをクローズします。
  7. 新しい取引日の始まりに、前のセッションからの残り注文を防ぐためにすべてのキャッシュされたレベルがリセットされます。

調整済みポイント。 戦略は、銘柄のPriceStepを乗じることでポイントベースの入力を実際の価格距離に変換します。有価証券が3または5桁の小数点以下を持つ場合、オリジナルEAで使用されるpipロジックを模倣するために値を追加的に10倍スケーリングします。

パラメーター

パラメーター 説明 デフォルト値
StartHour 統合ウィンドウが始まる時刻(0-23)。 14
StopHour 統合ウィンドウが終わる時刻(0-23)。 16
MaxRangePoints 調整済みポイントで測定されたセッションボックスの最大高さ。 50
TakeProfitPoints ブレイクアウト価格からの調整済みポイントでのテイクプロフィット距離。 50
DistancePoints 注文を有効化する前の現在の価格とブレイクアウトレベルの最小距離。 20
OrderVolume 各ブレイクアウト取引のボリューム(戦略のVolumeにも適用)。 1
CandleType セッションボックスの構築に使用するローソク足タイプ。デフォルトは1時間時間軸。 1h

実装上の注意事項

  • 戦略はローソク足と注文帳の両方を購読します。最良のbid/ask値が距離フィルターの評価に使用され、深度が利用できない場合は最後のローソク足のクローズにフォールバックします。
  • エントリーは成行注文で実行されます。これはオリジナルの保留ストップ注文の動作を反映しながら、高レベルAPI内に留まります。
  • ストップロスとテイクプロフィットの決定は、バー内高値と安値に基づいてローソク足のクローズ時に行われ、追加のチャイルド注文を登録せずにMQLバージョンの保護レベルをエミュレートします。
  • 日次状態管理は、カレンダー日付が変わると、アクティブな注文をキャンセルし、キャッシュされた高値/安値をリセットします。
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>
/// Big Dog range breakout strategy that trades breakouts from a tight range built between configurable hours.
/// </summary>
public class BigDogStrategy : Strategy
{
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _stopHour;
	private readonly StrategyParam<decimal> _maxRangePoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _distancePoints;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _rangeHigh;
	private decimal? _rangeLow;
	private DateTime? _rangeDate;
	private bool _longReady;
	private bool _shortReady;
	private decimal? _longStopPrice;
	private decimal? _longTakeProfitPrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakeProfitPrice;
	private decimal _longEntryPrice;
	private decimal _shortEntryPrice;
	private decimal? _bestBid;
	private decimal? _bestAsk;
	private decimal _adjustedPointSize;

	/// <summary>
	/// Hour (0-23) when the consolidation range calculation starts.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour (0-23) when the consolidation range calculation stops.
	/// </summary>
	public int StopHour
	{
		get => _stopHour.Value;
		set => _stopHour.Value = value;
	}

	/// <summary>
	/// Maximum acceptable range height measured in adjusted points.
	/// </summary>
	public decimal MaxRangePoints
	{
		get => _maxRangePoints.Value;
		set => _maxRangePoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance measured in adjusted points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Minimum distance required between the current price and breakout level, measured in adjusted points.
	/// </summary>
	public decimal DistancePoints
	{
		get => _distancePoints.Value;
		set => _distancePoints.Value = value;
	}

	/// <summary>
	/// Order volume that will be used for entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Candle type used for range calculation and breakout detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="BigDogStrategy"/> class.
	/// </summary>
	public BigDogStrategy()
	{
		_startHour = Param(nameof(StartHour), 2)
			.SetRange(0, 23)
			.SetDisplay("Start Hour", "Hour to begin measuring the range", "Session");

		_stopHour = Param(nameof(StopHour), 8)
			.SetRange(0, 23)
			.SetDisplay("Stop Hour", "Hour to stop measuring the range", "Session");

		_maxRangePoints = Param(nameof(MaxRangePoints), 50000m)
			.SetGreaterThanZero()
			.SetDisplay("Max Range", "Maximum allowed height of the consolidation range (points)", "Trading");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit", "Take-profit distance in adjusted points", "Trading");

		_distancePoints = Param(nameof(DistancePoints), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Min Distance", "Minimum distance from current price to breakout level (points)", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume used for each breakout order", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candles timeframe used for range detection", "Data");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_rangeHigh = null;
		_rangeLow = null;
		_rangeDate = null;
		_longReady = false;
		_shortReady = false;
		_longStopPrice = null;
		_longTakeProfitPrice = null;
		_shortStopPrice = null;
		_shortTakeProfitPrice = null;
		_longEntryPrice = 0m;
		_shortEntryPrice = 0m;
		_bestBid = null;
		_bestAsk = null;
		_adjustedPointSize = 0m;
	}

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

		Volume = OrderVolume;
		_adjustedPointSize = CalculateAdjustedPointSize();

		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;

		var currentDate = candle.OpenTime.Date;

		if (_rangeDate != currentDate)
		{
			ResetDailyState(currentDate);
		}

		UpdateRange(candle);

		if (candle.OpenTime.Hour >= StopHour)
		{
			PrepareBreakoutLevels(candle);
		}

		ProcessEntries(candle);
		ProcessRiskManagement(candle);
	}

	private void ResetDailyState(DateTime date)
	{
		_rangeDate = date;
		_rangeHigh = null;
		_rangeLow = null;
		_longReady = false;
		_shortReady = false;
		_longStopPrice = null;
		_longTakeProfitPrice = null;
		_shortStopPrice = null;
		_shortTakeProfitPrice = null;
	}

	private void UpdateRange(ICandleMessage candle)
	{
		var hour = candle.OpenTime.Hour;

		if (hour < StartHour || hour >= StopHour)
			return;

		_rangeHigh = _rangeHigh.HasValue
			? Math.Max(_rangeHigh.Value, candle.HighPrice)
			: candle.HighPrice;

		_rangeLow = _rangeLow.HasValue
			? Math.Min(_rangeLow.Value, candle.LowPrice)
			: candle.LowPrice;
	}

	private void PrepareBreakoutLevels(ICandleMessage candle)
	{
		if (!_rangeHigh.HasValue || !_rangeLow.HasValue)
			return;

		var rangeHeight = _rangeHigh.Value - _rangeLow.Value;
		var maxRange = ConvertToPrice(MaxRangePoints);

		if (rangeHeight >= maxRange)
		{
			// Reset pending plans when the range becomes too wide.
			_longReady = false;
			_shortReady = false;
			return;
		}

		var minDistance = ConvertToPrice(DistancePoints);
		var ask = _bestAsk ?? candle.ClosePrice;
		var bid = _bestBid ?? candle.ClosePrice;

		if (!_longReady && Position >= 0 && (_rangeHigh.Value - ask) > minDistance)
		{
			_longReady = true;
			_longEntryPrice = _rangeHigh.Value;
			_longStopPrice = _rangeLow.Value;
			_longTakeProfitPrice = _rangeHigh.Value + ConvertToPrice(TakeProfitPoints);
		}

		if (!_shortReady && Position <= 0 && (bid - _rangeLow.Value) > minDistance)
		{
			_shortReady = true;
			_shortEntryPrice = _rangeLow.Value;
			_shortStopPrice = _rangeHigh.Value;
			_shortTakeProfitPrice = _rangeLow.Value - ConvertToPrice(TakeProfitPoints);
		}
	}

	private void ProcessEntries(ICandleMessage candle)
	{
		var volume = OrderVolume;

		if (_longReady && candle.HighPrice >= _longEntryPrice && Position <= 0)
		{
			// Enter long on breakout of the session high.
			BuyMarket();

			_longReady = false;
			_shortReady = false;
		}

		if (_shortReady && candle.LowPrice <= _shortEntryPrice && Position >= 0)
		{
			// Enter short on breakout of the session low.
			SellMarket();

			_shortReady = false;
			_longReady = false;
		}
	}

	private void ProcessRiskManagement(ICandleMessage candle)
	{
		if (Position > 0 && _longStopPrice.HasValue && _longTakeProfitPrice.HasValue)
		{
			// Close the long position if stop-loss or take-profit levels are touched.
			if (candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket();
				_longStopPrice = null;
				_longTakeProfitPrice = null;
			}
			else if (candle.HighPrice >= _longTakeProfitPrice.Value)
			{
				SellMarket();
				_longStopPrice = null;
				_longTakeProfitPrice = null;
			}
		}
		else if (Position < 0 && _shortStopPrice.HasValue && _shortTakeProfitPrice.HasValue)
		{
			// Close the short position if stop-loss or take-profit levels are touched.
			if (candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket();
				_shortStopPrice = null;
				_shortTakeProfitPrice = null;
			}
			else if (candle.LowPrice <= _shortTakeProfitPrice.Value)
			{
				BuyMarket();
				_shortStopPrice = null;
				_shortTakeProfitPrice = null;
			}
		}
		else
		{
			_longStopPrice = null;
			_longTakeProfitPrice = null;
			_shortStopPrice = null;
			_shortTakeProfitPrice = null;
		}
	}

	private decimal ConvertToPrice(decimal points)
	{
		return points * _adjustedPointSize;
	}

	private decimal CalculateAdjustedPointSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var decimals = Security?.Decimals ?? 0;
		var multiplier = decimals == 3 || decimals == 5 ? 10m : 1m;

		return step * multiplier;
	}
}