Auf GitHub ansehen

Statistische Arbitrage-Strategie

Dieser statistische Arbitrage-Ansatz handelt ein Paar verwandter Wertpapiere basierend auf ihrer relativen Positionierung um gleitende Durchschnitte. Durch den Vergleich jedes Vermögenswerts mit seinem eigenen Durchschnitt versucht die Strategie, kurzfristige Disbalancen auszunutzen, die im Laufe der Zeit konvergieren sollten.

Tests zeigen eine durchschnittliche jährliche Rendite von etwa 94%. Sie funktioniert am besten am Aktienmarkt.

Eine Long-Position wird eingeleitet, wenn der erste Vermögenswert unter seinem gleitenden Durchschnitt handelt, während der zweite Vermögenswert über seinem eigenen Durchschnitt handelt. Eine Short-Position tritt auf, wenn der erste Vermögenswert über seinem Durchschnitt und der zweite darunter liegt. Positionen werden geschlossen, wenn der erste Vermögenswert wieder durch seinen gleitenden Durchschnitt kreuzt, was signalisiert, dass sich die Spread normalisiert hat.

Die Methode ist ideal für marktneutrale Trader, die mit der Balance von Exponierung über zwei Instrumente vertraut sind. Der eingebaute Stop-Loss begrenzt Drawdowns, wenn sich die Spread weiter ausweitet, anstatt sich umzukehren.

Details

  • Einstiegskriterien:
    • Long: Asset1 < MA1 && Asset2 > MA2
    • Short: Asset1 > MA1 && Asset2 < MA2
  • Long/Short: Beide Seiten.
  • Ausstiegskriterien:
    • Long: Ausstieg, wenn Asset1 über MA1 schließt
    • Short: Ausstieg, wenn Asset1 unter MA1 schließt
  • Stops: Ja, prozentualer Stop-Loss auf Spread.
  • Standardwerte:
    • LookbackPeriod = 20
    • StopLossPercent = 2
    • CandleType = TimeSpan.FromMinutes(15)
    • SecondSecurity — erforderlich, ohne Standardwert Asset1 ist das Security der Strategie und Asset2 ist SecondSecurity; MA1 und MA2 sind die einfachen gleitenden Durchschnitte ihrer letzten LookbackPeriod Schlusskurse auf Kerzen derselben Zeit. Ein Long kauft Asset1 und verkauft Asset2, ein Short umgekehrt, jedes Bein mit Volume, ein Gegensignal dreht beide Beine. Der Stop schließt beide Beine, sobald sich der Spread, Asset1 minus Asset2, um StopLossPercent seines Einstiegswerts gegen das Paar bewegt, geprüft bei Kerzenschluss; 0 schaltet ihn ab.
  • Filter:
    • Kategorie: Arbitrage
    • Richtung: Beide
    • Indikatoren: Moving Averages
    • Stops: Ja
    • Komplexität: Mittel
    • Zeitrahmen: Intraday
    • Saisonalität: Nein
    • Neuronale Netze: Nein
    • Divergenz: Ja
    • Risikolevel: Mittel
using System;
using System.Collections.Generic;
using System.Linq;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Statistical arbitrage strategy.
/// Each instrument is compared with the simple moving average of its last LookbackPeriod closes on candles of the same time. The first
/// instrument below its average while the second is above its own buys the first and sells the second, the mirror does the opposite,
/// reversing an opposite pair. Both legs close once the first instrument closes back across its average, or once the spread, the first
/// close minus the second, moves StopLossPercent of its entry value against the pair.
/// </summary>
public class StatisticalArbitrageStrategy : Strategy
{
	private readonly StrategyParam<int> _lookbackPeriod;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<Security> _secondSecurity;

	private readonly Dictionary<DateTime, decimal> _firstCloses = [];
	private readonly Dictionary<DateTime, decimal> _secondCloses = [];
	private readonly Queue<decimal> _firstHistory = [];
	private readonly Queue<decimal> _secondHistory = [];
	// 1 while long the spread, -1 while short it, 0 while flat.
	private int _side;
	private decimal _entrySpread;

