Auf GitHub ansehen

Multi Arbitrage Strategie

Überblick

Die Multi Arbitrage Strategie ist ein StockSharp-Port des MetaTrader-Expertenberaters "Multi_arbitration 1.000". Das ursprüngliche Skript bewertet kontinuierlich bestehende Kauf- und Verkaufspositionen, fügt neue Trades in der Richtung mit schwächerem schwebenden Gewinn hinzu, und führt eine globale Liquidation durch, sobald die Gesamtgewinnziele erreicht sind. Diese C#-Implementierung behält die Kernentscheidungslogik bei und passt sie an StockSharp's Netting-Portfolio-Modell und die High-Level-Strategie-API an.

Die Strategie:

  • Öffnet eine anfängliche Long-Position, sobald die erste abgeschlossene Kerze eintrifft.
  • Vergleicht den nicht realisierten Gewinn der aktiven Richtung mit der alternativen Richtung, um zu entscheiden, ob ein Richtungswechsel erforderlich ist.
  • Erzwingt eine flache Position, wenn das konfigurierte Gewinnziel überschritten wird oder wenn der Positionsdruck über eine konfigurierbare Grenze hinauswächst.
  • Verwendet nur Market-Orders (BuyMarket / SellMarket) für Einfachheit und schnelle Ausführung.

Handelslogik

  1. Anfangsorder – Die allererste abgeschlossene Kerze löst eine Long-Market-Order mit dem konfigurierten Handelsvolumen aus. Dies reproduziert den sofortigen Markteinstieg des MetaTrader-Expertenberaters.
  2. Gewinnvergleich – Bei jeder abgeschlossenen Kerze berechnet die Strategie den schwebenden PnL der aktuellen Richtung:
    • Long-Gewinn = (close - entry) * volume
    • Short-Gewinn = (entry - close) * volume
  3. Positionsauswahl – Wenn die alternative Richtung derzeit besser als die aktive abschneiden würde, dreht die Strategie die Position um, indem sie eine Market-Order sendet, die so dimensioniert ist, dass sie das bestehende Engagement abdeckt und eine neue Position in die neue Richtung eröffnet. Wenn keine Position offen ist, wählt der Algorithmus standardmäßig einen Long-Einstieg, was dem ursprünglichen Expertenberater entspricht.
  4. Positionslimitschutz – Ein konfigurierbarer MaxOpenPositions-Parameter spiegelt die MetaTrader-Prüfung gegen LimitOrders() wider. Wenn das kombinierte Long/Short-Engagement diesen Grenzwert erreicht und die Strategie profitabel ist, flacht sie das Buch ab, um Überhebelung zu vermeiden.
  5. Gewinnzielausstieg – Wenn der Konto-PnL (realisiert + nicht realisiert) den ProfitForClose-Schwellenwert überschreitet, schließt die Strategie alle Positionen, genau wie die ursprüngliche Equity - Balance-Prüfung.

Parameter

Name Beschreibung Standard
TradeVolume Für jede Market-Order verwendetes Volumen. Entspricht der Mindest-Lotgröße im Original-EA. 1
ProfitForClose Gewinnschwelle, die einen globalen Ausstieg auslöst, sobald sie überschritten wird. 300
MaxOpenPositions Maximale Anzahl gleichzeitiger Positionen, bevor die Strategie eine Abflachung erzwingt. Entspricht limit - 15. 15
CandleType Kerzen-Datentyp zur Synchronisierung von Handelsentscheidungen. Standard ist 1-Minuten-Zeitrahmen. 1-Minuten-Kerzen

Implementierungshinweise

  • StockSharp verwendet ein Netting-Positionsmodell, daher kann die Strategie nur eine Netto-Richtung gleichzeitig halten. Richtungswechsel werden durch Dimensionierung von Market-Orders behandelt, die sowohl das bestehende Engagement schließen als auch eine neue Position in die entgegengesetzte Richtung eröffnen.
  • Der StartProtection()-Aufruf wird verwendet, um integriertes Risiko-Handling zu erben (z.B. Stop-out bei Nicht-Null-Positionen, wenn die Strategie gestoppt wird).
  • Alle Zustandsvariablen (_entryPrice, _currentSide, _initialOrderPlaced) werden bei OnReseted zurückgesetzt, um Neustarts und wiederholte Simulationen ohne veraltete Daten zu unterstützen.
  • Die Strategie reagiert nur auf abgeschlossene Kerzen, um Doppelzählungen von Gewinnen auf teilweise gebildeten Balken zu vermeiden.

