Стратегия Multi Hedging Scheduler
Описание
Multi Hedging Scheduler Strategy — это конвертация советника MultiHedg_1.mq5 из MetaTrader 5 в инфраструктуру StockSharp. Стратегия рассчитана на счёта с поддержкой хеджирования и управляет до десяти инструментов одновременно. Она открывает позиции в заранее заданном направлении в пределах торгового окна и закрывает их либо по времени, либо при достижении заданных порогов по доходности/просадке капитала.
Для синхронизации используется поток свечей (по умолчанию минутный), который служит исключительно источником времени. Каждая завершённая свеча инициирует проверку условий на открытие и закрытие позиций, а также контроль риска по капиталу. Индикаторы не применяются.
Логика работы
- Выбор инструментов. До десяти символов могут быть включены параметрами
UseSymbolX. Для каждого активированного инструмента стратегия ищет тикер через SecurityProvider, подписывается на свечи указанного таймфрейма и применяет единую логику.
- Торговое окно. Когда время свечи попадает в интервал [
TradeStartTime, TradeStartTime + TradeDuration), стратегия пытается открыть рыночную позицию с направлением TradeDirection. Если позиция в том же направлении уже есть, повторный вход не выполняется. При наличии обратной позиции объём увеличивается, чтобы развернуться в нужную сторону.
- Защита капитала. При включённом
CloseByEquityPercent стратегия сравнивает текущую стоимость портфеля с балансом на момент запуска. Превышение PercentProfit или просадка PercentLoss приводят к закрытию всех управляемых позиций.
- Временное закрытие. Если активирован
UseTimeClose, то при попадании времени в окно [CloseTime, CloseTime + TradeDuration) стратегия закрывает все позиции.
- Журналы. Все действия (входы, срабатывание ограничений по капиталу, закрытие по времени) фиксируются через
LogInfo.
Параметры
| Параметр |
Описание |
Значение по умолчанию |
TradeDirection |
Направление всех сделок (Buy или Sell). |
Buy |
TradeStartTime |
Время начала торгового окна. |
19:51 |
TradeDuration |
Длительность торгового и закрывающего окон. |
00:05:00 |
UseTimeClose |
Включает закрытие по времени. |
true |
CloseTime |
Время начала окна закрытия. |
20:50 |
CloseByEquityPercent |
Включает выход по проценту капитала. |
true |
PercentProfit |
Порог прибыли в процентах от стартового баланса. |
1.0 |
PercentLoss |
Порог просадки в процентах от стартового баланса. |
55.0 |
CandleType |
Тип свечей, используемый как таймер. |
1 минута |
UseSymbol0..9 |
Разрешение торговли по каждому слоту. |
true для 0–5, false для 6–9 |
Symbol0..9 |
Тикер каждого слота (ID для SecurityProvider). |
см. таблицу ниже |
Volume0..9 |
Объём заявки для каждого слота. |
0.1–1.0 |
Конфигурация по умолчанию
| Слот |
Вкл. |
Символ |
Объём |
| 0 |
✔ |
EURUSD |
0.1 |
| 1 |
✔ |
GBPUSD |
0.2 |
| 2 |
✔ |
GBPJPY |
0.3 |
| 3 |
✔ |
EURCAD |
0.4 |
| 4 |
✔ |
USDCHF |
0.5 |
| 5 |
✔ |
USDJPY |
0.6 |
| 6 |
✖ |
USDCHF |
0.7 |
| 7 |
✖ |
GBPUSD |
0.8 |
| 8 |
✖ |
EURUSD |
0.9 |
| 9 |
✖ |
USDJPY |
1.0 |
Рекомендации по использованию
- Для точного воспроизведения поведения MT5 убедитесь, что торговый счёт поддерживает хеджирование. На неттинговых счетах стратегия автоматически перекрывает противоположные позиции при смене направления.
- Перед запуском проверьте, что значения
SymbolX совпадают с идентификаторами в используемом источнике данных StockSharp (например, EURUSD@FXCM).
- Поток свечей служит только для синхронизации. При необходимости измените
CandleType на другой таймфрейм.
- При перезапуске стратегия заново фиксирует стартовый баланс. Это нужно учитывать при оценке срабатывания порогов
PercentProfit и PercentLoss.
- В стратегии нет индивидуальных стоп-лоссов и тейк-профитов — выходы контролируются только глобальными условиями.
Особенности конверсии
- В MT5 советник работал на
OnTick. В StockSharp логика привязана к завершённым свечам, что соответствует высокоуровневому API и упрощает управление подписками.
- Проверка
magic number заменена фильтрацией списка отслеживаемых инструментов. Метод CloseAllManagedPositions закрывает только те позиции, которые открывала стратегия.
- Звуковые оповещения и комментарии на графике не перенесены; вместо них используется журнал
LogInfo.
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>
/// Hedging scheduler strategy that opens positions during a configurable time window
/// and closes when equity targets are reached or a separate exit window arrives.
/// Simplified to single-security from the original multi-symbol version.
/// </summary>
public class MultiHedgingSchedulerStrategy : Strategy
{
private readonly StrategyParam<Sides> _tradeDirection;
private readonly StrategyParam<TimeSpan> _tradeStartTime;
private readonly StrategyParam<TimeSpan> _tradeDuration;
private readonly StrategyParam<bool> _enableTimeClose;
private readonly StrategyParam<TimeSpan> _closeTime;
private readonly StrategyParam<bool> _enableEquityClose;
private readonly StrategyParam<decimal> _profitPercent;
private readonly StrategyParam<decimal> _lossPercent;
private readonly StrategyParam<DataType> _candleType;
private decimal _initialBalance;
private bool _positionOpened;
/// <summary>
/// Trading direction used when opening positions.
/// </summary>
public Sides TradeDirection
{
get => _tradeDirection.Value;
set => _tradeDirection.Value = value;
}
/// <summary>
/// Time of day when the trading window starts.
/// </summary>
public TimeSpan TradeStartTime
{
get => _tradeStartTime.Value;
set => _tradeStartTime.Value = value;
}
/// <summary>
/// Duration of the trading and optional closing windows.
/// </summary>
public TimeSpan TradeDuration
{
get => _tradeDuration.Value;
set => _tradeDuration.Value = value;
}
/// <summary>
/// Enables the separate time based close window.
/// </summary>
public bool UseTimeClose
{
get => _enableTimeClose.Value;
set => _enableTimeClose.Value = value;
}
/// <summary>
/// Time of day when the closing window starts.
/// </summary>
public TimeSpan CloseTime
{
get => _closeTime.Value;
set => _closeTime.Value = value;
}
/// <summary>
/// Enables closing when equity reaches profit or loss thresholds.
/// </summary>
public bool CloseByEquityPercent
{
get => _enableEquityClose.Value;
set => _enableEquityClose.Value = value;
}
/// <summary>
/// Percentage profit target based on starting balance.
/// </summary>
public decimal PercentProfit
{
get => _profitPercent.Value;
set => _profitPercent.Value = value;
}
/// <summary>
/// Percentage loss threshold based on starting balance.
/// </summary>
public decimal PercentLoss
{
get => _lossPercent.Value;
set => _lossPercent.Value = value;
}
/// <summary>
/// Candle series driving the scheduling logic.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes the strategy.
/// </summary>
public MultiHedgingSchedulerStrategy()
{
_tradeDirection = Param(nameof(TradeDirection), Sides.Buy)
.SetDisplay("Trade Direction", "Direction used for opening positions", "General");
_tradeStartTime = Param(nameof(TradeStartTime), new TimeSpan(10, 0, 0))
.SetDisplay("Trade Start", "Time of day to begin opening positions", "Scheduling");
_tradeDuration = Param(nameof(TradeDuration), TimeSpan.FromMinutes(5))
.SetDisplay("Window Length", "Duration of trading and closing windows", "Scheduling");
_enableTimeClose = Param(nameof(UseTimeClose), true)
.SetDisplay("Use Close Window", "Enable time based portfolio closing", "Scheduling");
_closeTime = Param(nameof(CloseTime), new TimeSpan(17, 0, 0))
.SetDisplay("Close Start", "Time of day to start the close window", "Scheduling");
_enableEquityClose = Param(nameof(CloseByEquityPercent), true)
.SetDisplay("Use Equity Targets", "Enable equity based exit", "Risk Management");
_profitPercent = Param(nameof(PercentProfit), 1m)
.SetDisplay("Profit %", "Equity percentage gain to close all positions", "Risk Management");
_lossPercent = Param(nameof(PercentLoss), 55m)
.SetDisplay("Loss %", "Equity percentage loss to close all positions", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Candle series driving the scheduler", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_initialBalance = 0m;
_positionOpened = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_initialBalance = Portfolio?.CurrentValue ?? 0m;
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormed)
return;
var timeOfDay = candle.OpenTime.TimeOfDay;
if (CloseByEquityPercent && TryHandleEquityTargets())
return;
if (UseTimeClose && IsWithinWindow(timeOfDay, CloseTime, TradeDuration))
{
if (Position > 0)
SellMarket(Math.Abs(Position));
else if (Position < 0)
BuyMarket(Math.Abs(Position));
_positionOpened = false;
return;
}
var direction = TradeDirection;
if (!IsWithinWindow(timeOfDay, TradeStartTime, TradeDuration))
return;
if (_positionOpened)
return;
var volume = Volume;
if (volume <= 0m)
volume = 1m;
if (direction == Sides.Buy && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(volume);
_positionOpened = true;
}
else if (direction == Sides.Sell && Position >= 0)
{
if (Position > 0)
SellMarket(Position);
SellMarket(volume);
_positionOpened = true;
}
}
private bool TryHandleEquityTargets()
{
if (_initialBalance <= 0m)
return false;
var equity = Portfolio?.CurrentValue;
if (equity == null)
return false;
var profitLevel = _initialBalance * (1m + PercentProfit / 100m);
var lossLevel = _initialBalance * (1m - PercentLoss / 100m);
if (equity.Value >= profitLevel || equity.Value <= lossLevel)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
else if (Position < 0)
BuyMarket(Math.Abs(Position));
_positionOpened = false;
return true;
}
return false;
}
private static bool IsWithinWindow(TimeSpan current, TimeSpan start, TimeSpan length)
{
if (length <= TimeSpan.Zero)
return current == start;
var end = start + length;
if (end < TimeSpan.FromDays(1))
return current >= start && current < end;
var overflow = end - TimeSpan.FromDays(1);
return current >= start || current < overflow;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
SIDE_BUY = 0
SIDE_SELL = 1
class multi_hedging_scheduler_strategy(Strategy):
def __init__(self):
super(multi_hedging_scheduler_strategy, self).__init__()
self._trade_direction = self.Param("TradeDirection", SIDE_BUY)
self._trade_start_hour = self.Param("TradeStartHour", 10)
self._trade_start_minute = self.Param("TradeStartMinute", 0)
self._trade_duration_minutes = self.Param("TradeDurationMinutes", 5)
self._enable_time_close = self.Param("UseTimeClose", True)
self._close_hour = self.Param("CloseHour", 17)
self._close_minute = self.Param("CloseMinute", 0)
self._enable_equity_close = self.Param("CloseByEquityPercent", True)
self._profit_percent = self.Param("PercentProfit", 1.0)
self._loss_percent = self.Param("PercentLoss", 55.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1)))
self._initial_balance = 0.0
self._position_opened = False
@property
def TradeDirection(self):
return self._trade_direction.Value
@TradeDirection.setter
def TradeDirection(self, value):
self._trade_direction.Value = value
@property
def TradeStartHour(self):
return self._trade_start_hour.Value
@TradeStartHour.setter
def TradeStartHour(self, value):
self._trade_start_hour.Value = value
@property
def TradeStartMinute(self):
return self._trade_start_minute.Value
@TradeStartMinute.setter
def TradeStartMinute(self, value):
self._trade_start_minute.Value = value
@property
def TradeDurationMinutes(self):
return self._trade_duration_minutes.Value
@TradeDurationMinutes.setter
def TradeDurationMinutes(self, value):
self._trade_duration_minutes.Value = value
@property
def UseTimeClose(self):
return self._enable_time_close.Value
@UseTimeClose.setter
def UseTimeClose(self, value):
self._enable_time_close.Value = value
@property
def CloseHour(self):
return self._close_hour.Value
@CloseHour.setter
def CloseHour(self, value):
self._close_hour.Value = value
@property
def CloseMinute(self):
return self._close_minute.Value
@CloseMinute.setter
def CloseMinute(self, value):
self._close_minute.Value = value
@property
def CloseByEquityPercent(self):
return self._enable_equity_close.Value
@CloseByEquityPercent.setter
def CloseByEquityPercent(self, value):
self._enable_equity_close.Value = value
@property
def PercentProfit(self):
return self._profit_percent.Value
@PercentProfit.setter
def PercentProfit(self, value):
self._profit_percent.Value = value
@property
def PercentLoss(self):
return self._loss_percent.Value
@PercentLoss.setter
def PercentLoss(self, value):
self._loss_percent.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _is_within_window(self, current_minutes, start_hour, start_minute, duration_minutes):
start_total = start_hour * 60 + start_minute
end_total = start_total + duration_minutes
if end_total < 1440:
return current_minutes >= start_total and current_minutes < end_total
else:
overflow = end_total - 1440
return current_minutes >= start_total or current_minutes < overflow
def OnStarted2(self, time):
super(multi_hedging_scheduler_strategy, self).OnStarted2(time)
self._initial_balance = 0.0
self._position_opened = False
self.SubscribeCandles(self.CandleType).Bind(self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._initial_balance == 0.0 and self.Portfolio is not None and self.Portfolio.CurrentValue is not None:
self._initial_balance = float(self.Portfolio.CurrentValue)
open_time = candle.OpenTime
hour = open_time.Hour
minute = open_time.Minute
current_minutes = hour * 60 + minute
duration = int(self.TradeDurationMinutes)
if self.CloseByEquityPercent and self._try_handle_equity_targets():
return
if self.UseTimeClose and self._is_within_window(current_minutes, int(self.CloseHour), int(self.CloseMinute), duration):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._position_opened = False
return
if not self._is_within_window(current_minutes, int(self.TradeStartHour), int(self.TradeStartMinute), duration):
return
if self._position_opened:
return
direction = int(self.TradeDirection)
if direction == SIDE_BUY and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._position_opened = True
elif direction == SIDE_SELL and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._position_opened = True
def _try_handle_equity_targets(self):
if self._initial_balance <= 0.0:
return False
if self.Portfolio is None or self.Portfolio.CurrentValue is None:
return False
equity = float(self.Portfolio.CurrentValue)
profit_level = self._initial_balance * (1.0 + float(self.PercentProfit) / 100.0)
loss_level = self._initial_balance * (1.0 - float(self.PercentLoss) / 100.0)
if equity >= profit_level or equity <= loss_level:
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._position_opened = False
return True
return False
def OnReseted(self):
super(multi_hedging_scheduler_strategy, self).OnReseted()
self._initial_balance = 0.0
self._position_opened = False
def CreateClone(self):
return multi_hedging_scheduler_strategy()