Auf GitHub ansehen

Zwei-Paar-Korrelationsstrategie

Überblick

Die Zwei-Paar-Korrelationsstrategie portiert den MetaTrader-Expertenberater "2-Paar-Korrelation EA" (Paket MQL/52043) auf die StockSharp-Hochebene API. Es überwacht die Geldkurse zweier stark korrelierender Kryptosymbole (BTCUSD als Primärzweig und ETHUSD als Absicherungszweig) und führt einen marktneutralen Handel durch, wenn deren Spread von einem konfigurierbaren Schwellenwert abweicht.

Kernworkflow

  1. Risk Gating – das Portfolioeigenkapital wird kontinuierlich überwacht. Wenn der Rückgang vom historischen Höchstwert MaxDrawdownPercent überschreitet, werden neue Geschäfte ausgesetzt, bis sich das Eigenkapital wieder über RecoveryPercent des Höchstwerts erholt.
  2. Volatilitätsfilter – beide Instrumente speisen einen 5-minütigen Kerzenstrom in einen AverageTrueRange-Indikator der Länge AtrPeriod ein. Der Handel wird übersprungen, wenn einer von ATR den Wert PriceDifferenceThreshold * 0.01 überschreitet, was die „Hohe Volatilitätspause“ aus dem Code MQL nachahmt.
  3. Spread-Erkennung – Die Strategie abonniert Level-1-Daten für beide Instrumente und bewertet die Geld-Preis-Spanne bei jeder Aktualisierung. Bei Bid(BTCUSD) - Bid(ETHUSD) > PriceDifferenceThreshold wird BTCUSD gekauft und ETHUSD verkauft. Wenn der Spread unter -PriceDifferenceThreshold fällt, werden die Positionen umgekehrt (Short BTCUSD, Long ETHUSD).
  4. Dynamische Lot-Größe – das Volumen pro Bein wird aus RiskPercent des aktuellen Portfolio-Eigenkapitals dividiert durch die synthetische Stop-Distanz StopLossPips * PriceStep abgeleitet. Das Ergebnis wird mit den Börsenvolumenbeschränkungen normalisiert, bevor Orders gesendet werden.
  5. Basket-Exit – der gesamte variable Gewinn beider Zweige wird in der Kontowährung erfasst. Sobald es MinimumTotalProfit erreicht, schließt die Strategie das gesamte Paar, unabhängig von der Einstiegsrichtung.

Erforderliche Marktdaten

  • Level1 (bester Geld-/Briefkurs) sowohl für das primäre Wertpapier (Security) als auch für das Hedge-Wertpapier (SecondSecurity).
  • Kerzen vom Typ AtrCandleType (standardmäßig 5-Minuten-Zeitrahmen) für dieselben zwei Instrumente zur Versorgung des ATR-Filters.

Stellen Sie sicher, dass die Wertpapiere aussagekräftige Werte für PriceStep, StepPrice, VolumeStep und minimale/maximale Volumenwerte aufweisen, damit die Losgröße und die Gewinnumrechnung das MetaTrader-Verhalten widerspiegeln.

Parameter

Name Typ Standard Beschreibung
SecondSecurity Security Absicherungsinstrument (ETHUSD im Original EA).
MaxDrawdownPercent decimal 20 Drawdown-Schwellenwert, der neue Trades pausiert.
RiskPercent decimal 2 Portfolioanteil, der pro Trade für die Positionsgröße riskiert wird.
PriceDifferenceThreshold decimal 100 Zur Eröffnung des Paares ist eine Divergenz zwischen Geldkurs und Kurs erforderlich.
MinimumTotalProfit decimal 0.30 Gewinnziel in Kontowährung für den Abschluss beider Legs.
AtrPeriod int 14 ATR Länge für den Volatilitätsfilter.
RecoveryPercent decimal 95 Prozentsatz des Spitzenkapitals, der erforderlich ist, um den Handel nach einem Drawdown wieder aufzunehmen.
StopLossPips int 50 Synthetischer Stopp, der zur Umwandlung von RiskPercent in Lose verwendet wird.
AtrCandleType DataType TimeSpan.FromMinutes(5).TimeFrame() Für die ATR-Berechnung verwendete Kerzenserie.

