在 GitHub 上查看

Time EA 策略

Time EA Strategy 将 MetaTrader 平台的 "TimeEA" 智能交易系统迁移到 StockSharp。策略完全依据时间控制单一方向的持仓:在设定时间开仓,保持多头或空头头寸,并在预设的离场时间或触发止损 / 止盈时退出。

该实现强调交易时段管理而非技术指标。在开仓前会先平掉相反方向的仓位,确保每天仅开仓一次,并强制执行最小保护距离,从而模拟券商对止损间距的限制。

工作流程

  1. 订阅可配置的蜡烛序列(默认 1 分钟),仅处理收盘完成的蜡烛。
  2. 当蜡烛的收盘时间跨过设定的 开仓时间 时:
    • 如果存在反向仓位,先行平仓。
    • 按照设定方向(买入或卖出)以给定手数市价开仓。
    • 依据输入的点数和最小距离倍数计算止损、止盈价格。
  3. 在持仓期间持续监控蜡烛:
    • 只要最低价 / 最高价触及记录的止损或止盈,立即平仓。
    • 当蜡烛覆盖到设定的 平仓时间 时,无条件平仓。
  4. 当天平仓后保持空仓状态,直到下一交易日再次触发开仓时间。

这种流程复刻了原始 MQL 版本中依赖 TimeCurrent()Time[0] 判断时间窗口的“每日一次”逻辑。

参数说明

参数 描述
Open Time 开仓时间(HH:MM:SS)。
Close Time 强制平仓时间,可跨越午夜。
Position Type 方向选择(BuySell)。
Order Volume 市价单数量。
Stop Loss (points) 止损点数(价格步长)。设为 0 表示不启用。
Take Profit (points) 止盈点数(价格步长)。设为 0 表示不启用。
Minimum Distance Multiplier 止损与止盈的最小间距(以价格步长计),模拟原策略中“点差 × 倍数”的约束。
Candle Type 用于检测时间窗口的蜡烛类型,默认分钟线。

实战提示

  • 每日仅开仓一次:开仓时间触发后,无论仓位是否提前止损,都会等到下一天才允许再次开仓。
  • 支持跨午夜时段:当开仓或平仓时间跨越 00:00 时,辅助函数会自动在隔日触发。
  • 手数管理:市价单数量来自 Order Volume,请根据品种合约大小调整。
  • 止损距离模拟:最小距离倍数确保止损 / 止盈至少偏离开仓价指定的价格步数,即使无法读取实时点差。
  • 数据要求:请使用与交易所时区一致的蜡烛数据,避免时间错位。
  • 风险控制:止损和止盈由策略内部跟踪并通过市价平仓完成,并不会在服务器端挂出联动委托。

适用场景

  • 需要在特定时间(如伦敦开盘或纽约开盘)自动进场的策略。
  • 方向预先确定、重视时间纪律的交易系统。
  • 希望在 StockSharp 高级 API 中实现 MetaTrader 风格的时间触发逻辑。

注意事项

  • 滑点由市价成交自然产生,未实现 MetaTrader 中的 Deviation 参数。
  • 最小距离倍数为静态值,不会根据实时点差变化。
  • 策略实例仅管理一个标的证券。

快速开始

  1. 在 Designer 或代码中设置开 / 平仓时间、方向、手数以及止损/止盈点数。
  2. 关联目标证券与数据源。
  3. 确认蜡烛时间轴与目标交易时区一致。
  4. 启动策略并监控成交日志,必要时可在图表上绘制蜡烛与交易记录。

完整的实现与英文注释位于 CS/TimeEaStrategy.cs 文件中,详细解释了每一步骤。

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 strategy that opens a single directional position at the configured time
/// and closes it at another time or when optional stop/target levels are hit.
/// </summary>
public class TimeEaStrategy : Strategy
{
	private readonly StrategyParam<TimeSpan> _openTime;
	private readonly StrategyParam<TimeSpan> _closeTime;
	private readonly StrategyParam<TimeEaPositionTypes> _openedType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _minSpreadMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private DateTime? _lastEntryDate;
	private DateTime? _lastCloseDate;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;

	/// <summary>
	/// Time of day to open the position.
	/// </summary>
	public TimeSpan OpenTime
	{
		get => _openTime.Value;
		set => _openTime.Value = value;
	}

	/// <summary>
	/// Time of day to close the position.
	/// </summary>
	public TimeSpan CloseTime
	{
		get => _closeTime.Value;
		set => _closeTime.Value = value;
	}

	/// <summary>
	/// Direction of the position opened at the scheduled time.
	/// </summary>
	public TimeEaPositionTypes OpenedType
	{
		get => _openedType.Value;
		set => _openedType.Value = value;
	}

	/// <summary>
	/// Market order volume for opening trades.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Stop loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Minimal distance multiplier applied to stops and targets.
	/// </summary>
	public int MinSpreadMultiplier
	{
		get => _minSpreadMultiplier.Value;
		set => _minSpreadMultiplier.Value = value;
	}

