Ver en GitHub

Estrategia de Trading de Dispersión

La estrategia de trading de dispersión explota los períodos en que un índice de renta variable y sus componentes divergen. Cuando la correlación media por pares entre los miembros del índice cae por debajo de un umbral, la estrategia compra las acciones individuales y vende el índice en corto, apostando a que las correlaciones volverán a la media.

Las velas diarias alimentan una ventana deslizante de correlación. Si las correlaciones se recuperan por encima del umbral, se cierran todas las posiciones. Se impone un valor mínimo de operación para evitar órdenes pequeñas.

Detalles

  • Universo: Un instrumento de índice más las acciones que lo componen.
  • Señal: Abrir una operación de dispersión cuando la correlación media de los componentes está por debajo de CorrThreshold.
  • Rebalanceo: La correlación se comprueba cada día.
  • Posicionamiento: Largo en los componentes y corto en el índice mientras la señal está activa.
  • Parámetros:
    • Constituents – lista de valores componentes.
    • LookbackDays – tamaño de la ventana para el cálculo de correlación.
    • CorrThreshold – nivel de correlación que desencadena las operaciones.
    • MinTradeUsd – valor mínimo de orden en USD.
    • CandleType – marco temporal de las velas (predeterminado: 1 día).
  • Nota: El ejemplo omite los costes de transacción y asume igual ponderación.
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>
/// Dispersion trading strategy.
/// Trades an equity index against its constituent securities when the average correlation falls below a threshold.
/// </summary>
public class DispersionTradingStrategy : Strategy
{
	private readonly StrategyParam<IEnumerable<Security>> _constituents;
	private readonly StrategyParam<int> _lookbackDays;
	private readonly StrategyParam<decimal> _corrThreshold;
	private readonly StrategyParam<decimal> _minTradeUsd;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Dictionary<Security, RollingWindow<decimal>> _windows = [];
	private readonly Dictionary<Security, decimal> _latestPrices = [];
	private DateTime _lastDay = DateTime.MinValue;
	private bool _open;

	/// <summary>
	/// Securities representing index constituents.
	/// </summary>
	public IEnumerable<Security> Constituents
	{
		get => _constituents.Value;
		set => _constituents.Value = value;
	}

	/// <summary>
	/// Number of days used for correlation calculation.
	/// </summary>
	public int LookbackDays
	{
		get => _lookbackDays.Value;
		set => _lookbackDays.Value = value;
	}

	/// <summary>
	/// Correlation threshold for opening dispersion.
	/// </summary>
	public decimal CorrThreshold
	{
		get => _corrThreshold.Value;
		set => _corrThreshold.Value = value;
	}

