在 GitHub 上查看

21hour 策略

概述

21hour 策略复刻了 MQL4 专家顾问 21hour.mq4 的逻辑。它围绕每日的时间窗口运行:在设定的启动小时放置一对突破型止损挂单,并在设定的结束小时撤销所有敞口。StockSharp 版本保持“双向止损挂单包围价格”的思路,同时借助高级 API 处理行情订阅、下单及自动化的止盈管理。

交易逻辑

  • 每个交易日开始时,当服务器时间到达 StartHour:00,策略会读取最新的买卖报价,同时放置 buy stop 和 sell stop。
    • buy stop 的触发价位于当前 ask 之上,距离为 StepPoints * PriceStep
    • sell stop 的触发价位于当前 bid 之下,距离相同。
    • TakeProfitPoints 通过合约的最小跳动转换为价格距离并传给 StartProtection,因此多头和空头持仓在成交后立即获得保护性的止盈单。
  • 每天只允许存在一组挂单。如果只剩下单侧挂单处于激活状态(例如另一侧已成交),策略会撤销剩余挂单,以符合原始 EA 的行为。
  • 当时间到达 StopHour:00 时,策略会以市价平掉所有持仓,并撤销所有未成交挂单,即使突破没有发生也会执行。
  • 默认使用一分钟 K 线流,仅用于在收盘蜡烛上触发整点检查,等同于 MQL 中基于 prevtime 的防重逻辑。

参数

参数 说明 默认值
Volume 两个挂单使用的下单手数。 0.1
StartHour 生成挂单的小时(0–23)。 10
StopHour 平仓并撤单的小时(0–23)。 22
StepPoints 当前 bid/ask 与止损挂单触发价之间的点数距离,通过 PriceStep 转换为价格。 15
TakeProfitPoints 从成交价到止盈目标的点数距离,由 StartProtection 管理;0 表示不使用止盈。 200
CandleType 用于时间检测的蜡烛类型,默认是一分钟时间框架(TimeSpan.FromMinutes(1).TimeFrame())。 1 分钟

实现说明

  • 使用 SubscribeCandles 订阅蜡烛并在每根收盘蜡烛后检查时间窗口,避免重复执行。
  • 通过 SubscribeLevel1() 订阅一级行情以获取最新的 bid/ask,用于精确计算挂单价格。
  • 调用 StartProtection 并传入止盈距离,从而模拟原始 EA 中附加在挂单上的止盈功能,而无需手动管理保护单。
  • 维护 buy stop 与 sell stop 的引用,如果只剩一侧处于激活状态则调用 CancelOrder 撤销它,确保不会遗留单侧挂单。
  • 在停止时间使用 BuyMarket / SellMarket 等高级方法平仓,完全依赖 StockSharp 策略 API。

其他说明

  • 策略需要经纪商连接提供 PriceStep 信息;如果无法获得,则不会对价格进行步长四舍五入。
  • 每个自然日仅生成一次挂单组合;即便前一日的突破未成交,次日到达启动时间后仍会重新挂单。
  • TakeProfitPoints 设为 0 时,策略仍会挂出突破单,但不会再自动管理止盈。
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Time-based breakout strategy. At start hour, detects breakout direction from previous candle range.
/// At stop hour, closes all positions.
/// </summary>
public class TwentyOneHourStrategy : Strategy
{
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _stopHour;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevHigh;
	private decimal _prevLow;
	private bool _hasPrev;
	private bool _tradedToday;
	private int _lastTradeDay;

	public TwentyOneHourStrategy()
	{
		_startHour = Param(nameof(StartHour), 10)
			.SetDisplay("Start Hour", "Hour to look for breakout entries.", "Schedule");

		_stopHour = Param(nameof(StopHour), 22)
			.SetDisplay("Stop Hour", "Hour to close positions.", "Schedule");

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

	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	public int StopHour
	{
		get => _stopHour.Value;
		set => _stopHour.Value = value;
	}

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

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

		_prevHigh = 0;
		_prevLow = 0;
		_hasPrev = false;
		_tradedToday = false;
		_lastTradeDay = -1;
	}

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

		_prevHigh = 0;
		_prevLow = 0;
		_hasPrev = false;
		_tradedToday = false;
		_lastTradeDay = -1;

		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 hour = candle.OpenTime.Hour;
		var day = candle.OpenTime.DayOfYear;

		// Reset daily flag
		if (day != _lastTradeDay)
		{
			_tradedToday = false;
			_lastTradeDay = day;
		}

		// Close at stop hour
		if (hour >= StopHour && Position != 0)
		{
			if (Position > 0)
				SellMarket();
			else
				BuyMarket();
		}

		// Entry at start hour window
		if (hour >= StartHour && hour < StopHour && !_tradedToday && _hasPrev && Position == 0)
		{
			if (candle.ClosePrice > _prevHigh)
			{
				BuyMarket();
				_tradedToday = true;
			}
			else if (candle.ClosePrice < _prevLow)
			{
				SellMarket();
				_tradedToday = true;
			}
		}

		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_hasPrev = true;
	}
}