GitHub で見る

デュアルロットステップヘッジ戦略

概要

デュアルロットステップヘッジ戦略は、MetaTrader 5のエキスパートアドバイザー*"x1 lot from high to low""x1 lot from low to high"*(フォルダMQL/19543)のC#ポートです。オリジナルのロボットは即座に買いと売りポジションのヘッジされたバスケットを開き、各新規エントリー後に注文ボリュームをサイクルし、固定の利益目標が達成されるとバスケット全体をクローズします。この実装は、クリーンなパラメーターと詳細な状態管理を公開しながら、StockSharpの高レベルAPI上でその動作を再現します。

2つの動作モードが利用可能です:

  • HighToLow – 最大ロット乗数から始まり、最初のヘッジバスケットを最大ボリュームで開き、最初のエントリー後に次のロットステップに減少します。
  • LowToHigh – 最小ロットステップから始まり、設定された乗数に達するまで各新規エントリー後にロットサイズを増加させ、その後そのサイズで取引を継続します。

戦略は買いと売りの両方のレッグを同時にアクティブに保持し、レッグごとのストップロスとテイクプロフィットレベルを管理し、バスケット全体の利益目標を強制するためにポートフォリオエクイティを監視します。

取引ロジック

  1. ポジションが存在しない場合、戦略は現在のロットサイズを使用して両方のロングとショートの成行注文を開きます。
  2. 正確に1つのレッグがアクティブな場合(例えば、反対側がストップアウトされた場合)、欠落しているレッグは現在のロットサイズで市場で再開されます。
  3. 各成功エントリー後、選択されたモード(HighToLowまたはLowToHigh)に応じてロットサイズが更新されます。
  4. レッグごとの保護出口は各着信取引ティックで評価されます:
    • ロングレッグは価格がストップロス(平均ロングエントリーのStopLossPips下)またはテイクプロフィット(平均エントリーのTakeProfitPips上)に達するとクローズします。
    • ショートレッグは価格がストップロス(平均ショートエントリーのStopLossPips上)またはテイクプロフィット(平均エントリーのTakeProfitPips下)に達するとクローズします。
  5. ポートフォリオエクイティ利益がMinProfitを超えると、戦略は残りすべてのポジションをクローズし、ロット状態をモードの開始サイズにリセットします。
  6. 安全ロジックは、複数の買いまたは売りポジションが予期せず検出された場合にバスケットをクローズしてすべてをリセットします。

すべての注文は高レベルヘルパーBuyMarketSellMarketを通じて送信されます。戦略はOnOwnTradeReceivedでフィルを追跡し、レッグごとの集積エクスポージャーを維持し、エントリーまたは出口がまだ保留中の間は重複注文を防ぎます。

パラメーター

パラメーター 説明
LotMultiplier 最小ボリュームステップで表される最大ロット乗数(デフォルト10)。
StopLossPips 各レッグのpipsでのストップロス距離(デフォルト50)。0に設定して無効にする。
TakeProfitPips 各レッグのpipsでのテイクプロフィット距離(デフォルト150)。0に設定して無効にする。
MinProfit 口座通貨でのバスケット利益目標。エクイティ利益がこの値を超えると、すべてのポジションがクローズされる(デフォルト27)。
ScalingMode ロットステップ動作。HighToLowは"x1 lot from high to low" EAを反映し、LowToHighは"x1 lot from low to high"を反映する。

戦略はSecurity.VolumeStepから最小ボリュームステップを自動的に導出し、証券価格ステップ(従来の4/5桁のforex調整付き)を使用してpip値を計算します。

リセットとボリュームサイクリング

  • HighToLow – 最高ボリューム(VolumeStep * LotMultiplier)で最初のバスケットを開きます。エントリー後、内部ボリュームは1ステップ減少します。バスケット利益目標が達成されると、次のサイクルが再び最大から始まるようにボリュームが0にリセットされます。
  • LowToHigh – 最小ロットステップから始まります。各エントリー後、乗数の上限に達するまでロットが1ステップ増加します。バスケット利益目標が達成されると、ボリュームは最小ステップにリセットされます。

