在 GitHub 上查看

开盘时间策略

概述

开盘时间策略是基于时间计划的交易系统,移植自 MetaTrader 5 专家顾问 OpenTime。策略在每根完成的K线后检查市场时间,只在可配置的开仓时间窗口内下单。它可以在单独的平仓时间窗口中关闭已有仓位,支持可选的移动止损,并按照点数执行初始止损和止盈。

与原始 MT5 对冲版本不同,本移植在净持仓账户上运行:如果新的信号与当前持仓方向相反,策略会先平掉相反方向,再用设定的手数开入目标方向。

交易流程

  1. 平仓窗口 – 勾选“使用平仓窗口”后,只要当前时间落在平仓窗口内,策略立即关闭所有持仓,在该时间段内不会产生新的交易。
  2. 移动止损更新 – 如果启用移动止损且价格相对于持仓方向至少前进了 TrailingStop + TrailingStep 个点,则将止损价向价格靠近 TrailingStop 个点,复现 MT5 中仅在达到最小步长后才移动止损的逻辑。
  3. 风险检查 – 在每根收盘K线上检查是否触及止损或止盈阈值。一旦触发,对应方向的仓位立即平掉,并清空内部状态。
  4. 开仓窗口 – 当前时间位于开仓窗口内时评估入场条件:
    • 若允许做多且当前净头寸为空或为空头,买入设定的手数,并补齐所有空头以实现反手。
    • 若允许做空且当前净头寸为空或为多头,卖出设定手数,并补齐所有多头以实现反手。

每次入场都会记录成交价以及止损、止盈距离(当它们非零时),这些值随后用于移动止损和风险管理判定。

参数

参数 默认值 说明
Candle Type 1 分钟K线 用于时间追踪的数据类型,仅在K线收盘时执行逻辑。
Use Close Window true 是否启用自动平仓窗口。
Close Hour / Close Minute 20:50 平仓窗口起始时间,小时支持 0–24,24 表示次日 0 点。
Enable Trailing false 是否启用移动止损。
Trailing Stop 30 点 移动止损与价格之间的距离,会根据合约最小报价单位转换为价格。
Trailing Step 3 点 再次移动止损前价格需要额外运行的点数。
Trade Hour / Trade Minute 18:50 允许开仓的时间窗口起始。
Duration 300 秒 开仓和平仓窗口共用的持续时间。
Enable Sell / Enable Buy Sell = true, Buy = false 允许交易的方向。
Volume 0.1 每次新开仓的手数。反手时会自动加上对冲原仓位所需的数量。
Stop Loss 0 点 初始止损距离,0 表示禁用固定止损,只依赖移动止损或时间窗口。
Take Profit 0 点 初始止盈距离,0 表示禁用固定止盈。

实现细节

  • 根据 Security.PriceStep 计算点值,对于三位或五位小数报价的品种会额外乘以 10,以复现 MT5 中的“点”换算。
  • 移动止损与固定止损/止盈均使用 K 线最高价与最低价判断,借此在高阶API中模拟逐笔触发效果。
  • 每次平仓后都会重置内部记录,避免在下一笔交易中沿用旧的止损、止盈或入场价格。
  • StockSharp 默认采用净持仓模型,因此无法同时持有多空仓。反手时会先平掉相反头寸,再开入目标方向,以模拟 MT5 的对冲效果。

