Auf GitHub ansehen

Händlerhandel v7.51 RIVOT (C#)

Zusammenfassung

Dealers Trade v7.51 ist eine Rasterstrategie im Martingal-Stil, die ursprünglich als MetaTrader 4 Expert Advisor Dealers_Trade_v_7.51_RIVOT.mq4 bereitgestellt wurde. Der Port hält die ursprüngliche Idee des Handels fern von einer Pivot-basierten Richtungsausrichtung und skaliert in die dominante Seite, wenn der Preis um eine konfigurierbare Pip-Distanz zurückgeht. Die StockSharp-Implementierung verwendet hochrangige Strategiehelfer, um Kerzen zu abonnieren, die Pivot-Zonen zu berechnen und Positionsgröße, Risiko und Ausstiege zu verwalten.

Handelslogik

  1. Pivot-Framework

    • Die Strategie erstellt für jede fertige Kerze zwei Referenzpreise:
      • Klassischer Pivot (P) = (previous high + previous low + previous close + current open) / 4.
      • Floating Pivot (FLP) = (current high + current low + current close) / 3.
    • Eine Pips-Lücke zwischen P und FLP muss größer oder gleich GapThreshold sein, um den Handel für den aktuellen Balken zu ermöglichen.
  2. Richtungsfehler

    • Wenn der Kerzenschluss über beiden Pivots liegt und der Gap-Filter erfüllt ist, wechselt die Tendenz zu long.
    • Wenn der Schlusskurs der Kerze unter beiden Pivotpunkten liegt und die Lücke bestätigt ist, wechselt die Tendenz zu Short.
    • Die Vorspannung bleibt bestehen, bis die Positionsreihe vollständig geschlossen ist oder nach Ende der Reihe der gegenteilige Zustand eintritt.
  3. Skalierungseinträge

    • Es kann jeweils nur eine Handelsreihe aktiv sein.
    • Der erste Eintrag folgt sofort der Voreingenommenheit.
    • Zusätzliche Einträge werden nur eröffnet, wenn der Preis gegenüber der aktiven Tendenz um mindestens PipDistance Pips seit der letzten Füllung zurückgeht, was der ursprünglichen Martingal-Durchschnittsbildung nachempfunden ist.
    • Jede neue Bestellung multipliziert die vorherige Größe mit VolumeMultiplier, überschreitet jedoch niemals MaxVolume.
    • Die Anzahl der gestapelten Einträge ist auf MaxTrades begrenzt.
  4. Risikokontrollen

    • Ein harter Stop-Loss bei StopLoss Pips vom volumengewichteten Durchschnittseintritt schließt die gesamte Serie ab.
    • Ein fester Take-Profit bei TakeProfit Pips sichert Gewinne, sobald der Preis wieder positiv wird.
    • Wenn der Trailing-Stop aktiviert ist, sperrt er Gewinne dynamisch, indem er jedes Mal näher an den Preis heranrückt, wenn er sich um mehr als TrailingStop Pips über den durchschnittlichen Einstieg hinaus bewegt.
  5. Bedingungen zurücksetzen

    • Jeder vollständige Ausstieg (Stop-Loss, Take-Profit, Trailing-Stop oder manuelles Abflachen der Position) setzt die Martingal-Zähler zurück und entfernt die Richtungsabweichung.

Parameter

Parameter Standard Beschreibung
Volume 1 Basisauftragsgröße für den ersten Eintrag.
MaxTrades 5 Maximale Anzahl gemittelter Einträge pro Serie.
PipDistance 4 Vor dem Hinzufügen einer neuen Position ist eine minimale Gegenbewegung (in Pips) erforderlich.
TakeProfit 15 Abstand vom volumengewichteten Durchschnittseintrag bis zum Abschluss des gesamten Rasters im Gewinn.
StopLoss 90 Abstand vom durchschnittlichen Eingang, der einen Schutzausgang auslöst.
TrailingStop 15 Der Trailing-Stop-Offset wird angewendet, sobald sich der Preis positiv entwickelt. auf Null setzen, um das Nachziehen zu deaktivieren.
VolumeMultiplier 1.5 Faktor, der verwendet wird, um die Auftragsgröße für jeden weiteren Eintrag zu erhöhen.
MaxVolume 5 Obergrenze für das Einzelauftragsvolumen nach Anwendung des Multiplikators.
GapThreshold 7 Mindestlücke (in Pips) zwischen dem klassischen und dem Floating-Pivot, die erforderlich ist, um die Voreingenommenheit zu aktivieren.
CandleType 15-minütige Zeitrahmenkerzen Kerzentyp, der für Berechnungen und Entscheidungsfindung verwendet wird.

Alle Parameter werden über StrategyParam<T> konfiguriert, sodass sie im StockSharp Designer oder Strategy Runner optimiert werden können.

Nutzungshinweise

  • Die Strategie basiert ausschließlich auf Kerzendaten; Es ist kein direkter Bid/Ask-Stream auf Tick-Ebene erforderlich. Stellen Sie sicher, dass Ihr Datenanbieter die ausgewählten CandleType liefern kann.
  • Da StockSharp Positionen standardmäßig aggregiert, verwaltet die Implementierung einen internen volumengewichteten Durchschnitt, um das MT4-Rasterbuch zu emulieren. Kommt es zu Teilfüllungen, sorgt die integrierte Positionsabrechnung dafür, dass die Werte konsistent bleiben.
  • Beim Rendern von Diagrammen werden dem Diagrammbereich zwei horizontale Linien (Pivot und FloatingPivot) hinzugefügt, sofern diese verfügbar sind.
  • Es gibt keinen automatischen Reverse-Trading; Das System wartet auf das Ende der laufenden Serie, bevor es einen Bias-Flip akzeptiert.

Unterschiede zur MQL-Version

  • Das ursprüngliche Skript zeichnete mehrere Beschriftungen und Kommentare zum MT4-Diagramm. Der Port behält nur die funktionale Handelslogik bei und ersetzt die visuellen Elemente durch StockSharp Diagrammlinien.
  • Kontoschutzfunktionen basierend auf der Gesamtzahl der offenen Bestellungen, manuelle Filterung magischer Zahlen und symbolspezifische Pip-Werttabellen sind in StockSharp nicht erforderlich und wurden weggelassen.
  • Der Auftragsabschluss zu genauen Tick-Preisen (Ask == tp) im MetaTrader-Code wird durch Preisvergleiche bei Candle-Closings angenähert.
  • Das Handelsmanagement wird mit Marktaufträgen (BuyMarket/SellMarket) anstelle von MT4-Ticketschleifen implementiert. Trailing Stops und Exits erfolgen bei Kerzenaktualisierungen.

Best Practices

  • Testen Sie die Strategie immer im Papierhandel oder in historischen Simulationen mit realistischen Spread-/Provisionsmodellen, bevor Sie live gehen.
  • Erwägen Sie, VolumeMultiplier oder MaxTrades bei hochvolatilen Instrumenten zu senken, um den Drawdown zu kontrollieren.
  • Passen Sie für Intraday-Produkte CandleType an, um der Datengranularität der ursprünglichen Einrichtung zu entsprechen (der Standardwert ist 15 Minuten, aber EA wurde häufig in M15 und H1 verwendet).

Dateien

  • CS/DealersTradeV751RivotStrategy.cs – Haupt-C#-Implementierung.
  • README_zh.md – Chinesische Dokumentation.
  • README_ru.md – Russische Dokumentation.
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>
/// Dealers Trade v7.51 strategy ported from MetaTrader 4 implementation.
/// Builds directional bias from classic pivot and floating pivot levels
/// and scales into the bias when price retraces by a fixed pip distance.
/// Applies martingale-style position sizing with configurable stop-loss,
/// take-profit, and trailing-stop management.
/// </summary>
public class DealersTradeV751RivotStrategy : Strategy
{
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<decimal> _pipDistance;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _gapThreshold;
	private readonly StrategyParam<DataType> _candleType;

	private ICandleMessage _previousCandle;
	private decimal _pivotLevel;
	private decimal _floatingPivot;
	private decimal _gapInPips;
	private decimal _lastEntryPrice;
	private decimal _averageEntryPrice;
	private decimal? _trailingStopLevel;
	private int _direction; // -1 short, 0 neutral, 1 long
	private int _entriesInSeries;

	/// <summary>
	/// Maximum number of entries allowed in one scaling series.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Distance in pips between martingale entries.
	/// </summary>
	public decimal PipDistance
	{
		get => _pipDistance.Value;
		set => _pipDistance.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Trailing-stop distance in pips.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Multiplier applied to volume for each additional entry.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum allowed volume for a single entry.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Minimum pivot gap in pips required to activate the bias.
	/// </summary>
	public decimal GapThreshold
	{
		get => _gapThreshold.Value;
		set => _gapThreshold.Value = value;
	}

	/// <summary>
	/// Type of candles used for pivot calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public DealersTradeV751RivotStrategy()
	{
		_maxTrades = Param(nameof(MaxTrades), 2)
		.SetGreaterThanZero()
		.SetDisplay("Max Trades", "Maximum number of martingale entries", "Position Sizing")
		
		.SetOptimize(1, 10, 1);

		_pipDistance = Param(nameof(PipDistance), 10m)
		.SetGreaterThanZero()
		.SetDisplay("Pip Distance", "Distance between averaged entries in pips", "Position Sizing")
		
		.SetOptimize(2m, 15m, 1m);

		_takeProfit = Param(nameof(TakeProfit), 15m)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit", "Take-profit distance in pips", "Risk Management")
		
		.SetOptimize(5m, 50m, 5m);

		_stopLoss = Param(nameof(StopLoss), 90m)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Risk Management")
		
		.SetOptimize(30m, 200m, 10m);

		_trailingStop = Param(nameof(TrailingStop), 15m)
		.SetGreaterThanZero()
		.SetDisplay("Trailing Stop", "Trailing-stop distance in pips", "Risk Management")
		
		.SetOptimize(5m, 40m, 5m);

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.5m)
		.SetGreaterThanZero()
		.SetDisplay("Volume Multiplier", "Multiplier applied after each new entry", "Position Sizing")
		
		.SetOptimize(1.1m, 3m, 0.1m);

		_maxVolume = Param(nameof(MaxVolume), 5m)
		.SetGreaterThanZero()
		.SetDisplay("Max Volume", "Upper limit for single-entry volume", "Position Sizing");

		_gapThreshold = Param(nameof(GapThreshold), 15m)
		.SetGreaterThanZero()
		.SetDisplay("Gap Threshold", "Minimal pivot gap required to enable trading", "Signal")
		
		.SetOptimize(3m, 15m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles used for pivot calculations", "Signal");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetSeries();
		_previousCandle = null;
		_pivotLevel = 0m;
		_floatingPivot = 0m;
		_gapInPips = 0m;
	}

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

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

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

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
		{
			// Reset martingale state once the position is closed externally.
			ResetSeries();
		}
	}

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

		if (_previousCandle == null)
		{
			_previousCandle = candle;
			return;
		}

		UpdatePivots(candle);

		if (Position == 0m && _entriesInSeries > 0)
		{
			// Force reset when no exposure remains but scaling data still exists.
			ResetSeries();
		}

		if (_entriesInSeries > 0)
		{
			ManageRisk(candle.ClosePrice);
		}

		if (_entriesInSeries >= MaxTrades)
		{
			_previousCandle = candle;
			return;
		}

		if (_direction == 0)
		{
			EvaluateDirection(candle);
		}

		TryEnter(candle);

		_previousCandle = candle;
	}

	private void UpdatePivots(ICandleMessage candle)
	{
		var step = GetPriceStep();
		_pivotLevel = (_previousCandle!.HighPrice + _previousCandle.LowPrice + _previousCandle.ClosePrice + candle.OpenPrice) / 4m;
		_floatingPivot = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
		_gapInPips = step == 0m ? 0m : Math.Abs(_pivotLevel - _floatingPivot) / step;
	}

	private void EvaluateDirection(ICandleMessage candle)
	{
		var price = candle.ClosePrice;

		if (price > _pivotLevel && price > _floatingPivot && _gapInPips >= GapThreshold)
		{
			_direction = 1;
			LogInfo($"Bias switched to long. Pivot={_pivotLevel:F5}, Floating={_floatingPivot:F5}, Gap={_gapInPips:F2} pips.");
		}
		else if (price < _pivotLevel && price < _floatingPivot && _gapInPips >= GapThreshold)
		{
			_direction = -1;
			LogInfo($"Bias switched to short. Pivot={_pivotLevel:F5}, Floating={_floatingPivot:F5}, Gap={_gapInPips:F2} pips.");
		}
	}

	private void TryEnter(ICandleMessage candle)
	{
		if (_direction == 0)
		return;

		var price = candle.ClosePrice;
		var step = GetPriceStep();
		var distance = PipDistance * step;

		if (_direction > 0)
		{
			if (_entriesInSeries == 0 || (_lastEntryPrice - price) >= distance)
			{
				EnterLong(price);
			}
		}
		else
		{
			if (_entriesInSeries == 0 || (price - _lastEntryPrice) >= distance)
			{
				EnterShort(price);
			}
		}
	}

	private void EnterLong(decimal price)
	{
		var volume = CalculateNextVolume();
		_lastEntryPrice = price;
		_averageEntryPrice = UpdateAveragePrice(price, volume, true);
		_entriesInSeries++;
		LogInfo($"Opening long entry #{_entriesInSeries} at {price:F5} with volume {volume}.");
		BuyMarket(volume);
	}

	private void EnterShort(decimal price)
	{
		var volume = CalculateNextVolume();
		_lastEntryPrice = price;
		_averageEntryPrice = UpdateAveragePrice(price, volume, false);
		_entriesInSeries++;
		LogInfo($"Opening short entry #{_entriesInSeries} at {price:F5} with volume {volume}.");
		SellMarket(volume);
	}

	private decimal CalculateNextVolume()
	{
		var volume = Volume;

		for (var i = 0; i < _entriesInSeries; i++)
		{
			volume *= VolumeMultiplier;
			if (volume >= MaxVolume)
			{
				volume = MaxVolume;
				break;
			}
		}

		var volumeStep = Security?.VolumeStep ?? 0.01m;
		if (volumeStep > 0m)
		{
			volume = Math.Ceiling(volume / volumeStep) * volumeStep;
		}

		return volume;
	}

	private decimal UpdateAveragePrice(decimal price, decimal volume, bool isLong)
	{
		var existingVolume = Math.Abs(Position);
		var side = isLong ? 1m : -1m;

		if (existingVolume <= 0m)
		{
			return price;
		}

		var totalVolume = existingVolume + volume;
		var weightedAverage = ((_averageEntryPrice * existingVolume * side) + (price * volume)) / totalVolume;
		return Math.Abs(weightedAverage);
	}

	private void ManageRisk(decimal price)
	{
		if (_entriesInSeries == 0)
		{
			_trailingStopLevel = null;
			return;
		}

		var step = GetPriceStep();
		var stopDistance = StopLoss * step;
		var takeDistance = TakeProfit * step;
		var trailingDistance = TrailingStop * step;

		if (_direction > 0)
		{
			var lossLevel = _averageEntryPrice - stopDistance;
			var profitLevel = _averageEntryPrice + takeDistance;

			if (price <= lossLevel)
			{
				LogInfo($"Long stop-loss triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				SellMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (price >= profitLevel)
			{
				LogInfo($"Long take-profit triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				SellMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (TrailingStop > 0m)
			{
				var candidate = price - trailingDistance;
				if (_trailingStopLevel == null || candidate > _trailingStopLevel)
				{
					_trailingStopLevel = candidate;
				}

				if (_trailingStopLevel != null && price <= _trailingStopLevel)
				{
					LogInfo($"Long trailing stop activated at {price:F5}.");
					SellMarket(Math.Abs(Position));
					ResetSeries();
				}
			}
		}
		else if (_direction < 0)
		{
			var lossLevel = _averageEntryPrice + stopDistance;
			var profitLevel = _averageEntryPrice - takeDistance;

			if (price >= lossLevel)
			{
				LogInfo($"Short stop-loss triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				BuyMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (price <= profitLevel)
			{
				LogInfo($"Short take-profit triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				BuyMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (TrailingStop > 0m)
			{
				var candidate = price + trailingDistance;
				if (_trailingStopLevel == null || candidate < _trailingStopLevel)
				{
					_trailingStopLevel = candidate;
				}

				if (_trailingStopLevel != null && price >= _trailingStopLevel)
				{
					LogInfo($"Short trailing stop activated at {price:F5}.");
					BuyMarket(Math.Abs(Position));
					ResetSeries();
				}
			}
		}
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step == 0m)
		{
			// Fallback to four decimal places when instrument metadata is unknown.
			step = 0.0001m;
		}
		return step;
	}

	private void ResetSeries()
	{
		_direction = 0;
		_entriesInSeries = 0;
		_lastEntryPrice = 0m;
		_averageEntryPrice = 0m;
		_trailingStopLevel = null;
	}
}