Auf GitHub ansehen

Hinterhalt-Strategie

Die Hinterhalt-Strategie umgibt den Markt kontinuierlich mit einem Paar Buy-Stop- und Sell-Stop-Aufträgen. Die ausstehenden Aufträge werden in einem konfigurierbaren Abstand über dem besten Ask und unter dem besten Bid platziert, mit einer dynamischen Überschreibung, die eine Mindestdistanz basierend auf dem aktuellen Spread erzwingt. Sobald eine Seite ausgelöst wird, baut die Strategie sofort beide Aufträge neu auf, sodass der Markt von beiden Richtungen "im Hinterhalt" bleibt. Ein einfacher eigenkapitalbasierter Schutzschalter kann Positionen abflachen, sobald ein tägliches Gewinnziel oder Verlustziel erreicht ist.

Diese C#-Implementierung repliziert das Verhalten des ursprünglichen MetaTrader 5-Experten von Zuzabush. Sie arbeitet ausschließlich mit Level-1-Notierungen und benötigt keine Kerzen oder Indikatoren. Jede Entscheidung wird durch Echtzeit-Bid/Ask-Änderungen getrieben, daher ist die Strategie am besten für liquide Instrumente mit engen Spreads geeignet.

Handelslogik

  1. Marktdatenaufnahme
    • Die Strategie abonniert Level-1-Updates und verfolgt den neuesten besten Bid und Best Ask.
    • Berechnungen werden gestoppt, bis beide Seiten des Orderbuchs verfügbar sind und die Strategie handeln darf.
  2. Eigenkapital-Schutzmaßnahmen
    • Der realisierte PnL (PnL) und die unrealisierte Komponente aus dem aktuellen Bid/Ask und PositionPrice werden summiert.
    • Wenn das kombinierte Eigenkapital EquityTakeProfit überschreitet oder unter -EquityStopLoss fällt, wird die aktuelle Nettoposition mit einem Marktauftrag abgeflacht. Ausstehende Aufträge bleiben intakt und entsprechen dem ursprünglichen Expertenverhalten.
  3. Platzierung ausstehender Aufträge
    • Der Spread in Preiseinheiten wird mit MaxSpreadPoints verglichen. Wenn der Spread zu breit ist, werden keine neuen Aufträge platziert.
    • Andernfalls wird eine Distanz als max(IndentationPoints * step, spread * 3) berechnet. Dieser Wert repliziert die MT5-Logik, entweder den Benutzereinzug zu respektieren oder drei Spreads zu erzwingen, wenn der Broker StopsLevel null ist.
    • Ein Buy-Stop-Auftrag wird bei ask + Distanz platziert und ein Sell-Stop bei bid - Distanz. Preise werden auf den nächsten Tick normalisiert. Nur ein aktiver Auftrag pro Seite ist erlaubt; veraltete Aufträge werden bereinigt, wenn ihr Status auf Done, Failed oder Canceled wechselt.
  4. Trailing ausstehender Aufträge
    • Wenn TrailingStopPoints größer als null ist, berechnet die Strategie periodisch (nicht häufiger als Pause) die Stop-Distanz mithilfe von max((TrailingStopPoints + TrailingStepPoints) * step, spread * 3) neu und registriert die Aufträge erneut, wenn die Änderung einen halben Tick überschreitet.
    • Trailing hält die Aufträge nahe am Markt, während die Mindestdistanz eingehalten wird, die ein vorzeitiges Auslösen vermeidet.

Das Endergebnis ist eine gitterartige Ausbruchsmaschine, die ständig darauf wartet, dass der Preis in eine der Richtungen ausschlägt.

Parameter

Parameter Beschreibung
IndentationPoints Basisdistanz in Punkten zwischen dem Markt und jedem ausstehenden Stop-Auftrag.
MaxSpreadPoints Maximaler zulässiger Spread (in Punkten). Aufträge werden ausgesetzt, während der Spread breiter ist.
TrailingStopPoints Basis-Trailing-Distanz in Punkten für bestehende ausstehende Aufträge. Auf null setzen, um Trailing zu deaktivieren.
TrailingStepPoints Zusätzlicher Puffer, der zur Basis-Trailing-Distanz hinzugefügt wird.
Pause Mindestzeit zwischen zwei Trailing-Neuberechnungen. Der Standard entspricht der Ein-Sekunden-Pause des MT5-Experten.
EquityTakeProfit Eigenkapitalgewinn in Kontowährung, der ein sofortiges Abflachen der Position auslöst.
EquityStopLoss Zulässiger Eigenkapital-Drawdown, bevor die offene Position geschlossen wird.
Volume Auftragsgröße aus der Strategy-Basisklasse. Das Broker-Minimum verwenden, um den MT5-Standard nachzuahmen.

Alle Preisabstände werden von Punkten in tatsächliche Preiseinheiten umgerechnet unter Verwendung von Security.PriceStep. Falls das Instrument keinen Preisschritt bietet, wird ein Fallback-Wert von 1 verwendet.

