符号同步策略
概述
Symbol Sync Strategy 在 StockSharp 平台中复刻 MetaTrader 工具 SymbolSyncEA 的行为。策略会监控主策略当前使用的品种,并把该证券同步到所有已注册的从属策略,使整个工作空间始终聚焦同一个标的。
核心思路
- 启动时保存初始证券,作为后续恢复的备用值。
- 维护一个可配置的同步列表,列表中的策略都会跟随主策略的证券。
- 允许通过直接赋值
Security或者提供证券标识两种方式触发切换。 - 提供手动同步与恢复到初始证券的方法,最大程度地还原原始 EA 的功能。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
ChartLimit |
允许同步的策略最大数量,用于防止意外的大规模变更。 | 10 |
SyncSecurityId |
将要传播的证券标识,留空时自动使用当前策略的证券。 | "" |
公共方法
RegisterLinkedStrategy(Strategy strategy):把策略加入同步列表,成功返回true。UnregisterLinkedStrategy(Strategy strategy):从列表中移除指定策略。ChangeSyncSecurity(Security security):使用给定的Security实例并同步所有从属策略。ChangeSyncSecurity(string securityId):通过SecurityProvider查找标识并调用上一方法。ResetToInitialSecurity():恢复为启动时保存的证券。SyncSymbols():在不修改标识的情况下强制执行一次同步。
使用流程
- 创建
SymbolSyncStrategy,在启动前设置主策略的Security或SyncSecurityId。 - 调用
RegisterLinkedStrategy注册所有需要跟随的子策略(例如不同的时间周期、统计模块等)。 - 当需要切换主品种时,调用
ChangeSyncSecurity(Security)或ChangeSyncSecurity(string)。 - 如果外部组件可能修改过子策略,可调用
SyncSymbols()手动强制同步。
与 MQL 版本的差异
- 针对 StockSharp 的
Strategy实例,而非 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class symbol_sync_strategy(Strategy):
"""Fast/slow SMA crossover (10/30) on 5-min candles."""
def __init__(self):
super(symbol_sync_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candle timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnStarted2(self, time):
super(symbol_sync_strategy, self).OnStarted2(time)
fast = SimpleMovingAverage()
fast.Length = 10
slow = SimpleMovingAverage()
slow.Length = 30
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(fast, slow, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, fast)
self.DrawIndicator(area, slow)
self.DrawOwnTrades(area)
def OnProcess(self, candle, fast_val, slow_val):
if candle.State != CandleStates.Finished:
return
if fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
def CreateClone(self):
return symbol_sync_strategy()