Ver en GitHub

Estrategia de correlación de dos pares

Descripción general

La Estrategia de correlación de dos pares transfiere el asesor experto MetaTrader "Correlación de 2 pares EA" (paquete MQL/52043) al API de alto nivel de StockSharp. Observa los precios de oferta de dos símbolos criptográficos altamente correlacionados (BTCUSD como tramo principal y ETHUSD como tramo de cobertura) y realiza una operación neutral en el mercado cuando su diferencial se desvía de un umbral configurable.

Flujo de trabajo principal

  1. Control de riesgos: el capital de la cartera se monitorea continuamente. Si la reducción desde el pico histórico supera MaxDrawdownPercent, las nuevas operaciones se suspenden hasta que el capital se recupere por encima de RecoveryPercent del valor máximo.
  2. Filtro de volatilidad: ambos instrumentos introducen un flujo de velas de 5 minutos en un indicador AverageTrueRange de longitud AtrPeriod. Las operaciones se omiten cuando ATR excede PriceDifferenceThreshold * 0.01, imitando la "pausa de alta volatilidad" del código MQL.
  3. Detección de diferencial: la estrategia se suscribe a datos de nivel uno para ambos instrumentos y evalúa el diferencial de precio de oferta en cada actualización. Cuando Bid(BTCUSD) - Bid(ETHUSD) > PriceDifferenceThreshold, compra BTCUSD y vende ETHUSD. Cuando el diferencial cae por debajo de -PriceDifferenceThreshold, las posiciones se invierten (BTCUSD corto, ETHUSD largo).
  4. Tamaño de lote dinámico: el volumen por tramo se deriva de RiskPercent del capital de la cartera actual, dividido por la distancia de parada sintética StopLossPips * PriceStep. El resultado se normaliza con las restricciones de volumen de intercambio antes de enviar las órdenes.
  5. Salida de la cesta: el beneficio flotante total de ambos tramos se rastrea en la moneda de la cuenta. Una vez que llega a MinimumTotalProfit, la estrategia cierra todo el par independientemente de la dirección de entrada.

Datos de mercado requeridos

  • Nivel 1 (mejor oferta/demanda) tanto para el valor principal (Security) como para el valor de cobertura (SecondSecurity).
  • Velas de tipo AtrCandleType (predeterminado en un período de tiempo de 5 minutos) para que los mismos dos instrumentos alimenten el filtro ATR.

Asegúrese de que los valores expongan valores significativos de volumen PriceStep, StepPrice, VolumeStep y mínimo/máximo para que el tamaño del lote y la conversión de ganancias reflejen el comportamiento de MetaTrader.

Parámetros

Nombre Tipo Predeterminado Descripción
SecondSecurity Security Instrumento de cobertura (ETHUSD en el EA original).
MaxDrawdownPercent decimal 20 Umbral de reducción que detiene nuevas operaciones.
RiskPercent decimal 2 Participación de la cartera arriesgada por operación para el tamaño de la posición.
PriceDifferenceThreshold decimal 100 Se requiere divergencia del precio de oferta para abrir el par.
MinimumTotalProfit decimal 0.30 Objetivo de beneficio en la moneda de la cuenta para cerrar ambos tramos.
AtrPeriod int 14 ATR longitud para el filtro de volatilidad.
RecoveryPercent decimal 95 Porcentaje del capital máximo necesario para reanudar la negociación después de una reducción.
StopLossPips int 50 Parada sintética utilizada para traducir RiskPercent en lotes.
AtrCandleType DataType TimeSpan.FromMinutes(5).TimeFrame() Serie de velas utilizada para el cálculo de ATR.

Archivos

  • CS/TwoPairCorrelationStrategy.cs: implementación de estrategia basada en el nivel alto API.
  • README.md – esta documentación (inglés).
  • README_zh.md – documentación en chino.
  • README_ru.md – documentación en ruso.
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;
		}
	}
}