From 8ca57f4231132213f2724dd9ecb25c242dae62e9 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 2 Jul 2026 12:28:19 -0400 Subject: [PATCH] Auto-subscribe symbols when registering indicators and consolidators Registering an indicator or consolidator for a symbol that had not been subscribed to threw 'Please register to receive data for symbol ...'. Order submission already auto-subscribes the symbol on the user's behalf; this applies the same behavior to indicator/consolidator registration. GetSubscription now adds the security automatically when it has no subscription (guarded by the shared CanAutoAddSecurity check, which is also used by order submission) before falling back to the error. The subscription lookup is exposed via a TryGetSubscription out-parameter helper. --- ...rWithoutSubscriptionRegressionAlgorithm.cs | 197 ++++++++++++++++++ Algorithm/QCAlgorithm.Indicators.cs | 52 +++-- Algorithm/QCAlgorithm.Trading.cs | 22 +- 3 files changed, 247 insertions(+), 24 deletions(-) create mode 100644 Algorithm.CSharp/RegisterIndicatorAndConsolidatorWithoutSubscriptionRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/RegisterIndicatorAndConsolidatorWithoutSubscriptionRegressionAlgorithm.cs b/Algorithm.CSharp/RegisterIndicatorAndConsolidatorWithoutSubscriptionRegressionAlgorithm.cs new file mode 100644 index 000000000000..96cc321a2066 --- /dev/null +++ b/Algorithm.CSharp/RegisterIndicatorAndConsolidatorWithoutSubscriptionRegressionAlgorithm.cs @@ -0,0 +1,197 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Indicators; +using QuantConnect.Interfaces; +using QuantConnect.Orders; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting that a symbol can be used without the user having subscribed to it first. + /// Lean auto-subscribes the symbol on the user's behalf when: + /// - registering an indicator or a consolidator for it (), and + /// - submitting an order for it (, see ). + /// + public class RegisterIndicatorAndConsolidatorWithoutSubscriptionRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + // used without subscription to register an indicator and a consolidator + private Symbol _spy = QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA); + // used without subscription to submit an order, exercising the auto-add done by order submission + private Symbol _aig = QuantConnect.Symbol.Create("AIG", SecurityType.Equity, Market.USA); + + private SimpleMovingAverage _sma; + private int _consolidatedBarCount; + + private OrderTicket _orderTicket; + private bool _orderFilled; + + /// + /// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized. + /// + public override void Initialize() + { + SetStartDate(2013, 10, 07); + SetEndDate(2013, 10, 11); + SetCash(100000); + + // Note: we never call AddEquity/AddSecurity. Registering an indicator or a consolidator + // for a symbol that hasn't been subscribed to used to throw. It now auto-subscribes the symbol, + // mirroring what order submission does. + + _sma = new SimpleMovingAverage(10); + try + { + RegisterIndicator(_spy, _sma, Resolution.Minute); + } + catch (Exception ex) + { + throw new RegressionTestException($"Expected RegisterIndicator to auto-subscribe {_spy}, but it threw: {ex.Message}"); + } + + try + { + Consolidate(_spy, TimeSpan.FromMinutes(30), (TradeBar bar) => _consolidatedBarCount++); + } + catch (Exception ex) + { + throw new RegressionTestException($"Expected Consolidate to auto-subscribe {_spy}, but it threw: {ex.Message}"); + } + + if (!Securities.ContainsKey(_spy)) + { + throw new RegressionTestException($"Expected {_spy} to have been automatically subscribed to after registering an indicator/consolidator for it."); + } + } + + /// + /// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. + /// + /// Slice object keyed by symbol containing the stock data + public override void OnData(Slice slice) + { + if (!Portfolio.Invested) + { + // AIG was never subscribed to: order submission will auto-subscribe it before placing the order + _orderTicket = MarketOrder(_aig, 100); + + if (!Securities.ContainsKey(_aig)) + { + throw new RegressionTestException($"Expected {_aig} to have been automatically subscribed to after submitting an order for it."); + } + } + } + + public override void OnOrderEvent(OrderEvent orderEvent) + { + if (orderEvent.Status == OrderStatus.Filled) + { + _orderFilled = true; + } + } + + public override void OnEndOfAlgorithm() + { + if (!_sma.IsReady || _sma.Samples == 0) + { + throw new RegressionTestException($"Expected the SMA indicator to have received data through its auto-subscription, but Samples={_sma.Samples}."); + } + + if (_consolidatedBarCount == 0) + { + throw new RegressionTestException("Expected the consolidator to have produced bars through its auto-subscription, but it produced none."); + } + + if (_orderTicket == null) + { + throw new RegressionTestException("Expected an order to have been placed for the auto-subscribed symbol, but none was."); + } + + if (_orderTicket.Status != OrderStatus.Filled || !_orderFilled) + { + throw new RegressionTestException($"Expected the order for {_aig} to have been filled, but its status was {_orderTicket.Status}."); + } + + if (!Portfolio[_aig].Invested) + { + throw new RegressionTestException($"Expected to be invested in {_aig} after the order was filled."); + } + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public List Languages { get; } = new() { Language.CSharp }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 7842; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 10; + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "1"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "6.467%"}, + {"Drawdown", "0.200%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "100080.15"}, + {"Net Profit", "0.080%"}, + {"Sharpe Ratio", "3.91"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "64.635%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "-0.079"}, + {"Beta", "0.072"}, + {"Annual Standard Deviation", "0.016"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-9.261"}, + {"Tracking Error", "0.207"}, + {"Treynor Ratio", "0.871"}, + {"Total Fees", "$1.00"}, + {"Estimated Strategy Capacity", "$11000000.00"}, + {"Lowest Capacity Asset", "AIG R735QTJ8XC9X"}, + {"Portfolio Turnover", "0.83%"}, + {"Drawdown Recovery", "3"}, + {"OrderListHash", "5bb87da5d4faaf7c85a9e263890c3d64"} + }; + } +} diff --git a/Algorithm/QCAlgorithm.Indicators.cs b/Algorithm/QCAlgorithm.Indicators.cs index 9e48affbac30..e86ffd96b864 100644 --- a/Algorithm/QCAlgorithm.Indicators.cs +++ b/Algorithm/QCAlgorithm.Indicators.cs @@ -3160,33 +3160,47 @@ public string CreateIndicatorName(Symbol symbol, string type, Resolution? resolu /// The SubscriptionDataConfig for the specified symbol private SubscriptionDataConfig GetSubscription(Symbol symbol, TickType? tickType = null) { - SubscriptionDataConfig subscription; - try + if (!TryGetSubscription(symbol, tickType, out var subscription)) { - // deterministic ordering is required here - var subscriptions = SubscriptionManager.SubscriptionDataConfigService - .GetSubscriptionDataConfigs(symbol) - // make sure common lean data types are at the bottom - .OrderByDescending(x => LeanData.IsCommonLeanDataType(x.Type)) - .ThenBy(x => x.TickType) - .ToList(); - - // find our subscription - subscription = subscriptions.FirstOrDefault(x => tickType == null || tickType == x.TickType); + // The symbol was not manually subscribed to. Mirror the behavior of order submission + // (see GetSecurityForOrder): add the security automatically so users can register + // indicators and consolidators without a prior AddSecurity()/AddEquity() call. + if (CanAutoAddSecurity(symbol)) + { + AddSecurity(symbol); + TryGetSubscription(symbol, tickType, out subscription); + } + if (subscription == null) { - // if we can't locate the exact subscription by tick type just grab the first one we find - subscription = subscriptions.First(); + // this will happen if we did not find the subscription, let's give the user a decent error message + throw new Exception($"Please register to receive data for symbol \'{symbol}\' using the AddSecurity() function."); } } - catch (InvalidOperationException) - { - // this will happen if we did not find the subscription, let's give the user a decent error message - throw new Exception($"Please register to receive data for symbol \'{symbol}\' using the AddSecurity() function."); - } return subscription; } + /// + /// Gets the subscription for the given symbol and optional tick type + /// + /// True if a subscription was found for the symbol; false otherwise + private bool TryGetSubscription(Symbol symbol, TickType? tickType, out SubscriptionDataConfig subscription) + { + // deterministic ordering is required here + var subscriptions = SubscriptionManager.SubscriptionDataConfigService + .GetSubscriptionDataConfigs(symbol) + // make sure common lean data types are at the bottom + .OrderByDescending(x => LeanData.IsCommonLeanDataType(x.Type)) + .ThenBy(x => x.TickType) + .ToList(); + + // find our subscription + subscription = subscriptions.FirstOrDefault(x => tickType == null || tickType == x.TickType) + // if we can't locate the exact subscription by tick type just grab the first one we find + ?? subscriptions.FirstOrDefault(); + return subscription != null; + } + /// /// Creates and registers a new consolidator to receive automatic updates at the specified resolution as well as configures /// the indicator to receive updates from the consolidator. diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index 7fd23b7624eb..6bd29151e320 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -1278,12 +1278,9 @@ private Security GetSecurityForOrder(Symbol symbol) if (security == null || !security.IsTradable) { // Try to add and seed the security, but don't is it's a canonical symbol - if (!isCanonical && + if (CanAutoAddSecurity(symbol) && // Indexes are not tradable by default - symbol.SecurityType != SecurityType.Index && - (!symbol.HasUnderlying || - (symbol.SecurityType.IsOption() && !OptionSymbol.IsOptionContractExpired(symbol, UtcTime)) || - (symbol.SecurityType == SecurityType.Future && !FuturesExpiryUtilityFunctions.IsFutureContractExpired(symbol, UtcTime, MarketHoursDatabase)))) + symbol.SecurityType != SecurityType.Index) { // Send one time warning security = AddSecurity(symbol); @@ -1301,6 +1298,21 @@ private Security GetSecurityForOrder(Symbol symbol) "and cannot be re-added due to it being delisted or no longer tradable."); } + /// + /// Determines whether the given symbol can be automatically added to the algorithm on the user's behalf, + /// so that it does not need to be explicitly subscribed to before being used. This is the case both when + /// submitting an order (see ) and when registering an indicator or + /// consolidator for a symbol the user has not subscribed to. Canonical symbols (universes) and expired + /// option/future contracts are excluded. + /// + private bool CanAutoAddSecurity(Symbol symbol) + { + return !symbol.IsCanonical() && + (!symbol.HasUnderlying || + (symbol.SecurityType.IsOption() && !OptionSymbol.IsOptionContractExpired(symbol, UtcTime)) || + (symbol.SecurityType == SecurityType.Future && !FuturesExpiryUtilityFunctions.IsFutureContractExpired(symbol, UtcTime, MarketHoursDatabase))); + } + /// /// Liquidate your portfolio holdings ///