	/// <summary>
	/// Minimum trade value in USD.
	/// </summary>
	public decimal MinTradeUsd
	{
		get => _minTradeUsd.Value;
		set => _minTradeUsd.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="DispersionTradingStrategy"/>.
	/// </summary>
	public DispersionTradingStrategy()
	{
		_constituents = Param<IEnumerable<Security>>(nameof(Constituents), [])
			.SetDisplay("Constituents", "Index constituent securities", "General");

		_lookbackDays = Param(nameof(LookbackDays), 60)
			.SetDisplay("Lookback Days", "Days for rolling correlation", "Parameters");

		_corrThreshold = Param(nameof(CorrThreshold), 0.4m)
			.SetDisplay("Correlation Threshold", "Average correlation threshold", "Parameters");

		_minTradeUsd = Param(nameof(MinTradeUsd), 100m)
			.SetDisplay("Minimum Trade USD", "Minimal order value", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for analysis", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return Constituents.Append(Security).Select(s => (s, CandleType));
	}

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

		_windows.Clear();
		_latestPrices.Clear();
		_lastDay = default;
		_open = default;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		if (Security == null)
			throw new InvalidOperationException("IndexSec is not set.");

		if (Constituents == null || !Constituents.Any())
			throw new InvalidOperationException("Constituents collection is empty.");

		foreach (var (sec, dt) in GetWorkingSecurities())
		{
			_windows[sec] = new RollingWindow<decimal>(LookbackDays + 1);

			SubscribeCandles(dt, true, sec)
				.Bind(c => ProcessCandle(c, sec))
				.Start();
		}
	}

	private void ProcessCandle(ICandleMessage candle, Security security)
	{
		// Skip unfinished candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Store the latest closing price for this security.
		_latestPrices[security] = candle.ClosePrice;

		_windows[security].Add(candle.ClosePrice);

		var day = candle.OpenTime.Date;
		if (day == _lastDay)
			return;

		_lastDay = day;

		if (_windows.Values.Any(w => !w.IsFull()))
			return;

		// Daily check after windows are full.
		EvaluateSignal();
	}

	private void EvaluateSignal()
	{
		var indexRet = Returns(_windows[Security]);

		var corrs = new List<decimal>();
		foreach (var s in Constituents)
			corrs.Add(Corr(Returns(_windows[s]), indexRet));

		var avg = corrs.Average();

		if (avg < CorrThreshold && !_open)
			OpenDispersion();
		else if (avg >= CorrThreshold && _open)
			CloseAll();
	}

	private void OpenDispersion()
	{
		var count = Constituents.Count();
		var portfolioValue = Portfolio.CurrentValue ?? 0m;
		var capLeg = portfolioValue * 0.5m;
		var eachLong = capLeg / count;

		foreach (var s in Constituents)
		{
			var price = GetLatestPrice(s);
			if (price > 0)
				TradeToTarget(s, eachLong / price);
		}

		var indexPrice = GetLatestPrice(Security);
		if (indexPrice > 0)
			TradeToTarget(Security, -capLeg / indexPrice); // short index

		_open = true;
		LogInfo("Opened dispersion spread");
	}

	private void CloseAll()
	{
		foreach (var position in Positions)
			TradeToTarget(position.Security, 0m);

		_open = false;
		LogInfo("Closed dispersion spread");
	}

	private decimal GetLatestPrice(Security security)
	{
		return _latestPrices.TryGetValue(security, out var price) ? price : 0m;
	}

	#region Helper math / trading

	private decimal[] Returns(RollingWindow<decimal> win)
	{
		var arr = win.ToArray();
		var r = new decimal[arr.Length - 1];

		for (var i = 1; i < arr.Length; i++)
			r[i - 1] = (arr[i] - arr[i - 1]) / arr[i - 1];

		return r;
	}

	private decimal Corr(decimal[] x, decimal[] y)
	{
		var n = Math.Min(x.Length, y.Length);
		var meanX = x.Take(n).Average();
		var meanY = y.Take(n).Average();

		decimal num = 0, dx = 0, dy = 0;

		for (var i = 0; i < n; i++)
		{
			var a = x[i] - meanX;
			var b = y[i] - meanY;
			num += a * b;
			dx += a * a;
			dy += b * b;
		}

		return dx > 0 && dy > 0 ? num / (decimal)Math.Sqrt((double)(dx * dy)) : 0m;
	}

	private void TradeToTarget(Security s, decimal tgtQty)
	{
		var diff = tgtQty - PositionBy(s);
		var price = GetLatestPrice(s);

		if (price <= 0 || Math.Abs(diff) * price < MinTradeUsd)
			return;

		RegisterOrder(new Order
		{
			Security = s,
			Portfolio = Portfolio,
			Side = diff > 0 ? Sides.Buy : Sides.Sell,
			Volume = Math.Abs(diff),
			Type = OrderTypes.Market,
			Comment = "Dispersion"
		});
	}

	private decimal PositionBy(Security s) => GetPositionValue(s, Portfolio) ?? 0;

	#endregion

	#region RollingWindow

	private class RollingWindow<T>
	{
		private readonly Queue<T> _queue = [];
		private readonly int _size;

		public RollingWindow(int size)
		{
			_size = size;
		}

		public void Add(T value)
		{
			if (_queue.Count == _size)
				_queue.Dequeue();

			_queue.Enqueue(value);
		}

		public bool IsFull() => _queue.Count == _size;

		public T Last() => _queue.Last();

		public T[] ToArray() => [.. _queue];
	}

	#endregion
}