Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions Algorithm/QCAlgorithm.Python.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
Expand All @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -284,6 +284,21 @@ private Security AddDataImpl(Type dataType, Symbol symbol, Resolution? resolutio
return AddToUserDefinedUniverse(security, new List<SubscriptionDataConfig> { config });
}

/// <summary>
/// Resolves the custom data <see cref="Type"/> from the PyObject argument of <see cref="AddData(PyObject, string, Resolution?)"/> and overloads,
/// throwing a clear exception if the caller passed something other than a custom data class (e.g. a string ticker).
/// </summary>
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()));
}

/// <summary>
/// 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 (<see cref="TimeZones.NewYork"/>)
Expand Down
10 changes: 10 additions & 0 deletions Common/Messages/Messages.Algorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ public static string SetWarmupAlreadyInitialized()
{
return $"{AlgorithmPrefix()}.{FormatCode("SetWarmup")}(): This method cannot be used after algorithm initialized";
}

/// <summary>
/// Returns a string message saying the first argument to AddData must be a custom data class
/// </summary>
[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")}.";
}
}

/// <summary>
Expand Down
17 changes: 17 additions & 0 deletions Tests/Algorithm/AlgorithmAddDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ArgumentException>(() => qcAlgorithm.AddData(pyTicker, "VIX", Resolution.Daily));
StringAssert.Contains("AddData", ex.Message);
StringAssert.Contains("AddEquity", ex.Message);
}

[Test]
public void AppendsCustomDataTypeName_ToSecurityIdentifierSymbol()
{
Expand Down
Loading