GitHub で見る

時間ベースのレンジブレイクアウト戦略

概要

この戦略は、MetaTrader 4 エキスパート アドバイザー Tttttt_www_forex-instruments_info.mq4 を直接移植したものです。 1 日に 1 回、設定可能な時間に日中のブレイクアウト レベルを構築します。価格がこれらのレベルを超えて終了すると、戦略はブレイクアウト方向にポジションをオープンします。イグジットは、過去の日の範囲の平均から導出される動的な利益と損失の距離によって管理されます。

中核ロジック

  1. 毎日のスナップショット時間CheckHour:CheckMinute で、戦略は当日の高値と安値を凍結し、オープン ポジションをすべて閉じます。
  2. 平均範囲の計算 – アルゴリズムは最新の DaysToCheck 統計を集計します。
    • CheckMode = 1: 完了した各日の高値/低値範囲全体を使用します。
    • CheckMode = 2: 連続する日のチェックタイム終了間の絶対差を使用します。
  3. レベルの構築 – 平均値を OffsetFactor で割って、その日の高値/安値の周囲に上下のブレイクアウト バンドを作成します。同じ平均を ProfitFactorLossFactor で割って、動的な利食い距離とストップ距離を導き出します。
  4. エントリーウィンドウ – 毎日のスナップショットの後、戦略は 23:00 までローソク足が閉じるのを監視します。終値が上部バンドを突き抜け、オープンポジションがない場合は買います。下のバンドが壊れていても販売されます。 1 日あたりのエントリ数は TradesPerDay に制限されています。
  5. 出口管理 – ポジションがある間、この戦略は終値と平均エントリー価格 (Strategy.PositionPrice) を比較します。有利または反対の動きが設定された利益または損失の距離に達すると、ポジションは市場でクローズされます。 CloseMode = 2 の場合、残ったポジションも次の取引日の開始時に決済されます。

パラメーター

名前 説明 デフォルト
CheckHour 日次範囲スナップショットが取得される時間 (0 ~ 23)。 8
CheckMinute スナップショットが作成されたときの分 (0 ~ 59)。 0
DaysToCheck 平均化に使用される過去の日数。 7
CheckMode 1 = 毎日の高値/安値範囲を使用、2 = 連続するチェック時間終了間の絶対差を使用します。 1
ProfitFactor 平均値を除算して利益目標距離を取得します。 2
LossFactor 平均値を除算して損失距離を求めます。 2
OffsetFactor 平均値を除算して、高値/安値付近のブレイクアウト オフセットを取得します。 2
CloseMode 1 = ポジションを一晩維持し、2 = 暦日が変わるとフラットになります。 1
TradesPerDay 1 日に許可されるエントリの最大数。 1
CandleType すべての計算に使用されるローソク足シリーズ (デフォルトは 15 分足ローソク足)。 15m 時間枠

すべてのパラメータは Strategy.Param を通じて作成されるため、すぐに最適化がサポートされます。

MQL バージョンとの違い

  • MetaTrader は変動利益を直接追跡します。 StockSharp ポートは、出口を評価するときに、PositionPositionPrice からポートを再構築します。
  • MT4 コードは、チケット ループを介してアクティブな注文をカウントしました。ポートは、TradesPerDay と集計ポジションを併用して、同日取引の数を管理します。
  • 元のスクリプトは履歴バッファー (例: HighestLowest) に依存していました。 StockSharp バージョンは、毎日の統計を内部に保存し、明示的なインジケーター バッファーを回避しながら、高レベルの API ガイドラインを尊重します。
  • 保護的なストップロス注文と利食い注文は、MT4 での市場エントリーと一緒に送信されました。ポートは、ローソク足の終値を監視し、しきい値に達したときに市場撤退注文を送信することで、同等のリスク管理を実行します。