Dateien

  • CS/TwoPairCorrelationStrategy.cs – Strategieumsetzung basierend auf dem High-Level API.
  • README.md – diese Dokumentation (Englisch).
  • README_zh.md – Dokumentation auf Chinesisch.
  • README_ru.md – Dokumentation auf Russisch.
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>
/// Mean-reversion strategy with ATR volatility filter and drawdown control.
/// Simplified from the two-pair correlation EA to single security.
/// </summary>
public class TwoPairCorrelationStrategy : Strategy
{
	private readonly StrategyParam<decimal> _maxDrawdownPercent;
	private readonly StrategyParam<decimal> _priceDifferenceThreshold;
	private readonly StrategyParam<decimal> _minimumTotalProfit;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private AverageTrueRange _atr;
	private SimpleMovingAverage _sma;
	private decimal _atrValue;
	private decimal _entryPrice;
	private decimal _peakEquity;
	private bool _tradingPaused;

	/// <summary>
	/// Maximum drawdown percentage that pauses new entries.
	/// </summary>
	public decimal MaxDrawdownPercent
	{
		get => _maxDrawdownPercent.Value;
		set => _maxDrawdownPercent.Value = value;
	}

	/// <summary>
	/// Price deviation threshold from SMA for entry.
	/// </summary>
	public decimal PriceDifferenceThreshold
	{
		get => _priceDifferenceThreshold.Value;
		set => _priceDifferenceThreshold.Value = value;
	}

	/// <summary>
	/// Floating profit target for closing.
	/// </summary>
	public decimal MinimumTotalProfit
	{
		get => _minimumTotalProfit.Value;
		set => _minimumTotalProfit.Value = value;
	}

	/// <summary>
	/// ATR period for volatility filter.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public TwoPairCorrelationStrategy()
	{
		_maxDrawdownPercent = Param(nameof(MaxDrawdownPercent), 20m)
			.SetGreaterThanZero()
			.SetDisplay("Max Drawdown %", "Maximum drawdown before trading is paused", "Risk")
			.SetOptimize(5m, 50m, 5m);

		_priceDifferenceThreshold = Param(nameof(PriceDifferenceThreshold), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Price Deviation", "Distance from SMA required to enter", "Signals")
			.SetOptimize(1m, 20m, 1m);

		_minimumTotalProfit = Param(nameof(MinimumTotalProfit), 3m)
			.SetGreaterThanZero()
			.SetDisplay("Profit Target", "Floating profit required to close position", "Risk")
			.SetOptimize(1m, 10m, 1m);

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "Number of candles for volatility filter", "Indicators")
			.SetOptimize(5, 40, 1);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle series for signals", "Indicators");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_atr = null;
		_sma = null;
		_atrValue = 0m;
		_entryPrice = 0m;
		_peakEquity = 0m;
		_tradingPaused = false;
	}

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

		_peakEquity = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;

		_atr = new AverageTrueRange { Length = AtrPeriod };
		_sma = new SimpleMovingAverage { Length = 20 };

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

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

		if (_atr == null || _sma == null || !_atr.IsFormed || !_sma.IsFormed)
			return;

		_atrValue = atrValue;
		var price = candle.ClosePrice;

		// Drawdown control
		UpdateDrawdownState();

		// Check profit target
		if (Position != 0 && _entryPrice > 0m)
		{
			var pnl = Position > 0
				? price - _entryPrice
				: _entryPrice - price;

			var profitTarget = Math.Max(MinimumTotalProfit, _atrValue * 0.5m);
			if (profitTarget > 0m && pnl >= profitTarget)
			{
				if (Position > 0)
					SellMarket(Math.Abs(Position));
				else
					BuyMarket(Math.Abs(Position));

				_entryPrice = 0m;
				return;
			}
		}

		if (_tradingPaused)
			return;

		if (Position != 0)
			return;

		var deviation = price - smaValue;
		var entryThreshold = Math.Max(PriceDifferenceThreshold, _atrValue);

		if (deviation > entryThreshold)
		{
			SellMarket();
			_entryPrice = price;
		}
		else if (deviation < -entryThreshold)
		{
			BuyMarket();
			_entryPrice = price;
		}
	}

	private void UpdateDrawdownState()
	{
		if (Portfolio == null)
			return;

		var equity = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
		if (equity <= 0m)
			return;

		if (equity > _peakEquity)
			_peakEquity = equity;

		if (MaxDrawdownPercent <= 0m || _peakEquity <= 0m)
		{
			_tradingPaused = false;
			return;
		}

		var drawdown = (_peakEquity - equity) / _peakEquity * 100m;

		if (!_tradingPaused && drawdown >= MaxDrawdownPercent)
		{
			_tradingPaused = true;
		}
		else if (_tradingPaused && drawdown < MaxDrawdownPercent * 0.5m)
		{
			_tradingPaused = false;
		}
	}
}