GitHub で見る

始値時間戦略

概要

始値時間戦略は、MetaTrader 5 のエキスパートアドバイザー OpenTime の動作を再現する時間スケジュール型トレーディングシステムです。この戦略は確定済みのローソク足のマーケット時計を監視し、設定可能な時間ウィンドウ内でのみ取引を開始します。専用の決済ウィンドウ中にアクティブなポジションを閉じたり、任意のトレーリングストップを適用したり、pips で表現された基本的なストップロスおよびテイクプロフィットのルールを適用できます。

オリジナルのヘッジバージョンとは異なり、この StockSharp 版はネット方式のポートフォリオで動作します。現在のポジションと矛盾するシグナルが現れた場合、まず反対方向のエクスポージャーを閉じてから、設定されたボリュームで要求された方向のポジションを開きます。

取引フロー

  1. 決済ウィンドウUse Close Window フラグが有効で、現在時刻が決済ウィンドウ内に入った場合、戦略はオープンポジションを即座に決済します。ウィンドウが終了するまで新しい取引は許可されません。
  2. トレーリング更新 – トレーリングが有効で、市場が現在のポジションに有利な方向に少なくとも TrailingStop + TrailingStep pips 動いた場合、トレーリングストップは TrailingStop で定義された距離だけ価格に近づけられます。これはストップレベルが最小ステップの後にのみ変更される MT5 のロジックを再現します。
  3. リスクチェック – 確定済みのローソク足ごとに、ストップロスまたはテイクプロフィットの閾値に達したかどうかを確認します。いずれかのレベルにヒットした場合、ポジションは閉じられ、その方向の内部状態がすべてリセットされます。
  4. エントリーウィンドウ – 時刻が取引ウィンドウ内にある場合、戦略は方向スイッチを評価します:
    • ロングエントリーが有効で現在のネットポジションがフラットまたはショートの場合、設定されたボリュームに加えて既存のショートポジションをカバーするために必要な数量を買い付けます。
    • ショートエントリーが有効でネットポジションがフラットまたはロングの場合、設定されたボリュームに加えて既存のロングポジションを解消するために必要な数量を売ります。

実行された各エントリーは、エントリー価格とストップおよびターゲットのオフセット(ゼロでない場合)を保存します。これらの値はトレーリングロジックとその後の決済チェックで再利用されます。

パラメーター

パラメーター デフォルト 説明
Candle Type 1分足ローソク 時間追跡に使用するデータ型;戦略は確定済みローソク足にのみ反応します。
Use Close Window true 自動決済ウィンドウを有効にします。
Close Hour / Close Minute 20:50 決済ウィンドウの開始時刻。時間は 0–24 の値をサポート;24 は翌日に繰り越します。
Enable Trailing false トレーリングストップのロジックを有効にします。
Trailing Stop 30 pips 価格とトレーリングストップ間の距離。銘柄のティックサイズに応じて価格単位に変換されます。
Trailing Step 3 pips トレーリングストップが再度前進する前に必要な追加の動き。
Trade Hour / Trade Minute 18:50 新規エントリーを許可する取引ウィンドウの開始時刻。
Duration 300 秒 開始ウィンドウと決済ウィンドウの共通の継続時間。
Enable Sell / Enable Buy Sell = true, Buy = false 許可する方向を選択します。
Volume 0.1 新規エントリーで送信する注文ボリューム。反転時には、反対方向のエクスポージャーを解消するための追加ボリュームが加算されます。
Stop Loss 0 pips 初期ストップロス距離。ゼロの場合、静的ストップは無効になり、決済の制御はトレーリングまたは決済ウィンドウに委ねられます。
Take Profit 0 pips 初期テイクプロフィット距離。ゼロの場合、利益目標は無効になります。

