Auf GitHub ansehen

Zeitbasierte Range-Breakout-Strategie

Überblick

Diese Strategie ist eine direkte Portierung des MetaTrader 4 Expert Advisors Tttttt_www_forex-instruments_info.mq4. Es erstellt einmal pro Tag zu einem konfigurierbaren Zeitpunkt Intraday-Breakout-Level. Immer wenn der Preis über diesen Niveaus schließt, eröffnet die Strategie eine Position in Ausbruchsrichtung. Exits werden durch dynamische Gewinn- und Verlustdistanzen gesteuert, die aus einem Durchschnitt historischer Tagesbereiche abgeleitet werden.

Kernlogik

  1. Tägliche Snapshot-Zeit – Bei CheckHour:CheckMinute friert die Strategie die Höchst- und Tiefststände des aktuellen Tages ein und schließt alle offenen Positionen.
  2. Berechnung des durchschnittlichen Bereichs – Der Algorithmus aggregiert die letzten DaysToCheck-Statistiken:
    • CheckMode = 1: nutzt den gesamten Hoch-/Tief-Bereich jedes abgeschlossenen Tages.
    • CheckMode = 2: Verwendet die absolute Differenz zwischen den Prüfzeitabschlüssen aufeinanderfolgender Tage.
  3. Level-Konstruktion – Der Durchschnittswert wird durch OffsetFactor dividiert, um ein oberes und unteres Ausbruchsband um das Hoch/Tief des aktuellen Tages zu erstellen. Der gleiche Durchschnitt wird durch ProfitFactor und LossFactor dividiert, um dynamische Take-Profit- und Stop-Distanzen abzuleiten.
  4. Eintrittsfenster – Nach dem täglichen Schnappschuss beobachtet die Strategie, dass die Kerze bis 23:00 Uhr schließt. Wenn ein Schlusskurs das obere Band durchbricht und keine Position offen ist, wird gekauft; Wenn das untere Band durchbrochen wird, wird es verkauft. Die Anzahl der Einträge pro Tag ist auf TradesPerDay begrenzt.
  5. Exit-Management – Während Sie sich in einer Position befinden, vergleicht die Strategie den Schlusskurs mit dem durchschnittlichen Einstiegspreis (Strategy.PositionPrice). Sobald die Bewegung dafür oder dagegen die konfigurierten Gewinn- oder Verlustabstände erreicht, wird die Position zum Marktwert geschlossen. Bei CloseMode = 2 werden alle verbleibenden Positionen ebenfalls zu Beginn des nächsten Handelstages geschlossen.

Parameter

Name Beschreibung Standard
CheckHour Stunde (0–23), in der der tägliche Bereichs-Snapshot erstellt wird. 8
CheckMinute Minute (0-59), in der der Schnappschuss aufgenommen wird. 0
DaysToCheck Anzahl der historischen Tage, die für die Mittelwertbildung verwendet werden. 7
CheckMode 1 = tägliche Höchst-/Tiefstspanne verwenden, 2 = absolute Differenz zwischen aufeinanderfolgenden Prüfzeitabschlüssen verwenden. 1
ProfitFactor Dividiert den gemittelten Wert, um die Gewinnzielentfernung zu erhalten. 2
LossFactor Dividiert den gemittelten Wert, um die Verlustdistanz zu erhalten. 2
OffsetFactor Dividiert den gemittelten Wert, um den Ausbruchsversatz um Hoch/Tief zu erhalten. 2
CloseMode 1 = Positionen über Nacht halten, 2 = reduzieren, wenn sich der Kalendertag ändert. 1
TradesPerDay Maximal zulässige Anzahl an Einträgen pro Tag. 1
CandleType Für alle Berechnungen verwendete Kerzenserie (standardmäßig 15-Minuten-Kerzen). 15m Zeitrahmen

Alle Parameter werden über Strategy.Param erstellt, sodass sie die Optimierung sofort unterstützen.

Unterschiede zur MQL-Version

  • MetaTrader verfolgt variable Gewinne direkt; Der StockSharp-Port rekonstruiert es aus Position und PositionPrice, wenn Exits ausgewertet werden.
  • Der MT4-Code zählte aktive Bestellungen über Ticketschleifen. Der Port verwendet TradesPerDay zusammen mit der aggregierten Position, um die Anzahl der Transaktionen am selben Tag unter Kontrolle zu halten.
  • Das ursprüngliche Skript stützte sich auf historische Puffer (z. B. Highest, Lowest). Die StockSharp-Version speichert tägliche Statistiken intern, vermeidet explizite Indikatorpuffer und respektiert gleichzeitig die übergeordneten API-Richtlinien.
  • Schützende Stop-Loss- und Take-Profit-Orders wurden zusammen mit dem Markteintritt in MT4 gesendet. Der Hafen führt eine entsprechende Risikokontrolle durch, indem er Kerzenschließungen überwacht und Marktausstiegsaufträge sendet, wenn Schwellenwerte erreicht werden.

Nutzungshinweise

  • Verwenden Sie eine Kerzenserie, die der Balkengröße des ursprünglichen MQL-Setups entspricht (in der Referenzdatei wurden 15-Minuten-Balken verwendet).
  • Stellen Sie mindestens DaysToCheck abgeschlossene Tage an historischen Daten bereit, bevor Sie mit der Strategie beginnen, andernfalls bleiben Ausbruchsniveaus inaktiv.
  • Halten Sie bei der Optimierung die Faktoren positiv, um sinnvolle Ausbruchs- und Risikoschwellen beizubehalten.
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();
		}
	}
}