Auf GitHub ansehen

Strategie zur Auftragsstabilisierung

Überblick

Die Order Stabilization Strategy ist eine Umsetzung des MetaTrader-Expertenberaters hjueiisyx8lp2o379e_www_forex-instruments_info.mq4. Der ursprüngliche Roboter platziert ein Paar Stop-Orders rund um den aktuellen Preis und wartet auf einen Ausbruch. Sobald eine Position eröffnet wird, überwacht das System die jüngsten Kerzenkörper, um festzustellen, ob die Preisbewegung ins Stocken geraten („stabilisiert“) ist, und verlässt den Handel, wenn der Markt an Dynamik verliert oder eine vordefinierte Gewinnschwelle erreicht wird.

Dieser C#-Port behält die gleiche Logik bei, indem er das übergeordnete StockSharp API verwendet. Es basiert auf abgeschlossenen Kerzen statt auf rohen Ticks, was das Verhalten beim Backtesting und Live-Handel deterministisch macht.

Handelsregeln

  1. Wenn keine offenen Positionen oder aktiven Aufträge vorhanden sind, legt die Strategie einen Kaufstopp oberhalb des Marktes und einen Verkaufsstopp unterhalb des Marktes fest. Die Entfernung wird in MetaTrader Punkten gemessen (normalerweise gleich einem Pip).
  2. Wenn eine Stop-Order ausgeführt wird:
    • Der ausgeführte Auftrag eröffnet eine Position von OrderVolume Lots.
    • Die entgegengesetzte Stop-Order bleibt ausstehend, um einen Ausbruch in die andere Richtung abzufangen.
  3. Während eine Position offen ist, überprüft die Strategie die Körpergröße der beiden zuletzt abgeschlossenen Kerzen:
    • Wenn der letzte Kerzenkörper kleiner als StabilizationPoints ist und der variable Gewinn höher als ProfitThreshold ist, wird die Position geschlossen und die entgegengesetzte ausstehende Order storniert.
    • Wenn zwei aufeinanderfolgende Kerzen kleiner als StabilizationPoints sind, wird der Handel unabhängig vom aktuellen Gewinn geschlossen.
    • Wenn der Gewinn AbsoluteFixation erreicht, wird der Handel sofort geschlossen.
  4. Ausstehende Bestellungen werden nach ExpirationMinutes storniert und neu erstellt, es sei denn, der Wert ist auf Null gesetzt (unendliche Lebensdauer).

Parameter

Name Beschreibung Standard
OrderVolume Handelsvolumen in Lots, das für beide Stop-Eingaben verwendet wird. 0.1
OrderDistancePoints Abstand zwischen dem aktuellen Schlusskurs und jeder Stop-Order, ausgedrückt in MetaTrader Punkten. 20
ProfitThreshold Mindesterforderlicher variabler Gewinn (Kontowährung), bevor ein durch Stabilisierung ausgelöster Ausstieg zulässig ist. -2
AbsoluteFixation Gewinnniveau (Kontowährung), das einen sofortigen Ausstieg erzwingt. 30
StabilizationPoints Maximale Kerzenkörpergröße (Punkte), die einen flachen Markt signalisiert. 25
ExpirationMinutes Lebensdauer ausstehender Stop-Orders in Minuten. 0 deaktiviert den Ablauf. 20
CandleType Kerzentyp, der zur Bewertung der Stabilisierung verwendet wird (standardmäßig 5-Minuten-Zeitrahmen). TimeFrame(5m)

Konvertierungshinweise

  • Der ursprüngliche Fachberater operierte mit Chart-Ticks. Dieser Port wertet nur fertige Kerzen aus, wobei die Logik erhalten bleibt und gleichzeitig reproduzierbare Backtests gewährleistet werden.
  • MetaTrader „Punkte“ werden dem StockSharp PriceStep zugeordnet. Fehlt dem Instrument eine Preisstufe, wird eine Stufe von 1 angenommen.
  • Der Gewinn wird mithilfe von PriceStep und StepPrice angenähert, um Preisbewegungen in die Kontowährung umzurechnen.
  • Alle Codekommentare wurden in Englisch umgeschrieben und die Parametermetadaten enthalten benutzerfreundliche Beschreibungen mit Gruppierung.

