GitHub で見る

2 ペア相関戦略

概要

2 ペア相関戦略 は、MetaTrader エキスパート アドバイザー 「2 ペア相関 EA」 (パッケージ MQL/52043) を StockSharp の高レベル API に移植します。相関性の高い 2 つの暗号通貨シンボル (プライマリ レッグとして BTCUSD、ヘッジ レッグとして ETHUSD) の入札価格を監視し、スプレッドが設定可能なしきい値から逸脱した場合に市場中立取引を実行します。

コアワークフロー

  1. リスクゲーティング – ポートフォリオの資本は継続的に監視されます。過去のピークからのドローダウンが MaxDrawdownPercent を超えた場合、資本がピーク値の RecoveryPercent を超えて回復するまで、新しい取引は一時停止されます。
  2. ボラティリティフィルター – どちらの金融商品も、5 分間のローソク足ストリームを長さ AtrPeriodAverageTrueRange インジケーターに供給します。 ATR が PriceDifferenceThreshold * 0.01 を超えると取引はスキップされ、MQL コードの「高ボラティリティ一時停止」を模倣します。
  3. スプレッド検出 – この戦略は両方の商品のレベル 1 データをサブスクライブし、更新ごとに入札価格のスプレッドを評価します。 Bid(BTCUSD) - Bid(ETHUSD) > PriceDifferenceThreshold の場合、BTCUSD が購入され、ETHUSD が販売されます。スプレッドが -PriceDifferenceThreshold を下回ると、ポジションが逆転します (BTCUSD のショート、ETHUSD のロング)。
  4. 動的ロットサイジング – レッグあたりの出来高は、現在のポートフォリオ株式の RiskPercent を合成ストップ距離 StopLossPips * PriceStep で割ったものから導出されます。結果は、注文が送信される前に、交換量の制約を使用して正規化されます。
  5. バスケットエグジット – 両方のレッグの変動利益の合計が口座通貨で追跡されます。 MinimumTotalProfit に達すると、エントリー方向に関係なく、戦略はペア全体を閉じます。

必要な市場データ

  • プライマリ証券 (Security) とヘッジ証券 (SecondSecurity) の両方の レベル 1 (最良の買値/売値)。
  • ATR フィルターに供給する同じ 2 つの金融商品のタイプ AtrCandleType (デフォルトは 5 分の時間枠) の キャンドル

ロットサイジングと利益換算が MetaTrader の動作を反映するように、有価証券が意味のある PriceStepStepPriceVolumeStep、最小/最大出来高値を公開していることを確認します。

パラメーター

名前 種類 デフォルト 説明
SecondSecurity Security ヘッジ手段(オリジナルの EA では ETHUSD)。
MaxDrawdownPercent decimal 20 新しい取引を一時停止するドローダウンしきい値。
RiskPercent decimal 2 ポジションサイジングのための取引ごとのポートフォリオシェアのリスク。
PriceDifferenceThreshold decimal 100 ペアをオープンするには入札価格の乖離が必要です。
MinimumTotalProfit decimal 0.30 両レッグをクローズするためのアカウント通貨での利益目標。
AtrPeriod int 14 ボラティリティフィルターの ATR の長さ。
RecoveryPercent decimal 95 ドローダウン後に取引を再開するために必要なピーク資本の割合。
StopLossPips int 50 RiskPercent をロットに変換するために使用される合成ストップ。
AtrCandleType DataType TimeSpan.FromMinutes(5).TimeFrame() ATR の計算に使用されるローソク足シリーズ。

ファイル

  • CS/TwoPairCorrelationStrategy.cs – 高レベルの API に基づいて構築された戦略の実装。
  • README.md – このドキュメント (英語)。
  • README_zh.md – 中国語のドキュメント。
  • README_ru.md – ロシア語のドキュメント。
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;
		}
	}
}