指数测试策略
概述
指数测试策略 是 MetaTrader 5 "Indices Tester" 专家顾问的移植版本。系统专注于指数的日内交易,在极短的时间窗口内寻找单次做多机会,并完全依赖时间过滤器和操作限制:
- 通过一个可配置的 K 线序列驱动内部时钟。
- 只有在设定的开始时间与结束时间之间才能开仓。
- 每个交易日允许的开仓次数有限,以避免重复入场。
- 到达指定的平仓时间后会立即平掉所有头寸。
- 策略仅执行多头交易,与原始 EA 的行为保持一致。
该实现使用 StockSharp 的高级 API,通过 SubscribeCandles 订阅 K 线,在 ProcessCandle 回调中执行交易决策。策略不依赖任何技术指标,整体逻辑简洁、专注于时段与风险控制。
交易逻辑
- 每日重置:策略追踪当前交易日,当检测到新的交易日时会重置计数器,重新允许当日的开仓次数。
- 入场窗口:只有当 K 线收盘时间严格位于
[SessionStart, SessionEnd)区间内时才会考虑开仓,对应原脚本中的TimeStart与TimeEnd判断。 - 仓位与交易限制:如果当日的开仓次数已达到
DailyTradeLimit,或当前持仓数量超过MaxOpenPositions,则忽略入场信号。 - 下单执行:当所有条件满足时,策略会以市价买入
TradeVolume数量的合约,并立即累加当日的交易次数。 - 强制离场:若 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);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class indices_tester_strategy(Strategy):
def __init__(self):
super(indices_tester_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._session_start = self.Param("SessionStart", TimeSpan(0, 0, 0))
self._session_end = self.Param("SessionEnd", TimeSpan(23, 0, 0))
self._close_time = self.Param("CloseTime", TimeSpan(23, 30, 0))
self._daily_trade_limit = self.Param("DailyTradeLimit", 1)
self._max_open_positions = self.Param("MaxOpenPositions", 1)
self._trade_volume = self.Param("TradeVolume", 0.1)
self._current_day = None
self._trades_opened_today = 0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def SessionStart(self):
return self._session_start.Value
@SessionStart.setter
def SessionStart(self, value):
self._session_start.Value = value
@property
def SessionEnd(self):
return self._session_end.Value
@SessionEnd.setter
def SessionEnd(self, value):
self._session_end.Value = value
@property
def CloseTime(self):
return self._close_time.Value
@CloseTime.setter
def CloseTime(self, value):
self._close_time.Value = value
@property
def DailyTradeLimit(self):
return self._daily_trade_limit.Value
@DailyTradeLimit.setter
def DailyTradeLimit(self, value):
self._daily_trade_limit.Value = value
@property
def MaxOpenPositions(self):
return self._max_open_positions.Value
@MaxOpenPositions.setter
def MaxOpenPositions(self, value):
self._max_open_positions.Value = value
@property
def TradeVolume(self):
return self._trade_volume.Value
@TradeVolume.setter
def TradeVolume(self, value):
self._trade_volume.Value = value
def OnReseted(self):
super(indices_tester_strategy, self).OnReseted()
self._current_day = None
self._trades_opened_today = 0
def OnStarted2(self, time):
super(indices_tester_strategy, self).OnStarted2(time)
self._current_day = None
self._trades_opened_today = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
candle_time = candle.CloseTime
candle_date = candle_time.Date
if self._current_day is None or self._current_day != candle_date:
self._current_day = candle_date
self._trades_opened_today = 0
time_of_day = candle_time.TimeOfDay
# Liquidate open positions once the configured close time is reached
if self.Position > 0 and time_of_day >= self.CloseTime:
self.SellMarket(self.Position)
return
# Only evaluate entries strictly inside the trading window
if time_of_day <= self.SessionStart or time_of_day >= self.SessionEnd:
return
# Respect the daily trade allowance
if self._trades_opened_today >= self.DailyTradeLimit:
return
# Skip if already have max positions
if self._get_open_position_count() >= self.MaxOpenPositions:
return
volume = self.TradeVolume
if volume <= 0:
return
# Long-only: buy
self.BuyMarket(volume)
self._trades_opened_today += 1
def _get_open_position_count(self):
if self.Position == 0:
return 0
volume = self.TradeVolume
if volume <= 0:
return 1
import math
return int(math.ceil(abs(float(self.Position)) / float(volume)))
def CreateClone(self):
return indices_tester_strategy()