Auf GitHub ansehen

BrakeoutTraderV1

BrakeoutTraderV1 ist ein einfaches Ausbruchssystem, das auf einem statischen Preisniveau basiert. Die Strategie beobachtet die Schlusskurse abgeschlossener Kerzen und steigt ein, wenn der Markt durch das gewählte Ausbruchsniveau schließt. Wenn der Schluss über das Niveau steigt, wird eine Long-Position eröffnet (abhängig von Richtungsfiltern); wenn er darunter fällt, wird eine Short-Position eingegangen. Die Positionsgröße wird aus dem konfigurierten Risikoanteil und dem Abstand zum Stop-Loss berechnet, was eine automatische Skalierung mit dem Kontokapital ermöglicht.

Handelslogik

  • Verarbeite nur abgeschlossene Kerzen des ausgewählten CandleType. Unvollständige Kerzen werden ignoriert.
  • Halte den zuletzt geschlossenen Preis, um Ausbrüche des benutzerdefinierten BreakoutLevel zu erkennen.
  • Long-Einstieg: Die neueste Kerze schließt über BreakoutLevel, während der vorherige Schluss auf oder unter dem Niveau lag, und EnableLong ist wahr. Jede offene Short-Position wird vor der neuen Order glattgestellt.
  • Short-Einstieg: Die neueste Kerze schließt unter BreakoutLevel, während der vorherige Schluss auf oder über dem Niveau lag, und EnableShort ist wahr. Jede Long-Position wird zuerst geschlossen.
  • Orders werden zu Marktpreisen gesendet. Die Menge wird so berechnet, dass der Verlust zwischen Einstiegspreis und Stop-Loss-Abstand dem RiskPercent des aktuellen Kontokapitals entspricht. Wenn die risikobasierte Größe nicht bestimmt werden kann, fällt die Strategie auf den Basis-Volume-Wert zurück.
  • Nach jedem Einstieg speichert die Strategie statische Gewinnmitnahme- und Stop-Loss-Niveaus in Pips (StopLossPoints und TakeProfitPoints). Wenn der Preis eines der Niveaus erreicht, wird die offene Position zu Marktpreisen geschlossen und die gecachten Niveaus werden gelöscht.
  • Es gibt niemals mehrere gleichzeitige offene Trades in dieselbe Richtung, da die Nettoposition explizit verwaltet wird.

Positionsverwaltung

  • Ein Schutz-Stop wird unterhalb des Einstiegs für Long-Trades und oberhalb für Shorts gesetzt. Der Abstand beträgt StopLossPoints * Pip, wobei Pip aus Security.PriceStep und seiner Genauigkeit abgeleitet wird (3 oder 5 Dezimalstellen implizieren eine zehnfache Anpassung, wie in der originalen MQL-Implementierung).
  • Ein Gewinnziel wird symmetrisch mit TakeProfitPoints gesetzt.
  • Wenn sowohl Stop als auch Ziel in derselben Kerze ausgelöst würden, wird der Stop zuerst bewertet, was konservative serverseitige Ausführung widerspiegelt.
  • Gegensätzliche Signale schließen immer jede aktive Position, bevor die neue aufgebaut wird, um gehämmerte Exposition zu verhindern.
  • Der Helper setzt gecachte Niveaus automatisch zurück, wenn die Position auf null zurückkehrt.

Parameter

  • BreakoutLevel – Statisches Preisniveau, das auf Ausbrüche überwacht wird.
  • EnableLong / EnableShort – Richtungsfilter, die das Öffnen von Long- oder Short-Positionen erlauben.
  • StopLossPoints – Stop-Loss-Abstand in Pips (Vielfache der abgeleiteten Pip-Größe).
  • TakeProfitPoints – Take-Profit-Abstand in Pips.
  • RiskPercent – Prozentsatz des Kontokapitals, das pro Trade riskiert wird. Wird verwendet, um das Ordervolumen aus dem Stop-Loss-Abstand zu bestimmen.
  • CandleType – Kerzendatenserie für die Signalgenerierung (Standard: 15-Minuten-Kerzen).
  • Volume – Basisordergröße, die verwendet wird, wenn die risikobasierte Berechnung nicht verfügbar ist.