	/// <summary>
	/// Number of closes each moving average spans.
	/// </summary>
	public int LookbackPeriod
	{
		get => _lookbackPeriod.Value;
		set => _lookbackPeriod.Value = value;
	}

	/// <summary>
	/// Adverse spread move, in percent of the entry spread, that closes the pair.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

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

	/// <summary>
	/// Second security in the pair.
	/// </summary>
	public Security SecondSecurity
	{
		get => _secondSecurity.Value;
		set => _secondSecurity.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public StatisticalArbitrageStrategy()
	{
		_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Lookback Period", "Closes each moving average spans", "Parameters");

		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetNotNegative()
			.SetDisplay("Stop-loss %", "Adverse spread move in percent of the entry spread", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");

		_secondSecurity = Param<Security>(nameof(SecondSecurity))
			.SetDisplay("Second Security", "Second security in the pair", "General")
			.SetRequired();
	}

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

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

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

		if (SecondSecurity == null)
			throw new InvalidOperationException("Second security is not specified.");

		ResetState();

		var firstSubscription = SubscribeCandles(CandleType);
		firstSubscription.Bind(candle => ProcessCandle(candle, _firstCloses)).Start();

		var secondSubscription = SubscribeCandles(CandleType, security: SecondSecurity);
		secondSubscription.Bind(candle => ProcessCandle(candle, _secondCloses)).Start();

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

	private void ResetState()
	{
		_firstCloses.Clear();
		_secondCloses.Clear();
		_firstHistory.Clear();
		_secondHistory.Clear();
		_side = 0;
		_entrySpread = 0;
	}

	private void ProcessCandle(ICandleMessage candle, Dictionary<DateTime, decimal> closes)
	{
		if (candle.State != CandleStates.Finished)
			return;

		closes[candle.OpenTime] = candle.ClosePrice;

		// The spread needs both instruments' candles of the same time, whichever arrives last.
		if (!_firstCloses.TryGetValue(candle.OpenTime, out var first) || !_secondCloses.TryGetValue(candle.OpenTime, out var second))
			return;

		foreach (var stale in _firstCloses.Keys.Where(t => t <= candle.OpenTime).ToArray())
			_firstCloses.Remove(stale);

		foreach (var stale in _secondCloses.Keys.Where(t => t <= candle.OpenTime).ToArray())
			_secondCloses.Remove(stale);

		_firstHistory.Enqueue(first);
		_secondHistory.Enqueue(second);

		if (_firstHistory.Count > LookbackPeriod)
		{
			_firstHistory.Dequeue();
			_secondHistory.Dequeue();
		}

		if (_firstHistory.Count < LookbackPeriod)
			return;

		var firstAverage = _firstHistory.Average();
		var secondAverage = _secondHistory.Average();
		var spread = first - second;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var stopDistance = Math.Abs(_entrySpread) * StopLossPercent / 100;

		if (first < firstAverage && second > secondAverage && _side <= 0)
		{
			MoveLegs(1);
			_entrySpread = spread;
		}
		else if (first > firstAverage && second < secondAverage && _side >= 0)
		{
			MoveLegs(-1);
			_entrySpread = spread;
		}
		else if (_side > 0 && (first > firstAverage || (StopLossPercent > 0 && spread <= _entrySpread - stopDistance)))
		{
			MoveLegs(0);
		}
		else if (_side < 0 && (first < firstAverage || (StopLossPercent > 0 && spread >= _entrySpread + stopDistance)))
		{
			MoveLegs(0);
		}
	}

	private void MoveLegs(int side)
	{
		_side = side;

		// Long the spread holds the first instrument and is short the second, each by Volume.
		var firstChange = side * Volume - Position;

		if (firstChange > 0)
			BuyMarket(firstChange);
		else if (firstChange < 0)
			SellMarket(-firstChange);

		var secondChange = -side * Volume - (GetPositionValue(SecondSecurity, Portfolio) ?? 0m);

		if (secondChange > 0)
			BuyMarket(secondChange, SecondSecurity);
		else if (secondChange < 0)
			SellMarket(-secondChange, SecondSecurity);
	}
}