Auf GitHub ansehen

Parabolic SAR First-Dot-Strategie

Überblick

Die Parabolic SAR First Dot Strategy ist die StockSharp High-Level-Konvertierung des MetaTrader Expert Advisors pSAR_bug_4 aus dem Ordner MQL/9954. Das System reagiert auf den allerersten Punkt des Parabolic SAR, der auf der gegenüberliegenden Seite des Preises erscheint. Wenn der SAR unter den Schlusskurs fällt, wird ein Long-Trade eröffnet; Wenn der SAR über den Schlusskurs springt, wird ein Short-Trade ausgeführt. Jede Position ist durch feste Stop-Loss- und Take-Profit-Abstände geschützt, ausgedrückt in Parabolic SAR „Punkten“, genau wie in der ursprünglichen MQL-Version.

Handelslogik

  1. Daten- und Indikatorvorbereitung. Die Strategie abonniert einen konfigurierbaren Kerzentyp (standardmäßig 15-Minuten-Kerzen) und bindet einen Parabolic SAR-Indikator mit benutzerdefiniertem Beschleunigungsschritt und maximaler Beschleunigung.
  2. Statusverfolgung. Bei der ersten abgeschlossenen Kerze merkt sich die Strategie, ob der SAR über oder unter dem Schlusskurs liegt. Spätere Kerzen vergleichen die neue SAR-Position mit dem vorherigen Zustand.
  3. Eintrittsregeln.
    • Langer Einstieg: Der SAR wechselt von über dem Schlusskurs zu unter dem Schlusskurs. Eine eventuell bestehende Short-Position wird geschlossen und eine neue Long-Position mit dem konfigurierten Volumen zum Marktwert eröffnet.
    • Kurzer Einstieg: Der SAR wechselt von unter dem Schlusskurs auf über dem Schlusskurs. Eine bestehende Long-Position wird geschlossen, bevor eine neue Short-Position eröffnet wird.
  4. Schutzanordnungen. Unmittelbar nach dem Einstieg speichert die Strategie Stop-Loss- und Take-Profit-Level, die aus dem Kerzenschluss berechnet werden, indem StopLossPoints oder TakeProfitPoints mit dem Wertpapier PriceStep multipliziert wird. Wenn UseStopMultiplier aktiviert ist (Standardverhalten, kopiert von MetaTrader), wird die Distanz mit 10 multipliziert, um Brokern Rechnung zu tragen, die mit gebrochenen Pips quotieren.
  5. Ausgangsregeln. Bei jeder fertigen Kerze vergleicht die Strategie die Höchst- und Tiefststände mit den gespeicherten Stop-Loss- und Take-Profit-Werten. Wenn das Hoch oder Tief das Niveau durchbricht, wird die Position zum Marktwert geschlossen. Wenn ein entgegengesetztes SAR-Signal eintrifft, wird die Position ebenfalls umgekehrt, indem ein Auftrag gesendet wird, der so dimensioniert ist, dass das aktuelle Risiko geglättet und der neue Handel eröffnet wird.

Risikomanagement

  • Stop-Loss- und Take-Profit-Abstände werden für jede neue Position neu berechnet.
  • Der Code führt einen konservativen Fallback durch: Wenn das Wertpapier keinen Preisschritt bereitstellt, wird ein Wert von 0.0001 verwendet, um Nullabstände zu vermeiden.
  • Bei allen Handelsentscheidungen wird der IsFormedAndOnlineAndAllowTrading()-Helfer verwendet, um sicherzustellen, dass das Abonnement aktiv und live ist.

Parameter

Name Standard Beschreibung
TradeVolume 0.1 Auftragsvolumen, das für neue Positionen verwendet wird. Der Parameter aktualisiert auch die Basiseigenschaft Strategy.Volume.
StopLossPoints 90 Stop-Loss-Distanz, ausgedrückt in Parabolic SAR Punkten. Der Wert wird mit der Sicherheit PriceStep multipliziert (und optional mit 10, wenn UseStopMultiplier wahr ist).
TakeProfitPoints 20 Take-Profit-Distanz in Parabolic SAR Punkten, umgewandelt durch den Preisschritt.
UseStopMultiplier true Wenn aktiviert, werden die Stop-Loss- und Take-Profit-Distanzen mit 10 multipliziert, um den StopMult-Schalter des MetaTrader-Experten nachzuahmen.
SarAccelerationStep 0.02 Anfänglicher Beschleunigungsfaktor, der dem Indikator Parabolic SAR zugeführt wird.
SarAccelerationMax 0.2 Maximaler Beschleunigungsfaktor für den Indikator Parabolic SAR.
CandleType 15m time-frame Kerzentyp, der für die Indikator- und Signalberechnungen verwendet wird.