使用上の注意

  • 元の MQL セットアップの足のサイズと一致するローソク足シリーズを使用します (参照ファイルでは 15 分足が使用されました)。
  • 戦略を開始する前に、少なくとも DaysToCheck 日分の完了日の履歴データを提供してください。そうしないと、ブレイクアウト レベルは非アクティブなままになります。
  • 最適化するときは、意味のあるブレイクアウトとリスクのしきい値を維持するために、要因をポジティブに保ちます。
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>
/// Breakout strategy that prepares daily buy/sell levels at a specified time.
/// The offset and profit targets are derived from the average range of previous days.
/// </summary>
public class TimeBasedRangeBreakoutStrategy : Strategy
{

	private readonly StrategyParam<int> _checkHour;
	private readonly StrategyParam<int> _checkMinute;
	private readonly StrategyParam<int> _daysToCheck;
	private readonly StrategyParam<int> _checkMode;
	private readonly StrategyParam<decimal> _profitFactor;
	private readonly StrategyParam<decimal> _lossFactor;
	private readonly StrategyParam<decimal> _offsetFactor;
	private readonly StrategyParam<int> _closeMode;
	private readonly StrategyParam<int> _tradesPerDay;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _lastOpenHour;

	private Queue<decimal> _rangeHistory;
	private Queue<decimal> _closeDiffHistory;

	private DateTime? _currentDay;
	private DateTime? _levelsDay;
	private decimal _dayHigh;
	private decimal _dayLow;
	private decimal _buyBreakout;
	private decimal _sellBreakout;
	private decimal _profitDistance;
	private decimal _lossDistance;
	private decimal? _previousCheckClose;
	private decimal? _currentCheckClose;
	private int _tradesOpenedToday;
	private bool _levelsReady;
	private decimal _entryPrice;

	/// <summary>
	/// Hour of the day when the reference range is calculated.
	/// </summary>
	public int CheckHour
	{
		get => _checkHour.Value;
		set => _checkHour.Value = value;
	}

	/// <summary>
	/// Minute of the hour when the reference range is calculated.
	/// </summary>
	public int CheckMinute
	{
		get => _checkMinute.Value;
		set => _checkMinute.Value = value;
	}

	/// <summary>
	/// Number of previous days used for averaging.
	/// </summary>
	public int DaysToCheck
	{
		get => _daysToCheck.Value;
		set => _daysToCheck.Value = value;
	}

	/// <summary>
	/// Mode of averaging: 1 - daily range, 2 - absolute close-to-close difference.
	/// </summary>
	public int CheckMode
	{
		get => _checkMode.Value;
		set => _checkMode.Value = value;
	}

	/// <summary>
	/// Divisor applied to convert the average range into a take-profit distance.
	/// </summary>
	public decimal ProfitFactor
	{
		get => _profitFactor.Value;
		set => _profitFactor.Value = value;
	}

	/// <summary>
	/// Divisor applied to convert the average range into a stop-loss distance.
	/// </summary>
	public decimal LossFactor
	{
		get => _lossFactor.Value;
		set => _lossFactor.Value = value;
	}

	/// <summary>
	/// Divisor applied to convert the average range into the breakout offset.
	/// </summary>
	public decimal OffsetFactor
	{
		get => _offsetFactor.Value;
		set => _offsetFactor.Value = value;
	}

	/// <summary>
	/// Defines whether to flatten at the daily boundary (2 = close on new day).
	/// </summary>
	public int CloseMode
	{
		get => _closeMode.Value;
		set => _closeMode.Value = value;
	}

