Auf GitHub ansehen

Precipice Martin-Strategie (C#)

Übersicht

Die Precipice Martin-Strategie ist ein mechanischer Gitter-Ansatz, der beim Abschluss jeder verarbeiteten Kerze eine Marktorder eröffnet. Der ursprüngliche MetaTrader 5-Experte erstellte bei jedem neuen Balken eine symmetrische Kauf- und Verkaufsposition und verwaltete Ausstiege mit statischen Stop-Loss- und Take-Profit-Offsets, die in Pips ausgedrückt waren. Verlusttrades erhöhten die nächste Ordergröße um einen Martingale-Multiplikator, während profitable Trades die Positionsgröße auf das Mindestlot zurücksetzten.

Dieser C#-Port folgt derselben High-Level-Logik unter Verwendung der StockSharp High-Level-API. Für jede abgeschlossene Kerze:

  1. Aktualisiert die Strategie bestehende Long- und Short-Positionen und schließt sie, wenn die Kerzenbandbreite das konfigurierte Stop-Loss- oder Take-Profit-Niveau durchstochen hat.
  2. Wenn flat, wechselt sie zwischen dem Eröffnen einer Long- oder Short-Marktposition (wenn beide Richtungen aktiviert sind), um das Doppeleinstiegsverhalten des Quell-Roboters zu emulieren, während es mit StockSharps Nettopositionen-Abrechnung kompatibel bleibt.
  3. Wendet optionales Martingale-Sizing an, sodass aufeinanderfolgende Verlusttrades das Volumen um den konfigurierten Multiplikator erhöhen.
  4. Berechnet Stop-Loss- und Take-Profit-Ziele aus benutzerdefinierten Pip-Distanzen, die basierend auf der Tick-Größe des Instruments in absolute Preis-Offsets umgerechnet werden.

Konvertierungshinweise

  • Der ursprüngliche EA eröffnete eine Long- und Short-Position auf jedem neuen Balken, wenn beide Schalter aktiviert waren. Da StockSharp standardmäßig Nettopositionen verwendet, wechselt die C#-Version bei aufeinanderfolgenden Gelegenheiten zwischen Richtungen, um ein sofortiges Ausgleichen der Nettoposition zu vermeiden. Dies stellt sicher, dass beide Marktseiten im Laufe der Zeit gehandelt werden.
  • Stop-Loss- und Take-Profit-Management erfolgt intern, indem geprüft wird, ob der Hoch-/Tiefwert einer Kerze das entsprechende Niveau ausgelöst hätte. Wenn ein Niveau erreicht wird, schließt die Strategie die Position mit einer Marktorder und zeichnet den realisierten Gewinn oder Verlust für die Martingale-Logik auf.
  • Lot-Validierung repliziert die LotCheck-Routine aus MQL5, indem das berechnete Volumen auf den Exchange VolumeStep gerundet, Mindest- und Höchstgrenzen eingehalten und die Order storniert wird, wenn der gerundete Wert null wird.
  • Die Martingale-Routine spiegelt CalculateLot: Jeder nicht profitable Ausstieg multipliziert die nächste Ordergröße mit MartingaleCoefficient, während ein profitabler Ausstieg den Multiplikator auf eins zurücksetzt.

Parameter

Parameter Beschreibung
Use Buy Aktiviert das Eröffnen von Long-Positionen.
Buy SL/TP (pips) Distanz (in Pips) sowohl für den Stop-Loss als auch für den Take-Profit von Long-Trades. Ein Wert von 0 deaktiviert Ausstiege für diese Seite.
Use Sell Aktiviert das Eröffnen von Short-Positionen.
Sell SL/TP (pips) Distanz (in Pips) sowohl für den Stop-Loss als auch für den Take-Profit von Short-Trades.
Use Martingale Schaltet das Martingale-Positions-Sizing um. Wenn deaktiviert, verwendet jede Order die Mindestlotgröße.
Martingale Coefficient Multiplikator, der nach jedem nicht profitablen Trade auf das Mindestlot angewendet wird.
Candle Type Zeitrahmen der von der Strategie verarbeiteten Kerzen. Standardmäßig arbeitet die Strategie auf Ein-Minuten-Balken, aber jeder verfügbare Zeitrahmen kann ausgewählt werden.

Handelslogik

  1. Pip-Größenberechnung – die Strategie leitet den Pip-Wert aus der Tick-Größe des Instruments ab. Für Instrumente mit Bruchteil-Pips (5-stellige FX-Symbole) wird der Pip als 10 Ticks betrachtet, entsprechend der MT5-Implementierung.
  2. Einstiegsauswahl – wenn sowohl Use Buy als auch Use Sell aktiviert sind, wechselt die Strategie zwischen Long- und Short-Einstiegen, wann immer sie flat ist. Wenn nur eine Richtung aktiviert ist, folgen alle Trades dieser Richtung. Einstiege werden unmittelbar nach Abschluss einer Kerze ausgelöst und die Strategie ist online.
  3. Stop/Take-Niveaus – wenn ein Trade eröffnet wird, werden Stop-Loss und Take-Profit als absolute Preise relativ zum Einstieg unter Verwendung der gewählten Pip-Distanz gespeichert. Ein Wert von null deaktiviert beide Niveaus für diese Richtung.
  4. Ausstiegshandling – bei jeder abgeschlossenen Kerze werden die Hoch-/Tiefwerte geprüft. Wenn das Tief den Long-Stop verletzt oder das Hoch das Long-Ziel übertrifft, wird die Long-Position geschlossen. Für Shorts ist die Logik gespiegelt. Ausstiege werden mit Marktorders unter Verwendung des zuletzt aufgezeichneten Volumens für diese Position ausgeführt.
  5. Martingale-Sizing – das nächste Ordervolumen entspricht dem Mindestlot des Instruments multipliziert mit dem aktuellen Martingale-Multiplikator. Verlusttrades (einschließlich Break-Even-Ergebnisse) multiplizieren den Multiplikator mit MartingaleCoefficient; profitable Trades setzen ihn auf eins zurück. Volumenrundung auf den Exchange-Schritt wird vor dem Einreichen der Order angewendet.
  6. Sicherheitsprüfungen – wenn das gerundete Volumen unter dem Exchange-Mindestlot liegt, wird die Order übersprungen, um "Nicht genug Geld"-Fehler zu vermeiden, die der ursprüngliche EA über CheckVolume behandelte.

Verwendungsrichtlinien

  1. Konfigurieren Sie den gewünschten Zeitrahmen in Candle Type, um dem in MT5 verwendeten Chartperiode zu entsprechen.
  2. Passen Sie die Pip-Distanzen an das gewünschte Stop-Loss- und Take-Profit-Verhalten an. Beachten Sie, dass die Offsets absolute Preise sind, sodass der tatsächliche Stop in Währung vom Symbol abhängt.
  3. Aktivieren oder deaktivieren Sie das Martingale-Sizing entsprechend Ihrer Risikobereitschaft. Da das Volumen nach aufeinanderfolgenden Verlusten exponentiell wächst, verwenden Sie konservative Multiplikatoren.
  4. Setzen Sie die Strategie auf einem Wertpapier ein, das Echtzeit-Kerzen bereitstellt. Die Strategie benötigt abgeschlossene Balken und wird nicht auf unvollständigen Kerzen handeln.
  5. Überwachen Sie die Margennutzung, wenn Martingale aktiv ist. Die StockSharp-Version wechselt absichtlich die Richtungen, wenn beide Seiten aktiviert sind, sodass zu jedem Zeitpunkt nur eine Nettoposition offen ist.

Unterschiede zur MT5-Implementierung

  • Nettopositionen – die Wechsellogik ersetzt die gleichzeitigen gehedgten Einstiege des ursprünglichen Algorithmus. Wenn ein echtes Hedging-Konto erforderlich ist, können Sie zwei Instanzen der Strategie ausführen (eine mit Use Buy, eine mit Use Sell).
  • Order-Platzierung – Schutzorders werden nicht im Exchange-Buch platziert. Stattdessen werden Ausstiege über Marktorders ausgeführt, wenn die Strategie erkennt, dass das Stop- oder Take-Niveau überschritten wurde.
  • Historien-Scan – das MT5-Skript berechnete den Martingale-Koeffizienten durch Scannen der gesamten Handelshistorie bei jedem Tick neu. Die C#-Version verwaltet den Multiplikator inkrementell, um den Overhead zu reduzieren und gleichzeitig das Verhalten zu erhalten.

Risikohinweis

Martingale-basierte Strategien können während Verliererserien sehr große Positionen generieren, die die Kontorisikogrenzen überschreiten können. Testen Sie die Strategie immer auf simulierten Daten vor dem Live-Einsatz und stellen Sie sicher, dass der gewählte Multiplikator und die Pip-Distanzen zur Volatilität des gehandelten Instruments passen.

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>
/// Grid style strategy that opens a position on every new bar with optional martingale sizing.
/// </summary>
public class PrecipiceMartinStrategy : Strategy
{
	private readonly StrategyParam<bool> _useBuy;
	private readonly StrategyParam<int> _buyStepPips;
	private readonly StrategyParam<bool> _useSell;
	private readonly StrategyParam<int> _sellStepPips;
	private readonly StrategyParam<bool> _useMartingale;
	private readonly StrategyParam<decimal> _martingaleCoefficient;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal _martingaleMultiplier;
	private decimal? _longEntryPrice;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortEntryPrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;
	private decimal _lastLongVolume;
	private decimal _lastShortVolume;
	private bool _preferLongEntry;

	public bool UseBuy
	{
		get => _useBuy.Value;
		set => _useBuy.Value = value;
	}

	public int BuyStepPips
	{
		get => _buyStepPips.Value;
		set => _buyStepPips.Value = value;
	}

	public bool UseSell
	{
		get => _useSell.Value;
		set => _useSell.Value = value;
	}

	public int SellStepPips
	{
		get => _sellStepPips.Value;
		set => _sellStepPips.Value = value;
	}

	public bool UseMartingale
	{
		get => _useMartingale.Value;
		set => _useMartingale.Value = value;
	}

	public decimal MartingaleCoefficient
	{
		get => _martingaleCoefficient.Value;
		set => _martingaleCoefficient.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public PrecipiceMartinStrategy()
	{
		_useBuy = Param(nameof(UseBuy), true)
			.SetDisplay("Use Buy", "Enable opening long positions", "Trading");
		_buyStepPips = Param(nameof(BuyStepPips), 89)
			.SetDisplay("Buy SL/TP (pips)", "Stop loss and take profit distance for longs", "Trading");
		_useSell = Param(nameof(UseSell), true)
			.SetDisplay("Use Sell", "Enable opening short positions", "Trading");
		_sellStepPips = Param(nameof(SellStepPips), 89)
			.SetDisplay("Sell SL/TP (pips)", "Stop loss and take profit distance for shorts", "Trading");
		_useMartingale = Param(nameof(UseMartingale), true)
			.SetDisplay("Use Martingale", "Increase volume after losing trades", "Position sizing");
		_martingaleCoefficient = Param(nameof(MartingaleCoefficient), 1.6m)
			.SetDisplay("Martingale Coefficient", "Multiplier applied after losses", "Position sizing")
			.SetGreaterThanZero();
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to generate trading bars", "General");
	}

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

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

		_pipSize = 0m;
		_martingaleMultiplier = 1m;
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_lastLongVolume = 0m;
		_lastShortVolume = 0m;
		_preferLongEntry = true;
	}

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

		// Calculate the pip size based on the instrument tick size.
		_pipSize = (Security?.PriceStep ?? 1m) * 10m;
		if (_pipSize <= 0m)
			_pipSize = Security?.PriceStep ?? 1m;
		if (_pipSize <= 0m)
			_pipSize = 1m;

		_martingaleMultiplier = 1m;

		// Subscribe to candle data and process every completed bar.
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore unfinished candles because the original strategy trades on bar close.
		if (candle.State != CandleStates.Finished)
			return;

		// Manage exits before looking for new entries.
		var closedLong = TryCloseLong(candle);
		var closedShort = TryCloseShort(candle);

		// Do not open new trades while any position is still active.
		if (Position != 0)
			return;

		// Avoid immediate re-entry for a direction that has just closed on this bar.
		if (closedLong)
			return;
		if (closedShort)
			return;

		if (_longEntryPrice.HasValue || _shortEntryPrice.HasValue)
			return;

		if (UseBuy && UseSell)
		{
			if (_preferLongEntry)
			{
				if (TryEnterLong(candle))
				{
					_preferLongEntry = false;
					return;
				}

				if (TryEnterShort(candle))
				{
					_preferLongEntry = false;
				}
			}
			else
			{
				if (TryEnterShort(candle))
				{
					_preferLongEntry = true;
					return;
				}

				if (TryEnterLong(candle))
				{
					_preferLongEntry = true;
				}
			}
		}
		else
		{
			if (UseBuy)
			{
				TryEnterLong(candle);
			}

			if (UseSell)
			{
				TryEnterShort(candle);
			}
		}
	}

	private bool TryEnterLong(ICandleMessage candle)
	{
		// Prevent duplicate long entries.
		if (_longEntryPrice.HasValue)
			return false;

		// Ensure no net position exists before opening a new long.
		if (Position != 0)
			return false;

		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return false;

		var entryPrice = candle.ClosePrice;

		Volume = volume;
		BuyMarket();

		_longEntryPrice = entryPrice;
		_lastLongVolume = volume;

		if (BuyStepPips > 0)
		{
			var offset = BuyStepPips * _pipSize;
			_longStopPrice = entryPrice - offset;
			_longTakePrice = entryPrice + offset;
		}
		else
		{
			_longStopPrice = null;
			_longTakePrice = null;
		}

		return true;
	}

	private bool TryEnterShort(ICandleMessage candle)
	{
		// Prevent duplicate short entries.
		if (_shortEntryPrice.HasValue)
			return false;

		// Ensure no net position exists before opening a new short.
		if (Position != 0)
			return false;

		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return false;

		var entryPrice = candle.ClosePrice;

		Volume = volume;
		SellMarket();

		_shortEntryPrice = entryPrice;
		_lastShortVolume = volume;

		if (SellStepPips > 0)
		{
			var offset = SellStepPips * _pipSize;
			_shortStopPrice = entryPrice + offset;
			_shortTakePrice = entryPrice - offset;
		}
		else
		{
			_shortStopPrice = null;
			_shortTakePrice = null;
		}

		return true;
	}

	private bool TryCloseLong(ICandleMessage candle)
	{
		if (!_longEntryPrice.HasValue)
			return false;

		var volume = Position;
		if (volume <= 0m)
			volume = _lastLongVolume;

		if (volume <= 0m)
			return false;

		var stopHit = _longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value;
		var takeHit = _longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value;

		if (!stopHit && !takeHit)
			return false;

		var exitPrice = stopHit ? _longStopPrice!.Value : _longTakePrice!.Value;

		SellMarket();

		var pnl = (exitPrice - _longEntryPrice.Value) * volume;
		UpdateMartingale(pnl);

		ResetLongState();
		return true;
	}

	private bool TryCloseShort(ICandleMessage candle)
	{
		if (!_shortEntryPrice.HasValue)
			return false;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			volume = _lastShortVolume;

		if (volume <= 0m)
			return false;

		var stopHit = _shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value;
		var takeHit = _shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value;

		if (!stopHit && !takeHit)
			return false;

		var exitPrice = stopHit ? _shortStopPrice!.Value : _shortTakePrice!.Value;

		BuyMarket();

		var pnl = (_shortEntryPrice.Value - exitPrice) * volume;
		UpdateMartingale(pnl);

		ResetShortState();
		return true;
	}

	private decimal CalculateOrderVolume()
	{
		var minVolume = Security?.MinVolume ?? Volume;
		if (minVolume <= 0m)
			minVolume = 1m;

		var multiplier = UseMartingale ? _martingaleMultiplier : 1m;
		var volume = minVolume * multiplier;

		return AdjustVolume(volume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		var step = Security?.VolumeStep;
		if (step.HasValue && step.Value > 0m)
		{
			var steps = Math.Truncate(volume / step.Value);
			volume = steps * step.Value;
		}

		var min = Security?.MinVolume;
		if (min.HasValue && min.Value > 0m && volume < min.Value)
			volume = 0m;

		var max = Security?.MaxVolume;
		if (max.HasValue && max.Value > 0m && volume > max.Value)
			volume = max.Value;

		return volume;
	}

	private void UpdateMartingale(decimal realizedPnl)
	{
		if (!UseMartingale)
		{
			_martingaleMultiplier = 1m;
			return;
		}

		// Reset the multiplier after profitable trades and scale up after losses.
		_martingaleMultiplier = realizedPnl > 0m
			? 1m
			: _martingaleMultiplier * MartingaleCoefficient;
	}

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

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