在 GitHub 上查看

指数测试策略

概述

指数测试策略 是 MetaTrader 5 "Indices Tester" 专家顾问的移植版本。系统专注于指数的日内交易,在极短的时间窗口内寻找单次做多机会,并完全依赖时间过滤器和操作限制:

  • 通过一个可配置的 K 线序列驱动内部时钟。
  • 只有在设定的开始时间与结束时间之间才能开仓。
  • 每个交易日允许的开仓次数有限,以避免重复入场。
  • 到达指定的平仓时间后会立即平掉所有头寸。
  • 策略仅执行多头交易,与原始 EA 的行为保持一致。

该实现使用 StockSharp 的高级 API,通过 SubscribeCandles 订阅 K 线,在 ProcessCandle 回调中执行交易决策。策略不依赖任何技术指标,整体逻辑简洁、专注于时段与风险控制。

交易逻辑

  1. 每日重置:策略追踪当前交易日,当检测到新的交易日时会重置计数器,重新允许当日的开仓次数。
  2. 入场窗口:只有当 K 线收盘时间严格位于 [SessionStart, SessionEnd) 区间内时才会考虑开仓,对应原脚本中的 TimeStartTimeEnd 判断。
  3. 仓位与交易限制:如果当日的开仓次数已达到 DailyTradeLimit,或当前持仓数量超过 MaxOpenPositions,则忽略入场信号。
  4. 下单执行:当所有条件满足时,策略会以市价买入 TradeVolume 数量的合约,并立即累加当日的交易次数。
  5. 强制离场:若 K 线收盘时间晚于 CloseTime 且仍有多头持仓,将通过市价卖出全部持仓,对应原脚本中的定时平仓逻辑。

默认参数下策略每天只会尝试一次入场。通过调整参数可以让策略在更短周期内运行并增加交易频率。

参数说明

名称 说明
CandleType 驱动策略的主要 K 线周期(默认 1 分钟)。
SessionStart 允许开仓的起始时间。
SessionEnd 停止开仓的截止时间。
CloseTime 强制平掉所有持仓的时间。
DailyTradeLimit 每个交易日允许的最大开仓次数。
MaxOpenPositions 同时允许存在的最大多头持仓数量(按单笔交易数量计)。
TradeVolume 每次市价单使用的下单数量。

注意事项与差异

  • StockSharp 不提供 MetaTrader 的交易时段表,因此本移植通过 K 线时间戳与 IsFormedAndOnlineAndAllowTrading() 的检查组合来控制时段。
  • 原脚本使用秒级定时器,本实现通过 K 线收盘事件来驱动入场与强制平仓,对于分钟级别的窗口来说已经足够精确。
  • 交易计数在每个交易日的首根 K 线时重置,只要行情源的时间与目标交易所匹配即可保持行为一致。

使用建议

  • 请确保所选 CandleType 与目标市场匹配,使时间过滤器与真实交易时段对齐。
  • 若希望在同一天内多次尝试入场,可适当提高 DailyTradeLimit
  • MaxOpenPositions 默认值为 1,可保持与原始 EA 完全一致;仅在需要分批加仓时才建议提高该值。
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>
/// Port of the MetaTrader expert advisor "Indices Tester".
/// Implements a time filtered long-only session with daily trade and position limits.
/// </summary>
public class IndicesTesterStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<TimeSpan> _sessionStart;
	private readonly StrategyParam<TimeSpan> _sessionEnd;
	private readonly StrategyParam<TimeSpan> _closeTime;
	private readonly StrategyParam<int> _dailyTradeLimit;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<decimal> _tradeVolume;

	private DateTime _currentDay;
	private int _tradesOpenedToday;

	/// <summary>
	/// Candle type used to drive the strategy clock.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Session start time when new long positions may be opened.
	/// </summary>
	public TimeSpan SessionStart
	{
		get => _sessionStart.Value;
		set => _sessionStart.Value = value;
	}

	/// <summary>
	/// Session end time after which new positions are not allowed.
	/// </summary>
	public TimeSpan SessionEnd
	{
		get => _sessionEnd.Value;
		set => _sessionEnd.Value = value;
	}

	/// <summary>
	/// Time of day when all active positions are closed.
	/// </summary>
	public TimeSpan CloseTime
	{
		get => _closeTime.Value;
		set => _closeTime.Value = value;
	}

	/// <summary>
	/// Maximum number of entries that can be opened during a single trading day.
	/// </summary>
	public int DailyTradeLimit
	{
		get => _dailyTradeLimit.Value;
		set => _dailyTradeLimit.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous long positions measured in trade units.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Order volume submitted with every market entry.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="IndicesTesterStrategy"/> class.
	/// </summary>
	public IndicesTesterStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe that drives the logic", "General");

		_sessionStart = Param(nameof(SessionStart), new TimeSpan(0, 0, 0))
			.SetDisplay("Session Start", "Time of day when entries become eligible", "Trading");

		_sessionEnd = Param(nameof(SessionEnd), new TimeSpan(23, 0, 0))
			.SetDisplay("Session End", "Time of day when new entries stop", "Trading");

		_closeTime = Param(nameof(CloseTime), new TimeSpan(23, 30, 0))
			.SetDisplay("Close Time", "Time of day used to liquidate open positions", "Risk");

		_dailyTradeLimit = Param(nameof(DailyTradeLimit), 1)
			.SetGreaterThanZero()
			.SetDisplay("Daily Trades", "Maximum number of trades per day", "Risk");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
			.SetGreaterThanZero()
			.SetDisplay("Open Positions", "Maximum simultaneous long positions", "Risk");

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Market order volume for new positions", "Trading");
	}

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

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

		_currentDay = default;
		_tradesOpenedToday = 0;
	}

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

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore unfinished candles because the original EA worked on closed data.
		if (candle.State != CandleStates.Finished)
			return;

		var candleTime = candle.CloseTime;
		if (_currentDay != candleTime.Date)
		{
			// Reset the intraday counters on the first candle of a new session.
			_currentDay = candleTime.Date;
			_tradesOpenedToday = 0;
		}

		var timeOfDay = candleTime.TimeOfDay;

		// Liquidate open positions once the configured close time is reached.
		if (Position > 0m && timeOfDay >= CloseTime)
		{
			SellMarket(Position);
			return;
		}

		// Only evaluate entries strictly inside the trading window.
		if (timeOfDay <= SessionStart || timeOfDay >= SessionEnd)
			return;

		// Respect the daily trade allowance taken from the original EA.
		if (_tradesOpenedToday >= DailyTradeLimit)
			return;

		// Skip entries when the simultaneous position limit would be exceeded.
		if (GetOpenPositionCount() >= MaxOpenPositions)
			return;

		var volume = TradeVolume;
		if (volume <= 0m)
			return;

		// Submit the market order and immediately update the per-day trade counter.
		BuyMarket(volume);
		_tradesOpenedToday++;
	}

	private int GetOpenPositionCount()
	{
		if (Position == 0m)
			return 0;

		var volume = TradeVolume;
		if (volume <= 0m)
			return 1;

		return (int)Math.Ceiling(Math.Abs(Position) / volume);
	}
}