Hinweise zur Konvertierung

  • MetaTrader Stop-Loss- und Take-Profit-Orders waren Broker-seitige Schutzorder. StockSharp reproduziert sie, indem es Kerzenhochs und -tiefs überwacht und Marktausgänge sendet, wenn die Schwellenwerte überschritten werden.
  • Der MetaTrader-Experte multiplizierte die Stop-Distanzen mit zehn, wenn StopMult wahr war, um die Kompatibilität mit Brokern zu verbessern, die mit gebrochenen Pips quotieren. Der Parameter UseStopMultiplier implementiert das gleiche Verhalten.
  • Bei der Konvertierung werden die übergeordneten API von StockSharp (SubscribeCandles, Bind, BuyMarket, SellMarket) gemäß den Projektrichtlinien verwendet. Es wird noch keine zusätzliche Python-Version bereitgestellt, die der Aufgabenanforderung entspricht.
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>
/// Parabolic SAR first dot reversal strategy.
/// Opens a position when Parabolic SAR flips relative to the close and protects it with classic stops.
/// </summary>
public class ParabolicSarFirstDotStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<bool> _useStopMultiplier;
	private readonly StrategyParam<decimal> _sarAccelerationStep;
	private readonly StrategyParam<decimal> _sarAccelerationMax;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;
	private bool? _prevIsSarAbovePrice;
	private decimal _priceStep;
	private DateTimeOffset _lastTradeTime;

	/// <summary>
	/// Trading volume in lots.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set
		{
			_tradeVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Stop-loss distance expressed in Parabolic SAR points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in Parabolic SAR points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Multiply stop distances by ten to mirror the MetaTrader implementation.
	/// </summary>
	public bool UseStopMultiplier
	{
		get => _useStopMultiplier.Value;
		set => _useStopMultiplier.Value = value;
	}

	/// <summary>
	/// Initial acceleration factor for Parabolic SAR.
	/// </summary>
	public decimal SarAccelerationStep
	{
		get => _sarAccelerationStep.Value;
		set => _sarAccelerationStep.Value = value;
	}

	/// <summary>
	/// Maximum acceleration factor for Parabolic SAR.
	/// </summary>
	public decimal SarAccelerationMax
	{
		get => _sarAccelerationMax.Value;
		set => _sarAccelerationMax.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="ParabolicSarFirstDotStrategy"/>.
	/// </summary>
	public ParabolicSarFirstDotStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Volume", "Order volume in lots", "General")
			.SetGreaterThanZero();

		_stopLossPoints = Param(nameof(StopLossPoints), 90)
			.SetDisplay("Stop-Loss Points", "Stop-loss distance converted through the instrument price step", "Risk Management")
			.SetGreaterThanZero()
			
			.SetOptimize(30, 150, 10);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 20)
			.SetDisplay("Take-Profit Points", "Take-profit distance converted through the instrument price step", "Risk Management")
			.SetGreaterThanZero()
			
			.SetOptimize(10, 80, 10);

		_useStopMultiplier = Param(nameof(UseStopMultiplier), true)
			.SetDisplay("Use Stop Multiplier", "Multiply distances by ten to reproduce MetaTrader stop handling", "Risk Management");

		_sarAccelerationStep = Param(nameof(SarAccelerationStep), 0.02m)
			.SetDisplay("SAR Step", "Initial acceleration factor for Parabolic SAR", "Indicator")
			.SetRange(0.01m, 0.05m)
			
			.SetOptimize(0.01m, 0.05m, 0.01m);

		_sarAccelerationMax = Param(nameof(SarAccelerationMax), 0.2m)
			.SetDisplay("SAR Max", "Maximum acceleration factor for Parabolic SAR", "Indicator")
			.SetRange(0.1m, 0.4m)
			
			.SetOptimize(0.1m, 0.4m, 0.05m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used for calculations", "General");

		Volume = _tradeVolume.Value;
	}

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

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

		_prevIsSarAbovePrice = null;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
		Volume = _tradeVolume.Value;
		_priceStep = 0;
		_lastTradeTime = default;
	}

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

		_priceStep = GetPriceStep();

		var parabolicSar = new ParabolicSar
		{
			Acceleration = SarAccelerationStep,
			AccelerationMax = SarAccelerationMax
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(parabolicSar, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal sarValue)
	{
		// Work only with completed candles to match MetaTrader logic.
		if (candle.State != CandleStates.Finished)
			return;

		// Wait for Parabolic SAR indicator to be ready.

		// Check whether existing positions should be closed by protective levels.
		CheckProtectiveLevels(candle);

		var isSarAbovePrice = sarValue > candle.ClosePrice;

		// Initialize state on the first value.
		if (_prevIsSarAbovePrice == null)
		{
			_prevIsSarAbovePrice = isSarAbovePrice;
			return;
		}

		var sarSwitchedBelow = _prevIsSarAbovePrice.Value && !isSarAbovePrice;
		var sarSwitchedAbove = !_prevIsSarAbovePrice.Value && isSarAbovePrice;

		if (sarSwitchedBelow)
			TryEnterLong(candle, sarValue);
		else if (sarSwitchedAbove)
			TryEnterShort(candle, sarValue);

		_prevIsSarAbovePrice = isSarAbovePrice;
	}

	private void TryEnterLong(ICandleMessage candle, decimal sarValue)
	{
		// Prevent duplicate long entries.
		if (Position > 0m)
			return;

		// Cooldown: at least 2 days between trades to avoid over-trading.
		if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime).TotalHours < 48)
			return;

		var volume = Volume + Math.Abs(Position);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		_lastTradeTime = candle.OpenTime;

		var entryPrice = candle.ClosePrice;
		var stopDistance = GetDistance(StopLossPoints);
		var takeDistance = GetDistance(TakeProfitPoints);

		_longStop = entryPrice - stopDistance;
		_longTake = entryPrice + takeDistance;
		_shortStop = null;
		_shortTake = null;

		LogInfo($"Long entry after SAR flip. Close={entryPrice}, SAR={sarValue}, Stop={_longStop}, Take={_longTake}");
	}

	private void TryEnterShort(ICandleMessage candle, decimal sarValue)
	{
		// Prevent duplicate short entries.
		if (Position < 0m)
			return;

		// Cooldown: at least 2 days between trades to avoid over-trading.
		if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime).TotalHours < 48)
			return;

		var volume = Volume + Math.Abs(Position);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		_lastTradeTime = candle.OpenTime;

		var entryPrice = candle.ClosePrice;
		var stopDistance = GetDistance(StopLossPoints);
		var takeDistance = GetDistance(TakeProfitPoints);

		_shortStop = entryPrice + stopDistance;
		_shortTake = entryPrice - takeDistance;
		_longStop = null;
		_longTake = null;

		LogInfo($"Short entry after SAR flip. Close={entryPrice}, SAR={sarValue}, Stop={_shortStop}, Take={_shortTake}");
	}

	private void CheckProtectiveLevels(ICandleMessage candle)
	{
		var position = Position;

		if (position > 0m)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Math.Abs(position));
				LogInfo($"Long stop-loss triggered at {stop}.");
				ResetLongTargets();
			}
			else if (_longTake is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Math.Abs(position));
				LogInfo($"Long take-profit triggered at {take}.");
				ResetLongTargets();
			}
		}
		else if (position < 0m)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(position));
				LogInfo($"Short stop-loss triggered at {stop}.");
				ResetShortTargets();
			}
			else if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(Math.Abs(position));
				LogInfo($"Short take-profit triggered at {take}.");
				ResetShortTargets();
			}
		}
	}

	private void ResetLongTargets()
	{
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortTargets()
	{
		_shortStop = null;
		_shortTake = null;
	}

	private decimal GetDistance(int basePoints)
	{
		var multiplier = UseStopMultiplier ? 10 : 1;
		return basePoints * multiplier * _priceStep;
	}

	private decimal GetPriceStep()
	{
		// Use security price step when available, otherwise fall back to a minimal tick.
		var step = Security?.PriceStep ?? 0.0001m;
		return step > 0m ? step : 0.0001m;
	}
}