GitHub で見る

TimeEA戦略

TimeEA戦略は、MetaTraderの元のエキスパートアドバイザー「TimeEA」をStockSharp内で再現したものです。時刻のみに基づいて単一のポジションを管理します:設定された時刻に開き、一定方向にポジションを保持し、予定クローズ時刻にまたはオプションのストップロス/テイクプロフィットレベルが破られると退場します。

インジケーター駆動型システムとは異なり、この実装は規律あるセッション管理に焦点を当てています。1日に1回のみのエントリーを確保し、開く前に反対のエクスポージャーをクリアし、ブローカーのストップレベル制限を模倣するための保護注文の最小距離を設定します。

動作方法

  1. 戦略は設定可能なローソク足シリーズ (デフォルトは1分) をサブスクライブし、完了済みのローソク足のみを評価します。
  2. ローソク足の終値が設定された オープン時刻 を越えると、戦略は:
    • まだ開いている可能性のある反対のポジションを閉じます。
    • 選択した方向 (買いまたは売り) で指定したボリュームで成行注文を置きます。
    • 最小距離乗数を適用して、エントリーからのポイント (価格ステップ) でストップロスとテイクプロフィット価格を記録します。
  3. セッション中、戦略はローソク足を監視します:
    • ローソク足が保存されたストップロスまたはテイクプロフィットレベルに触れると、ポジションはすぐに閉じられます。
    • ローソク足が クローズ時刻 窓を越えると、利益または損失に関わらずポジションがフラット化されます。
  4. トレードのクローズ後 (ストップ、ターゲット、またはスケジュールによる)、戦略は次の取引日まで無ポジション状態を保ちます。

このフローは、TimeCurrent()Time[0] 比較に依存していたMetaTraderバージョンの「1日1回開く」動作を再現します。

パラメーター

名前 説明
Open Time 取引を開く時刻。HH:MM:SS を受け入れます。
Close Time すべてのポジションをフラット化する時刻。同じ日または翌日になる場合があります。
Position Type ポジションの方向 (Buy または Sell)。
Order Volume 成行注文の送信時に使用する数量。
Stop Loss (points) プロテクティブストップの価格ステップ距離。無効にするには0に設定。
Take Profit (points) 利益目標の価格ステップ距離。無効にするには0に設定。
Minimum Distance Multiplier ストップとターゲットの両方に適用される最小オフセット (価格ステップ単位)。スプレッドに対する元のストップレベルチェックを模倣します。
Candle Type 時間境界を検出するために使用するデータシリーズ。デフォルトは1分ローソク足。

実践的なメモ

  • 1日1回エントリー – オープン時刻が発動されると、ポジションが早期にストップされた場合でも、翌暦日まで再エントリーしません。
  • 深夜越えサポート – オープン時刻とクローズ時刻の両方を深夜の前後に設定できます。ヘルパーは00:00を過ぎて続くセッションを尊重します。
  • ボリューム処理 – 成行注文は Order Volume パラメーターを尊重します。選択したインストゥルメントのコントラクトサイズに合わせて調整してください。
  • ストップレベルエミュレーション – 最小距離乗数は、ストップ/ターゲットがエントリーから少なくとも定義された数のポイント離れていることを確保し、元の「スプレッド × 乗数」ルールを反映します。
  • データ要件 – 戦略はタイミングに一貫したローソク足に依存します。タイムゾーンドリフトを避けるために取引所ローカルの時間軸を使用してください。
  • リスク管理 – ストップとターゲットは内部で管理されます。サーバー側のOCO注文は作成されません。ローソク足が閾値を越えると、戦略は退場するための成行注文を発行します。

ユースケース

  • セッションベースのエントリーの自動化 (例:ロンドンやニューヨークのオープン時にポジションを開く)。
  • 方向が事前にわかっているが、執行は正確なスケジュールに従わなければならない方向性バイアス戦略の実行。
  • 手動タイマーなしでStockSharpのハイレベルAPI内でMetaTraderスタイルのタイムトリガーをエミュレートする。

制限事項

  • スリッページは成行注文によって暗黙的に処理されます。MetaTraderのような個別の偏差パラメーターはありません。
  • 最小距離乗数は動的スプレッドを読み取りません。価格ステップで表現された静的なクッションを適用します。
  • 戦略はインスタンスごとに1つのインストゥルメント/証券のみが取引されることを前提としています。

始め方

  1. Designerまたはコードで戦略パラメーター (オープン/クローズ時刻、方向、ボリューム、リスク距離) を設定します。
  2. 戦略を希望する証券とデータソースに接続します。
  3. ローソク足シリーズが意図したスケジュールと同じタイムゾーンを使用していることを確認します。
  4. 戦略を実行してトレードログを監視します。必要に応じて DrawCandlesDrawOwnTrades でビジュアルオーバーレイを有効にできます。

ロジックは CS/TimeEaStrategy.cs に完全に含まれており、ワークフローの各ステージを説明する詳細なインラインコメントがあります。

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 strategy that opens a single directional position at the configured time
/// and closes it at another time or when optional stop/target levels are hit.
/// </summary>
public class TimeEaStrategy : Strategy
{
	private readonly StrategyParam<TimeSpan> _openTime;
	private readonly StrategyParam<TimeSpan> _closeTime;
	private readonly StrategyParam<TimeEaPositionTypes> _openedType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _minSpreadMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private DateTime? _lastEntryDate;
	private DateTime? _lastCloseDate;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;

	/// <summary>
	/// Time of day to open the position.
	/// </summary>
	public TimeSpan OpenTime
	{
		get => _openTime.Value;
		set => _openTime.Value = value;
	}