Details

  • Einstiegskriterien: Schluss kreuzt über/unter BreakoutLevel bei der letzten abgeschlossenen Kerze.
  • Long/Short: Handelt beide Richtungen, gesteuert durch die EnableLong- und EnableShort-Flags.
  • Ausstiegskriterien: Statische Stop-Loss- und Take-Profit-Niveaus sowie Glattstellung bei entgegengesetzten Ausbruchssignalen.
  • Stops: Fest-Abstands-Stop-Loss in Pips gemessen.
  • Standardwerte: BreakoutLevel = 0, StopLossPoints = 140, TakeProfitPoints = 180, RiskPercent = 10, CandleType = 15 Minuten, EnableLong = EnableShort = true.
  • Filter: Keine über die Richtungsschalter hinaus; keine Trend- oder Volatilitätsfilter werden angewendet.

Verwendungshinweise

  • Das Instrument sollte die vom Original-EA verwendete Pip-Berechnung unterstützen. Bei Symbolen mit 3 oder 5 Dezimalstellen wird der Pip automatisch um zehn skaliert.
  • Sicherstellen, dass das verbundene Portfolio CurrentValue bereitstellt, damit die risikobasierte Größenberechnung korrekt funktioniert. Wenn das Kapital nicht verfügbar ist, fallen Trades auf das konfigurierte Volume zurück.
  • Da Orders zu Marktpreisen ausgeführt werden, können die tatsächlichen Füllungen vom Kerzenschluss abweichen. Stop- und Take-Abstände bei Bedarf anpassen, um Slippage zu berücksichtigen.
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 trades when the closing price crosses a predefined level.
/// The strategy sizes positions based on the selected risk percentage and applies static stop-loss and take-profit levels.
/// </summary>
public class BrakeoutTraderV1Strategy : Strategy
{
	private readonly StrategyParam<decimal> _breakoutLevel;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _previousClose;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _pipSize;

	/// <summary>
	/// Price level that must be broken to generate signals.
	/// </summary>
	public decimal BreakoutLevel
	{
		get => _breakoutLevel.Value;
		set => _breakoutLevel.Value = value;
	}

	/// <summary>
	/// Enables or disables long breakout trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Enables or disables short breakout trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in points relative to the pip size.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in points relative to the pip size.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Percentage of account equity to risk on each trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Candle type used to evaluate breakouts.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="BrakeoutTraderV1Strategy"/>.
	/// </summary>
	public BrakeoutTraderV1Strategy()
	{
		_breakoutLevel = Param(nameof(BreakoutLevel), 65000m)
			.SetDisplay("Breakout Level", "Static price level monitored for breakouts", "Signal");

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow long breakout positions", "Signal");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow short breakout positions", "Signal");

		_stopLossPoints = Param(nameof(StopLossPoints), 140m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss Points", "Stop-loss distance expressed in pip points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 180m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit Points", "Take-profit distance expressed in pip points", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Percentage of equity risked per trade", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for breakout detection", "General");
	}

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

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

