在 GitHub 上查看

定时开仓策略

概述

该策略重现了 MetaTrader 专家顾问 “OpenTime” 的核心逻辑。它会在每天指定的时间窗口内提交市价单,在单独的平仓窗口中选择性地退出全部仓位,并通过固定止损、止盈以及追踪止损等简易风控规则保护资金。移植版本完全基于 StockSharp 的高级 Strategy API,可方便地与平台内的其他组件组合使用。

工作流程

  1. 订阅所选周期的 K 线后,每根收盘的蜡烛都会触发一次时间检查。
  2. 当当前时间处于交易窗口时,策略会针对每个启用的方向提交市价单:
    • 如果只启用单向交易,策略会在保持原有净头寸的同时扩仓或反向开仓,直到达到设定的目标数量。
    • 如果同时启用多空方向,买单与卖单会在同一窗口内先后发送。由于 StockSharp 按净头寸记账,第二个方向的下单会先抵消已有敞口,然后再建立新的头寸。
  3. 当平仓窗口激活时,策略只调用一次 ClosePosition() 来平掉所有剩余仓位。
  4. 止损、止盈与追踪止损距离交由 StartProtection 统一处理,该方法会自动通过市价单管理保护性退出。

参数说明

  • Enable Close Window —— 对应原脚本的 TimeClose 选项。启用后,Close Position TimeWindow Length 定义何时强制平仓。
  • Close Position Time —— 每日开始平仓窗口的时间(默认 20:50)。
  • Trading Time —— 允许开仓的每日时间(默认 18:50)。
  • Window Length —— 交易窗口与平仓窗口的持续时间(默认 5 分钟,对应 MQL 的 Duration)。
  • Allow Sell Entries —— 对应 MQL 参数 Sell,启用做空(默认 true)。
  • Allow Buy Entries —— 对应 MQL 参数 Buy,启用做多(默认 false)。
  • Order Volume —— 每次信号的目标净仓量(默认 0.1 手)。当出现反向信号时会自动加上现有仓位的绝对值,实现一步反向。
  • Stop-Loss Points —— 止损距离(点),0 表示关闭止损。
  • Take-Profit Points —— 止盈距离(点),0 表示关闭止盈。
  • Use Trailing Stop —— 是否启用追踪止损,对应原版的 SimpleTrailing 功能。
  • Trailing Stop Points —— 追踪止损的基础距离(点,默认 300)。
  • Trailing Step Points —— 在调整追踪止损前所需的额外盈利距离(点,默认 3)。
  • Candle Type —— 用于时间判断的 K 线周期(默认 1 分钟)。

其他说明

  • 点值根据品种的最小报价步长计算。对于 3 位或 5 位小数报价,会额外乘以 10,以匹配原脚本中的 pip 处理方式。
  • 只有在至少一个距离大于 0 时才会调用 StartProtection。若只启用追踪止损而没有固定止损,则会将追踪距离作为初始保护值传入。
  • 策略不再实现原脚本中的多次下单重试逻辑,因为 StockSharp 对市价单已经具备完整的错误重试机制。
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Open Time Daily Window: trades during a specific time window using
/// EMA direction for entry. Closes position at the end of window.
/// </summary>
public class OpenTimeDailyWindowStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _emaLength;
	private readonly StrategyParam<int> _tradeHour;
	private readonly StrategyParam<int> _windowMinutes;
	private readonly StrategyParam<int> _closeHour;

	private decimal _prevEma;
	private decimal _entryPrice;

	public OpenTimeDailyWindowStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_emaLength = Param(nameof(EmaLength), 20)
			.SetDisplay("EMA Length", "EMA period for direction.", "Indicators");

		_tradeHour = Param(nameof(TradeHour), 10)
			.SetDisplay("Trade Hour", "Hour when trading window opens (UTC).", "Schedule");

		_windowMinutes = Param(nameof(WindowMinutes), 120)
			.SetDisplay("Window Minutes", "Duration of trading window.", "Schedule");

		_closeHour = Param(nameof(CloseHour), 20)
			.SetDisplay("Close Hour", "Hour to close positions (UTC).", "Schedule");
	}

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

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

	public int TradeHour
	{
		get => _tradeHour.Value;
		set => _tradeHour.Value = value;
	}

	public int WindowMinutes
	{
		get => _windowMinutes.Value;
		set => _windowMinutes.Value = value;
	}

	public int CloseHour
	{
		get => _closeHour.Value;
		set => _closeHour.Value = value;
	}

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

		_prevEma = 0;
		_entryPrice = 0;
	}

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

		var ema = new ExponentialMovingAverage { Length = EmaLength };

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

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

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

		var hour = candle.OpenTime.Hour;
		var minute = candle.OpenTime.Minute;
		var totalMinutes = hour * 60 + minute;
		var tradeStart = TradeHour * 60;
		var tradeEnd = tradeStart + WindowMinutes;
		var closeStart = CloseHour * 60;

		var close = candle.ClosePrice;

		// Close position at close hour
		if (Position != 0 && totalMinutes >= closeStart && totalMinutes < closeStart + 30)
		{
			if (Position > 0)
				SellMarket();
			else
				BuyMarket();
			_entryPrice = 0;
		}

		if (_prevEma == 0)
		{
			_prevEma = emaVal;
			return;
		}

		// Trade within window
		var inWindow = totalMinutes >= tradeStart && totalMinutes < tradeEnd;

		if (Position == 0 && inWindow)
		{
			var emaRising = emaVal > _prevEma;
			var emaFalling = emaVal < _prevEma;

			if (emaRising && close > emaVal)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (emaFalling && close < emaVal)
			{
				_entryPrice = close;
				SellMarket();
			}
		}

		_prevEma = emaVal;
	}
}