GitHub で見る

チャネル EA 指値注文戦略

概要

  • 起源: MetaTrader 5 エキスパート ChannelEA1.mq5 から変換。
  • 目的: ユーザー定義の 2 つの時間の間でイントラデイ価格チャネルを監視し、そのウィンドウの終わりに指値注文をキューに入れる。
  • アプローチ: 戦略はセッション中に観察された最高値と最安値を追跡し、チャネルの反対側に向かう潜在的な反転を取引するために対称的な指値注文を配置します。

この戦略は、日次レンジが確立されると平均回帰を示す銘柄に適しています。設計上、ネッティング口座で動作します:約定した売り指値注文は、新しいショートを開く前に既存のロングポジションを閉じます(逆も同様)。

パラメーター

名前 デフォルト 説明
BeginHour 1 イントラデイレンジ追跡が始まる時間(0-23)。この時間に戦略は未決注文をキャンセルしポジションを閉じます。
EndHour 10 累積レンジが評価され、新しい指値注文が配置される時間(0-23)。オーバーナイトセッションをサポート:BeginHour > EndHour の場合、セッションは深夜をまたぎます。
OrderVolume 1 すべての未決注文に適用されるボリューム。
CandleType 1 時間タイムフレーム チャネルを構築するために使用されるローソク足シリーズ。StockSharp でサポートされている任意のタイムフレームに切り替えられます。

トレードロジック

  1. セッション処理
    • 戦略はローソク足のタイムスタンプを使用して BeginHour および EndHour パラメーターからセッションの開始と終了のタイムスタンプを導きます。BeginHour > EndHour の場合、終了は翌日に移されます。
    • 閉じる時間が開始境界に達する最初の完成したローソク足で、戦略はすべてのアクティブな注文をキャンセルし、オープンポジションを閉じ、セッション統計をリセットします。
  2. チャネル構築
    • セッションウィンドウ内に開く時間があるローソク足のみがレンジに貢献します。戦略はセッションの最高値と最安値を追跡し、貢献するローソク足の数をカウントします。
    • 有効なレンジを形成するには少なくとも 2 つの完成したローソク足が必要で、元の MQL5 エキスパートの動作(n > 2 条件)を反映しています。
  3. セッション終了時の注文配置
    • 完成したローソク足が終了境界を越えた場合、戦略はレンジが形成されており安値が高値を厳密に下回っていることを確認します。
    • 次に 2 つの未決注文を配置します:
      • OrderVolume ボリュームで記録されたセッション安値に BuyLimit
      • 同じボリュームで記録されたセッション高値に SellLimit
    • 注文は次のセッションが始まるまでアクティブのままです。戦略はネッティング口座で実行されるため、これらの注文はエントリーと出口の両方として機能します:例えば、SellLimit は新しいショートを確立する前にセッション高値で既存のロングポジションを閉じます。
  4. 次のセッションの準備
    • 次の開始境界で、戦略は残っているポジションを閉じ、新しいチャネルを測定する前に残った未決注文を削除します。

追加注意事項

  • 明示的なストップロスは設定されていません。リスク管理はポジションサイジング、手動の上書き、または外部の保護ロジックを通じて制御する必要があります。
  • ロジックは元の EA 動作に合わせるために完成したローソク足(CandleStates.Finished)のみを使用します。
  • セッション境界は取引所/現地時間で評価されるため、データフィードとサーバーのタイムゾーンが期待と一致していることを確認してください。
  • 最適化する際は、取引時間とローソク足の期間の両方を考慮してください;記録されたレンジは選択したタイムフレームに依存するため、戦略はその組み合わせに敏感です。
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>
/// Channel trading strategy that places limit orders at the end of the monitored session.
/// </summary>
public class ChannelEaLimitsStrategy : Strategy
{
	private readonly StrategyParam<int> _beginHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private DateTimeOffset _sessionStart;
	private DateTimeOffset _sessionEnd;
	private decimal _sessionHigh;
	private decimal _sessionLow;
	private int _barsInSession;
	private DateTimeOffset? _prevCandleClose;
	private bool _ordersPlaced;
	private bool _needsSessionReset;
	private bool _tradeTaken;

