Ver no GitHub

Estratégia de correlação de dois pares

Visão geral

A Estratégia de correlação de dois pares transporta o consultor especialista MetaTrader "Correlação de 2 pares EA" (pacote MQL/52043) para o StockSharp API de alto nível. Ele observa os preços de oferta de dois símbolos criptográficos altamente correlacionados (BTCUSD como perna primária e ETHUSD como perna de hedge) e realiza uma negociação neutra em termos de mercado quando seu spread se desvia de um limite configurável.

Fluxo de trabalho principal

  1. Restrição de risco – o patrimônio do portfólio é monitorado continuamente. Se o rebaixamento do pico histórico exceder MaxDrawdownPercent, novas negociações serão suspensas até que o patrimônio se recupere acima de RecoveryPercent do valor de pico.
  2. Filtro de volatilidade – ambos os instrumentos alimentam um fluxo de vela de 5 minutos em um indicador AverageTrueRange de comprimento AtrPeriod. A negociação é ignorada quando ATR excede PriceDifferenceThreshold * 0.01, imitando a "pausa de alta volatilidade" do código MQL.
  3. Detecção de spread – a estratégia assina dados de nível um para ambos os instrumentos e avalia o spread do preço de compra em cada atualização. Quando Bid(BTCUSD) - Bid(ETHUSD) > PriceDifferenceThreshold, compra BTCUSD e vende ETHUSD. Quando o spread cai abaixo de -PriceDifferenceThreshold, as posições são invertidas (short BTCUSD, long ETHUSD).
  4. Dimensionamento dinâmico de lote – o volume por perna é derivado de RiskPercent do patrimônio atual do portfólio, dividido pela distância de stop sintética StopLossPips * PriceStep. O resultado é normalizado com as restrições de volume de câmbio antes do envio dos pedidos.
  5. Saída da cesta – o lucro flutuante total de ambas as pernas é rastreado na moeda da conta. Ao atingir MinimumTotalProfit, a estratégia fecha o par inteiro, independentemente da direção de entrada.

Dados de mercado necessários

  • Nível1 (melhor oferta/venda) tanto para o título primário (Security) quanto para o título de hedge (SecondSecurity).
  • Velas do tipo AtrCandleType (o padrão é o período de 5 minutos) para os mesmos dois instrumentos para alimentar o filtro ATR.

Certifique-se de que os títulos exponham valores significativos de PriceStep, StepPrice, VolumeStep e volume mínimo/máximo para que o tamanho do lote e a conversão de lucro espelhem o comportamento de MetaTrader.

Parâmetros

Nome Tipo Padrão Descrição
SecondSecurity Security - Instrumento de hedge (ETHUSD no original EA).
MaxDrawdownPercent decimal 20 Limite de rebaixamento que pausa novas negociações.
RiskPercent decimal 2 Participação da carteira arriscada por negociação para dimensionamento de posição.
PriceDifferenceThreshold decimal 100 Divergência do preço de compra necessária para abrir o par.
MinimumTotalProfit decimal 0.30 Meta de lucro na moeda da conta para fechamento de ambas as pernas.
AtrPeriod int 14 Comprimento ATR para o filtro de volatilidade.
RecoveryPercent decimal 95 Porcentagem do pico de patrimônio necessário para retomar a negociação após uma redução.
StopLossPips int 50 Parada sintética usada para converter RiskPercent em lotes.
AtrCandleType DataType TimeSpan.FromMinutes(5).TimeFrame() Série de velas usada para cálculo de ATR.

Arquivos

  • CS/TwoPairCorrelationStrategy.cs – implementação de estratégia baseada no API de alto nível.
  • README.md – esta documentação (inglês).
  • README_zh.md – documentação em chinês.
  • README_ru.md – documentação em russo.
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;
		}
	}
}