在 GitHub 上查看

21小时时段突破策略

该策略在 StockSharp 中复现 MetaTrader 上的 “21hour” 专家顾问。它围绕两个可配置的交易时段运作:在每个时段开始时布置双向突破挂单,在时段结束时清空持仓并撤销所有挂单,确保新时段从零仓位开始。

核心思想

  • 完全依赖时间驱动:仅在指定小时开始时寻找突破机会。
  • 时段启动时,在当前最优卖价之上放置买入止损挂单,在最优买价之下放置卖出止损挂单,距离由 StepPoints 控制。
  • 任一挂单成交后立即取消另一侧挂单,同时按 TakeProfitPoints 设置固定距离的止盈限价单。
  • 到达时段结束时间后,无论盈亏,都会市价平仓并撤销所有剩余挂单。

数据来源

  • K线: 默认使用 1 分钟 K 线(可通过 CandleType 调整)来触发时间检查。
  • 盘口: 订阅 Level 1 行情以获取实时最优买价/卖价,计算挂单价格。

交易规则

入场逻辑

  • 当时间到达 FirstSessionStartHour(默认 08:00)或 SecondSessionStartHour(默认 22:00)时:
    • Ask + StepPoints * PriceStep 处提交买入止损单。
    • Bid - StepPoints * PriceStep 处提交卖出止损单。
  • 策略只允许一侧持仓,如果在新时段开始时已经持仓,会先取消现有挂单后重新布置。

持仓管理

  • 某一侧挂单成交后立刻撤销另一侧挂单。
  • 根据成交价在固定距离处挂出止盈限价单。
  • 所有订单的数量由 Volume 控制,默认 1 手。

离场逻辑

  • 止盈单触发后自动平仓。
  • 到达 FirstSessionStopHour(默认 21:00)或 SecondSessionStopHour(默认 23:00)时,强制市价平仓并撤销所有订单。
  • 如果仓位被外部平掉,策略会自动撤销尚未成交的止盈单。

参数说明

参数 默认值 说明
Volume 1 每笔订单使用的固定手数。
FirstSessionStartHour 8 第一交易时段开始的小时(0-23)。
FirstSessionStopHour 21 第一交易时段结束的小时。
SecondSessionStartHour 22 第二交易时段开始的小时,必须晚于第一时段开始。
SecondSessionStopHour 23 第二交易时段结束的小时,必须晚于第一时段结束。
StepPoints 5 距离最优价的止损挂单间隔(价格步长的倍数)。
TakeProfitPoints 40 入场价格到止盈价格的距离(价格步长的倍数)。
CandleType 1 分钟 用于驱动时间逻辑的 K 线类型。

参数在启动时会进行完整校验,防止时段重叠或非法的时间组合。

策略特性

  • 风格: 时段突破 / 时间驱动趋势跟随。
  • 方向: 做多和做空皆可。
  • 周期: 日内,根据时间触发(K线仅用于计时)。
  • 风险控制: 固定止盈 + 时段结束强制平仓(无显式止损)。
  • 适用市场: 连续交易的外汇、指数及其他高流动性品种。
  • 复杂度: 低,无技术指标计算。

实现细节

  • 策略必须能够获取 Security.PriceStep;若缺少价格步长或报价则不会下单。
  • 止盈单的数量优先使用实际成交量,其次为当前仓位,再次为配置的 Volume
  • 代码添加了英文注释,并使用 StockSharp 的高级 API(SubscribeCandlesSubscribeOrderBook 等)来实现与原始 MQL 程序等价的逻辑。
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// 21-hour session breakout strategy. Places simulated stop entries via candle breakout logic.
/// </summary>
public class TwentyOneHourSessionBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _firstSessionStartHour;
	private readonly StrategyParam<int> _firstSessionStopHour;
	private readonly StrategyParam<decimal> _stepPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _sessionOpen;
	private decimal _entryPrice;
	private bool _inSession;

	public int FirstSessionStartHour
	{
		get => _firstSessionStartHour.Value;
		set => _firstSessionStartHour.Value = value;
	}

	public int FirstSessionStopHour
	{
		get => _firstSessionStopHour.Value;
		set => _firstSessionStopHour.Value = value;
	}

	public decimal StepPoints
	{
		get => _stepPoints.Value;
		set => _stepPoints.Value = value;
	}

	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

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

	public TwentyOneHourSessionBreakoutStrategy()
	{
		_firstSessionStartHour = Param(nameof(FirstSessionStartHour), 2)
			.SetDisplay("Session Start", "Hour of the trading window start", "Schedule");

		_firstSessionStopHour = Param(nameof(FirstSessionStopHour), 20)
			.SetDisplay("Session Stop", "Hour of the trading window stop", "Schedule");

		_stepPoints = Param(nameof(StepPoints), 40m)
			.SetGreaterThanZero()
			.SetDisplay("Step Points", "Distance from session open to breakout level", "Orders");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 200m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit Points", "Take-profit distance", "Orders");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candles used to drive the trading schedule", "Data");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_sessionOpen = null;
		_entryPrice = 0m;
		_inSession = false;
	}

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

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

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

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

		var hour = candle.OpenTime.Hour;
		var priceStep = Security?.PriceStep ?? 1m;

		// Session start: record the open price
		if (hour >= FirstSessionStartHour && hour < FirstSessionStopHour)
		{
			if (!_inSession)
			{
				_sessionOpen = candle.OpenPrice;
				_inSession = true;
			}

			if (_sessionOpen == null)
				return;

			var stepOffset = StepPoints * priceStep;
			var buyLevel = _sessionOpen.Value + stepOffset;
			var sellLevel = _sessionOpen.Value - stepOffset;

			// Breakout entry
			if (Position == 0)
			{
				if (candle.HighPrice >= buyLevel)
				{
					BuyMarket();
					_entryPrice = buyLevel;
				}
				else if (candle.LowPrice <= sellLevel)
				{
					SellMarket();
					_entryPrice = sellLevel;
				}
			}

			// Take profit
			if (Position > 0)
			{
				var tp = _entryPrice + TakeProfitPoints * priceStep;
				if (candle.HighPrice >= tp)
				{
					SellMarket();
					_sessionOpen = candle.ClosePrice;
				}
			}
			else if (Position < 0)
			{
				var tp = _entryPrice - TakeProfitPoints * priceStep;
				if (candle.LowPrice <= tp)
				{
					BuyMarket();
					_sessionOpen = candle.ClosePrice;
				}
			}
		}
		else
		{
			// Session end: close position
			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();

			_inSession = false;
			_sessionOpen = null;
		}
	}
}