使用ノート

  • 戦略はオリジナルのMetaTraderボットがティックイベントで実行されるため、ティック取引(DataType.Ticks)を購読します。それに応じて履歴プロバイダーまたはライブコネクターを設定してください。
  • ストップロスとテイクプロフィットのチェックはアルゴリズム内で行われるため、exchangeに追加の保護注文は登録されません。
  • 両方のレッグが市場で開かれるため、戦略はヘッジポジションと小さなスプレッドをサポートするブローカーで最も機能します。ネッティングベニューでは引き続き機能しますが、内部ロジックによってどちらかがクローズされるまでレッグは効果的に相殺されます。
  • デフォルトパラメーターはオリジナルのMQL設定をコピーします。注意深く調整してください:高ボリュームのヘッジはバスケット利益目標が達成される前に大幅なドローダウンを生成する可能性があります。

オリジナルMQLロジックへのマッピング

MetaTrader変数 C#プロパティ/動作
InpLots 自動ボリュームステップ処理を持つLotMultiplier
InpStopLoss & InpTakeProfit PriceStepに基づくpip変換を持つStopLossPipsTakeProfitPips
InpMinProfit MinProfitとポートフォリオエクイティチェック。
LotCheck 最小ステップと最大ボリュームを強制するLotCheckヘルパー。
CalculatePositions OnOwnTradeReceivedを通じた内部ロング/ショートエクスポージャートラッキング。
CloseAllPositions() 保留注文調整と状態リセットを持つCloseAllPositionsメソッド。

リスク管理の考慮事項

戦略はロングとショートのポジションを意図的に開いたまま保持し、スプレッドコストとスワップレートへの継続的なエクスポージャーを引き起こします。実際の資本で実行する前に:

  • StockSharpエミュレーターまたはペーパートレードで動作を検証する。
  • ブローカーがヘッジをサポートしていることを確認する;そうでない場合、ロング/ショートレッグは即座にネットされます。
  • ストップロス、テイクプロフィット、利益目標値をインストゥルメントのボラティリティに調整する。
  • 同時のロング/ショートレッグが名目エクスポージャーを2倍にするため、マージン使用を監視する。

ファイル

  • CS/DualLotStepHedgeStrategy.cs – 広範なインラインコメント付きのStockSharp戦略実装。
  • README_ru.md – 詳細な指示付きのロシア語翻訳。
  • README_zh.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>
/// Recreates the "x1 lot from high to low" and "x1 lot from low to high" MetaTrader robots.
/// Opens hedged long/short positions with adjustable lot cycling and closes the basket once
/// a profit target is achieved.
/// </summary>
public class DualLotStepHedgeStrategy : Strategy
{
	private readonly StrategyParam<int> _lotMultiplier;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<LotScalingModes> _scalingMode;

	private decimal _volumeStep;
	private decimal _maxVolume;
	private decimal _currentVolume;
	private decimal _pipValue;
	private decimal _initialEquity;

	private decimal _longVolume;
	private decimal _shortVolume;
	private decimal _longAveragePrice;
	private decimal _shortAveragePrice;

	private bool _longEntryInProgress;
	private bool _shortEntryInProgress;
	private bool _longExitInProgress;
	private bool _shortExitInProgress;

	private decimal _pendingLongEntryVolume;
	private decimal _pendingShortEntryVolume;
	private decimal _pendingLongExitVolume;
	private decimal _pendingShortExitVolume;

	private bool _resetRequested;

	/// <summary>
	/// Defines the lot stepping mode that matches the original MetaTrader experts.
	/// </summary>
	public enum LotScalingModes
	{
		/// <summary>
		/// Start with the maximum lot multiplier and drop to the next step after the first cycle.
		/// </summary>
		HighToLow,

		/// <summary>
		/// Start with the minimum lot step and grow until the configured multiplier is reached.
		/// </summary>
		LowToHigh,
	}