Nutzungsempfehlungen

  • Richten Sie den TradeVolume-Parameter an der Lotgröße des Instruments oder dem Kontraktmultiplikator aus.
  • Der ProfitForClose-Wert sollte in derselben Währung wie der Konto-PnL festgelegt werden (z.B. USD für FX-Konten).
  • Erhöhen oder verringern Sie MaxOpenPositions je nachdem, wie aggressiv die Strategie Engagement aufbauen soll, bevor eine Abflachung erzwungen wird.
  • Da die Strategie immer mit einem Long-Trade beginnt, sollten Sie sie manuell starten, wenn Long-Einstiege für das gehandelte Instrument akzeptabel sind.

Unterschiede zur MetaTrader-Version

  • MetaTrader's Hedging-Modus erlaubt gleichzeitige Long- und Short-Positionen, während dieser Port in einer Netting-Umgebung arbeitet. Die Entscheidungslogik vergleicht immer noch die Richtungsrentabilität, aber nur eine Netto-Position wird jederzeit gehalten.
  • Plattformspezifische Prüfungen (Terminal-Handelsberechtigungen, Auswahl des Füllungstyps, Konto-Magic-Nummern) werden durch StockSharp-Äquivalente wie StartProtection() und Kerzenabonnements ersetzt.
  • Kommentierte Diagnosen aus der MQL-Datei werden nicht reproduziert; verlassen Sie sich auf StockSharp-Logging, wenn Laufzeitinformationen benötigt werden.
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>
/// Multi-direction arbitration strategy adapted from MetaTrader logic.
/// </summary>
public class MultiArbitrationStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitForClose;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<DataType> _candleType;

	private bool _initialOrderPlaced;
	private decimal _entryPrice;
	private Sides? _currentSide;

	/// <summary>
	/// Target profit that triggers a full position exit.
	/// </summary>
	public decimal ProfitForClose
	{
		get => _profitForClose.Value;
		set => _profitForClose.Value = value;
	}

	/// <summary>
	/// Volume used when sending market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous positions allowed before forcing a flatten.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Candle type used for synchronization and decision making.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MultiArbitrationStrategy"/> class.
	/// </summary>
	public MultiArbitrationStrategy()
	{
		_profitForClose = Param(nameof(ProfitForClose), 300m)
			.SetDisplay("Profit Threshold", "Profit required before flattening all positions.", "Risk");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Volume used when opening new positions.", "Trading");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 15)
			.SetGreaterThanZero()
			.SetDisplay("Max Open Positions", "Maximum simultaneous positions allowed before closing everything.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used to synchronize trading decisions.", "Data");
	}

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

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

		_initialOrderPlaced = false;
		_entryPrice = 0m;
		_currentSide = null;
	}

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

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

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

		if (!_initialOrderPlaced)
		{
			OpenLong(candle);
			_initialOrderPlaced = true;
		}

		var longCount = _currentSide == Sides.Buy ? 1 : 0;
		var shortCount = _currentSide == Sides.Sell ? 1 : 0;

		var longProfit = _currentSide == Sides.Buy ? (candle.ClosePrice - _entryPrice) * Volume : 0m;
		var shortProfit = _currentSide == Sides.Sell ? (_entryPrice - candle.ClosePrice) * Volume : 0m;

		if (longCount + shortCount < MaxOpenPositions)
		{
			if (longProfit < shortProfit && _currentSide != Sides.Buy)
			{
				OpenLong(candle);
			}
			else if (shortProfit < longProfit && _currentSide != Sides.Sell)
			{
				OpenShort(candle);
			}
			else if (longProfit == 0m && shortProfit == 0m && Position == 0 && _currentSide is null)
			{
				OpenLong(candle);
			}
		}
		else if (PnL > 0m && Position != 0)
		{
			FlattenPosition(candle);
		}

		if (PnL > ProfitForClose && Position != 0)
		{
			FlattenPosition(candle);
		}
	}

	private void OpenLong(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Already holding a long position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Buy;
			return;
		}

		BuyMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Buy;
	}

	private void OpenShort(ICandleMessage candle)
	{
		if (Position < 0)
		{
			// Already holding a short position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Sell;
			return;
		}

		SellMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Sell;
	}

	private void FlattenPosition(ICandleMessage candle)
	{
		if (_currentSide is null)
			return;

		if (Position > 0)
		{
			SellMarket();
		}
		else if (Position < 0)
		{
			BuyMarket();
		}

		_currentSide = null;
		_entryPrice = 0m;
	}
}