Открыть на GitHub

Стратегия Multi Hedging Scheduler

Описание

Multi Hedging Scheduler Strategy — это конвертация советника MultiHedg_1.mq5 из MetaTrader 5 в инфраструктуру StockSharp. Стратегия рассчитана на счёта с поддержкой хеджирования и управляет до десяти инструментов одновременно. Она открывает позиции в заранее заданном направлении в пределах торгового окна и закрывает их либо по времени, либо при достижении заданных порогов по доходности/просадке капитала.

Для синхронизации используется поток свечей (по умолчанию минутный), который служит исключительно источником времени. Каждая завершённая свеча инициирует проверку условий на открытие и закрытие позиций, а также контроль риска по капиталу. Индикаторы не применяются.

Логика работы

  1. Выбор инструментов. До десяти символов могут быть включены параметрами UseSymbolX. Для каждого активированного инструмента стратегия ищет тикер через SecurityProvider, подписывается на свечи указанного таймфрейма и применяет единую логику.
  2. Торговое окно. Когда время свечи попадает в интервал [TradeStartTime, TradeStartTime + TradeDuration), стратегия пытается открыть рыночную позицию с направлением TradeDirection. Если позиция в том же направлении уже есть, повторный вход не выполняется. При наличии обратной позиции объём увеличивается, чтобы развернуться в нужную сторону.
  3. Защита капитала. При включённом CloseByEquityPercent стратегия сравнивает текущую стоимость портфеля с балансом на момент запуска. Превышение PercentProfit или просадка PercentLoss приводят к закрытию всех управляемых позиций.
  4. Временное закрытие. Если активирован UseTimeClose, то при попадании времени в окно [CloseTime, CloseTime + TradeDuration) стратегия закрывает все позиции.
  5. Журналы. Все действия (входы, срабатывание ограничений по капиталу, закрытие по времени) фиксируются через 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;
	}
}