Auf GitHub ansehen

Tages-Bruchpunkt-Strategie

Überblick

Die Tages-Bruchpunkt-Strategie ist eine StockSharp-Portierung des MetaTrader 5-Experten «Daily BreakPoint» (Build 19498). Der Algorithmus überwacht den Abstand zwischen dem aktuellen Preis und der täglichen Eröffnung. Wenn die Bewegung von der täglichen Eröffnung einen konfigurierbaren Schwellenwert überschreitet und die vorherige Kerze strenge Körpergrößenanforderungen erfüllt, tritt die Strategie in Richtung des Ausbruchs ein oder dreht die bestehende Exposition, abhängig vom CloseBySignal-Flag.

Die Strategie arbeitet gleichzeitig mit zwei Datenstreams:

  1. Intraday-Kerzen, definiert durch den CandleType-Parameter, für die Signalgenerierung.
  2. Tageskerzen, um den letzten Sitzungseröffnungspreis zu verfolgen.

Handelslogik

  1. Wenn eine neue Intraday-Kerze schließt, liest die Strategie den letzten täglichen Eröffnungspreis und berechnet die Ausbruchsniveaus mit BreakPointPips (in absolute Preise über die Tick-Größe des Instruments umgerechnet).
  2. Die Körpergröße der kürzlich geschlossenen Kerze muss innerhalb des Bereichs [LastBarSizeMinPips, LastBarSizeMaxPips] liegen.
  3. Bullisches Setup
    • Die Kerze muss über ihrer Eröffnung schließen (Close > Open).
    • Der Schlusskurs muss mindestens BreakPointPips über der täglichen Eröffnung liegen.
    • Der Ausbruchspreis (tägliche Eröffnung + Bruchpunkt) muss innerhalb des Kerzenkörpers liegen.
    • Wenn CloseBySignal = false, öffnet die Strategie eine Long-Position. Andernfalls schließt sie jede offene Long-Exposition und eröffnet eine Short-Position.
  4. Bärisches Setup spiegelt den bullischen Fall: eine bärische Kerze, deren Schlusskurs mindestens BreakPointPips unter der täglichen Eröffnung liegt und deren Körper das Ausbruchsniveau enthält, löst entweder einen Short-Einstieg (CloseBySignal = false) oder eine Umkehr in eine Long-Position (CloseBySignal = true) aus.
  5. Orders werden als Marktorders mit dem konfigurierten OrderVolume gesendet. Die Positionsgröße ist kumulativ, sodass mehrere Signale die Position in beide Richtungen skalieren können.

Risikomanagement

  • Stop-Loss / Take-Profit: Optionale feste Ziele definiert in Pips (StopLossPips, TakeProfitPips). Wenn auf null gesetzt, ist das entsprechende Level deaktiviert. Die Strategie wertet Kerzen-Hochs und -Tiefs aus, um Treffer zu erkennen.
  • Trailing-Stop: Aktiviert wenn TrailingStopPips > 0. Sobald der offene Gewinn TrailingStopPips + TrailingStepPips überschreitet, wird der Stop um TrailingStopPips hinter dem Preis her gezogen. Der Schritt-Parameter verhindert häufige Stop-Anpassungen in flachen Märkten.
  • Alle Preisabstände werden aus Pips über den PriceStep des Instruments umgerechnet. Bei 3- oder 5-Dezimalstellen-Kursstellung entspricht ein Pip zehn Preisschritten, was das ursprüngliche Experten-Verhalten repliziert.

Parameter

Name Beschreibung
OrderVolume Basisvolumen für jede Marktorder.
CloseBySignal Wenn true, schließt die Strategie bestehende Positionen und öffnet die entgegengesetzte Richtung, wenn ein Ausbruchssignal erscheint.
BreakPointPips Abstand von der täglichen Eröffnung erforderlich, um einen Ausbruch zu bestätigen.
LastBarSizeMinPips / LastBarSizeMaxPips Minimale und maximale Körpergröße der Auslösekerze.
TrailingStopPips Trailing-Stop-Abstand. Auf 0 setzen zum Deaktivieren.
TrailingStepPips Zusätzliche Bewegung vor jeder Trailing-Anpassung erforderlich.
StopLossPips Optionaler fester Stop-Loss. 0 deaktiviert ihn.
TakeProfitPips Optionaler fester Take-Profit. 0 deaktiviert ihn.
CandleType Intraday-Kerzenserie für die Signalgenerierung.

