diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index a2af01ea34fc..e433603c9074 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -109,7 +109,7 @@ public Security AddData(PyObject type, Symbol underlying, Resolution? resolution [DocumentationAttribute(AddingData)] public Security AddData(PyObject type, string ticker, Resolution? resolution, DateTimeZone timeZone, bool fillForward = false, decimal leverage = 1.0m) { - return AddData(type.CreateType(), ticker, resolution, timeZone, fillForward, leverage); + return AddData(GetCustomDataType(type), ticker, resolution, timeZone, fillForward, leverage); } /// @@ -135,7 +135,7 @@ public Security AddData(PyObject type, string ticker, Resolution? resolution, Da [DocumentationAttribute(AddingData)] public Security AddData(PyObject type, Symbol underlying, Resolution? resolution, DateTimeZone timeZone, bool fillForward = false, decimal leverage = 1.0m) { - return AddData(type.CreateType(), underlying, resolution, timeZone, fillForward, leverage); + return AddData(GetCustomDataType(type), underlying, resolution, timeZone, fillForward, leverage); } /// @@ -219,7 +219,7 @@ public Security AddData(Type dataType, Symbol underlying, Resolution? resolution public Security AddData(PyObject type, string ticker, SymbolProperties properties, SecurityExchangeHours exchangeHours, Resolution? resolution = null, bool fillForward = false, decimal leverage = 1.0m) { // Get the right key for storage of base type symbols - var dataType = type.CreateType(); + var dataType = GetCustomDataType(type); var key = SecurityIdentifier.GenerateBaseSymbol(dataType, ticker); // Add entries to our Symbol Properties DB and MarketHours DB @@ -284,6 +284,21 @@ private Security AddDataImpl(Type dataType, Symbol symbol, Resolution? resolutio return AddToUserDefinedUniverse(security, new List { config }); } + /// + /// Resolves the custom data from the PyObject argument of and overloads, + /// throwing a clear exception if the caller passed something other than a custom data class (e.g. a string ticker). + /// + private static Type GetCustomDataType(PyObject type) + { + if (type.TryCreateType(out var dataType)) + { + return dataType; + } + + using var _ = Py.GIL(); + throw new ArgumentException(Messages.QCAlgorithm.AddDataInvalidPyObjectType(type.Repr())); + } + /// /// Creates a new universe and adds it to the algorithm. This is for coarse fundamental US Equity data and /// will be executed on day changes in the NewYork time zone () diff --git a/Common/Messages/Messages.Algorithm.cs b/Common/Messages/Messages.Algorithm.cs index 1f4b3e50fb17..288a0197cbcc 100644 --- a/Common/Messages/Messages.Algorithm.cs +++ b/Common/Messages/Messages.Algorithm.cs @@ -89,6 +89,16 @@ public static string SetWarmupAlreadyInitialized() { return $"{AlgorithmPrefix()}.{FormatCode("SetWarmup")}(): This method cannot be used after algorithm initialized"; } + + /// + /// Returns a string message saying the first argument to AddData must be a custom data class + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string AddDataInvalidPyObjectType(string repr) + { + return $"{AlgorithmPrefix()}.{FormatCode("AddData")}(): the first argument must be a custom data type (a Python class deriving from {FormatCode("PythonData")} or a CLR {FormatCode("BaseData")} type), but received {repr}. " + + $"To subscribe to built-in asset classes use, for example, {FormatCode("AddEquity")} or {FormatCode("AddCrypto")}."; + } } /// diff --git a/Tests/Algorithm/AlgorithmAddDataTests.cs b/Tests/Algorithm/AlgorithmAddDataTests.cs index 374c654efd26..d4834147b4aa 100644 --- a/Tests/Algorithm/AlgorithmAddDataTests.cs +++ b/Tests/Algorithm/AlgorithmAddDataTests.cs @@ -19,6 +19,7 @@ using Newtonsoft.Json; using NodaTime; using NUnit.Framework; +using Python.Runtime; using QuantConnect.Algorithm; using QuantConnect.Algorithm.Framework.Selection; using QuantConnect.Algorithm.Selection; @@ -637,6 +638,22 @@ public void AddingInvalidDataTypeThrows() DateTimeZone.Utc)); } + [Test] + public void AddDataWithStringAsTypeArgumentThrowsClearError() + { + var qcAlgorithm = new QCAlgorithm(); + qcAlgorithm.SubscriptionManager.SetDataManager(new DataManagerStub(qcAlgorithm)); + + using var _ = Py.GIL(); + using var pyTicker = "VIX".ToPython(); + + // Passing a string instead of a custom data class as the first argument used to silently build a + // dynamic assembly named after the string and later hang/fail downstream. It must now throw. + var ex = Assert.Throws(() => qcAlgorithm.AddData(pyTicker, "VIX", Resolution.Daily)); + StringAssert.Contains("AddData", ex.Message); + StringAssert.Contains("AddEquity", ex.Message); + } + [Test] public void AppendsCustomDataTypeName_ToSecurityIdentifierSymbol() {