GitHub で見る

マルチヘッジスケジューラー戦略

概要

マルチヘッジスケジューラー戦略は、元のMetaTrader 5エキスパートアドバイザーMultiHedg_1.mq5のStockSharpへの直接変換です。この戦略はヘッジを許可するアカウント用に設計されており、最大10の異なるインストゥルメントを同時に管理できます。設定可能な取引ウィンドウ中に同じ方向のポジションを開き、時間またはエクイティパーセンテージの閾値に基づくポートフォリオレベルのエグジットロジックを提供します。

インジケーターに依存する代わりに、戦略は1分のローソク足ストリーム(設定可能)を純粋なタイミングソースとして使用します。各完成ローソク足は、取引を開くチェック、取引ウィンドウが期限切れになった時にすべてを閉じること、エクイティベースのリスクルールを実施することをトリガーします。したがって、この戦略は価格パターンよりもスケジュールによって実行が駆動されるポートフォリオに適しています。

トレードロジック

  1. インストゥルメント選択 – 最大10のシンボルを有効にできます。各有効なエントリについて、戦略はSecurityProviderを通じてティッカーを解決し、設定されたタイプのローソク足をサブスクライブし、すべてのインストゥルメントに同じロジックを使用します。
  2. 取引ウィンドウ – ローソク足のタイムスタンプがTradeStartTimeウィンドウ(TradeDuration持続)に入ると、戦略はその方向にすでにオープンポジションを持っていない各有効シンボルに対して、設定された方向(TradeDirection)で成行ポジションを開きます。反対のポジションが存在する場合、ボリュームは希望する側に転換するために増加されます。
  3. エクイティ保護CloseByEquityPercentが有効で、ポートフォリオエクイティが開始残高からPercentProfitまたはPercentLossだけ逸脱すると、戦略によって管理されているすべてのオープンポジションが閉じられます。
  4. 時間ベースのエグジットUseTimeCloseが有効な場合、時計がCloseTimeウィンドウ(TradeDuration持続)に達すると、戦略はすべての追跡ポジションを閉じます。
  5. ログ記録 – エントリー、エクイティベースのエグジット、時間ベースのエグジットなどのアクションは、追跡可能性のためにLogInfo呼び出しを通じてログに記録されます。

パラメーター

パラメーター 説明 デフォルト
TradeDirection すべての注文の方向(BuyまたはSell)。 Buy
TradeStartTime エントリーウィンドウが開く現地時間。 19:51
TradeDuration エントリーウィンドウと終了ウィンドウの両方の長さ。 00:05:00
UseTimeClose 時間ベースのクローズウィンドウを有効にする。 true
CloseTime クローズウィンドウが開く現地時間。 20:50
CloseByEquityPercent エクイティ閾値ですべてのポジションを閉じることを有効にする。 true
PercentProfit グローバルクローズをトリガーするエクイティの利益パーセンテージ。 1.0
PercentLoss グローバルクローズをトリガーするエクイティのドローダウンパーセンテージ。 55.0
CandleType スケジューリングドライバーとして使用されるローソク足タイプ。 1分時間軸
UseSymbol0..9 対応するシンボルの取引を切り替える。 シンボル0〜5はtrue、6〜9はfalse
Symbol0..9 SecurityProvider.LookupByIdで解決される各スロットのティッカー。 下記デフォルト参照
Volume0..9 各スロットの注文ボリューム(元のEAのロット)。 0.1–1.0

デフォルトシンボル設定

スロット 有効 シンボル ボリューム
0 EURUSD 0.1
1 GBPUSD 0.2
2 GBPJPY 0.3
3 EURCAD 0.4
4 USDCHF 0.5
5 USDJPY 0.6
6 USDCHF 0.7
7 GBPUSD 0.8
8 EURUSD 0.9
9 USDJPY 1.0

