Ver en GitHub

Estrategia de Arbitraje Estadístico

Este enfoque de arbitraje estadístico opera un par de valores relacionados basándose en su posición relativa alrededor de las medias móviles. Al comparar cada activo con su propia media, la estrategia busca explotar las dislocaciones a corto plazo que deberían converger con el tiempo.

Las pruebas indican un retorno anual promedio de aproximadamente 94%. Funciona mejor en el mercado de acciones.

Se inicia una posición larga cuando el primer activo cotiza por debajo de su media móvil mientras el segundo activo cotiza por encima de su propia media. Una posición corta ocurre cuando el primer activo está por encima de su media y el segundo está por debajo. Las posiciones se cierran cuando el primer activo cruza de vuelta a través de su media móvil, señalando que el diferencial se ha normalizado.

El método es ideal para traders neutrales al mercado cómodos balanceando la exposición entre dos instrumentos. El stop-loss incorporado limita las caídas si el diferencial se amplía en lugar de revertirse.

Detalles

  • Criterios de entrada:
    • Largo: Asset1 < MA1 && Asset2 > MA2
    • Corto: Asset1 > MA1 && Asset2 < MA2
  • Largo/Corto: Ambos lados.
  • Criterios de salida:
    • Largo: Salir cuando Asset1 cierra por encima de su MA1
    • Corto: Salir cuando Asset1 cierra por debajo de su MA1
  • Stops: Sí, stop-loss porcentual sobre el diferencial.
  • Valores predeterminados:
    • LookbackPeriod = 20
    • StopLossPercent = 2m
    • CandleType = TimeSpan.FromMinutes(15)
  • Filtros:
    • Categoría: Arbitraje
    • Dirección: Ambos
    • Indicadores: Moving Averages
    • Stops: Sí
    • Complejidad: Intermedio
    • Marco temporal: Intradía
    • Estacionalidad: No
    • Redes neuronales: No
    • Divergencia: Sí
    • Nivel de riesgo: Medio
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>
/// Statistical Arbitrage strategy that trades pairs of securities based on their relative mean reversion.
/// Enters when one asset is below its mean while the other is above its mean.
/// </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 SimpleMovingAverage _firstMA;
	private SimpleMovingAverage _secondMA;
	
	private decimal _lastFirstPrice;
	private decimal _lastSecondPrice;
	private decimal _entrySpread;
	private decimal _secondMAValue;
	
	/// <summary>
	/// Period for calculating moving averages.
	/// </summary>
	public int LookbackPeriod
	{
		get => _lookbackPeriod.Value;
		set => _lookbackPeriod.Value = value;
	}
	
	/// <summary>
	/// Stop-loss percentage parameter.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}
	
	/// <summary>
	/// Candle type parameter.
	/// </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", "Period for calculating moving averages", "Parameters")
			
			.SetOptimize(10, 30, 5);
			
		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Stop-loss %", "Stop-loss as percentage of entry price", "Risk Management")
			
			.SetOptimize(1m, 3m, 0.5m);
			
		_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();

		_firstMA = null;
		_secondMA = null;
		_lastFirstPrice = 0;
		_lastSecondPrice = 0;
		_entrySpread = 0;
		_secondMAValue = 0;
	}

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

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

		// Initialize indicators
		_firstMA = new() { Length = LookbackPeriod };
		_secondMA = new() { Length = LookbackPeriod };

		// Create subscriptions for both securities
		var firstSecuritySubscription = SubscribeCandles(CandleType);
		var secondSecuritySubscription = SubscribeCandles(CandleType, security: SecondSecurity);

		// Bind to first security candles
		firstSecuritySubscription
			.Bind(_firstMA, ProcessFirstSecurityCandle)
			.Start();

		// Bind to second security candles
		secondSecuritySubscription
			.Bind(ProcessSecondSecurityCandle)
			.Start();

		// Enable position protection with stop-loss
		StartProtection(
			takeProfit: new Unit(0, UnitTypes.Absolute), // No take-profit
			stopLoss: new Unit(StopLossPercent, UnitTypes.Percent) // Stop-loss as percentage
		);
		
		// Setup chart if available
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, firstSecuritySubscription);
			DrawIndicator(area, _firstMA);
			DrawOwnTrades(area);
		}
	}
	
	private void ProcessFirstSecurityCandle(ICandleMessage candle, decimal firstMAValue)
	{
		// Skip unfinished candles
		if (candle.State != CandleStates.Finished)
			return;

		// Store current price
		_lastFirstPrice = candle.ClosePrice;

		// Skip if we don't have both prices or if indicators aren't formed
		if (_lastSecondPrice == 0 || !_firstMA.IsFormed || !_secondMA.IsFormed)
			return;

		// Get last second MA value stored earlier
		decimal secondMAValue = _secondMAValue;
		
		// Trading logic
		bool isFirstBelowMA = _lastFirstPrice < firstMAValue;
		bool isSecondAboveMA = _lastSecondPrice > secondMAValue;
		bool isFirstAboveMA = _lastFirstPrice > firstMAValue;
		bool isSecondBelowMA = _lastSecondPrice < secondMAValue;
		
		decimal currentSpread = _lastFirstPrice - _lastSecondPrice;
		
		// Long signal: First asset below MA, Second asset above MA
		if (isFirstBelowMA && isSecondAboveMA)
		{
			// If we're not already in a long position
			if (Position <= 0)
			{
				BuyMarket(Volume + Math.Abs(Position));
				_entrySpread = currentSpread;
				LogInfo($"Long Signal: {Security.Code}({_lastFirstPrice:F4}) < MA({firstMAValue:F4}) && " + 
						$"{SecondSecurity.Code}({_lastSecondPrice:F4}) > MA({secondMAValue:F4})");
				
				// Note: In a real implementation, you would also place a sell order
				// for the second security here, using a different strategy instance or connector
			}
		}
		// Short signal: First asset above MA, Second asset below MA
		else if (isFirstAboveMA && isSecondBelowMA)
		{
			// If we're not already in a short position
			if (Position >= 0)
			{
				SellMarket(Volume + Math.Abs(Position));
				_entrySpread = currentSpread;
				LogInfo($"Short Signal: {Security.Code}({_lastFirstPrice:F4}) > MA({firstMAValue:F4}) && " + 
						$"{SecondSecurity.Code}({_lastSecondPrice:F4}) < MA({secondMAValue:F4})");
				
				// Note: In a real implementation, you would also place a buy order
				// for the second security here, using a different strategy instance or connector
			}
		}
		// Exit signals
		else if ((Position > 0 && isFirstAboveMA) || (Position < 0 && isFirstBelowMA))
		{
			// Exit position when first asset crosses its moving average
			if (Position > 0)
			{
				SellMarket(Math.Abs(Position));
				LogInfo($"Exit Long: {Security.Code}({_lastFirstPrice:F4}) > MA({firstMAValue:F4})");
			}
			else if (Position < 0)
			{
				BuyMarket(Math.Abs(Position));
				LogInfo($"Exit Short: {Security.Code}({_lastFirstPrice:F4}) < MA({firstMAValue:F4})");
			}
		}
	}
	
	private void ProcessSecondSecurityCandle(ICandleMessage candle)
	{
		// Skip unfinished candles
		if (candle.State != CandleStates.Finished)
			return;

		// Store current price
		_lastSecondPrice = candle.ClosePrice;
	
		// Process through MA indicator and store last value
		var maValue = _secondMA.Process(new DecimalIndicatorValue(_secondMA, candle.ClosePrice, candle.ServerTime) { IsFinal = true });
		_secondMAValue = maValue.ToDecimal();
	}
}