GitHub で見る

時間指定保留注文戦略

この戦略は、クラシックなMetaTraderエキスパート「Pending orders by time」をStockSharp向けに再現します。離散的なスケジュールで動作します:毎日、新しいセッション時間が始まると市場周辺に対称的なストップ注文を発注し、指定されたクロージング時間にすべての注文とオープンポジションをクリアします。実装は元のpipベースの入力を維持し、ネイティブ価格単位に変換し、リスク管理に高レベルAPIを使用します。

仕組み

  1. 時間ベースのトリガー – 設定されたオープニング時間に終わるローソク足を受信すると、戦略はaskの上にバイストップ、bidの下にセルストップを送信します。両方の注文は価格単位に変換されたDistance (pips)パラメーターでオフセットされます。
  2. 保護注文StartProtectionはパラメーターで定義されたpips距離を使用してストップロスとテイクプロフィット保護を自動的に付加します。ManageRiskは、完了したローソク足がしきい値を超えたことを示す場合に残余ポジションを閉じるセーフガードとしても機能します。
  3. セッション終了 – クロージング時間が来ると、戦略は残っている保留注文をキャンセルし、利益や損失に関係なく開いている取引を強制的に終了させます。これはセッション終了時にリセットする元のエキスパートの動作を再現します。
  4. 桁対応のpipサイズ – pipの乗数は、3桁または5桁の小数点で引用される銘柄(例:JPYや5桁のFXペア)に対して価格ステップを10倍することでMetaTrader実装をエミュレートします。これによりブローカー間でレガシー入力の一貫性が保たれます。

デフォルトのローソク足タイプはH1より短い期間という元の制限の下に留まるために30分バーです。結果として得られる時間のタイムスタンプが望ましいセッション時間と一致する限り、他の時間軸も使用できます。

パラメーター

名前 説明 デフォルト
Opening Hour 戦略がストップ注文のペアを発注する時間(0-23)。 9
Closing Hour すべての注文がキャンセルされポジションが閉じられる時間(0-23)。 2
Distance (pips) 現在の価格と保留ストップエントリーの間のオフセット(pips単位)。 20
Stop Loss (pips) ポジションがオープンになった後の保護ストップのpips距離。 20
Take Profit (pips) ポジションがオープンになった後の利益目標のpips距離。 500
Order Volume 各保留ストップ注文を発注するときに使用する数量。 0.1
Candle Type 時間スケジュールを駆動する時間軸。 30分時間軸

すべてのパラメーターは最適化可能です。pipベースの入力は銘柄の価格ステップを使用して内部的に変換されるため、異なる小数精度を持つFX銘柄間でポータブルのままです。

日次ワークフロー

  1. 各ローソク足クローズ時 戦略はストップロスまたはテイクプロフィット距離に達したかどうかを確認します。達した場合は、アクティブなポジションを成行で閉じます。
  2. クロージング時間に達すると 未約定の保留注文をキャンセルし、次のセッションの前にブックが平坦であることを確認するためにポジションを終了します。
  3. オープニング時間に達すると(戦略が平坦のとき)念のため古い注文をキャンセルし、bidの下に新しいセルストップ、askの上にバイストップを送信します。注文はスプレッドの周りに反映されるため、どちらのブレイクアウトも捉えることができます。
  4. セッション全体を通じて StartProtectionによって作成されたプラットフォームレベルの保護がストップロスとテイクプロフィットを付加した状態を維持し、バー内の価格アクションがしきい値を達した場合に即座に反応します。

使用上の注意

  • pipの調整が元のエキスパートを反映するように、ティックサイズが単一の「ポイント」を表す銘柄を使用してください。エキゾチックなティックサイズは距離パラメーターの手動調整が必要な場合があります。
  • ロジックは1日あたり1つの取引サイクルを想定しています。複数のオープニング/クロージングの一致を持つイントラデイデータを使用する場合は、それに応じて時間を調整してください。
  • すべてのアクションはローソク足の完了時に発生するため、スケジュールを評価する頻度に合ったローソク足のサイズを選択してください。例えば、時間足はMetaTraderバージョンと同じケイデンスを提供します。
  • 戦略はポジションが平坦な場合にのみ新しい保留注文を発注し、次のオープニング時間中にブレイクアウト取引がまだ有効な場合に過剰エクスポージャーを避けます。