		_previousClose = null;
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
		_pipSize = 0m;
	}

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

		var priceStep = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals;
		_pipSize = priceStep;

		if (decimals is 3 or 5)
			_pipSize *= 10m;

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

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

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

		// removed StartProtection(null, null)
	}

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

		if (ManageOpenPosition(candle))
		{
			UpdatePreviousClose(candle.ClosePrice);
			return;
		}

		var prevClose = _previousClose;
		if (prevClose is null)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		var currentClose = candle.ClosePrice;
		var prevValue = prevClose.Value;

		var breakoutUp = currentClose > BreakoutLevel && prevValue <= BreakoutLevel;
		var breakoutDown = currentClose < BreakoutLevel && prevValue >= BreakoutLevel;

		if (breakoutUp && EnableLong)
		{
			EnterLong(currentClose);
		}
		else if (breakoutDown && EnableShort)
		{
			EnterShort(currentClose);
		}

		_previousClose = currentClose;
	}

	private bool ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Exit long if stop-loss is touched.
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket();
				ResetPositionState();
				return true;
			}

			// Exit long if take-profit is reached.
			if (_takePrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket();
				ResetPositionState();
				return true;
			}
		}
		else if (Position < 0)
		{
			// Exit short if stop-loss is touched.
			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				ResetPositionState();
				return true;
			}

			// Exit short if take-profit is reached.
			if (_takePrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket();
				ResetPositionState();
				return true;
			}
		}
		else if (_entryPrice.HasValue)
		{
			// Reset cached levels after position is fully closed.
			ResetPositionState();
		}

		return false;
	}

	private void EnterLong(decimal price)
	{
		if (Position > 0)
			return;

		var volume = CalculateOrderVolume();
		var closingVolume = Position < 0 ? Math.Abs(Position) : 0m;
		var totalVolume = closingVolume + volume;

		if (totalVolume <= 0m)
			return;

		if (closingVolume > 0m)
			ResetPositionState();

		BuyMarket();
		SetPositionTargets(price, true, volume > 0m);
	}

	private void EnterShort(decimal price)
	{
		if (Position < 0)
			return;

		var volume = CalculateOrderVolume();
		var closingVolume = Position > 0 ? Position : 0m;
		var totalVolume = closingVolume + volume;

		if (totalVolume <= 0m)
			return;

		if (closingVolume > 0m)
			ResetPositionState();

		SellMarket();
		SetPositionTargets(price, false, volume > 0m);
	}

	private void SetPositionTargets(decimal entryPrice, bool isLong, bool hasNewPosition)
	{
		if (!hasNewPosition)
		{
			return;
		}

		_entryPrice = entryPrice;

		if (StopLossPoints > 0m && _pipSize > 0m)
			_stopPrice = isLong
				? entryPrice - StopLossPoints * _pipSize
				: entryPrice + StopLossPoints * _pipSize;
		else
			_stopPrice = null;

		if (TakeProfitPoints > 0m && _pipSize > 0m)
			_takePrice = isLong
				? entryPrice + TakeProfitPoints * _pipSize
				: entryPrice - TakeProfitPoints * _pipSize;
		else
			_takePrice = null;
	}

	private decimal CalculateOrderVolume()
	{
		var baseVolume = Volume;
		var stopDistance = StopLossPoints * _pipSize;

		if (stopDistance <= 0m || RiskPercent <= 0m)
			return AdjustVolume(baseVolume);

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return AdjustVolume(baseVolume);

		var riskValue = equity * RiskPercent / 100m;
		if (riskValue <= 0m)
			return AdjustVolume(baseVolume);

		var qty = riskValue / stopDistance;
		var adjusted = AdjustVolume(qty);

		return adjusted > 0m ? adjusted : AdjustVolume(baseVolume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		var security = Security;
		if (security != null)
		{
			var step = security.VolumeStep;
			if (step is decimal s && s > 0m)
			{
				volume = Math.Floor(volume / s) * s;
			}

			var min = security.MinVolume;
			if (min is decimal minVol && volume < minVol)
				volume = minVol;

			var max = security.MaxVolume;
			if (max is decimal maxVol && maxVol > 0m && volume > maxVol)
				volume = maxVol;

			if (volume <= 0m)
				volume = step is decimal stepVal && stepVal > 0m ? stepVal : 0m;
		}

		if (volume <= 0m)
			volume = volume == 0m ? 1m : Math.Abs(volume);

		return volume;
	}

	private void UpdatePreviousClose(decimal close)
	{
		_previousClose = close;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}
}