	/// <summary>
	/// Maximum number of trades allowed per day.
	/// </summary>
	public int TradesPerDay
	{
		get => _tradesPerDay.Value;
		set => _tradesPerDay.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}
	/// <summary>
	/// Last hour of the day when breakout orders are allowed to remain open.
	/// </summary>
	public int LastOpenHour
	{
		get => _lastOpenHour.Value;
		set => _lastOpenHour.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public TimeBasedRangeBreakoutStrategy()
	{
		_checkHour = Param(nameof(CheckHour), 8)
		.SetDisplay("Check Hour", "Hour of the day used for daily calculations", "Schedule")
		.SetRange(0, 23);

		_checkMinute = Param(nameof(CheckMinute), 0)
		.SetDisplay("Check Minute", "Minute of the hour used for daily calculations", "Schedule")
		.SetRange(0, 59);

		_daysToCheck = Param(nameof(DaysToCheck), 7)
		.SetGreaterThanZero()
		.SetDisplay("Days To Check", "Number of previous days used in averaging", "Averaging")
		
		.SetOptimize(3, 15, 1);

		_checkMode = Param(nameof(CheckMode), 1)
		.SetDisplay("Check Mode", "1 - use daily range, 2 - use absolute close difference", "Averaging")
		.SetRange(1, 2);

		_profitFactor = Param(nameof(ProfitFactor), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Profit Factor", "Divisor applied to average range for take-profit", "Risk")
		
		.SetOptimize(1m, 4m, 0.5m);

		_lossFactor = Param(nameof(LossFactor), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Loss Factor", "Divisor applied to average range for stop-loss", "Risk")
		
		.SetOptimize(1m, 4m, 0.5m);

		_offsetFactor = Param(nameof(OffsetFactor), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Offset Factor", "Divisor applied to average range for breakout levels", "Entries")
		
		.SetOptimize(1m, 4m, 0.5m);

		_closeMode = Param(nameof(CloseMode), 1)
		.SetDisplay("Close Mode", "1 - keep positions overnight, 2 - close on new day", "Risk")
		.SetRange(1, 2);

		_tradesPerDay = Param(nameof(TradesPerDay), 1)
		.SetGreaterThanZero()
		.SetDisplay("Trades Per Day", "Maximum entries allowed within one day", "Risk")
		
		.SetOptimize(1, 3, 1);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Primary candle series used by the strategy", "Data");
		_lastOpenHour = Param(nameof(LastOpenHour), 23)
			.SetDisplay("Last Open Hour", "Hour after which new trades are not opened", "Schedule")
			.SetRange(0, 23);
	}

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

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

		_rangeHistory = null;
		_closeDiffHistory = null;
		_currentDay = null;
		_levelsDay = null;
		_dayHigh = 0m;
		_dayLow = 0m;
		_buyBreakout = 0m;
		_sellBreakout = 0m;
		_profitDistance = 0m;
		_lossDistance = 0m;
		_previousCheckClose = null;
		_currentCheckClose = null;
		_tradesOpenedToday = 0;
		_levelsReady = false;
		_entryPrice = 0m;
	}

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

		_rangeHistory = new();
		_closeDiffHistory = new();

		StartProtection(null, null);

		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;

		UpdateDailyState(candle);
		TryCalculateLevels(candle);

		

		ManageOpenPosition(candle);
		TryEnterPosition(candle);
	}

	private void UpdateDailyState(ICandleMessage candle)
	{
		var candleDate = candle.OpenTime.Date;

		if (_currentDay is null || candleDate != _currentDay.Value)
		{
			if (_currentDay is not null)
			FinalizePreviousDay();

			if (CloseMode == 2 && Position != 0m)
			ClosePosition();

			_currentDay = candleDate;
			_dayHigh = candle.HighPrice;
			_dayLow = candle.LowPrice;
			_levelsReady = false;
			_levelsDay = null;
			_currentCheckClose = null;
			_tradesOpenedToday = 0;
		}
		else
		{
			if (candle.HighPrice > _dayHigh)
			_dayHigh = candle.HighPrice;

			if (candle.LowPrice < _dayLow)
			_dayLow = candle.LowPrice;
		}
	}

	private void FinalizePreviousDay()
	{
		var dayRange = _dayHigh - _dayLow;
		if (dayRange > 0m)
		if (_rangeHistory != null)
		EnqueueWithLimit(_rangeHistory, dayRange, DaysToCheck);

		if (_currentCheckClose is decimal checkClose)
		{
			if (_previousCheckClose is decimal previousClose)
			{
				var difference = Math.Abs(checkClose - previousClose);
				if (difference > 0m)
				if (_closeDiffHistory != null)
				EnqueueWithLimit(_closeDiffHistory, difference, DaysToCheck);
			}

			_previousCheckClose = checkClose;
		}

		_currentCheckClose = null;
	}

	private void TryCalculateLevels(ICandleMessage candle)
	{
		if (candle.OpenTime.Hour != CheckHour || candle.OpenTime.Minute != CheckMinute)
		return;

		_currentCheckClose = candle.ClosePrice;

		if (Position != 0m)
		ClosePosition();

		if (!TryGetAverage(out var average))
		{
			_levelsReady = false;
			_levelsDay = null;
			return;
		}

		var offset = OffsetFactor > 0m ? average / OffsetFactor : 0m;
		_profitDistance = ProfitFactor > 0m ? average / ProfitFactor : 0m;
		_lossDistance = LossFactor > 0m ? average / LossFactor : 0m;

		_buyBreakout = _dayHigh + offset;
		_sellBreakout = _dayLow - offset;
		_levelsReady = true;
		_levelsDay = _currentDay;

		LogInfo($"Levels prepared for {candle.OpenTime:yyyy-MM-dd}. High={_dayHigh}, Low={_dayLow}, Avg={average}, BuyLevel={_buyBreakout}, SellLevel={_sellBreakout}.");
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position == 0m)
		return;

		var entryPrice = _entryPrice;
		if (entryPrice == 0m)
		return;

		if (Position > 0m)
		{
			var reachedProfit = _profitDistance > 0m && candle.ClosePrice - entryPrice >= _profitDistance;
			var reachedLoss = _lossDistance > 0m && entryPrice - candle.ClosePrice >= _lossDistance;

			if (reachedProfit || reachedLoss)
			SellMarket();
		}
		else if (Position < 0m)
		{
			var reachedProfit = _profitDistance > 0m && entryPrice - candle.ClosePrice >= _profitDistance;
			var reachedLoss = _lossDistance > 0m && candle.ClosePrice - entryPrice >= _lossDistance;

			if (reachedProfit || reachedLoss)
			BuyMarket();
		}
	}

	private void TryEnterPosition(ICandleMessage candle)
	{
		if (!_levelsReady || _levelsDay is null || _currentDay is null)
		return;

		if (_levelsDay != _currentDay)
		return;

		if (_tradesOpenedToday >= TradesPerDay)
		return;

		if (candle.OpenTime.Hour > LastOpenHour)
		return;

		if (Position != 0m)
		return;

		if (candle.ClosePrice >= _buyBreakout)
		{
			BuyMarket();
			_entryPrice = candle.ClosePrice;
			_tradesOpenedToday++;
		}
		else if (candle.ClosePrice <= _sellBreakout)
		{
			SellMarket();
			_entryPrice = candle.ClosePrice;
			_tradesOpenedToday++;
		}
	}

	private bool TryGetAverage(out decimal average)
	{
		average = 0m;
		var source = CheckMode == 2 ? _closeDiffHistory : _rangeHistory;
		if (source == null)
		return false;

		var sum = 0m;
		var count = 0;

		foreach (var value in source)
		{
			sum += value;
			count++;
		}

		if (count == 0)
		return false;

		average = sum / count;
		return true;
	}

	private static void EnqueueWithLimit(Queue<decimal> queue, decimal value, int limit)
	{
		queue.Enqueue(value);

		while (queue.Count > limit)
		queue.Dequeue();
	}

	private void ClosePosition()
	{
		if (Position > 0m)
		{
			SellMarket();
		}
		else if (Position < 0m)
		{
			BuyMarket();
		}
	}
}