View on GitHub

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

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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;
		}
	}
}