Nutzung

  1. Fügen Sie die Strategie zu Ihrer StockSharp-Lösung hinzu und weisen Sie das gewünschte Wertpapier und Portfolio zu.
  2. Konfigurieren Sie die Parameter, insbesondere den Zeitrahmen der Kerze und die Entfernung in Punkten, um sie an die Eigenschaften des Instruments anzupassen.
  3. Starten Sie die Strategie. Es wird gepaarte Stop-Orders einreichen und Positionen gemäß der oben beschriebenen Stabilisierungslogik verwalten.

Weitere Ideen

  • Experimentieren Sie mit verschiedenen Kerzenintervallen, um Reaktionsfähigkeit und Geräuschfilterung in Einklang zu bringen.
  • Kombinieren Sie die Strategie mit Volatilitätsfiltern (ATR, Bollinger-Bänder), um den Handel während extrem ruhiger Sitzungen zu vermeiden.
  • Erweitern Sie die Logik mit Trailing Stops oder teilweisen Positionsausstiegen, sobald Sie sich dem absoluten Gewinnziel nähern.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.MatchingEngine;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Paired breakout stop orders with stabilization-based exits.
/// </summary>
public class OrderStabilizationStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _orderDistancePoints;
	private readonly StrategyParam<decimal> _profitThreshold;
	private readonly StrategyParam<decimal> _absoluteFixation;
	private readonly StrategyParam<decimal> _stabilizationPoints;
	private readonly StrategyParam<int> _expirationMinutes;
	private readonly StrategyParam<DataType> _candleType;

	private Order _buyStop;
	private Order _sellStop;
	private DateTime? _createdAt;
	private decimal _filledPosition;
	private decimal _entryValue;
	private decimal _previousBody;
	private bool _hasPreviousBody;

	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
	public decimal OrderDistancePoints { get => _orderDistancePoints.Value; set => _orderDistancePoints.Value = value; }
	public decimal ProfitThreshold { get => _profitThreshold.Value; set => _profitThreshold.Value = value; }
	public decimal AbsoluteFixation { get => _absoluteFixation.Value; set => _absoluteFixation.Value = value; }
	public decimal StabilizationPoints { get => _stabilizationPoints.Value; set => _stabilizationPoints.Value = value; }
	public int ExpirationMinutes { get => _expirationMinutes.Value; set => _expirationMinutes.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public OrderStabilizationStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero()
			.SetDisplay("Order Volume", "Trade volume in lots used for both stop entries", "Trading");
		_orderDistancePoints = Param(nameof(OrderDistancePoints), 20m).SetGreaterThanZero()
			.SetDisplay("Order Distance", "Distance between the current close price and each stop order, in points", "Trading");
		_profitThreshold = Param(nameof(ProfitThreshold), -2m)
			.SetDisplay("Profit Threshold", "Minimum floating profit (account currency) required before an exit triggered by stabilization is allowed", "Exit");
		_absoluteFixation = Param(nameof(AbsoluteFixation), 30m).SetNotNegative()
			.SetDisplay("Absolute Fixation", "Profit level (account currency) that forces an immediate exit", "Exit");
		_stabilizationPoints = Param(nameof(StabilizationPoints), 25m).SetGreaterThanZero()
			.SetDisplay("Stabilization", "Maximum candle body size (points) that signals a flat market", "Exit");
		_expirationMinutes = Param(nameof(ExpirationMinutes), 20).SetNotNegative()
			.SetDisplay("Expiration", "Lifetime of pending stop orders in minutes, 0 disables expiration", "Trading");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used to evaluate stabilization", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Ticks)];

	protected override void OnReseted()
	{
		base.OnReseted();
		_buyStop = null;
		_sellStop = null;
		_createdAt = null;
		_filledPosition = 0m;
		_entryValue = 0m;
		_previousBody = 0m;
		_hasPreviousBody = false;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		// Stop orders are matched against trade prints, so they fire between candle closes.
		Subscribe(new Subscription(DataType.Ticks, Security));
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

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

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

		var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);

		if (Position != 0m && IsExitDue(candle.ClosePrice, body, point))
		{
			CancelIfActive(_buyStop);
			CancelIfActive(_sellStop);
			Flatten();
		}
		else if ((Position == 0m && !IsWorking(_buyStop) && !IsWorking(_sellStop)) || IsExpired(candle.OpenTime))
		{
			ArmStops(candle.ClosePrice, candle.OpenTime, point);
		}

		_previousBody = body;
		_hasPreviousBody = true;
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var price = trade.Trade.Price;
		var volume = trade.Trade.Volume;
		var signed = trade.Order.Side == Sides.Buy ? volume : -volume;
		var previous = _filledPosition;

		_filledPosition += signed;

		if (_filledPosition == 0m)
			_entryValue = 0m;
		else if (previous == 0m || Math.Sign(previous) == Math.Sign(signed))
			_entryValue += price * volume;
		else if (Math.Sign(_filledPosition) != Math.Sign(previous))
			_entryValue = price * Math.Abs(_filledPosition);
		else
			_entryValue -= _entryValue / Math.Abs(previous) * volume;
	}

	private bool IsExitDue(decimal price, decimal body, decimal point)
	{
		var bodyLimit = StabilizationPoints * point;
		var pnl = FloatingPnL(price, point);
		var oneSmall = body <= bodyLimit;
		var twoSmall = oneSmall && _hasPreviousBody && _previousBody <= bodyLimit;

		return (oneSmall && pnl > ProfitThreshold) ||
			twoSmall ||
			(AbsoluteFixation > 0m && pnl >= AbsoluteFixation);
	}

	private bool IsExpired(DateTime time)
		=> ExpirationMinutes > 0 &&
			_createdAt is DateTime created &&
			time - created >= TimeSpan.FromMinutes(ExpirationMinutes);

	// Flat: both stops are placed around the close. With a position open only the pending
	// opposite stop is moved; the side that opened the position is never placed again.
	private void ArmStops(decimal center, DateTime time, decimal point)
	{
		var distance = OrderDistancePoints * point;

		if (Position == 0m || (Position < 0m && IsWorking(_buyStop)))
			_buyStop = RenewStop(_buyStop, Sides.Buy, center + distance);

		if (Position == 0m || (Position > 0m && IsWorking(_sellStop)))
			_sellStop = RenewStop(_sellStop, Sides.Sell, center - distance);

		_createdAt = IsWorking(_buyStop) || IsWorking(_sellStop) ? time : null;
	}

	private Order RenewStop(Order order, Sides side, decimal activationPrice)
	{
		if (order is { State: OrderStates.Active })
			CancelOrder(order);
		else if (IsWorking(order))
			return order;

		var stop = new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Side = side,
			Volume = OrderVolume,
			Type = OrderTypes.Conditional,
			Condition = new StopOrderCondition { ActivationPrice = activationPrice },
		};

		RegisterOrder(stop);
		return stop;
	}

	private static bool IsWorking(Order order)
		=> order is not null && order.State is not (OrderStates.Done or OrderStates.Failed);

	private void CancelIfActive(Order order)
	{
		if (order is { State: OrderStates.Active })
			CancelOrder(order);
	}

	private decimal FloatingPnL(decimal price, decimal point)
	{
		if (Position == 0m || _filledPosition == 0m)
			return 0m;

		var entryPrice = _entryValue / Math.Abs(_filledPosition);
		var direction = Position > 0m ? 1m : -1m;
		var move = (price - entryPrice) * direction;
		var stepPrice = Security?.StepPrice ?? 0m;

		return stepPrice > 0m
			? move / point * stepPrice * Math.Abs(Position)
			: move * (Security?.Multiplier ?? 1m) * Math.Abs(Position);
	}

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