GitHub で見る

マルチ裁定取引戦略

概要

マルチ裁定取引戦略は、MetaTrader エキスパートアドバイザー「Multi_arbitration 1.000」の StockSharp 移植版です。元のスクリプトは既存の買いと売りのポジションを継続的に評価し、浮動利益が弱い方向に新しいトレードを追加し、全体的な利益目標が達成されたときにグローバル清算を実行します。この C# 実装は、StockSharp のネッティングポートフォリオモデルと高レベル戦略 API に適応しながらコア決定ロジックを維持します。

この戦略は:

  • 最初の完成したローソク足が到着するとすぐに初期ロングポジションをオープンします。
  • アクティブな方向の未実現利益と代替方向を比較して、反転が必要かどうかを決定します。
  • 設定された利益目標を超えたとき、またはポジション圧力が設定可能な制限を超えて増加したときに、フラットポジションを強制します。
  • シンプルさと高速実行を維持するために成行注文(BuyMarket / SellMarket)のみを使用します。

取引ロジック

  1. 初期注文 – 最初の完成したローソク足が、設定された取引ボリュームでロング成行注文を引き起こします。これは MetaTrader エキスパートアドバイザーの即時市場参入を再現します。
  2. 利益比較 – 完成した各ローソク足で、戦略は現在の方向の浮動 PnL を計算します:
    • ロング利益 = (close - entry) * volume
    • ショート利益 = (entry - close) * volume
  3. ポジション選択 – 代替方向が現在アクティブな方向よりも良くなる場合、戦略は既存のエクスポージャーをカバーして新しい方向に新しいポジションをオープンするようにサイズ設定された成行注文を送ることでポジションを反転します。ポジションが開いていない場合、アルゴリズムはデフォルトでロングエントリーになり、元のエキスパートアドバイザーと一致します。
  4. ポジション制限ガード – 設定可能な MaxOpenPositions パラメーターは LimitOrders() に対する MetaTrader の確認を反映します。組み合わせたロング/ショートエクスポージャーがこの上限に達し、戦略が利益を出している場合、過剰レバレッジを避けるためにブックをフラットにします。
  5. 利益目標出口 – 口座の PnL(実現 + 未実現)が ProfitForClose 閾値を超えると、戦略は元の Equity - Balance チェックと同様に、すべてのポジションをクローズします。

パラメーター

名前 説明 デフォルト
TradeVolume 各成行注文に使用されるボリューム。元の EA の最小ロットサイズを表します。 1
ProfitForClose 超過したときにグローバル出口を引き起こす利益閾値。 300
MaxOpenPositions 戦略がフラットを強制する前に許可される同時ポジションの最大数。limit - 15 に相当。 15
CandleType 取引決定を同期するローソク足データタイプ。デフォルトは 1 分足時間軸。 1 分ローソク足

実装上の注意

  • StockSharp はネッティングポジションモデルを使用するため、戦略は一度に 1 つの正味方向のみを保持できます。反転は既存のエクスポージャーをクローズして反対方向に新しいポジションをオープンするように成行注文をサイズ設定することで処理されます。
  • StartProtection() 呼び出しは、組み込みのリスク管理を継承するために使用されます(例:戦略が停止されたときの非ゼロポジションのストップアウト)。
  • すべての状態変数(_entryPrice_currentSide_initialOrderPlaced)は、古いデータなしに再起動と繰り返しシミュレーションをサポートするために OnReseted でリセットされます。
  • 戦略は部分的に形成されたバーでの利益の二重計算を避けるために完成したローソク足にのみ反応します。

使用上の推奨事項

  • 商品のロットサイズまたは契約乗数に合わせて TradeVolume パラメーターを調整してください。
  • ProfitForClose の値は口座 PnL と同じ通貨を使用して設定する必要があります(例:FX 口座の場合は USD)。
  • フラットを強制する前に戦略がどれだけ積極的にエクスポージャーを蓄積するかに応じて MaxOpenPositions を増減してください。
  • 戦略は常にロングトレードから始まるため、ロングエントリーが取引される商品に適している場合に手動で開始することを検討してください。

MetaTrader バージョンとの違い

  • MetaTrader のヘッジモードは同時ロングとショートポジションを許可しますが、このポートはネッティング環境で動作します。決定ロジックは依然として方向性の収益性を比較しますが、どの時点でも 1 つのネットポジションのみが保持されます。
  • プラットフォーム固有のチェック(ターミナル取引権限、フィリングタイプ選択、口座マジックナンバー)は、StartProtection() やローソク足サブスクリプションなどの StockSharp 同等品に置き換えられます。
  • MQL ファイルからのコメントされた診断は再現されません;ランタイム情報が必要な場合は StockSharp ログに依存してください。
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>
/// Multi-direction arbitration strategy adapted from MetaTrader logic.
/// </summary>
public class MultiArbitrationStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitForClose;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<DataType> _candleType;

	private bool _initialOrderPlaced;
	private decimal _entryPrice;
	private Sides? _currentSide;

	/// <summary>
	/// Target profit that triggers a full position exit.
	/// </summary>
	public decimal ProfitForClose
	{
		get => _profitForClose.Value;
		set => _profitForClose.Value = value;
	}

	/// <summary>
	/// Volume used when sending market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous positions allowed before forcing a flatten.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Candle type used for synchronization and decision making.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MultiArbitrationStrategy"/> class.
	/// </summary>
	public MultiArbitrationStrategy()
	{
		_profitForClose = Param(nameof(ProfitForClose), 300m)
			.SetDisplay("Profit Threshold", "Profit required before flattening all positions.", "Risk");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Volume used when opening new positions.", "Trading");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 15)
			.SetGreaterThanZero()
			.SetDisplay("Max Open Positions", "Maximum simultaneous positions allowed before closing everything.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used to synchronize trading decisions.", "Data");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_initialOrderPlaced = false;
		_entryPrice = 0m;
		_currentSide = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_initialOrderPlaced)
		{
			OpenLong(candle);
			_initialOrderPlaced = true;
		}

		var longCount = _currentSide == Sides.Buy ? 1 : 0;
		var shortCount = _currentSide == Sides.Sell ? 1 : 0;

		var longProfit = _currentSide == Sides.Buy ? (candle.ClosePrice - _entryPrice) * Volume : 0m;
		var shortProfit = _currentSide == Sides.Sell ? (_entryPrice - candle.ClosePrice) * Volume : 0m;

		if (longCount + shortCount < MaxOpenPositions)
		{
			if (longProfit < shortProfit && _currentSide != Sides.Buy)
			{
				OpenLong(candle);
			}
			else if (shortProfit < longProfit && _currentSide != Sides.Sell)
			{
				OpenShort(candle);
			}
			else if (longProfit == 0m && shortProfit == 0m && Position == 0 && _currentSide is null)
			{
				OpenLong(candle);
			}
		}
		else if (PnL > 0m && Position != 0)
		{
			FlattenPosition(candle);
		}

		if (PnL > ProfitForClose && Position != 0)
		{
			FlattenPosition(candle);
		}
	}

	private void OpenLong(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Already holding a long position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Buy;
			return;
		}

		BuyMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Buy;
	}

	private void OpenShort(ICandleMessage candle)
	{
		if (Position < 0)
		{
			// Already holding a short position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Sell;
			return;
		}

		SellMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Sell;
	}

	private void FlattenPosition(ICandleMessage candle)
	{
		if (_currentSide is null)
			return;

		if (Position > 0)
		{
			SellMarket();
		}
		else if (Position < 0)
		{
			BuyMarket();
		}

		_currentSide = null;
		_entryPrice = 0m;
	}
}