実装の詳細

  • pips の値は Security.PriceStep から再計算されます。3桁または5桁の小数で見積もられる銘柄の場合、元の MT5 の「pip」変換を再現するためにステップに10を掛けます。
  • トレーリングと静的リスクレベルはいずれも、ローソク足ベースのハイレベル API で動作しながらティックごとの動作に近づけるために、ローソク足の極値(HighPrice/LowPrice)上で動作します。
  • 戦略は次の取引で古いストップやターゲットを再利用しないよう、各決済後に内部状態をリセットします。
  • StockSharp はデフォルトでネットポジションで動作するため、同時ロング・ショートポジションはサポートされていません。反転ロジックは、要求された方向を開く前に既存のエクスポージャーを相殺することで MT5 のヘッジを模倣します。

使用上の注意

  • 取引ウィンドウが要求する時間の粒度に合ったローソク足タイプを選択してください。より短い時間軸(例:1分)はより精度の高いタイミングを提供します。
  • 決済ウィンドウと開始ウィンドウは同じ継続時間パラメーターを共有します。いずれかのウィンドウを無効にするには、継続時間をゼロに設定するか Use Close Window をオフにしてください。
  • トレーリングストップは、記録されたエントリー価格から少なくとも Trailing Stop + Trailing Step pips 市場が前進した後にのみ有効になり、元のトレーリングステップの動作を再現します。
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>
/// Time based opening strategy with optional trailing stop logic.
/// </summary>
public class OpenTimeStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _useCloseTime;
	private readonly StrategyParam<int> _closeHour;
	private readonly StrategyParam<int> _closeMinute;
	private readonly StrategyParam<bool> _enableTrailing;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _tradeHour;
	private readonly StrategyParam<int> _tradeMinute;
	private readonly StrategyParam<int> _durationSeconds;
	private readonly StrategyParam<bool> _enableSell;
	private readonly StrategyParam<bool> _enableBuy;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;

	private decimal _pipSize;
	private decimal _stopOffset;
	private decimal _takeOffset;
	private decimal _trailOffset;
	private decimal _trailStep;

	private decimal? _longEntry;
	private decimal? _shortEntry;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Initializes a new instance of the <see cref="OpenTimeStrategy"/> class.
	/// </summary>
	public OpenTimeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle subscription type", "General");
		_useCloseTime = Param(nameof(UseCloseTime), true)
			.SetDisplay("Use Close Window", "Enable automatic closing window", "Trading");
		_closeHour = Param(nameof(CloseHour), 20)
			.SetDisplay("Close Hour", "Hour for the closing window", "Trading");
		_closeMinute = Param(nameof(CloseMinute), 50)
			.SetDisplay("Close Minute", "Minute for the closing window", "Trading");
		_enableTrailing = Param(nameof(EnableTrailing), false)
			.SetDisplay("Enable Trailing", "Use trailing stop logic", "Risk");
		_trailingStopPips = Param(nameof(TrailingStopPips), 30)
			.SetDisplay("Trailing Stop", "Trailing stop distance in pips", "Risk");
		_trailingStepPips = Param(nameof(TrailingStepPips), 3)
			.SetDisplay("Trailing Step", "Additional move required to shift the trail", "Risk");
		_tradeHour = Param(nameof(TradeHour), 10)
			.SetDisplay("Trade Hour", "Hour to start opening positions", "Trading");
		_tradeMinute = Param(nameof(TradeMinute), 0)
			.SetDisplay("Trade Minute", "Minute to start opening positions", "Trading");
		_durationSeconds = Param(nameof(DurationSeconds), 18000)
			.SetDisplay("Duration", "Window length in seconds", "Trading");
		_enableSell = Param(nameof(EnableSell), true)
			.SetDisplay("Enable Sell", "Allow short entries", "Trading");
		_enableBuy = Param(nameof(EnableBuy), true)
			.SetDisplay("Enable Buy", "Allow long entries", "Trading");
		_stopLossPips = Param(nameof(StopLossPips), 500)
			.SetDisplay("Stop Loss", "Initial stop loss in pips", "Risk");
		_takeProfitPips = Param(nameof(TakeProfitPips), 1000)
			.SetDisplay("Take Profit", "Initial take profit in pips", "Risk");
	}

	/// <summary>
	/// Candle subscription type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Indicates whether automatic closing window is enabled.
	/// </summary>
	public bool UseCloseTime
	{
		get => _useCloseTime.Value;
		set => _useCloseTime.Value = value;
	}

	/// <summary>
	/// Hour of the closing window (0-24).
	/// </summary>
	public int CloseHour
	{
		get => _closeHour.Value;
		set => _closeHour.Value = value;
	}

	/// <summary>
	/// Minute of the closing window (0-59).
	/// </summary>
	public int CloseMinute
	{
		get => _closeMinute.Value;
		set => _closeMinute.Value = value;
	}

	/// <summary>
	/// Enables trailing stop logic.
	/// </summary>
	public bool EnableTrailing
	{
		get => _enableTrailing.Value;
		set => _enableTrailing.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Additional movement required to move the trailing stop.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Hour when the strategy can open positions.
	/// </summary>
	public int TradeHour
	{
		get => _tradeHour.Value;
		set => _tradeHour.Value = value;
	}

	/// <summary>
	/// Minute when the strategy can open positions.
	/// </summary>
	public int TradeMinute
	{
		get => _tradeMinute.Value;
		set => _tradeMinute.Value = value;
	}

	/// <summary>
	/// Duration of the trading window in seconds.
	/// </summary>
	public int DurationSeconds
	{
		get => _durationSeconds.Value;
		set => _durationSeconds.Value = value;
	}

	/// <summary>
	/// Enables short entries.
	/// </summary>
	public bool EnableSell
	{
		get => _enableSell.Value;
		set => _enableSell.Value = value;
	}

	/// <summary>
	/// Enables long entries.
	/// </summary>
	public bool EnableBuy
	{
		get => _enableBuy.Value;
		set => _enableBuy.Value = value;
	}


	/// <summary>
	/// Initial stop loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Initial take profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

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

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

		_pipSize = 0m;
		_stopOffset = 0m;
		_takeOffset = 0m;
		_trailOffset = 0m;
		_trailStep = 0m;

		ResetLongState();
		ResetShortState();
	}

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

		// Convert pip-based inputs to absolute price offsets.
		_pipSize = CalculatePipSize();
		_stopOffset = StopLossPips * _pipSize;
		_takeOffset = TakeProfitPips * _pipSize;
		_trailOffset = TrailingStopPips * _pipSize;
		_trailStep = TrailingStepPips * _pipSize;

		// Subscribe to candle data used for time-based processing.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

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

		// no protection
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Work only with finished candles to avoid premature actions.
		if (candle.State != CandleStates.Finished)
			return;

		var now = candle.CloseTime;

		// Force-close any position during the configured closing window.
		if (UseCloseTime && IsWithinWindow(now, CloseHour, CloseMinute, DurationSeconds))
		{
			CloseActivePositions();
			return;
		}

		// Update trailing stops and exit if risk limits were exceeded.
		UpdateTrailingStops(candle);
		CheckRiskManagement(candle);

		// no bound indicators to check

		// Skip entries outside the trading window.
		if (!IsWithinWindow(now, TradeHour, TradeMinute, DurationSeconds))
			return;

		// Open or reverse long positions when buying is enabled.
		if (EnableBuy && Position <= 0)
		{
			if (Position < 0)
			{
				BuyMarket();
				ResetShortState();
			}

			BuyMarket();
			InitializeLongState(candle.ClosePrice);
		}
		else if (EnableSell && Position >= 0)
		{
			if (Position > 0)
			{
				SellMarket();
				ResetLongState();
			}

			SellMarket();
			InitializeShortState(candle.ClosePrice);
		}
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (!EnableTrailing || _trailOffset <= 0m)
			return;

		// Move the trailing stop for long positions once the minimal step is reached.
		if (Position > 0 && _longEntry.HasValue)
		{
			var distance = candle.ClosePrice - _longEntry.Value;
			if (distance > _trailOffset + _trailStep)
			{
				var triggerLevel = candle.ClosePrice - (_trailOffset + _trailStep);
				if (!_longStop.HasValue || _longStop.Value < triggerLevel)
					_longStop = candle.ClosePrice - _trailOffset;
			}
		}
		// Move the trailing stop for short positions in a symmetrical way.
		else if (Position < 0 && _shortEntry.HasValue)
		{
			var distance = _shortEntry.Value - candle.ClosePrice;
			if (distance > _trailOffset + _trailStep)
			{
				var triggerLevel = candle.ClosePrice + (_trailOffset + _trailStep);
				if (!_shortStop.HasValue || _shortStop.Value > triggerLevel)
					_shortStop = candle.ClosePrice + _trailOffset;
			}
		}
	}

	private void CheckRiskManagement(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				SellMarket();
				ResetLongState();
				return;
			}

			if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
			{
				SellMarket();
				ResetLongState();
			}
		}
		else if (Position < 0)
		{
			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				BuyMarket();
				ResetShortState();
				return;
			}

			if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
			{
				BuyMarket();
				ResetShortState();
			}
		}
	}

	private void CloseActivePositions()
	{
		// Flatten the portfolio and clear cached levels.
		if (Position > 0)
		{
			SellMarket();
			ResetLongState();
		}
		else if (Position < 0)
		{
			BuyMarket();
			ResetShortState();
		}
	}

	private void InitializeLongState(decimal price)
	{
		// Remember entry price and derived risk levels for long trades.
		_longEntry = price;
		_longStop = StopLossPips > 0 ? price - _stopOffset : null;
		_longTake = TakeProfitPips > 0 ? price + _takeOffset : null;
	}

	private void InitializeShortState(decimal price)
	{
		// Remember entry price and derived risk levels for short trades.
		_shortEntry = price;
		_shortStop = StopLossPips > 0 ? price + _stopOffset : null;
		_shortTake = TakeProfitPips > 0 ? price - _takeOffset : null;
	}

	private void ResetLongState()
	{
		_longEntry = null;
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortState()
	{
		_shortEntry = null;
		_shortStop = null;
		_shortTake = null;
	}

	private decimal CalculatePipSize()
	{
		// Convert MT5-style pip values into absolute price units.
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			return 1m;

		var decimals = CountDecimals(step);
		var multiplier = decimals == 3 || decimals == 5
			? 10m
			: 1m;
		return step * multiplier;
	}

	private static int CountDecimals(decimal value)
	{
		// Count decimal places by repeatedly shifting the decimal point.
		value = Math.Abs(value);
		var decimals = 0;
		while (value != Math.Truncate(value) && decimals < 10)
		{
			value *= 10m;
			decimals++;
		}
		return decimals;
	}

	private static bool IsWithinWindow(DateTimeOffset time, int hour, int minute, int durationSeconds)
	{
		if (durationSeconds <= 0)
			return false;

		var start = BuildReferenceTime(time, hour, minute);
		var end = start.AddSeconds(durationSeconds);
		return time >= start && time < end;
	}

	private static DateTimeOffset BuildReferenceTime(DateTimeOffset reference, int hour, int minute)
	{
		// Align the target time with the current trading day, allowing hour values above 23.
		var normalizedHour = hour;
		var day = new DateTimeOffset(reference.Year, reference.Month, reference.Day, 0, 0, 0, reference.Offset);

		while (normalizedHour >= 24)
		{
			normalizedHour -= 24;
			day = day.AddDays(1);
		}

		if (minute < 0)
			minute = 0;
		else if (minute > 59)
			minute = 59;

		return day.AddHours(normalizedHour).AddMinutes(minute);
	}
}