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
- 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.
- 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.
- 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).
- 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.
- 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;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class two_pair_correlation_strategy(Strategy):
def __init__(self):
super(two_pair_correlation_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._price_difference_threshold = self.Param("PriceDifferenceThreshold", 5.0)
self._minimum_total_profit = self.Param("MinimumTotalProfit", 3.0)
self._atr_period = self.Param("AtrPeriod", 14)
self._atr_value = 0.0
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def PriceDifferenceThreshold(self):
return self._price_difference_threshold.Value
@PriceDifferenceThreshold.setter
def PriceDifferenceThreshold(self, value):
self._price_difference_threshold.Value = value
@property
def MinimumTotalProfit(self):
return self._minimum_total_profit.Value
@MinimumTotalProfit.setter
def MinimumTotalProfit(self, value):
self._minimum_total_profit.Value = value
@property
def AtrPeriod(self):
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
def OnReseted(self):
super(two_pair_correlation_strategy, self).OnReseted()
self._atr_value = 0.0
self._entry_price = 0.0
def OnStarted2(self, time):
super(two_pair_correlation_strategy, self).OnStarted2(time)
self._atr_value = 0.0
self._entry_price = 0.0
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
sma = SimpleMovingAverage()
sma.Length = 20
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(atr, sma, self._process_candle).Start()
def _process_candle(self, candle, atr_value, sma_value):
if candle.State != CandleStates.Finished:
return
atr_val = float(atr_value)
sma_val = float(sma_value)
price = float(candle.ClosePrice)
self._atr_value = atr_val
# Check profit target
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
pnl = price - self._entry_price
else:
pnl = self._entry_price - price
profit_target = max(float(self.MinimumTotalProfit), atr_val * 0.5)
if profit_target > 0 and pnl >= profit_target:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
self._entry_price = 0.0
return
if self.Position != 0:
return
deviation = price - sma_val
entry_threshold = max(float(self.PriceDifferenceThreshold), atr_val)
if deviation > entry_threshold:
self.SellMarket()
self._entry_price = price
elif deviation < -entry_threshold:
self.BuyMarket()
self._entry_price = price
def CreateClone(self):
return two_pair_correlation_strategy()