Открыть на GitHub

Стратегия синхронизации символов

Обзор

Symbol Sync Strategy переносит функциональность утилиты MetaTrader SymbolSyncEA в среду StockSharp. Стратегия отслеживает активный символ и автоматически назначает его всем привязанным стратегиям. При изменении главного инструмента все подключённые стратегии получают тот же Security, поэтому рабочее пространство остаётся согласованным без ручного вмешательства.

Ключевые идеи

  • Сохранять исходный инструмент при запуске и использовать его как резервную точку возврата.
  • Поддерживать настраиваемый список стратегий, которые должны следовать за главным символом.
  • Позволять менять инструмент как прямым присвоением Security, так и через идентификатор.
  • Предоставлять методы ручной синхронизации и возврата к исходному инструменту, повторяя поведение исходного эксперта.

Параметры

Имя Описание Значение по умолчанию
ChartLimit Максимальное число стратегий, которые будут синхронизированы. Защищает от случайных массовых изменений. 10
SyncSecurityId Идентификатор инструмента, распространяемый на привязанные стратегии. Пустое значение означает использование текущего инструмента стратегии. ""

Публичные методы

  • RegisterLinkedStrategy(Strategy strategy) — добавляет стратегию в список синхронизации и возвращает true при успехе.
  • UnregisterLinkedStrategy(Strategy strategy) — удаляет стратегию из списка.
  • ChangeSyncSecurity(Security security) — назначает указанный инструмент и синхронизирует все привязанные стратегии.
  • ChangeSyncSecurity(string securityId) — пытается найти инструмент по идентификатору через SecurityProvider и вызывает предыдущий метод.
  • ResetToInitialSecurity() — возвращает инструмент, зафиксированный при старте.
  • SyncSymbols() — запускает принудительную синхронизацию без изменения сохранённого идентификатора.

Последовательность использования

  1. Создайте SymbolSyncStrategy, задайте основной Security или установите SyncSecurityId до запуска.
  2. Вызовите RegisterLinkedStrategy для каждой дочерней стратегии, которой требуется следовать за активным инструментом (например, разные таймфреймы или панели мониторинга).
  3. При смене главного инструмента вызывайте ChangeSyncSecurity(Security) либо ChangeSyncSecurity(string).
  4. При необходимости вызывайте SyncSymbols(), если внешний компонент мог изменить дочернюю стратегию.

Отличия от версии MQL

  • Работает со стратегиями StockSharp вместо окон графиков MetaTrader.
  • Использует SecurityProvider для поиска инструментов по идентификатору.
  • Добавляет защитное логирование и ограничение на количество синхронизируемых стратегий.
  • Предоставляет явные методы сброса и ручной синхронизации для сложных сценариев автоматизации.

Примечания

  • Стратегия не размещает торговые приказы — это вспомогательный инфраструктурный компонент.
  • Комментарии в коде оставлены на английском языке в соответствии с требованиями проекта.
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>
/// Strategy that synchronizes the security of linked strategies whenever the main symbol changes.
/// </summary>
public class SymbolSyncStrategy : Strategy
{
	private readonly StrategyParam<int> _chartLimit;
	private readonly StrategyParam<string> _syncSecurityId;

	private readonly List<Strategy> _linkedStrategies = new();

	private Security _initialSecurity;

	public SymbolSyncStrategy()
	{
		_chartLimit = Param(nameof(ChartLimit), 10)
			.SetNotNegative()
			.SetDisplay("Chart limit", "Maximum number of linked strategies that can be synchronized.", "General")
			;

		_syncSecurityId = Param(nameof(SyncSecurityId), string.Empty)
			.SetDisplay("Sync security ID", "Identifier of the security propagated to linked strategies.", "General")
			;
	}

	/// <summary>
	/// Maximum number of linked strategies that can follow the symbol changes.
	/// </summary>
	public int ChartLimit
	{
		get => _chartLimit.Value;
		set => _chartLimit.Value = value;
	}

	/// <summary>
	/// Identifier of the security that must be mirrored by linked strategies.
	/// </summary>
	public string SyncSecurityId
	{
		get => _syncSecurityId.Value;
		set => _syncSecurityId.Value = value ?? string.Empty;
	}

	private SimpleMovingAverage _smaFast;
	private SimpleMovingAverage _smaSlow;
	private int _candleCount;
	private int _lastTradeCandle;

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_linkedStrategies.Clear();
		_initialSecurity = default;
		_smaFast = default;
		_smaSlow = default;
		_candleCount = default;
		_lastTradeCandle = default;
		_syncSecurityId.Value = string.Empty;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_initialSecurity = Security;

		if (SyncSecurityId.IsEmpty() && Security != null)
			SyncSecurityId = Security.Id;

