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 ///