MQLバージョンとの違い

  • 保護的な決済はStockSharpの高レベルAPIを活用して保留注文チケットへの直接のストップロス割り当ての代わりにStartProtectionプラス明示的チェックを通じて処理されます。
  • bid/ask価格はSecurity.BestBidSecurity.BestAskから読み取られます。これらの見積もりが利用できない場合、ローソク足のクローズがフォールバック参照として使用されます。
  • 市場注文はシンプルさのためにクロージング時間にポジションを清算するために使用され、ブローカー固有の動作を避けます。
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>
/// Places simulated symmetric stop entries at scheduled hours and manages them with daily resets.
/// </summary>
public class PendingOrdersByTimeStrategy : Strategy
{
	private readonly StrategyParam<int> _openingHour;
	private readonly StrategyParam<int> _closingHour;
	private readonly StrategyParam<decimal> _distancePips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal? _pendingBuyPrice;
	private decimal? _pendingSellPrice;
	private decimal? _entryPrice;

	public int OpeningHour
	{
		get => _openingHour.Value;
		set => _openingHour.Value = value;
	}

	public int ClosingHour
	{
		get => _closingHour.Value;
		set => _closingHour.Value = value;
	}

	public decimal DistancePips
	{
		get => _distancePips.Value;
		set => _distancePips.Value = value;
	}

	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public PendingOrdersByTimeStrategy()
	{
		_openingHour = Param(nameof(OpeningHour), 2)
			.SetDisplay("Opening Hour", "Hour to activate pending orders", "Schedule")
			.SetRange(0, 23);

		_closingHour = Param(nameof(ClosingHour), 22)
			.SetDisplay("Closing Hour", "Hour to cancel orders and flat positions", "Schedule")
			.SetRange(0, 23);

		_distancePips = Param(nameof(DistancePips), 500m)
			.SetDisplay("Distance (pips)", "Offset for entry stop orders", "Orders")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 500m)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk")
			.SetGreaterThanZero();

		_takeProfitPips = Param(nameof(TakeProfitPips), 2000m)
			.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Working timeframe for the schedule", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
		_entryPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pipSize = CalculatePipSize();

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0.01m;

		if (step <= 0m)
			return 0.01m;

		return step;
	}

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

		var hour = candle.OpenTime.Hour;

		// Check pending stop entries
		CheckPendingEntries(candle);

		// Manage existing position
		ManageRisk(candle);

		if (hour == ClosingHour)
		{
			// Closing hour: cancel pending and exit any open trades.
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
			ExitPosition();
		}

		if (hour == OpeningHour && hour != ClosingHour && Position == 0m && !_pendingBuyPrice.HasValue)
		{
			// Opening hour: set up new pending entries.
			SetupPendingEntries(candle.ClosePrice);
		}
	}

	private void CheckPendingEntries(ICandleMessage candle)
	{
		if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
			_entryPrice = buyPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
			return;
		}

		if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
			_entryPrice = sellPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
	}

	private void ManageRisk(ICandleMessage candle)
	{
		if (_pipSize <= 0m || _entryPrice is not decimal entry)
			return;

		var takeProfitDistance = TakeProfitPips * _pipSize;
		var stopLossDistance = StopLossPips * _pipSize;

		if (Position > 0m)
		{
			if (takeProfitDistance > 0m && candle.HighPrice - entry >= takeProfitDistance)
			{
				SellMarket();
				_entryPrice = null;
				return;
			}

			if (stopLossDistance > 0m && entry - candle.LowPrice >= stopLossDistance)
			{
				SellMarket();
				_entryPrice = null;
			}
		}
		else if (Position < 0m)
		{
			if (takeProfitDistance > 0m && entry - candle.LowPrice >= takeProfitDistance)
			{
				BuyMarket();
				_entryPrice = null;
				return;
			}

			if (stopLossDistance > 0m && candle.HighPrice - entry >= stopLossDistance)
			{
				BuyMarket();
				_entryPrice = null;
			}
		}
	}

	private void ExitPosition()
	{
		if (Position > 0m)
			SellMarket();
		else if (Position < 0m)
			BuyMarket();
		_entryPrice = null;
	}

	private void SetupPendingEntries(decimal referencePrice)
	{
		if (_pipSize <= 0m)
			return;

		var distance = DistancePips * _pipSize;
		if (distance <= 0m)
			return;

		_pendingBuyPrice = referencePrice + distance;
		_pendingSellPrice = referencePrice - distance;
	}
}