	/// <summary>
	/// Maximum lot multiplier expressed in minimal volume steps.
	/// </summary>
	public int LotMultiplier
	{
		get => _lotMultiplier.Value;
		set => _lotMultiplier.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips from the average entry price of the leg.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips from the average entry price of the leg.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Basket profit target in account currency.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Selected lot stepping mode.
	/// </summary>
	public LotScalingModes ScalingMode
	{
		get => _scalingMode.Value;
		set => _scalingMode.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="DualLotStepHedgeStrategy"/>.
	/// </summary>
	public DualLotStepHedgeStrategy()
	{
		_lotMultiplier = Param(nameof(LotMultiplier), 10)
		.SetGreaterThanZero()
		.SetDisplay("Lot Multiplier", "Maximum lot multiplier over the minimal step", "Trading")
		
		.SetOptimize(1, 20, 1);

		_stopLossPips = Param(nameof(StopLossPips), 50m)
		.SetDisplay("Stop Loss (pips)", "Stop loss distance for each leg", "Risk")
		
		.SetOptimize(10m, 200m, 10m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 150m)
		.SetDisplay("Take Profit (pips)", "Take profit distance for each leg", "Risk")
		
		.SetOptimize(20m, 400m, 20m);

		_minProfit = Param(nameof(MinProfit), 27m)
		.SetDisplay("Basket Profit", "Target profit in account currency", "Trading")
		
		.SetOptimize(5m, 200m, 5m);

		_scalingMode = Param(nameof(ScalingMode), LotScalingModes.HighToLow)
		.SetDisplay("Scaling Mode", "How the lot size evolves after entries", "Trading");
	}

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

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

		_volumeStep = 0m;
		_maxVolume = 0m;
		_currentVolume = 0m;
		_pipValue = 0m;
		_initialEquity = 0m;

		_longVolume = 0m;
		_shortVolume = 0m;
		_longAveragePrice = 0m;
		_shortAveragePrice = 0m;

		_longEntryInProgress = false;
		_shortEntryInProgress = false;
		_longExitInProgress = false;
		_shortExitInProgress = false;

		_pendingLongEntryVolume = 0m;
		_pendingShortEntryVolume = 0m;
		_pendingLongExitVolume = 0m;
		_pendingShortExitVolume = 0m;

		_resetRequested = false;
	}

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

		_volumeStep = Security.VolumeStep ?? 0m;
			if (_volumeStep <= 0m)
		_volumeStep = 1m;

		_maxVolume = LotCheck(_volumeStep * LotMultiplier);
		if (_maxVolume <= 0m)
		_maxVolume = _volumeStep;

		_currentVolume = ScalingMode == LotScalingModes.HighToLow ? _maxVolume : _volumeStep;
		_pipValue = CalculatePipValue();

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(ProcessCandle).Start();
	}

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

			var price = candle.ClosePrice;

			if (_volumeStep <= 0m)
				return;

			if (_initialEquity <= 0m)
				_initialEquity = Portfolio.CurrentValue ?? 0m;

			CheckProtectiveLevels(price);

			if (_longExitInProgress || _shortExitInProgress)
				return;

			if (CheckProfitTarget())
				return;

			ResetCurrentVolumeIfNeeded();

			var buyCount = _longVolume > 0m ? 1 : 0;
			var sellCount = _shortVolume > 0m ? 1 : 0;

			if (buyCount > 1 || sellCount > 1)
			{
				CloseAllPositions();
				return;
			}

			if (_longEntryInProgress || _shortEntryInProgress)
				return;

