Auf GitHub ansehen

Multi-Paar-Schließer-Strategie

Überblick

Die Multi-Paar-Schließer-Strategie spiegelt das ursprüngliche MetaTrader-Skript wider, das einen Korb von Währungspaaren überwacht und alle offenen Positionen liquidiert, sobald der kombinierte schwebende Gewinn ein Ziel trifft oder der angesammelte Verlust einen Sicherheitsschwellenwert überschreitet. Die Konvertierung nutzt StockSharp's High-Level-API, um Gewinne zu verfolgen, eine Mindesthaltedauer durchzusetzen und Positionen über mehrere Wertpapiere in einer Aktion zu schließen.

Logik

  1. Die beobachteten Instrumente aus dem kommagetrennnten WatchedSymbols-Parameter auflösen. Wenn die Liste leer ist, wird das Haupt-Security verwendet.
  2. Den ausgewählten Kerzentyp (Standard: 1-Minuten-Zeitrahmen) für jedes Instrument abonnieren. Jede fertige Kerze löst eine Gewinnauswertung aus.
  3. Für jedes Instrument speichert die Strategie:
    • Den zuletzt berechneten Gewinn (Positions[i].PnL).
    • Den Timestamp, wann eine Position zuerst ungleich null wurde, um die MinAgeSeconds-Anforderung einzuhalten.
  4. Nach jeder Aktualisierung wird der Nettogewinn über alle beobachteten Symbole berechnet:
    • Wenn ProfitTarget erreicht wird, werden alle Positionen, die älter als das Mindestalter sind, mit BuyMarket / SellMarket-Orders geglättet.
    • Wenn der Nettogewinn unter -MaxLoss fällt, wird dieselbe Liquidationslogik als Schutzstop angewendet.
  5. Detaillierte Protokolle fassen den Gewinn pro Instrument und das aktuelle Korbergebnis nach jeder Auswertung zusammen.

Parameter

Parameter Beschreibung Standard
WatchedSymbols Kommagetrennte Liste von Wertpapieridentifikatoren zur Überwachung. Wenn leer, greift die Strategie auf das zugewiesene Security zurück. "GBPUSD,USDCAD,USDCHF,USDSEK"
ProfitTarget Nettogewinn (in Portfoliowährung), der erforderlich ist, um einen globalen Abschluss aller beobachteten Positionen auszulösen. 60
MaxLoss Maximaler akzeptabler Verlust (in Portfoliowährung), bevor die Strategie den Korb zwangsschließt. 60
Slippage Kompatibilitätsparameter, der den erlaubten Slippage aus dem ursprünglichen Skript widerspiegelt. Marktorders werden für Ausstiege verwendet, daher ist der Wert informativ. 10
MinAgeSeconds Mindestlebensdauer einer Position, bevor die Strategie sie schließen darf. 60
CandleType Kerzentyp, der für die periodische Überwachung verwendet wird (Standard: 1-Minuten-Kerzen). 1 minute

Hinweise

  • Die Strategie verlässt sich auf Positions[i].PnL, bereitgestellt von StockSharp, um den schwebenden Gewinn zu messen. Sie ruft keine Handelshistorie ab und berechnet Preise nicht manuell.
  • Positionen, die vor dem Start der Strategie geöffnet wurden, erben die Startzeit als ihren ersten gesehenen Timestamp. Sie werden erst geschlossen, nachdem das MinAgeSeconds-Intervall seit dem Strategiestart vergangen ist.
  • Ausstiege werden mit Marktorders ausgeführt, um die Wahrscheinlichkeit sofortiger Liquidation zu maximieren. Slippage wird aus Gründen der Parität mit der MQL-Version protokolliert, wird aber nicht auf Preisberechnungen angewendet.
  • Die Protokollausgabe repliziert das MetaTrader "Comment"-Fenster, indem der Gewinn jedes Symbols gefolgt vom Gesamtkorb-Total ausgegeben wird.

Anforderungen

  • Einen gültigen SecurityProvider zuweisen oder sicherstellen, dass die angeforderten Identifikatoren über den Connector verfügbar sind.
  • Ausreichende Volumenkonfiguration pro Wertpapier bereitstellen, damit Marktorders die Position vollständig glätten können.
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>
/// Closes the current position when floating PnL reaches a profit target or maximum loss.
/// Simplified from the multi-pair closer utility to work with a single security.
/// </summary>
public class MultiPairCloserStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<decimal> _maxLoss;
	private readonly StrategyParam<int> _minAgeSeconds;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _smaPeriod;

	private SimpleMovingAverage _sma;
	private decimal _entryPrice;
	private DateTimeOffset? _entryTime;

	/// <summary>
	/// Profit target in price units.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Maximum tolerated loss in price units.
	/// </summary>
	public decimal MaxLoss
	{
		get => _maxLoss.Value;
		set => _maxLoss.Value = value;
	}

	/// <summary>
	/// Minimum age of an open position in seconds before exit is permitted.
	/// </summary>
	public int MinAgeSeconds
	{
		get => _minAgeSeconds.Value;
		set => _minAgeSeconds.Value = value;
	}

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

	/// <summary>
	/// SMA period for entry signals.
	/// </summary>
	public int SmaPeriod
	{
		get => _smaPeriod.Value;
		set => _smaPeriod.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public MultiPairCloserStrategy()
	{
		_profitTarget = Param(nameof(ProfitTarget), 5m)
			.SetNotNegative()
			.SetDisplay("Profit Target", "Close position when floating profit reaches this value", "Risk Management");

		_maxLoss = Param(nameof(MaxLoss), 10m)
			.SetNotNegative()
			.SetDisplay("Maximum Loss", "Close position when floating loss reaches this value", "Risk Management");

		_minAgeSeconds = Param(nameof(MinAgeSeconds), 60)
			.SetNotNegative()
			.SetDisplay("Min Age (s)", "Minimum holding time before exit is allowed", "Execution");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle series for monitoring", "General");

		_smaPeriod = Param(nameof(SmaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("SMA Period", "Moving average period for entry signal", "Indicators");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_sma = null;
		_entryPrice = 0m;
		_entryTime = null;
	}

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

		_sma = new SimpleMovingAverage { Length = SmaPeriod };

		SubscribeCandles(CandleType)
			.Bind(_sma, ProcessCandle)
			.Start();
	}

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

		if (!IsFormed)
			return;

		var price = candle.ClosePrice;
		var time = candle.CloseTime;

		// Check exit conditions for open position
		if (Position != 0 && _entryPrice > 0m)
		{
			var pnl = Position > 0
				? price - _entryPrice
				: _entryPrice - price;

			var canClose = MinAgeSeconds <= 0 ||
				(_entryTime.HasValue && (time - _entryTime.Value).TotalSeconds >= MinAgeSeconds);

			if (canClose)
			{
				if ((ProfitTarget > 0m && pnl >= ProfitTarget) ||
					(MaxLoss > 0m && pnl <= -MaxLoss))
				{
					if (Position > 0)
						SellMarket(Math.Abs(Position));
					else
						BuyMarket(Math.Abs(Position));

					_entryPrice = 0m;
					_entryTime = null;
					return;
				}
			}
		}

		// Entry logic: trend following with SMA
		if (Position == 0)
		{
			if (price > smaValue)
			{
				BuyMarket();
				_entryPrice = price;
				_entryTime = time;
			}
			else if (price < smaValue)
			{
				SellMarket();
				_entryPrice = price;
				_entryTime = time;
			}
		}
	}
}