使用上の注意

  • 元のMetaTrader動作を複製する計画の場合、アカウントがヘッジをサポートしていることを確認してください。ネッティングアカウントでは、方向を切り替えると戦略は自動的に反対のポジションを相殺します。
  • SymbolXパラメーターにインストゥルメント識別子を、StockSharpのSecurityProviderに知られているとおりに正確に提供してください(例:EURUSD@FXCM)。
  • ローソク足ストリームはスケジューリングロジックを駆動するためにのみ使用されます。データソースが異なる集計間隔を提供する場合はCandleTypeを調整してください。
  • エクイティ保護はOnStartedでキャプチャされた残高に対してライブエクイティを比較します。戦略を再起動すると参照残高がリセットされます。
  • 戦略には保護ストップまたはテイクプロフィット注文は含まれていません。グローバルエグジットはエクイティパーセンテージとクローズウィンドウのみによって制御されます。

変換に関する注意事項

  • 元のMT5エキスパートはOnTickを使用していました。StockSharpバージョンでは、完成したローソク足がティックイベントを置き換えて、高レベルのイベント駆動方式で時間ウィンドウを評価します。
  • マジックナンバーフィルタリングは不要です。なぜなら戦略はStockSharpの戦略コンテナ内で動作するため、CloseAllManagedPositionsは設定されたシンボルのみを反復処理します。
  • サウンドアラートとチャートコメントは省略されましたが、戦略はLogInfoを通じてすべての重要なアクションをログに記録し、監査を容易にします。
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>
/// Hedging scheduler strategy that opens positions during a configurable time window
/// and closes when equity targets are reached or a separate exit window arrives.
/// Simplified to single-security from the original multi-symbol version.
/// </summary>
public class MultiHedgingSchedulerStrategy : Strategy
{
	private readonly StrategyParam<Sides> _tradeDirection;
	private readonly StrategyParam<TimeSpan> _tradeStartTime;
	private readonly StrategyParam<TimeSpan> _tradeDuration;
	private readonly StrategyParam<bool> _enableTimeClose;
	private readonly StrategyParam<TimeSpan> _closeTime;
	private readonly StrategyParam<bool> _enableEquityClose;
	private readonly StrategyParam<decimal> _profitPercent;
	private readonly StrategyParam<decimal> _lossPercent;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _initialBalance;
	private bool _positionOpened;

	/// <summary>
	/// Trading direction used when opening positions.
	/// </summary>
	public Sides TradeDirection
	{
		get => _tradeDirection.Value;
		set => _tradeDirection.Value = value;
	}

	/// <summary>
	/// Time of day when the trading window starts.
	/// </summary>
	public TimeSpan TradeStartTime
	{
		get => _tradeStartTime.Value;
		set => _tradeStartTime.Value = value;
	}

	/// <summary>
	/// Duration of the trading and optional closing windows.
	/// </summary>
	public TimeSpan TradeDuration
	{
		get => _tradeDuration.Value;
		set => _tradeDuration.Value = value;
	}

	/// <summary>
	/// Enables the separate time based close window.
	/// </summary>
	public bool UseTimeClose
	{
		get => _enableTimeClose.Value;
		set => _enableTimeClose.Value = value;
	}

	/// <summary>
	/// Time of day when the closing window starts.
	/// </summary>
	public TimeSpan CloseTime
	{
		get => _closeTime.Value;
		set => _closeTime.Value = value;
	}

	/// <summary>
	/// Enables closing when equity reaches profit or loss thresholds.
	/// </summary>
	public bool CloseByEquityPercent
	{
		get => _enableEquityClose.Value;
		set => _enableEquityClose.Value = value;
	}

	/// <summary>
	/// Percentage profit target based on starting balance.
	/// </summary>
	public decimal PercentProfit
	{
		get => _profitPercent.Value;
		set => _profitPercent.Value = value;
	}

	/// <summary>
	/// Percentage loss threshold based on starting balance.
	/// </summary>
	public decimal PercentLoss
	{
		get => _lossPercent.Value;
		set => _lossPercent.Value = value;
	}