	/// <summary>
	/// Time of day to close the position.
	/// </summary>
	public TimeSpan CloseTime
	{
		get => _closeTime.Value;
		set => _closeTime.Value = value;
	}

	/// <summary>
	/// Direction of the position opened at the scheduled time.
	/// </summary>
	public TimeEaPositionTypes OpenedType
	{
		get => _openedType.Value;
		set => _openedType.Value = value;
	}

	/// <summary>
	/// Market order volume for opening trades.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Stop loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Minimal distance multiplier applied to stops and targets.
	/// </summary>
	public int MinSpreadMultiplier
	{
		get => _minSpreadMultiplier.Value;
		set => _minSpreadMultiplier.Value = value;
	}

	/// <summary>
	/// Candle type used to evaluate time windows.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="TimeEaStrategy"/>.
	/// </summary>
	public TimeEaStrategy()
	{
		_openTime = Param(nameof(OpenTime), new TimeSpan(1, 0, 0))
			.SetDisplay("Open Time", "Time to enter the market", "Scheduling");

		_closeTime = Param(nameof(CloseTime), TimeSpan.Zero)
			.SetDisplay("Close Time", "Time to exit the market", "Scheduling");

		_openedType = Param(nameof(OpenedType), TimeEaPositionTypes.Buy)
			.SetDisplay("Position Type", "Direction to maintain", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Quantity for market orders", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance in price steps", "Risk");

		_minSpreadMultiplier = Param(nameof(MinSpreadMultiplier), 2)
			.SetNotNegative()
			.SetDisplay("Minimum Distance Multiplier", "Minimal offset applied to stops", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
			.SetDisplay("Candle Type", "Candles used for scheduling", "General");
	}

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

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

		_lastEntryDate = null;
		_lastCloseDate = null;
		ResetRiskLevels();
	}

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Use finished candles to evaluate the time windows.
		if (candle.State != CandleStates.Finished)
			return;

		var candleDate = candle.CloseTime.Date;

		if (ContainsTime(candle, OpenTime) && _lastEntryDate != candleDate)
		{
			_lastEntryDate = candleDate;
			HandleOpen(candle);
		}

		if (ContainsTime(candle, CloseTime) && _lastCloseDate != candleDate)
		{
			_lastCloseDate = candleDate;

			if (Position != 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
			return;
		}

		ManageRisk(candle);
	}

	private void HandleOpen(ICandleMessage candle)
	{
		// Close opposite exposure before opening a new position.
		if (OpenedType == TimeEaPositionTypes.Buy)
		{
			if (Position < 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}

			if (Position == 0 && OrderVolume > 0)
			{
				BuyMarket(OrderVolume);
				SetRiskLevels(candle.ClosePrice, true);
			}
		}
		else
		{
			if (Position > 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}

			if (Position == 0 && OrderVolume > 0)
			{
				SellMarket(OrderVolume);
				SetRiskLevels(candle.ClosePrice, false);
			}
		}
	}

	private void ManageRisk(ICandleMessage candle)
	{
		// Monitor active position for stop loss and take profit.
		if (Position > 0)
		{
			if (_stopPrice > 0m && candle.LowPrice <= _stopPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice > 0m && candle.HighPrice >= _stopPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
		}
	}

	private void SetRiskLevels(decimal closePrice, bool isLong)
	{
		_entryPrice = closePrice;

		var step = Security?.PriceStep ?? 1m;
		var minDistance = Math.Max(MinSpreadMultiplier, 0) * step;
		var stopDistance = StopLossPoints > 0 ? Math.Max(StopLossPoints * step, minDistance) : 0m;
		var takeDistance = TakeProfitPoints > 0 ? Math.Max(TakeProfitPoints * step, minDistance) : 0m;

		// Calculate price levels in the same direction logic as the original Expert Advisor.
		if (isLong)
		{
			_stopPrice = stopDistance > 0m ? closePrice - stopDistance : 0m;
			_takeProfitPrice = takeDistance > 0m ? closePrice + takeDistance : 0m;
		}
		else
		{
			_stopPrice = stopDistance > 0m ? closePrice + stopDistance : 0m;
			_takeProfitPrice = takeDistance > 0m ? closePrice - takeDistance : 0m;
		}
	}

	private void ResetRiskLevels()
	{
		_entryPrice = 0m;
		_stopPrice = 0m;
		_takeProfitPrice = 0m;
	}

	private static bool ContainsTime(ICandleMessage candle, TimeSpan target)
	{
		var openTime = candle.OpenTime;
		var closeTime = candle.CloseTime;

		var openSpan = openTime.TimeOfDay;
		var closeSpan = closeTime.TimeOfDay;

		var crossesMidnight = closeTime.Date > openTime.Date || closeSpan < openSpan;

		if (!crossesMidnight)
			return target >= openSpan && target <= closeSpan;

		var startMinutes = openSpan.TotalMinutes;
		var endMinutes = closeSpan.TotalMinutes + TimeSpan.FromDays(1).TotalMinutes;
		var targetMinutes = target.TotalMinutes;

		if (targetMinutes < startMinutes)
			targetMinutes += TimeSpan.FromDays(1).TotalMinutes;

		return targetMinutes >= startMinutes && targetMinutes <= endMinutes;
	}

	/// <summary>
	/// Supported position directions.
	/// </summary>
	public enum TimeEaPositionTypes
	{
		/// <summary>
		/// Open a long position.
		/// </summary>
		Buy,

		/// <summary>
		/// Open a short position.
		/// </summary>
		Sell
	}
}