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