			if (buyCount == 0 && sellCount == 0)
			{
				TryOpenHedge();
			}
			else if (buyCount == 1 && sellCount == 0)
			{
				OpenShortIfNeeded();
			}
			else if (buyCount == 0 && sellCount == 1)
			{
				OpenLongIfNeeded();
			}
		}

	private bool CheckProfitTarget()
	{
		if (_initialEquity <= 0m || MinProfit <= 0m)
		return false;

		var currentEquity = Portfolio.CurrentValue ?? 0m;
		if (currentEquity - _initialEquity >= MinProfit)
		{
			CloseAllPositions();
			return true;
		}

		return false;
	}

	private void TryOpenHedge()
	{
		if (_longEntryInProgress || _shortEntryInProgress)
		return;

		var volume = LotCheck(_currentVolume);
		if (volume <= 0m)
		return;

		var buyOk = ExecuteBuy(volume, true);
		var sellOk = ExecuteSell(volume, true);

		if (buyOk && sellOk)
		AdjustVolumeAfterEntry();
	}

	private void OpenLongIfNeeded()
	{
		if (_longEntryInProgress)
		return;

		var volume = LotCheck(_currentVolume);
		if (volume <= 0m)
		return;

		if (ExecuteBuy(volume, true))
		AdjustVolumeAfterEntry();
	}

	private void OpenShortIfNeeded()
	{
		if (_shortEntryInProgress)
		return;

		var volume = LotCheck(_currentVolume);
		if (volume <= 0m)
		return;

		if (ExecuteSell(volume, true))
		AdjustVolumeAfterEntry();
	}

	private void AdjustVolumeAfterEntry()
	{
		if (ScalingMode == LotScalingModes.HighToLow)
		{
			_currentVolume = LotCheck(_currentVolume - _volumeStep);
		}
		else
		{
			_currentVolume = LotCheck(_currentVolume + _volumeStep);
		}
	}

	private void CloseAllPositions()
	{
		if (_longVolume <= 0m && _shortVolume <= 0m && !_longExitInProgress && !_shortExitInProgress)
		{
			_resetRequested = true;
			ApplyResetIfFlat();
			return;
		}

		if (_longVolume > 0m && !_longExitInProgress)
		{
			if (ExecuteSell(_longVolume, false))
			_resetRequested = true;
		}

		if (_shortVolume > 0m && !_shortExitInProgress)
		{
			if (ExecuteBuy(_shortVolume, false))
			_resetRequested = true;
		}
	}

	private void CloseLong()
	{
		if (_longVolume <= 0m || _longExitInProgress)
		return;

		ExecuteSell(_longVolume, false);
	}

	private void CloseShort()
	{
		if (_shortVolume <= 0m || _shortExitInProgress)
		return;

		ExecuteBuy(_shortVolume, false);
	}

	private bool ExecuteBuy(decimal volume, bool openingLong)
	{
		if (volume <= 0m)
		return false;

		var order = BuyMarket(volume);
		if (order == null)
		return false;

		if (openingLong)
		{
			_longEntryInProgress = true;
			_pendingLongEntryVolume += volume;
		}
		else
		{
			_shortExitInProgress = true;
			_pendingShortExitVolume += volume;
		}

		return true;
	}

	private bool ExecuteSell(decimal volume, bool openingShort)
	{
		if (volume <= 0m)
		return false;

		var order = SellMarket(volume);
		if (order == null)
		return false;

		if (openingShort)
		{
			_shortEntryInProgress = true;
			_pendingShortEntryVolume += volume;
		}
		else
		{
			_longExitInProgress = true;
			_pendingLongExitVolume += volume;
		}

		return true;
	}

	private void CheckProtectiveLevels(decimal price)
	{
		if (_pipValue <= 0m)
		return;

		if (_longVolume > 0m && !_longExitInProgress)
		{
			var stop = StopLossPips > 0m ? _longAveragePrice - StopLossPips * _pipValue : decimal.MinValue;
			var take = TakeProfitPips > 0m ? _longAveragePrice + TakeProfitPips * _pipValue : decimal.MaxValue;

			if (StopLossPips > 0m && price <= stop)
			{
				CloseLong();
				return;
			}

			if (TakeProfitPips > 0m && price >= take)
			{
				CloseLong();
				return;
			}
		}

		if (_shortVolume > 0m && !_shortExitInProgress)
		{
			var stop = StopLossPips > 0m ? _shortAveragePrice + StopLossPips * _pipValue : decimal.MaxValue;
			var take = TakeProfitPips > 0m ? _shortAveragePrice - TakeProfitPips * _pipValue : decimal.MinValue;

			if (StopLossPips > 0m && price >= stop)
			{
				CloseShort();
				return;
			}

			if (TakeProfitPips > 0m && price <= take)
			{
				CloseShort();
			}
		}
	}

	private void ResetCurrentVolumeIfNeeded()
	{
		if (ScalingMode == LotScalingModes.HighToLow)
		{
			if (_currentVolume < _volumeStep)
			_currentVolume = _maxVolume;
		}
		else
		{
			if (_currentVolume < _volumeStep)
			_currentVolume = _volumeStep;
			else if (_currentVolume > _maxVolume)
			_currentVolume = _volumeStep;
		}
	}

	private decimal LotCheck(decimal volume)
	{
		if (volume <= 0m)
		return 0m;

		var step = _volumeStep;
		if (step <= 0m)
		return 0m;

		var ratio = Math.Floor(volume / step);
		var normalized = ratio * step;

		if (normalized < step)
		normalized = 0m;

		if (normalized > _maxVolume)
		normalized = _maxVolume;

		return normalized;
	}

	private decimal CalculatePipValue()
	{
		var step = Security.PriceStep ?? 0m;
		if (step <= 0m)
		return 1m;

		double stepDouble;
		try
		{
			stepDouble = Convert.ToDouble(step);
		}
		catch
		{
			return step;
		}

		if (stepDouble <= 0d)
		return step;

		var decimals = (int)Math.Round(-Math.Log10(stepDouble));
		if (decimals == 3 || decimals == 5)
		return step * 10m;

		return step;
	}

	private void ApplyResetIfFlat()
	{
		if (!_resetRequested)
		return;

		if (_longVolume > 0m || _shortVolume > 0m)
		return;

			if (_longExitInProgress || _shortExitInProgress)
		return;

		if (_pendingLongEntryVolume > 0m || _pendingShortEntryVolume > 0m)
		return;

		_resetRequested = false;
		_initialEquity = 0m;

		if (ScalingMode == LotScalingModes.HighToLow)
		{
			_currentVolume = 0m;
		}
		else
		{
			_currentVolume = _volumeStep;
		}
	}

	private void ApplyLongOpen(decimal volume, decimal price)
	{
		if (volume <= 0m)
		return;

		var total = _longVolume + volume;
		_longAveragePrice = _longVolume <= 0m
		? price
		: (_longAveragePrice * _longVolume + price * volume) / total;
		_longVolume = total;
	}

	private void ApplyShortOpen(decimal volume, decimal price)
	{
		if (volume <= 0m)
		return;

		var total = _shortVolume + volume;
		_shortAveragePrice = _shortVolume <= 0m
		? price
		: (_shortAveragePrice * _shortVolume + price * volume) / total;
		_shortVolume = total;
	}

	private void ApplyLongClose(decimal volume)
	{
		if (volume <= 0m || _longVolume <= 0m)
		return;

		var closed = Math.Min(_longVolume, volume);
		_longVolume -= closed;
		if (_longVolume <= 0m)
		{
			_longVolume = 0m;
			_longAveragePrice = 0m;
		}
	}

	private void ApplyShortClose(decimal volume)
	{
		if (volume <= 0m || _shortVolume <= 0m)
		return;

		var closed = Math.Min(_shortVolume, volume);
		_shortVolume -= closed;
		if (_shortVolume <= 0m)
		{
			_shortVolume = 0m;
			_shortAveragePrice = 0m;
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Order.Security != Security)
		return;

		var volume = trade.Trade.Volume;
		if (volume <= 0m)
		return;

		var price = trade.Trade.Price;

		if (trade.Order.Side == Sides.Buy)
		{
			ProcessBuyTrade(volume, price);
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			ProcessSellTrade(volume, price);
		}

		ApplyResetIfFlat();
	}

	private void ProcessBuyTrade(decimal volume, decimal price)
	{
		var remaining = volume;

		if (_pendingShortExitVolume > 0m)
		{
			var closing = Math.Min(_pendingShortExitVolume, remaining);
			ApplyShortClose(closing);
			_pendingShortExitVolume -= closing;
			remaining -= closing;

			if (_pendingShortExitVolume <= 0m)
			_shortExitInProgress = false;
		}

		if (remaining <= 0m)
		return;

		if (_pendingLongEntryVolume > 0m)
		{
			var opening = Math.Min(_pendingLongEntryVolume, remaining);
			ApplyLongOpen(opening, price);
			_pendingLongEntryVolume -= opening;
			remaining -= opening;

			if (_pendingLongEntryVolume <= 0m)
			_longEntryInProgress = false;
		}

		if (remaining > 0m)
		ApplyLongOpen(remaining, price);
	}

	private void ProcessSellTrade(decimal volume, decimal price)
	{
		var remaining = volume;

		if (_pendingLongExitVolume > 0m)
		{
			var closing = Math.Min(_pendingLongExitVolume, remaining);
			ApplyLongClose(closing);
			_pendingLongExitVolume -= closing;
			remaining -= closing;

			if (_pendingLongExitVolume <= 0m)
			_longExitInProgress = false;
		}

		if (remaining <= 0m)
		return;

		if (_pendingShortEntryVolume > 0m)
		{
			var opening = Math.Min(_pendingShortEntryVolume, remaining);
			ApplyShortOpen(opening, price);
			_pendingShortEntryVolume -= opening;
			remaining -= opening;

			if (_pendingShortEntryVolume <= 0m)
			_shortEntryInProgress = false;
		}

		if (remaining > 0m)
		ApplyShortOpen(remaining, price);
	}
}