Verwendungshinweise

  • Die Strategie abonniert automatisch sowohl Intraday- als auch Tageskerzen. Stellen Sie sicher, dass der Datenanbieter die angeforderten Zeitrahmen unterstützt.
  • Da die Logik fertige Kerzen auswertet, werden Orders beim Schlusskurs der Signalbar gesendet.
  • Die Pip-Konvertierung setzt Forex-Kursstellung voraus. Überprüfen Sie die Standardwerte bei Anwendung der Strategie auf Instrumente mit unkonventionellen Tick-Größen.
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>
/// Daily breakout strategy that reacts to the distance from the daily open.
/// Converted from the MetaTrader Daily BreakPoint expert advisor.
/// </summary>
public class DailyBreakPointStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<bool> _closeBySignal;
	private readonly StrategyParam<decimal> _breakPointPips;
	private readonly StrategyParam<decimal> _lastBarSizeMinPips;
	private readonly StrategyParam<decimal> _lastBarSizeMaxPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _currentDayOpen;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal _pipSize;

	/// <summary>
	/// Order volume.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Reverse the position when the opposite signal appears.
	/// </summary>
	public bool CloseBySignal
	{
		get => _closeBySignal.Value;
		set => _closeBySignal.Value = value;
	}

	/// <summary>
	/// Break distance from the daily open expressed in pips.
	/// </summary>
	public decimal BreakPointPips
	{
		get => _breakPointPips.Value;
		set => _breakPointPips.Value = value;
	}

	/// <summary>
	/// Minimum size of the previous bar body in pips.
	/// </summary>
	public decimal LastBarSizeMinPips
	{
		get => _lastBarSizeMinPips.Value;
		set => _lastBarSizeMinPips.Value = value;
	}

	/// <summary>
	/// Maximum size of the previous bar body in pips.
	/// </summary>
	public decimal LastBarSizeMaxPips
	{
		get => _lastBarSizeMaxPips.Value;
		set => _lastBarSizeMaxPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing stop step in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Fixed stop loss in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Fixed take profit in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Intraday candle type used for signal calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="DailyBreakPointStrategy"/> class.
	/// </summary>
	public DailyBreakPointStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Default order volume", "General");

		_closeBySignal = Param(nameof(CloseBySignal), true)
		.SetDisplay("Close By Signal", "Reverse existing position on opposite signal", "General");

		_breakPointPips = Param(nameof(BreakPointPips), 5m)
		.SetGreaterThanZero()
		.SetDisplay("Break Point (pips)", "Distance from the daily open", "Signals");

		_lastBarSizeMinPips = Param(nameof(LastBarSizeMinPips), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Last Bar Min (pips)", "Minimum body size of the previous bar", "Signals");

		_lastBarSizeMaxPips = Param(nameof(LastBarSizeMaxPips), 5000m)
		.SetGreaterThanZero()
		.SetDisplay("Last Bar Max (pips)", "Maximum body size of the previous bar", "Signals");

		_trailingStopPips = Param(nameof(TrailingStopPips), 2m)
		.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 2m)
		.SetDisplay("Trailing Step (pips)", "Minimum move before trailing", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 0m)
		.SetDisplay("Stop Loss (pips)", "Fixed stop loss distance", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 30m)
		.SetDisplay("Take Profit (pips)", "Fixed take profit distance", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Intraday candle series", "Data");
	}

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

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

		_currentDayOpen = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_longEntryPrice = null;
		_shortEntryPrice = null;
		_pipSize = 0m;
	}

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

		Volume = OrderVolume;
		_pipSize = CalculatePipSize();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent)
		);

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

		var dailySubscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		dailySubscription.Bind(ProcessDailyCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, intradaySubscription);
			DrawOwnTrades(area);
		}
	}

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

		var decimals = Security?.Decimals;
		if (decimals == 3 || decimals == 5)
		return step * 10m;

		return step;
	}

	private decimal NormalizePrice(decimal price)
	{
		var step = Security?.PriceStep;
		if (step is null || step.Value <= 0m)
		return price;

		var value = price / step.Value;
		var rounded = Math.Round(value, 0, MidpointRounding.AwayFromZero);
		return rounded * step.Value;
	}

	private void ProcessDailyCandle(ICandleMessage candle)
	{
		if (candle.State == CandleStates.Finished || candle.State == CandleStates.Active)
		_currentDayOpen = candle.OpenPrice;
	}

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

		Volume = OrderVolume;


		if (_pipSize <= 0m)
		_pipSize = CalculatePipSize();

		var dayOpen = _currentDayOpen;
		if (dayOpen is null)
		return;

		var breakOffset = BreakPointPips * _pipSize;
		var minBody = LastBarSizeMinPips * _pipSize;
		var maxBody = LastBarSizeMaxPips * _pipSize;
		var trailingStop = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;
		var stopLossOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
		var takeProfitOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;

		UpdateTrailing(candle, trailingStop, trailingStep);
		HandleRiskExits(candle);

		var bodySize = Math.Abs(candle.ClosePrice - candle.OpenPrice);
		var minPrice = Math.Min(candle.OpenPrice, candle.ClosePrice);
		var maxPrice = Math.Max(candle.OpenPrice, candle.ClosePrice);

		var breakBuy = dayOpen.Value + breakOffset;
		var breakSell = dayOpen.Value - breakOffset;

		var bullishBody = candle.ClosePrice > candle.OpenPrice;
		var bearishBody = candle.ClosePrice < candle.OpenPrice;

		var bullishSignal = bullishBody && breakOffset > 0m &&
		candle.ClosePrice - dayOpen.Value >= breakOffset &&
		bodySize >= minBody &&
		(maxBody <= 0m || bodySize <= maxBody);

		var bearishSignal = bearishBody && breakOffset > 0m &&
		dayOpen.Value - candle.ClosePrice >= breakOffset &&
		bodySize >= minBody &&
		(maxBody <= 0m || bodySize <= maxBody);

		if (bullishSignal)
		{
			ExecuteBullishSignal(candle.ClosePrice, stopLossOffset, takeProfitOffset);
		}
		else if (bearishSignal)
		{
			ExecuteBearishSignal(candle.ClosePrice, stopLossOffset, takeProfitOffset);
		}
	}

	private void UpdateTrailing(ICandleMessage candle, decimal trailingStop, decimal trailingStep)
	{
		if (trailingStop <= 0m)
		return;

		if (Position > 0 && _longEntryPrice.HasValue)
		{
			var profit = candle.ClosePrice - _longEntryPrice.Value;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = candle.ClosePrice - (trailingStop + trailingStep);
				if (!_longStopPrice.HasValue || _longStopPrice.Value < threshold)
				_longStopPrice = NormalizePrice(candle.ClosePrice - trailingStop);
			}
		}
		else if (Position < 0 && _shortEntryPrice.HasValue)
		{
			var profit = _shortEntryPrice.Value - candle.ClosePrice;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = candle.ClosePrice + (trailingStop + trailingStep);
				if (!_shortStopPrice.HasValue || _shortStopPrice.Value > threshold || _shortStopPrice.Value == 0m)
				_shortStopPrice = NormalizePrice(candle.ClosePrice + trailingStop);
			}
		}
	}

	private void HandleRiskExits(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0m && _longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket();
				ResetLongState();
				return;
			}

			if (volume > 0m && _longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value)
			{
				SellMarket();
				ResetLongState();
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0m && _shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket();
				ResetShortState();
				return;
			}

			if (volume > 0m && _shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value)
			{
				BuyMarket();
				ResetShortState();
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}
	}

	private void ExecuteBullishSignal(decimal entryPrice, decimal stopLossOffset, decimal takeProfitOffset)
	{
		if (CloseBySignal)
		{
			if (Position > 0)
			{
				var volume = Math.Abs(Position);
				SellMarket();
			}

			ResetLongState();

			SellMarket();

			_shortEntryPrice = entryPrice;
			_shortStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice + stopLossOffset) : null;
			_shortTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice - takeProfitOffset) : null;
		}
		else
		{
			BuyMarket();

			_longEntryPrice = entryPrice;
			_longStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice - stopLossOffset) : null;
			_longTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice + takeProfitOffset) : null;
			ResetShortState();
		}
	}

	private void ExecuteBearishSignal(decimal entryPrice, decimal stopLossOffset, decimal takeProfitOffset)
	{
		if (CloseBySignal)
		{
			if (Position < 0)
			{
				var volume = Math.Abs(Position);
				BuyMarket();
			}

			ResetShortState();

			BuyMarket();

			_longEntryPrice = entryPrice;
			_longStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice - stopLossOffset) : null;
			_longTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice + takeProfitOffset) : null;
		}
		else
		{
			SellMarket();

			_shortEntryPrice = entryPrice;
			_shortStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice + stopLossOffset) : null;
			_shortTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice - takeProfitOffset) : null;
			ResetLongState();
		}
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
	}
}