在 GitHub 上查看

Channel EA Limits 策略

概述

  • 来源:由 MetaTrader 5 专家顾问 ChannelEA1.mq5 转换而来。
  • 目标:在两个用户定义的交易时段之间监控日内价格通道,并在时段结束时挂出通道边界的限价单。
  • 方法:在会话期间持续记录最高价和最低价,结束时根据该区间同时布置买入和卖出限价单,以捕捉价格回到区间另一端的机会。

该策略适合日内存在均值回归特征的品种。设计为净额账户工作方式:SellLimit 成交时会先平掉现有多头,再开出新的空头,反之亦然。

参数

参数 默认值 说明
BeginHour 1 开始统计区间的小时(0-23)。达到该时间时会取消所有挂单并平掉头寸。
EndHour 10 结束统计区间的小时(0-23)。此时根据累计的价格区间挂出新的限价单。若 BeginHour > EndHour 则区间跨越午夜。
OrderVolume 1 每个限价单的下单量。
CandleType 1 小时 K 线 构建通道所使用的 K 线类型,可根据需要调整为其他周期。

交易逻辑

  1. 会话处理
    • 根据 BeginHourEndHour 结合 K 线时间计算出当前会话的起止时间;当开始时间晚于结束时间时,结束时间会顺延到下一天。
    • 当第一根收盘时间达到起始边界的 K 线完成时,策略会取消所有挂单、平掉当前头寸,并重置统计数据。
  2. 区间构建
    • 只有开盘时间落在会话窗口内的 K 线才会参与统计。策略维护区间内的最高价、最低价以及 K 线数量。
    • 至少需要两根完成的 K 线才能认定区间有效,这与原始 MQL5 策略中的 n > 2 条件保持一致。
  3. 会话结束时下单
    • 当某根完成的 K 线跨越结束边界时,若区间已形成且最低价小于最高价,就会挂出两张限价单:
      • 在记录的会话最低价挂 BuyLimit,数量为 OrderVolume
      • 在记录的会话最高价挂 SellLimit,数量相同。
    • 这些挂单会一直保留到下一次会话开始。由于采用净额账户模型,这些限价单既是潜在的入场点,也是已有头寸的止盈点。
  4. 准备下一会话
    • 到达下一次开始时间时,策略会清理未成交挂单、平掉剩余头寸,然后重新开始计算新的日内通道。

补充说明

  • 策略没有内置止损,风险控制需通过下单量、外部保护或人工干预实现。
  • 仅处理状态为 CandleStates.Finished 的 K 线,以忠实复现原始 EA 的行为。
  • 请确认行情源的时区与预期一致,因为会话边界基于交易所/数据源时间来计算。
  • 在进行参数优化时,需要同时考虑交易时间窗口与 K 线周期,二者共同决定区间的宽度。
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;
	}
}