Стратегия Time EA
Time EA Strategy — портирование советника MetaTrader "TimeEA" в экосистему StockSharp. Стратегия управляет одной позицией, ориентируясь исключительно на время суток: открывает сделку в заданный момент, удерживает её в фиксированном направлении и закрывает по расписанию или при срабатывании опциональных стоп-лосса и тейк-профита.
В отличие от индикаторных систем, акцент сделан на дисциплине торговой сессии. Перед входом стратегия закрывает противоположную позицию, гарантирует только одно открытие в день и поддерживает минимальную дистанцию защитных ордеров, имитируя проверку ограничений брокера по стоп-уровням.
Как работает
- Стратегия подписывается на выбранный поток свечей (по умолчанию 1 минута) и обрабатывает только завершённые свечи.
- Когда закрытие свечи пересекает заданное время открытия, выполняются действия:
- Закрывается противоположная позиция, если она ещё активна.
- Размещается рыночный ордер в выбранном направлении (Buy или Sell) с заданным объёмом.
- Фиксируются уровни стоп-лосса и тейк-профита в пунктах (шаг цены) от цены входа с учётом минимального отступа.
- В течение дня стратегия мониторит цены:
- Если свеча пробивает стоп-лосс или тейк-профит, позиция закрывается немедленно.
- Если свеча попадает во время закрытия, позиция ликвидируется вне зависимости от результата.
- После закрытия сделка не переоткрывается до следующего календарного дня, что воспроизводит поведение оригинального советника.
Параметры
| Параметр |
Описание |
| Open Time |
Время открытия позиции (ЧЧ:ММ:СС). |
| Close Time |
Время принудительного закрытия. Может быть в тот же день или после полуночи. |
| Position Type |
Направление позиции (Buy или Sell). |
| Order Volume |
Объём рыночного ордера. |
| Stop Loss (points) |
Дистанция стоп-лосса в шагах цены. 0 отключает стоп. |
| Take Profit (points) |
Дистанция тейк-профита в шагах цены. 0 отключает цель. |
| Minimum Distance Multiplier |
Минимальный отступ для стопа и тейка (в шагах цены), аналог множителя спреда в MQL версии. |
| Candle Type |
Тип свечей, используемых для расчёта времени (по умолчанию минутные). |
Практические заметки
- Одно открытие в день. После срабатывания времени входа позиция не переоткроется до следующего дня даже при раннем закрытии по стопу.
- Поддержка ночных сессий. Логика корректно обрабатывает расписания, пересекающие полночь.
- Управление объёмом. Объём ордера берётся из параметра
Order Volume; подберите значение под спецификацию инструмента.
- Эмуляция стоп-уровней. Минимальный отступ заставляет стопы и цели находиться не ближе заданного количества пунктов от входа, компенсируя отсутствие динамического спреда.
- Требования к данным. Используйте свечи в часовом поясе биржи, чтобы временные условия совпадали с ожидаемым расписанием.
- Управление риском. Стоп и тейк исполняются программно: при достижении уровней стратегия посылает рыночный ордер на закрытие.
Когда применять
- Для автоматизации входа в строго определённое время (например, открытие Лондона или Нью-Йорка).
- Когда направление заранее известно, но требуется жёсткое соблюдение расписания.
- Для переноса советников MetaTrader, основанных на времени, в StockSharp без низкоуровневого кода.
Ограничения
- Проскальзывание контролируется только рынком: параметра
Deviation, как в MetaTrader, нет.
- Минимальный отступ задаётся статически и не зависит от текущего спреда.
- Каждая стратегия управляет лишь одним инструментом.
Быстрый старт
- Настройте параметры (время открытия/закрытия, направление, объём, защитные дистанции) в Designer или в коде.
- Привяжите стратегию к нужному инструменту и источнику данных.
- Убедитесь, что временная зона свечей совпадает с планируемым расписанием.
- Запустите стратегию и контролируйте журнал сделок; при необходимости подключите визуализацию свечей и сделок.
Подробная логика реализована в файле 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
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class time_ea_strategy(Strategy):
TYPE_BUY = 0
TYPE_SELL = 1
def __init__(self):
super(time_ea_strategy, self).__init__()
self._open_time = self.Param("OpenTime", TimeSpan(1, 0, 0))
self._close_time = self.Param("CloseTime", TimeSpan.Zero)
self._opened_type = self.Param("OpenedType", self.TYPE_BUY)
self._order_volume = self.Param("OrderVolume", 0.1)
self._stop_loss_points = self.Param("StopLossPoints", 0)
self._take_profit_points = self.Param("TakeProfitPoints", 0)
self._min_spread_multiplier = self.Param("MinSpreadMultiplier", 2)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1)))
self._last_entry_date = None
self._last_close_date = None
self._entry_price = 0.0
self._stop_price = 0.0
self._take_profit_price = 0.0
@property
def OpenTime(self):
return self._open_time.Value
@property
def CloseTime(self):
return self._close_time.Value
@property
def OpenedType(self):
return self._opened_type.Value
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def MinSpreadMultiplier(self):
return self._min_spread_multiplier.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(time_ea_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
candle_date = candle.CloseTime.Date
if self._contains_time(candle, self.OpenTime) and self._last_entry_date != candle_date:
self._last_entry_date = candle_date
self._handle_open(candle)
if self._contains_time(candle, self.CloseTime) and self._last_close_date != candle_date:
self._last_close_date = candle_date
pos = float(self.Position)
if pos != 0:
if pos > 0:
self.SellMarket(pos)
elif pos < 0:
self.BuyMarket(abs(pos))
self._reset_risk_levels()
return
self._manage_risk(candle)
def _handle_open(self, candle):
pos = float(self.Position)
if self.OpenedType == self.TYPE_BUY:
if pos < 0:
self.BuyMarket(abs(pos))
self._reset_risk_levels()
if float(self.Position) == 0 and float(self.OrderVolume) > 0:
self.BuyMarket(float(self.OrderVolume))
self._set_risk_levels(float(candle.ClosePrice), True)
else:
if pos > 0:
self.SellMarket(pos)
self._reset_risk_levels()
if float(self.Position) == 0 and float(self.OrderVolume) > 0:
self.SellMarket(float(self.OrderVolume))
self._set_risk_levels(float(candle.ClosePrice), False)
def _manage_risk(self, candle):
pos = float(self.Position)
if pos > 0:
if self._stop_price > 0 and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(pos)
self._reset_risk_levels()
return
if self._take_profit_price > 0 and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket(pos)
self._reset_risk_levels()
elif pos < 0:
if self._stop_price > 0 and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(pos))
self._reset_risk_levels()
return
if self._take_profit_price > 0 and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket(abs(pos))
self._reset_risk_levels()
def _set_risk_levels(self, close_price, is_long):
self._entry_price = close_price
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
if step <= 0:
step = 1.0
min_distance = max(self.MinSpreadMultiplier, 0) * step
stop_dist = max(self.StopLossPoints * step, min_distance) if self.StopLossPoints > 0 else 0.0
take_dist = max(self.TakeProfitPoints * step, min_distance) if self.TakeProfitPoints > 0 else 0.0
if is_long:
self._stop_price = close_price - stop_dist if stop_dist > 0 else 0.0
self._take_profit_price = close_price + take_dist if take_dist > 0 else 0.0
else:
self._stop_price = close_price + stop_dist if stop_dist > 0 else 0.0
self._take_profit_price = close_price - take_dist if take_dist > 0 else 0.0
def _reset_risk_levels(self):
self._entry_price = 0.0
self._stop_price = 0.0
self._take_profit_price = 0.0
def _contains_time(self, candle, target):
open_time = candle.OpenTime
close_time = candle.CloseTime
open_span = open_time.TimeOfDay
close_span = close_time.TimeOfDay
crosses_midnight = close_time.Date > open_time.Date or close_span < open_span
if not crosses_midnight:
return target >= open_span and target <= close_span
start_min = open_span.TotalMinutes
end_min = close_span.TotalMinutes + 1440.0
target_min = target.TotalMinutes
if target_min < start_min:
target_min += 1440.0
return target_min >= start_min and target_min <= end_min
def OnReseted(self):
super(time_ea_strategy, self).OnReseted()
self._last_entry_date = None
self._last_close_date = None
self._reset_risk_levels()
def CreateClone(self):
return time_ea_strategy()