Praktische Hinweise

  • Da die Strategie nur mit Stop-Aufträgen arbeitet, sind keine Kerzen oder Indikatoren erforderlich. Sie kann bei Backtests ausgeführt werden, die keine historischen Kerzen liefern, solange Level-1-Daten verfügbar sind.
  • Broker, die einen Nicht-Null-StopsLevel erzwingen, sollten IndentationPoints so konfigurieren, dass der resultierende Preisunterschied die Börsenpflicht erfüllt. Das Dreifach-Spread-Sicherheitsnetz dient als sekundäre Absicherung.
  • Der Eigenkapitalfilter ist absichtlich leicht berührend und storniert keine ausstehenden Aufträge. Dies spiegelt das ursprüngliche Hinterhalt-Verhalten wider und ermöglicht neue Trades nach dem Abflach-Ereignis ohne manuelle Eingriffe.
  • Slippage und Auftragserfüllungstoleranz werden vom verbundenen Broker oder Simulator gesteuert. Volume und Parameterwerte anpassen, um der Instrumentenvolatilität zu entsprechen.

Diese Dokumentation bietet absichtlich das maximale Detailniveau, damit sowohl diskretionäre als auch algorithmische Trader die Konvertierung verstehen und die Strategie für ihren Ausführungsort anpassen können.

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 converted from the Ambush MQL5 expert.
/// Enters on breakouts above/below previous candle range with trailing stop management.
/// </summary>
public class AmbushStrategy : Strategy
{
	private readonly StrategyParam<decimal> _indentationPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<decimal> _equityTakeProfit;
	private readonly StrategyParam<decimal> _equityStopLoss;
	private readonly StrategyParam<DataType> _candleType;

	private ICandleMessage _previousCandle;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal _priceStep;

	/// <summary>
	/// Distance from the market price for breakout detection, in points.
	/// </summary>
	public decimal IndentationPoints
	{
		get => _indentationPoints.Value;
		set => _indentationPoints.Value = value;
	}

	/// <summary>
	/// Trailing distance for stop orders, in points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Trailing step added to the base trailing distance, in points.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Target equity profit that triggers position flattening.
	/// </summary>
	public decimal EquityTakeProfit
	{
		get => _equityTakeProfit.Value;
		set => _equityTakeProfit.Value = value;
	}

	/// <summary>
	/// Maximum equity drawdown allowed before flattening positions.
	/// </summary>
	public decimal EquityStopLoss
	{
		get => _equityStopLoss.Value;
		set => _equityStopLoss.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="AmbushStrategy"/> class.
	/// </summary>
	public AmbushStrategy()
	{
		_indentationPoints = Param(nameof(IndentationPoints), 10m)
			.SetNotNegative()
			.SetDisplay("Indentation (points)", "Distance from price for breakout", "Orders");

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 10m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Base trailing distance", "Orders");

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 1m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Additional trailing offset", "Orders");

		_equityTakeProfit = Param(nameof(EquityTakeProfit), 15m)
			.SetNotNegative()
			.SetDisplay("Equity Take Profit", "Flatten positions once this profit is reached", "Risk");

		_equityStopLoss = Param(nameof(EquityStopLoss), 5m)
			.SetNotNegative()
			.SetDisplay("Equity Stop Loss", "Flatten positions after this loss", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(6).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for breakout detection", "General");

		Volume = 1;
	}

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

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

		_previousCandle = null;
		_entryPrice = 0m;
		_stopPrice = null;
		_priceStep = 0m;
	}

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

		_priceStep = Security?.PriceStep ?? 1m;
		if (_priceStep <= 0m) _priceStep = 1m;

		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;

		// Check equity targets.
		var pnl = PnL;
		if (EquityTakeProfit > 0m && pnl >= EquityTakeProfit)
		{
			FlattenPosition();
			_previousCandle = candle;
			return;
		}
		if (EquityStopLoss > 0m && pnl <= -EquityStopLoss)
		{
			FlattenPosition();
			_previousCandle = candle;
			return;
		}

		// Check trailing stop.
		if (Position > 0 && _stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
		{
			SellMarket(Position);
			ResetTargets();
		}
		else if (Position < 0 && _stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
		{
			BuyMarket(Math.Abs(Position));
			ResetTargets();
		}

		// Update trailing stop.
		UpdateTrailing(candle);

		// Entry logic - breakout above/below previous candle range.
		if (Position == 0 && _previousCandle != null)
		{
			var indentation = IndentationPoints * _priceStep;
			var buyLevel = _previousCandle.HighPrice + indentation;
			var sellLevel = _previousCandle.LowPrice - indentation;

			if (candle.HighPrice >= buyLevel)
			{
				BuyMarket(Volume);
				_entryPrice = candle.ClosePrice;
				var trailDist = (TrailingStopPoints + TrailingStepPoints) * _priceStep;
				_stopPrice = trailDist > 0m ? _entryPrice - trailDist : null;
			}
			else if (candle.LowPrice <= sellLevel)
			{
				SellMarket(Volume);
				_entryPrice = candle.ClosePrice;
				var trailDist = (TrailingStopPoints + TrailingStepPoints) * _priceStep;
				_stopPrice = trailDist > 0m ? _entryPrice + trailDist : null;
			}
		}

		_previousCandle = candle;
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m)
			return;

		var trailDist = (TrailingStopPoints + TrailingStepPoints) * _priceStep;
		if (trailDist <= 0m)
			return;

		if (Position > 0)
		{
			var newStop = candle.ClosePrice - trailDist;
			if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
				_stopPrice = newStop;
		}
		else if (Position < 0)
		{
			var newStop = candle.ClosePrice + trailDist;
			if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
				_stopPrice = newStop;
		}
	}

	private void FlattenPosition()
	{
		if (Position > 0)
			SellMarket(Position);
		else if (Position < 0)
			BuyMarket(Math.Abs(Position));
		ResetTargets();
	}

	private void ResetTargets()
	{
		_entryPrice = 0m;
		_stopPrice = null;
	}
}