Ver en GitHub

Symbol Sync Strategy

Overview

The Symbol Sync Strategy replicates the MetaTrader utility SymbolSyncEA inside the StockSharp environment. The strategy keeps the main strategy symbol and all registered linked strategies synchronized. Whenever the primary symbol changes, the strategy automatically propagates the new security to every linked strategy, ensuring that the entire workspace follows the same instrument without manual intervention.

Core ideas

  • Capture the initial strategy security at startup and reuse it as a fallback option.
  • Keep a configurable list of linked strategies that should always mirror the main security.
  • Allow symbol changes triggered either by a direct Security assignment or by specifying a new security identifier.
  • Provide manual synchronization and reset operations to match the original Expert Advisor behaviour.

Parameters

Name Description Default
ChartLimit Maximum number of linked strategies that can be synchronized. Prevents accidental mass-updates. 10
SyncSecurityId Identifier of the security propagated to linked strategies. An empty value falls back to the strategy security. ""

Public methods

  • RegisterLinkedStrategy(Strategy strategy) – adds a strategy instance to the synchronization list. Returns true when successfully registered.
  • UnregisterLinkedStrategy(Strategy strategy) – removes a strategy from the list.
  • ChangeSyncSecurity(Security security) – switches to the provided security instance and propagates it to every linked strategy.
  • ChangeSyncSecurity(string securityId) – resolves the identifier through the current SecurityProvider and calls ChangeSyncSecurity(Security).
  • ResetToInitialSecurity() – restores the symbol captured at startup.
  • SyncSymbols() – forces a manual resynchronization without changing the stored identifier.

Usage workflow

  1. Instantiate SymbolSyncStrategy and set the primary Security or assign SyncSecurityId before starting the strategy.
  2. Call RegisterLinkedStrategy for each child strategy that must mirror the active symbol (for example different timeframes or dashboards).
  3. Whenever the main symbol should change, call ChangeSyncSecurity(Security) or ChangeSyncSecurity(string).
  4. Optionally call SyncSymbols() to force propagation if an external component modified a linked strategy.

Differences compared to the MQL version

  • Works with StockSharp Strategy instances instead of MetaTrader chart windows.
  • Uses the SecurityProvider abstraction to resolve identifiers.
  • Adds defensive logging and a configurable limit for synchronized strategies.
  • Offers explicit reset and manual synchronization methods for advanced automation scenarios.

Notes

  • The strategy does not issue market orders; it operates as an infrastructure helper.
  • All code comments are kept in English to comply with project requirements.
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;
	}
}