	/// <summary>
	/// Initializes a new instance of the <see cref="ChannelEaLimitsStrategy"/> class.
	/// </summary>
	public ChannelEaLimitsStrategy()
	{
		_beginHour = Param(nameof(BeginHour), 1)
			.SetDisplay("Begin Hour", "Hour when session tracking starts (0-23)", "Session")
			.SetRange(0, 23);

		_endHour = Param(nameof(EndHour), 10)
			.SetDisplay("End Hour", "Hour when limit orders are placed (0-23)", "Session")
			.SetRange(0, 23);

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetDisplay("Order Volume", "Volume for each limit order", "Trading")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to build the session channel", "General");
	}

	/// <summary>
	/// Hour when session tracking starts.
	/// </summary>
	public int BeginHour
	{
		get => _beginHour.Value;
		set => _beginHour.Value = value;
	}

	/// <summary>
	/// Hour when the strategy places new pending orders.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Volume per limit order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

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

		_sessionStart = DateTimeOffset.MinValue;
		_sessionEnd = DateTimeOffset.MinValue;
		_sessionHigh = decimal.MinValue;
		_sessionLow = decimal.MaxValue;
		_barsInSession = 0;
		_prevCandleClose = null;
		_ordersPlaced = false;
		_needsSessionReset = false;
		_tradeTaken = false;
	}

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

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

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

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

		var closeTime = candle.CloseTime;
		var sessionStart = CalculateSessionStart(closeTime);

		if (_sessionStart != sessionStart)
		{
			_sessionStart = sessionStart;
			_sessionEnd = CalculateSessionEnd(_sessionStart);
			ResetSessionState();
		}

		if (_needsSessionReset)
		{
			// Close any open position at session reset
			if (Position > 0)
				SellMarket(Position);
			else if (Position < 0)
				BuyMarket(-Position);
			_needsSessionReset = false;
		}

		if (candle.OpenTime >= _sessionStart && candle.OpenTime < _sessionEnd)
		{
			var high = candle.HighPrice;
			var low = candle.LowPrice;

			if (_sessionHigh == decimal.MinValue || high > _sessionHigh)
				_sessionHigh = high;

			if (_sessionLow == decimal.MaxValue || low < _sessionLow)
				_sessionLow = low;

			_barsInSession++;
		}

		// After session ends, trade breakouts of the channel
		if (_ordersPlaced && !_tradeTaken && _barsInSession >= 2 && _sessionLow < _sessionHigh)
		{
			if (Position == 0)
			{
				// Buy when price touches session low, sell when it touches session high
				if (candle.LowPrice <= _sessionLow)
				{
					BuyMarket(OrderVolume);
					_tradeTaken = true;
				}
				else if (candle.HighPrice >= _sessionHigh)
				{
					SellMarket(OrderVolume);
					_tradeTaken = true;
				}
			}
		}

		if (!_ordersPlaced && _prevCandleClose.HasValue)
		{
			var previousClose = _prevCandleClose.Value;

			if (previousClose < _sessionEnd && closeTime >= _sessionEnd)
			{
				if (_barsInSession >= 2 && _sessionLow < _sessionHigh)
				{
					_ordersPlaced = true;
				}
			}
		}

		_prevCandleClose = closeTime;
	}

	private void ResetSessionState()
	{
		_sessionHigh = decimal.MinValue;
		_sessionLow = decimal.MaxValue;
		_barsInSession = 0;
		_ordersPlaced = false;
		_needsSessionReset = true;
		_tradeTaken = false;
	}

	private DateTimeOffset CalculateSessionStart(DateTimeOffset time)
	{
		var offset = time.Offset;
		var day = new DateTimeOffset(time.Date, offset);
		var start = day.AddHours(BeginHour);
		var startHour = TimeSpan.FromHours(BeginHour);

		if (BeginHour <= EndHour)
		{
			if (time < start)
				start = start.AddDays(-1);
		}
		else
		{
			if (time.TimeOfDay < startHour)
				start = start.AddDays(-1);
		}

		return start;
	}

	private DateTimeOffset CalculateSessionEnd(DateTimeOffset sessionStart)
	{
		var offset = sessionStart.Offset;
		var day = new DateTimeOffset(sessionStart.Date, offset);
		var end = day.AddHours(EndHour);

		if (EndHour <= BeginHour || end <= sessionStart)
			end = end.AddDays(1);

		return end;
	}
}