Стратегия Indices Tester
Общее описание
Indices Tester — порт экспертного советника MetaTrader 5 с тем же названием. Стратегия ориентирована на дневную торговлю индексами и ищет одну возможность для входа в очень узком временном окне. Все решения принимаются на основе временных фильтров и операционных ограничений:
- Один настраиваемый поток свечей управляет внутренними событиями стратегии.
- Входы разрешены только между заданным временем начала и окончания сессии.
- Количество сделок в сутки ограничено, что предотвращает повторные входы.
- После наступления заданного времени закрытия все открытые позиции ликвидируются.
- Стратегия работает только в длинную сторону, полностью повторяя оригинальный алгоритм.
Реализация использует высокоуровневый API StockSharp: подписка на свечи производится через SubscribeCandles, а торговая логика размещена в обработчике ProcessCandle. Индикаторы не требуются, поэтому код остаётся компактным и сфокусированным на контроле времени и рисков.
Логика работы
- Суточный сброс — стратегия отслеживает текущую дату. При смене дня счётчики сбрасываются, позволяя снова совершить разрешённое число сделок.
- Окно входа — рассматриваются только свечи, чьё время закрытия строго попадает в интервал
[SessionStart, SessionEnd), что соответствует проверкам TimeStart и TimeEnd в исходном советнике.
- Ограничения по позициям — новая заявка не отправляется, если суточный лимит
DailyTradeLimit уже исчерпан или количество одновременно открытых позиций превышает MaxOpenPositions.
- Размещение ордера — при выполнении условий выставляется рыночная заявка на покупку объёмом
TradeVolume, после чего счётчик сделок увеличивается.
- Принудительное закрытие — если свеча закрылась позже
CloseTime, а позиция всё ещё открыта, выполняется рыночная продажа, что повторяет процедуру ClosePos() из MQL-версии.
В стандартной конфигурации стратегия делает одну попытку входа в сутки. При необходимости параметры позволяют увеличить частоту торгов и масштабировать позицию.
Параметры
| Название |
Описание |
CandleType |
Основной поток свечей, задающий временную сетку (по умолчанию 1 минута). |
SessionStart |
Время начала окна, в течение которого допускаются новые входы. |
SessionEnd |
Время окончания приёма новых сделок. |
CloseTime |
Момент закрытия всех оставшихся позиций. |
DailyTradeLimit |
Максимальное число сделок в сутки, после которого входы блокируются. |
MaxOpenPositions |
Максимальное количество одновременно открытых длинных позиций (в единицах объёма сделки). |
TradeVolume |
Объём рыночной заявки на каждую новую позицию. |
Особенности и отличия
- В StockSharp отсутствует прямой доступ к таблицам торговых сессий MetaTrader, поэтому контроль времени реализован через метки закрытия свечей и проверку
IsFormedAndOnlineAndAllowTrading().
- Вместо секундного таймера из MQL используется завершение свечи, что достаточно для минутных торговых окон и упрощает структуру кода.
- Счётчики сделок обнуляются при появлении первой свечи нового дня, поэтому важно, чтобы источник данных использовал правильное биржевое время.
Рекомендации по применению
- Убедитесь, что выбранный
CandleType соответствует торгуемому рынку, иначе временные фильтры могут не совпасть с фактической сессией.
- При необходимости увеличить количество попыток входа в течение дня повысьте значение
DailyTradeLimit.
- Значение
MaxOpenPositions лучше оставлять равным 1 для точного воспроизведения оригинального советника; увеличивайте его только при планируемом добавлении к позиции.
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()