	/// <summary>
	/// Candle type used to evaluate time windows.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="TimeEaStrategy"/>.
	/// </summary>
	public TimeEaStrategy()
	{
		_openTime = Param(nameof(OpenTime), new TimeSpan(1, 0, 0))
			.SetDisplay("Open Time", "Time to enter the market", "Scheduling");

		_closeTime = Param(nameof(CloseTime), TimeSpan.Zero)
			.SetDisplay("Close Time", "Time to exit the market", "Scheduling");

		_openedType = Param(nameof(OpenedType), TimeEaPositionTypes.Buy)
			.SetDisplay("Position Type", "Direction to maintain", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Quantity for market orders", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance in price steps", "Risk");

		_minSpreadMultiplier = Param(nameof(MinSpreadMultiplier), 2)
			.SetNotNegative()
			.SetDisplay("Minimum Distance Multiplier", "Minimal offset applied to stops", "Risk");

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

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

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

		_lastEntryDate = null;
		_lastCloseDate = null;
		ResetRiskLevels();
	}

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Use finished candles to evaluate the time windows.
		if (candle.State != CandleStates.Finished)
			return;

		var candleDate = candle.CloseTime.Date;

		if (ContainsTime(candle, OpenTime) && _lastEntryDate != candleDate)
		{
			_lastEntryDate = candleDate;
			HandleOpen(candle);
		}

		if (ContainsTime(candle, CloseTime) && _lastCloseDate != candleDate)
		{
			_lastCloseDate = candleDate;

			if (Position != 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
			return;
		}

		ManageRisk(candle);
	}

	private void HandleOpen(ICandleMessage candle)
	{
		// Close opposite exposure before opening a new position.
		if (OpenedType == TimeEaPositionTypes.Buy)
		{
			if (Position < 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}

			if (Position == 0 && OrderVolume > 0)
			{
				BuyMarket(OrderVolume);
				SetRiskLevels(candle.ClosePrice, true);
			}
		}
		else
		{
			if (Position > 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}

			if (Position == 0 && OrderVolume > 0)
			{
				SellMarket(OrderVolume);
				SetRiskLevels(candle.ClosePrice, false);
			}
		}
	}

	private void ManageRisk(ICandleMessage candle)
	{
		// Monitor active position for stop loss and take profit.
		if (Position > 0)
		{
			if (_stopPrice > 0m && candle.LowPrice <= _stopPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice > 0m && candle.HighPrice >= _stopPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
		}
	}

	private void SetRiskLevels(decimal closePrice, bool isLong)
	{
		_entryPrice = closePrice;

		var step = Security?.PriceStep ?? 1m;
		var minDistance = Math.Max(MinSpreadMultiplier, 0) * step;
		var stopDistance = StopLossPoints > 0 ? Math.Max(StopLossPoints * step, minDistance) : 0m;
		var takeDistance = TakeProfitPoints > 0 ? Math.Max(TakeProfitPoints * step, minDistance) : 0m;

		// Calculate price levels in the same direction logic as the original Expert Advisor.
		if (isLong)
		{
			_stopPrice = stopDistance > 0m ? closePrice - stopDistance : 0m;
			_takeProfitPrice = takeDistance > 0m ? closePrice + takeDistance : 0m;
		}
		else
		{
			_stopPrice = stopDistance > 0m ? closePrice + stopDistance : 0m;
			_takeProfitPrice = takeDistance > 0m ? closePrice - takeDistance : 0m;
		}
	}

	private void ResetRiskLevels()
	{
		_entryPrice = 0m;
		_stopPrice = 0m;
		_takeProfitPrice = 0m;
	}

	private static bool ContainsTime(ICandleMessage candle, TimeSpan target)
	{
		var openTime = candle.OpenTime;
		var closeTime = candle.CloseTime;

		var openSpan = openTime.TimeOfDay;
		var closeSpan = closeTime.TimeOfDay;

		var crossesMidnight = closeTime.Date > openTime.Date || closeSpan < openSpan;

		if (!crossesMidnight)
			return target >= openSpan && target <= closeSpan;

		var startMinutes = openSpan.TotalMinutes;
		var endMinutes = closeSpan.TotalMinutes + TimeSpan.FromDays(1).TotalMinutes;
		var targetMinutes = target.TotalMinutes;

		if (targetMinutes < startMinutes)
			targetMinutes += TimeSpan.FromDays(1).TotalMinutes;

		return targetMinutes >= startMinutes && targetMinutes <= endMinutes;
	}

	/// <summary>
	/// Supported position directions.
	/// </summary>
	public enum TimeEaPositionTypes
	{
		/// <summary>
		/// Open a long position.
		/// </summary>
		Buy,

		/// <summary>
		/// Open a short position.
		/// </summary>
		Sell
	}
}