使用提示

  • 选择与时间控制粒度匹配的K线类型,1分钟K线能提供更精确的时间判定。
  • 开仓和平仓窗口共用同一个持续时间参数。如需关闭其中某个窗口,可将持续时间设为0或关闭“使用平仓窗口”。
  • 移动止损只有在价格相对于入场价至少走出 Trailing Stop + Trailing Step 点后才会开始移动,与原策略的逻辑保持一致。
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>
/// Time based opening strategy with optional trailing stop logic.
/// </summary>
public class OpenTimeStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _useCloseTime;
	private readonly StrategyParam<int> _closeHour;
	private readonly StrategyParam<int> _closeMinute;
	private readonly StrategyParam<bool> _enableTrailing;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _tradeHour;
	private readonly StrategyParam<int> _tradeMinute;
	private readonly StrategyParam<int> _durationSeconds;
	private readonly StrategyParam<bool> _enableSell;
	private readonly StrategyParam<bool> _enableBuy;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;

	private decimal _pipSize;
	private decimal _stopOffset;
	private decimal _takeOffset;
	private decimal _trailOffset;
	private decimal _trailStep;

	private decimal? _longEntry;
	private decimal? _shortEntry;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Initializes a new instance of the <see cref="OpenTimeStrategy"/> class.
	/// </summary>
	public OpenTimeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle subscription type", "General");
		_useCloseTime = Param(nameof(UseCloseTime), true)
			.SetDisplay("Use Close Window", "Enable automatic closing window", "Trading");
		_closeHour = Param(nameof(CloseHour), 20)
			.SetDisplay("Close Hour", "Hour for the closing window", "Trading");
		_closeMinute = Param(nameof(CloseMinute), 50)
			.SetDisplay("Close Minute", "Minute for the closing window", "Trading");
		_enableTrailing = Param(nameof(EnableTrailing), false)
			.SetDisplay("Enable Trailing", "Use trailing stop logic", "Risk");
		_trailingStopPips = Param(nameof(TrailingStopPips), 30)
			.SetDisplay("Trailing Stop", "Trailing stop distance in pips", "Risk");
		_trailingStepPips = Param(nameof(TrailingStepPips), 3)
			.SetDisplay("Trailing Step", "Additional move required to shift the trail", "Risk");
		_tradeHour = Param(nameof(TradeHour), 10)
			.SetDisplay("Trade Hour", "Hour to start opening positions", "Trading");
		_tradeMinute = Param(nameof(TradeMinute), 0)
			.SetDisplay("Trade Minute", "Minute to start opening positions", "Trading");
		_durationSeconds = Param(nameof(DurationSeconds), 18000)
			.SetDisplay("Duration", "Window length in seconds", "Trading");
		_enableSell = Param(nameof(EnableSell), true)
			.SetDisplay("Enable Sell", "Allow short entries", "Trading");
		_enableBuy = Param(nameof(EnableBuy), true)
			.SetDisplay("Enable Buy", "Allow long entries", "Trading");
		_stopLossPips = Param(nameof(StopLossPips), 500)
			.SetDisplay("Stop Loss", "Initial stop loss in pips", "Risk");
		_takeProfitPips = Param(nameof(TakeProfitPips), 1000)
			.SetDisplay("Take Profit", "Initial take profit in pips", "Risk");
	}

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

	/// <summary>
	/// Indicates whether automatic closing window is enabled.
	/// </summary>
	public bool UseCloseTime
	{
		get => _useCloseTime.Value;
		set => _useCloseTime.Value = value;
	}

	/// <summary>
	/// Hour of the closing window (0-24).
	/// </summary>
	public int CloseHour
	{
		get => _closeHour.Value;
		set => _closeHour.Value = value;
	}

	/// <summary>
	/// Minute of the closing window (0-59).
	/// </summary>
	public int CloseMinute
	{
		get => _closeMinute.Value;
		set => _closeMinute.Value = value;
	}

	/// <summary>
	/// Enables trailing stop logic.
	/// </summary>
	public bool EnableTrailing
	{
		get => _enableTrailing.Value;
		set => _enableTrailing.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Additional movement required to move the trailing stop.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Hour when the strategy can open positions.
	/// </summary>
	public int TradeHour
	{
		get => _tradeHour.Value;
		set => _tradeHour.Value = value;
	}

	/// <summary>
	/// Minute when the strategy can open positions.
	/// </summary>
	public int TradeMinute
	{
		get => _tradeMinute.Value;
		set => _tradeMinute.Value = value;
	}

	/// <summary>
	/// Duration of the trading window in seconds.
	/// </summary>
	public int DurationSeconds
	{
		get => _durationSeconds.Value;
		set => _durationSeconds.Value = value;
	}

	/// <summary>
	/// Enables short entries.
	/// </summary>
	public bool EnableSell
	{
		get => _enableSell.Value;
		set => _enableSell.Value = value;
	}

	/// <summary>
	/// Enables long entries.
	/// </summary>
	public bool EnableBuy
	{
		get => _enableBuy.Value;
		set => _enableBuy.Value = value;
	}


	/// <summary>
	/// Initial stop loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Initial take profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

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

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

		_pipSize = 0m;
		_stopOffset = 0m;
		_takeOffset = 0m;
		_trailOffset = 0m;
		_trailStep = 0m;

		ResetLongState();
		ResetShortState();
	}

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

		// Convert pip-based inputs to absolute price offsets.
		_pipSize = CalculatePipSize();
		_stopOffset = StopLossPips * _pipSize;
		_takeOffset = TakeProfitPips * _pipSize;
		_trailOffset = TrailingStopPips * _pipSize;
		_trailStep = TrailingStepPips * _pipSize;

		// Subscribe to candle data used for time-based processing.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

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

		// no protection
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Work only with finished candles to avoid premature actions.
		if (candle.State != CandleStates.Finished)
			return;

		var now = candle.CloseTime;

		// Force-close any position during the configured closing window.
		if (UseCloseTime && IsWithinWindow(now, CloseHour, CloseMinute, DurationSeconds))
		{
			CloseActivePositions();
			return;
		}

		// Update trailing stops and exit if risk limits were exceeded.
		UpdateTrailingStops(candle);
		CheckRiskManagement(candle);

		// no bound indicators to check

		// Skip entries outside the trading window.
		if (!IsWithinWindow(now, TradeHour, TradeMinute, DurationSeconds))
			return;

		// Open or reverse long positions when buying is enabled.
		if (EnableBuy && Position <= 0)
		{
			if (Position < 0)
			{
				BuyMarket();
				ResetShortState();
			}

			BuyMarket();
			InitializeLongState(candle.ClosePrice);
		}
		else if (EnableSell && Position >= 0)
		{
			if (Position > 0)
			{
				SellMarket();
				ResetLongState();
			}

			SellMarket();
			InitializeShortState(candle.ClosePrice);
		}
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (!EnableTrailing || _trailOffset <= 0m)
			return;

		// Move the trailing stop for long positions once the minimal step is reached.
		if (Position > 0 && _longEntry.HasValue)
		{
			var distance = candle.ClosePrice - _longEntry.Value;
			if (distance > _trailOffset + _trailStep)
			{
				var triggerLevel = candle.ClosePrice - (_trailOffset + _trailStep);
				if (!_longStop.HasValue || _longStop.Value < triggerLevel)
					_longStop = candle.ClosePrice - _trailOffset;
			}
		}
		// Move the trailing stop for short positions in a symmetrical way.
		else if (Position < 0 && _shortEntry.HasValue)
		{
			var distance = _shortEntry.Value - candle.ClosePrice;
			if (distance > _trailOffset + _trailStep)
			{
				var triggerLevel = candle.ClosePrice + (_trailOffset + _trailStep);
				if (!_shortStop.HasValue || _shortStop.Value > triggerLevel)
					_shortStop = candle.ClosePrice + _trailOffset;
			}
		}
	}

	private void CheckRiskManagement(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				SellMarket();
				ResetLongState();
				return;
			}

			if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
			{
				SellMarket();
				ResetLongState();
			}
		}
		else if (Position < 0)
		{
			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				BuyMarket();
				ResetShortState();
				return;
			}

			if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
			{
				BuyMarket();
				ResetShortState();
			}
		}
	}

	private void CloseActivePositions()
	{
		// Flatten the portfolio and clear cached levels.
		if (Position > 0)
		{
			SellMarket();
			ResetLongState();
		}
		else if (Position < 0)
		{
			BuyMarket();
			ResetShortState();
		}
	}

	private void InitializeLongState(decimal price)
	{
		// Remember entry price and derived risk levels for long trades.
		_longEntry = price;
		_longStop = StopLossPips > 0 ? price - _stopOffset : null;
		_longTake = TakeProfitPips > 0 ? price + _takeOffset : null;
	}

	private void InitializeShortState(decimal price)
	{
		// Remember entry price and derived risk levels for short trades.
		_shortEntry = price;
		_shortStop = StopLossPips > 0 ? price + _stopOffset : null;
		_shortTake = TakeProfitPips > 0 ? price - _takeOffset : null;
	}

	private void ResetLongState()
	{
		_longEntry = null;
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortState()
	{
		_shortEntry = null;
		_shortStop = null;
		_shortTake = null;
	}

	private decimal CalculatePipSize()
	{
		// Convert MT5-style pip values into absolute price units.
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			return 1m;

		var decimals = CountDecimals(step);
		var multiplier = decimals == 3 || decimals == 5
			? 10m
			: 1m;
		return step * multiplier;
	}

	private static int CountDecimals(decimal value)
	{
		// Count decimal places by repeatedly shifting the decimal point.
		value = Math.Abs(value);
		var decimals = 0;
		while (value != Math.Truncate(value) && decimals < 10)
		{
			value *= 10m;
			decimals++;
		}
		return decimals;
	}

	private static bool IsWithinWindow(DateTimeOffset time, int hour, int minute, int durationSeconds)
	{
		if (durationSeconds <= 0)
			return false;

		var start = BuildReferenceTime(time, hour, minute);
		var end = start.AddSeconds(durationSeconds);
		return time >= start && time < end;
	}

	private static DateTimeOffset BuildReferenceTime(DateTimeOffset reference, int hour, int minute)
	{
		// Align the target time with the current trading day, allowing hour values above 23.
		var normalizedHour = hour;
		var day = new DateTimeOffset(reference.Year, reference.Month, reference.Day, 0, 0, 0, reference.Offset);

		while (normalizedHour >= 24)
		{
			normalizedHour -= 24;
			day = day.AddDays(1);
		}

		if (minute < 0)
			minute = 0;
		else if (minute > 59)
			minute = 59;

		return day.AddHours(normalizedHour).AddMinutes(minute);
	}
}