		_smaFast = new SimpleMovingAverage { Length = 10 };
		_smaSlow = new SimpleMovingAverage { Length = 30 };

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription
			.Bind(_smaFast, _smaSlow, ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_candleCount++;

		if (_candleCount - _lastTradeCandle < 200)
			return;

		if (fast > slow && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
			_lastTradeCandle = _candleCount;
		}
		else if (fast < slow && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
			_lastTradeCandle = _candleCount;
		}
	}

	/// <inheritdoc />
	protected override void OnStopped()
	{
		base.OnStopped();

		_initialSecurity = null;
	}

	/// <summary>
	/// Registers an additional strategy that must mirror the current security.
	/// </summary>
	/// <param name="strategy">Strategy that will receive symbol updates.</param>
	/// <returns><c>true</c> when the strategy is registered; otherwise <c>false</c>.</returns>
	public bool RegisterLinkedStrategy(Strategy strategy)
	{
		if (strategy == null)
			throw new ArgumentNullException(nameof(strategy));

		if (_linkedStrategies.Contains(strategy))
			return false;

		var limit = Math.Max(ChartLimit, 0);
		if (_linkedStrategies.Count >= limit)
		{
			LogWarning($"Chart limit of {limit} reached. Strategy '{strategy.Name}' cannot be synchronized.");
			return false;
		}

		_linkedStrategies.Add(strategy);
		ApplySymbol(strategy);
		return true;
	}

	/// <summary>
	/// Removes a strategy from the synchronization list.
	/// </summary>
	/// <param name="strategy">Strategy previously added with <see cref="RegisterLinkedStrategy"/>.</param>
	/// <returns><c>true</c> when the strategy was removed.</returns>
	public bool UnregisterLinkedStrategy(Strategy strategy)
	{
		if (strategy == null)
			throw new ArgumentNullException(nameof(strategy));

		return _linkedStrategies.Remove(strategy);
	}

	/// <summary>
	/// Restores the initial security captured when the strategy started.
	/// </summary>
	public void ResetToInitialSecurity()
	{
		if (_initialSecurity == null)
			return;

		ChangeSyncSecurity(_initialSecurity);
	}

	/// <summary>
	/// Changes the synchronization security using a resolved <see cref="Security"/> instance.
	/// </summary>
	/// <param name="security">Security that should be mirrored by linked strategies.</param>
	/// <returns><c>true</c> when the identifier changed.</returns>
	public bool ChangeSyncSecurity(Security security)
	{
		if (security == null)
			throw new ArgumentNullException(nameof(security));

		if (Security != security)
			Security = security;

		if (SyncSecurityId.EqualsIgnoreCase(security.Id))
		{
			SyncSymbols();
			return false;
		}

		SyncSecurityId = security.Id;
		SyncSymbols();
		return true;
	}

	/// <summary>
	/// Changes the synchronization security by resolving the identifier through <see cref="Strategy.Connector"/>.
	/// </summary>
	/// <param name="securityId">Identifier of the security to use for synchronization.</param>
	/// <returns><c>true</c> when the identifier resolved to a new security.</returns>
	public bool ChangeSyncSecurity(string securityId)
	{
		if (securityId.IsEmpty())
			throw new ArgumentNullException(nameof(securityId));

		if (Connector != null)
		{
			var resolved = Connector.LookupById(securityId);
			if (resolved != null)
				return ChangeSyncSecurity(resolved);

			LogWarning($"Security '{securityId}' not found by the security provider.");
		}

		SyncSecurityId = securityId;
		SyncSymbols();
		return false;
	}

	/// <summary>
	/// Synchronizes the security across every registered strategy.
	/// </summary>
	/// <returns><c>true</c> when a security was resolved and propagated.</returns>
	public bool SyncSymbols()
	{
		var security = ResolveSecurity();
		if (security == null)
		{
			LogWarning("No synchronization security resolved. Linked strategies keep their current assignments.");
			return false;
		}

		if (Security != security)
			Security = security;

		foreach (var strategy in _linkedStrategies)
			ApplySymbol(strategy);

		return true;
	}

	private void ApplySymbol(Strategy strategy)
	{
		if (strategy == null)
			return;

		var security = ResolveSecurity();
		if (security == null)
			return;

		if (strategy.Security == security)
			return;

		strategy.Security = security;
	}

	private Security ResolveSecurity()
	{
		if (Security != null && (SyncSecurityId.IsEmpty() || SyncSecurityId.EqualsIgnoreCase(Security.Id)))
			return Security;

		if (!SyncSecurityId.IsEmpty() && Connector != null)
		{
			var resolved = Connector.LookupById(SyncSecurityId);
			if (resolved != null)
				return resolved;
		}

		return Security;
	}
}