	/// <summary>
	/// Candle series driving the scheduling logic.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes the strategy.
	/// </summary>
	public MultiHedgingSchedulerStrategy()
	{
		_tradeDirection = Param(nameof(TradeDirection), Sides.Buy)
			.SetDisplay("Trade Direction", "Direction used for opening positions", "General");

		_tradeStartTime = Param(nameof(TradeStartTime), new TimeSpan(10, 0, 0))
			.SetDisplay("Trade Start", "Time of day to begin opening positions", "Scheduling");

		_tradeDuration = Param(nameof(TradeDuration), TimeSpan.FromMinutes(5))
			.SetDisplay("Window Length", "Duration of trading and closing windows", "Scheduling");

		_enableTimeClose = Param(nameof(UseTimeClose), true)
			.SetDisplay("Use Close Window", "Enable time based portfolio closing", "Scheduling");

		_closeTime = Param(nameof(CloseTime), new TimeSpan(17, 0, 0))
			.SetDisplay("Close Start", "Time of day to start the close window", "Scheduling");

		_enableEquityClose = Param(nameof(CloseByEquityPercent), true)
			.SetDisplay("Use Equity Targets", "Enable equity based exit", "Risk Management");

		_profitPercent = Param(nameof(PercentProfit), 1m)
			.SetDisplay("Profit %", "Equity percentage gain to close all positions", "Risk Management");

		_lossPercent = Param(nameof(PercentLoss), 55m)
			.SetDisplay("Loss %", "Equity percentage loss to close all positions", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle series driving the scheduler", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_initialBalance = 0m;
		_positionOpened = false;
	}

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

		_initialBalance = Portfolio?.CurrentValue ?? 0m;

		SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();
	}

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

		if (!IsFormed)
			return;

		var timeOfDay = candle.OpenTime.TimeOfDay;

		if (CloseByEquityPercent && TryHandleEquityTargets())
			return;

		if (UseTimeClose && IsWithinWindow(timeOfDay, CloseTime, TradeDuration))
		{
			if (Position > 0)
				SellMarket(Math.Abs(Position));
			else if (Position < 0)
				BuyMarket(Math.Abs(Position));

			_positionOpened = false;
			return;
		}

		var direction = TradeDirection;

		if (!IsWithinWindow(timeOfDay, TradeStartTime, TradeDuration))
			return;

		if (_positionOpened)
			return;

		var volume = Volume;
		if (volume <= 0m)
			volume = 1m;

		if (direction == Sides.Buy && Position <= 0)
		{
			if (Position < 0)
				BuyMarket(Math.Abs(Position));
			BuyMarket(volume);
			_positionOpened = true;
		}
		else if (direction == Sides.Sell && Position >= 0)
		{
			if (Position > 0)
				SellMarket(Position);
			SellMarket(volume);
			_positionOpened = true;
		}
	}

	private bool TryHandleEquityTargets()
	{
		if (_initialBalance <= 0m)
			return false;

		var equity = Portfolio?.CurrentValue;
		if (equity == null)
			return false;

		var profitLevel = _initialBalance * (1m + PercentProfit / 100m);
		var lossLevel = _initialBalance * (1m - PercentLoss / 100m);

		if (equity.Value >= profitLevel || equity.Value <= lossLevel)
		{
			if (Position > 0)
				SellMarket(Math.Abs(Position));
			else if (Position < 0)
				BuyMarket(Math.Abs(Position));

			_positionOpened = false;
			return true;
		}

		return false;
	}

	private static bool IsWithinWindow(TimeSpan current, TimeSpan start, TimeSpan length)
	{
		if (length <= TimeSpan.Zero)
			return current == start;

		var end = start + length;

		if (end < TimeSpan.FromDays(1))
			return current >= start && current < end;

		var overflow = end - TimeSpan.FromDays(1);
		return current >= start || current < overflow;
	}
}