Two Pair Correlation Strategy
Overview
The Two Pair Correlation Strategy ports the MetaTrader expert advisor "2-Pair Correlation EA" (package MQL/52043) to the StockSharp high-level API. It watches the bid prices of two highly correlated crypto symbols (BTCUSD as the primary leg and ETHUSD as the hedge leg) and performs a market-neutral trade when their spread deviates from a configurable threshold.
Core workflow
- Risk gating – portfolio equity is monitored continuously. If the drawdown from the historical peak exceeds
MaxDrawdownPercent, new trades are suspended until equity recovers above RecoveryPercent of the peak value.
- Volatility filter – both instruments feed a 5-minute candle stream into an
AverageTrueRange indicator of length AtrPeriod. Trading is skipped when either ATR exceeds PriceDifferenceThreshold * 0.01, mimicking the "high volatility pause" from the MQL code.
- Spread detection – the strategy subscribes to level-one data for both instruments and evaluates the bid-price spread on every update. When
Bid(BTCUSD) - Bid(ETHUSD) > PriceDifferenceThreshold, it buys BTCUSD and sells ETHUSD. When the spread drops below -PriceDifferenceThreshold, the positions are reversed (short BTCUSD, long ETHUSD).
- Dynamic lot sizing – the per-leg volume is derived from
RiskPercent of the current portfolio equity, divided by the synthetic stop distance StopLossPips * PriceStep. The result is normalised with the exchange volume constraints before orders are sent.
- Basket exit – the total floating profit of both legs is tracked in account currency. Once it reaches
MinimumTotalProfit, the strategy closes the entire pair regardless of the entry direction.
Required market data
- Level1 (best bid/ask) for both the primary security (
Security) and the hedge security (SecondSecurity).
- Candles of type
AtrCandleType (defaults to 5-minute time-frame) for the same two instruments to feed the ATR filter.
Ensure the securities expose meaningful PriceStep, StepPrice, VolumeStep, and min/max volume values so that the lot sizing and profit conversion mirror the MetaTrader behaviour.
Parameters
| Name |
Type |
Default |
Description |
SecondSecurity |
Security |
— |
Hedge instrument (ETHUSD in the original EA). |
MaxDrawdownPercent |
decimal |
20 |
Drawdown threshold that pauses new trades. |
RiskPercent |
decimal |
2 |
Portfolio share risked per trade for position sizing. |
PriceDifferenceThreshold |
decimal |
100 |
Bid-price divergence required to open the pair. |
MinimumTotalProfit |
decimal |
0.30 |
Profit target in account currency for closing both legs. |
AtrPeriod |
int |
14 |
ATR length for the volatility filter. |
RecoveryPercent |
decimal |
95 |
Percentage of the peak equity required to resume trading after a drawdown. |
StopLossPips |
int |
50 |
Synthetic stop used to translate RiskPercent into lots. |
AtrCandleType |
DataType |
TimeSpan.FromMinutes(5).TimeFrame() |
Candle series used for ATR calculation. |
Files
CS/TwoPairCorrelationStrategy.cs – strategy implementation built on the high-level API.
README.md – this documentation (English).
README_zh.md – documentation in Chinese.
README_ru.md – documentation in Russian.
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()