diff --git a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj
index 1a3b46d2d5ad..8009b278947e 100644
--- a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj
+++ b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj
@@ -32,7 +32,7 @@
portable
-
+
diff --git a/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj b/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj
index 014cc475babc..302d5b87c80d 100644
--- a/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj
+++ b/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj
@@ -29,7 +29,7 @@
LICENSE
-
+
diff --git a/Algorithm.Framework/Selection/ScheduledUniverseSelectionModel.cs b/Algorithm.Framework/Selection/ScheduledUniverseSelectionModel.cs
index 8b1f5d1e87ce..3b870f79fa1d 100644
--- a/Algorithm.Framework/Selection/ScheduledUniverseSelectionModel.cs
+++ b/Algorithm.Framework/Selection/ScheduledUniverseSelectionModel.cs
@@ -90,7 +90,12 @@ public ScheduledUniverseSelectionModel(IDateRule dateRule, ITimeRule timeRule, P
public ScheduledUniverseSelectionModel(DateTimeZone timeZone, IDateRule dateRule, ITimeRule timeRule, PyObject selector, UniverseSettings settings = null)
{
Func func;
- selector.TrySafeAs(out func);
+ if (!selector.TrySafeAs(out func))
+ {
+ throw new ArgumentException(
+ $"Unable to create a scheduled universe selector from {selector.ToDisplayString()}: it is not a function. " +
+ "Please provide a function that takes the firing date time and returns the selected symbols.");
+ }
_timeZone = timeZone;
_dateRule = dateRule;
_timeRule = timeRule;
diff --git a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj
index e30aeddd4d3e..d215f6a243c6 100644
--- a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj
+++ b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs
index e433603c9074..9859cbc4094b 100644
--- a/Algorithm/QCAlgorithm.Python.cs
+++ b/Algorithm/QCAlgorithm.Python.cs
@@ -1375,7 +1375,18 @@ public void SetBenchmark(PyObject benchmark)
SetBenchmark(pyBenchmark);
return;
}
- SetBenchmark((Symbol)benchmark.AsManagedObject(typeof(Symbol)));
+
+ try
+ {
+ SetBenchmark((Symbol)benchmark.AsManagedObject(typeof(Symbol)));
+ }
+ catch (InvalidCastException exception)
+ {
+ throw new ArgumentException(
+ $"Unable to set the benchmark from {benchmark.ToDisplayString()}: it is not a supported benchmark type. " +
+ MethodSignatureFormatter.FormatOverloads(typeof(QCAlgorithm).GetMethods().Where(m => m.Name == nameof(SetBenchmark))),
+ exception);
+ }
}
}
diff --git a/Algorithm/QuantConnect.Algorithm.csproj b/Algorithm/QuantConnect.Algorithm.csproj
index 747400eb0079..d995d6f34e10 100644
--- a/Algorithm/QuantConnect.Algorithm.csproj
+++ b/Algorithm/QuantConnect.Algorithm.csproj
@@ -29,7 +29,7 @@
LICENSE
-
+
diff --git a/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj b/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj
index af31988a3c76..30ca6c159c72 100644
--- a/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj
+++ b/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj
@@ -28,7 +28,7 @@
LICENSE
-
+
diff --git a/Common/Data/Consolidators/BaseDataConsolidator.cs b/Common/Data/Consolidators/BaseDataConsolidator.cs
index 3e8dc6a87fba..c0b4f5740f40 100644
--- a/Common/Data/Consolidators/BaseDataConsolidator.cs
+++ b/Common/Data/Consolidators/BaseDataConsolidator.cs
@@ -25,7 +25,7 @@ namespace QuantConnect.Data.Consolidators
public class BaseDataConsolidator : TradeBarConsolidatorBase
{
///
- /// Create a new TickConsolidator for the desired resolution
+ /// Create a new BaseDataConsolidator for the desired resolution
///
/// The resolution desired
/// A consolidator that produces data on the resolution interval
diff --git a/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs b/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs
index 683cf60166a9..1f6f69e29c03 100644
--- a/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs
+++ b/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs
@@ -103,8 +103,20 @@ protected PeriodCountConsolidatorBase(Func func)
///
/// Python object that defines either a function object that defines the start time of a consolidated data or a timespan
protected PeriodCountConsolidatorBase(PyObject pyObject)
- : this(GetPeriodSpecificationFromPyObject(pyObject))
{
+ try
+ {
+ _periodSpecification = GetPeriodSpecificationFromPyObject(pyObject);
+ }
+ catch (InvalidCastException exception)
+ {
+ var type = GetType();
+ throw new ArgumentException(
+ $"Unable to create a consolidator period from {pyObject.ToDisplayString()}: it is not a supported period type. " +
+ MethodSignatureFormatter.FormatOverloads(type.GetConstructors(), displayName: type.Name),
+ exception);
+ }
+ _period = _periodSpecification.Period;
}
///
diff --git a/Common/Data/Consolidators/QuoteBarConsolidator.cs b/Common/Data/Consolidators/QuoteBarConsolidator.cs
index d3018585aa95..c7de8407f268 100644
--- a/Common/Data/Consolidators/QuoteBarConsolidator.cs
+++ b/Common/Data/Consolidators/QuoteBarConsolidator.cs
@@ -25,6 +25,16 @@ namespace QuantConnect.Data.Consolidators
///
public class QuoteBarConsolidator : PeriodCountConsolidatorBase
{
+ ///
+ /// Create a new QuoteBarConsolidator for the desired resolution
+ ///
+ /// The resolution desired
+ /// A consolidator that produces data on the resolution interval
+ public static QuoteBarConsolidator FromResolution(Resolution resolution)
+ {
+ return new QuoteBarConsolidator(resolution.ToTimeSpan());
+ }
+
///
/// Initializes a new instance of the class
///
diff --git a/Common/Data/DynamicData.cs b/Common/Data/DynamicData.cs
index bc2ed9c68ff1..c9fadf8e4509 100644
--- a/Common/Data/DynamicData.cs
+++ b/Common/Data/DynamicData.cs
@@ -59,7 +59,7 @@ public object SetProperty(string name, object value)
{
if (value is PyObject pyobject)
{
- Time = pyobject.As();
+ Time = ConvertPropertyValue(pyobject, name);
}
else
{
@@ -70,7 +70,7 @@ public object SetProperty(string name, object value)
{
if (value is PyObject pyobject)
{
- EndTime = pyobject.As();
+ EndTime = ConvertPropertyValue(pyobject, name);
}
else
{
@@ -81,7 +81,7 @@ public object SetProperty(string name, object value)
{
if (value is PyObject pyobject)
{
- Value = pyobject.As();
+ Value = ConvertPropertyValue(pyobject, name);
}
else
{
@@ -98,7 +98,7 @@ public object SetProperty(string name, object value)
{
if (value is PyObject pyobject)
{
- Symbol = pyobject.As();
+ Symbol = ConvertPropertyValue(pyobject, name);
}
else
{
@@ -196,5 +196,22 @@ public override BaseData Clone()
}
return clone;
}
+
+ ///
+ /// Converts the given Python value into the type expected by the reserved property with the given name
+ ///
+ private static T ConvertPropertyValue(PyObject value, string name)
+ {
+ try
+ {
+ return value.As();
+ }
+ catch (InvalidCastException exception)
+ {
+ throw new ArgumentException(
+ $"Unable to set the '{name}' property from {value.ToDisplayString()}: it is not a supported {typeof(T).Name} value.",
+ exception);
+ }
+ }
}
}
diff --git a/Common/Data/UniverseSelection/ScheduledUniverse.cs b/Common/Data/UniverseSelection/ScheduledUniverse.cs
index 195ed7f6b5db..01f6078e73c6 100644
--- a/Common/Data/UniverseSelection/ScheduledUniverse.cs
+++ b/Common/Data/UniverseSelection/ScheduledUniverse.cs
@@ -72,7 +72,12 @@ public ScheduledUniverse(IDateRule dateRule, ITimeRule timeRule, Func>(out var func);
+ if (!selector.TrySafeAs>(out var func))
+ {
+ throw new ArgumentException(
+ $"Unable to create a scheduled universe selector from {selector.ToDisplayString()}: it is not a function. " +
+ "Please provide a function that takes the firing date time and returns the selected symbols.");
+ }
_dateRule = dateRule;
_timeRule = timeRule;
_selector = func.ConvertSelectionSymbolDelegate();
diff --git a/Common/Extensions.cs b/Common/Extensions.cs
index 1a4be7977fff..be982b6c8f16 100644
--- a/Common/Extensions.cs
+++ b/Common/Extensions.cs
@@ -3039,6 +3039,20 @@ public static bool TryConvert(this PyObject pyObject, out T result, bool allo
return false;
}
+ ///
+ /// Gets a string representation of the given Python object and its type
+ /// to be used in user-facing messages, e.g. "'Daily' of type 'Resolution'"
+ ///
+ /// The Python object to represent
+ /// The string representation of the Python object
+ public static string ToDisplayString(this PyObject pyObject)
+ {
+ using (Py.GIL())
+ {
+ return $"'{pyObject}' of type '{pyObject.GetPythonType().Name}'";
+ }
+ }
+
///
/// Safely convert PyObject to ManagedObject using Py.GIL Lock
/// If no type is given it will convert the PyObject's Python Type to a ManagedObject Type
diff --git a/Common/QuantConnect.csproj b/Common/QuantConnect.csproj
index d0c834c7cc5d..bb5c50be53da 100644
--- a/Common/QuantConnect.csproj
+++ b/Common/QuantConnect.csproj
@@ -35,7 +35,7 @@
-
+
diff --git a/Common/Securities/Security.cs b/Common/Securities/Security.cs
index 808acff852a8..99976451e14f 100644
--- a/Common/Securities/Security.cs
+++ b/Common/Securities/Security.cs
@@ -992,7 +992,7 @@ public bool TryGet(string key, out T value)
{
if (Cache.Properties.TryGetValue(key, out var obj))
{
- value = CastDynamicPropertyValue(obj);
+ value = CastDynamicPropertyValue(key, obj);
return true;
}
value = default;
@@ -1007,7 +1007,7 @@ public bool TryGet(string key, out T value)
/// If the property is not found
public T Get(string key)
{
- return CastDynamicPropertyValue(Cache.Properties[key]);
+ return CastDynamicPropertyValue(key, Cache.Properties[key]);
}
///
@@ -1032,7 +1032,7 @@ public bool Remove(string key, out T value)
var result = Cache.Properties.Remove(key, out object objectValue);
if (result)
{
- value = CastDynamicPropertyValue(objectValue);
+ value = CastDynamicPropertyValue(key, objectValue);
}
return result;
}
@@ -1158,7 +1158,7 @@ private void UpdateMarketPrice(BaseData data)
/// Casts a dynamic property value to the specified type.
/// Useful for cases where the property value is a PyObject and we want to cast it to the underlying type.
///
- private static T CastDynamicPropertyValue(object obj)
+ private static T CastDynamicPropertyValue(string key, object obj)
{
T value;
var pyObj = obj as PyObject;
@@ -1166,7 +1166,16 @@ private static T CastDynamicPropertyValue(object obj)
{
using (Py.GIL())
{
- value = pyObj.As();
+ try
+ {
+ value = pyObj.As();
+ }
+ catch (InvalidCastException exception)
+ {
+ throw new InvalidCastException(
+ $"Unable to get the '{key}' property from {pyObj.ToDisplayString()}: it is not a supported {typeof(T).Name} value.",
+ exception);
+ }
}
}
else
diff --git a/Engine/QuantConnect.Lean.Engine.csproj b/Engine/QuantConnect.Lean.Engine.csproj
index 7dd671028788..e70c73386c02 100644
--- a/Engine/QuantConnect.Lean.Engine.csproj
+++ b/Engine/QuantConnect.Lean.Engine.csproj
@@ -41,7 +41,7 @@
-
+
diff --git a/Indicators/QuantConnect.Indicators.csproj b/Indicators/QuantConnect.Indicators.csproj
index f2685a85d8bf..1363a93227ee 100644
--- a/Indicators/QuantConnect.Indicators.csproj
+++ b/Indicators/QuantConnect.Indicators.csproj
@@ -31,7 +31,7 @@
-
+
diff --git a/Report/QuantConnect.Report.csproj b/Report/QuantConnect.Report.csproj
index 0f6f22714589..4c32859eac44 100644
--- a/Report/QuantConnect.Report.csproj
+++ b/Report/QuantConnect.Report.csproj
@@ -39,7 +39,7 @@
LICENSE
-
+
diff --git a/Research/QuantConnect.Research.csproj b/Research/QuantConnect.Research.csproj
index 6ec7766df3ff..a8d59a4f5c4f 100644
--- a/Research/QuantConnect.Research.csproj
+++ b/Research/QuantConnect.Research.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/Tests/Algorithm/AlgorithmBenchmarkTests.cs b/Tests/Algorithm/AlgorithmBenchmarkTests.cs
index 7850633d0494..5d012667b09c 100644
--- a/Tests/Algorithm/AlgorithmBenchmarkTests.cs
+++ b/Tests/Algorithm/AlgorithmBenchmarkTests.cs
@@ -18,6 +18,7 @@
using System.Collections.Generic;
using NodaTime;
using NUnit.Framework;
+using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Configuration;
using QuantConnect.Interfaces;
@@ -124,6 +125,30 @@ public void MisalignedBenchmarkAndAlgorithmTimeZones(Resolution resolution, bool
}
}
+ [Test]
+ public void PythonSetBenchmarkThrowsDescriptiveErrorForUnsupportedBenchmarkType()
+ {
+ var algorithm = new QCAlgorithm();
+ var dataManager = new DataManagerStub(algorithm, new MockDataFeed());
+ algorithm.SubscriptionManager.SetDataManager(dataManager);
+
+ using (Py.GIL())
+ {
+ // Not a benchmark producing function nor a symbol
+ using var pyBenchmark = Resolution.Daily.ToPython();
+ var exception = Assert.Throws(() => algorithm.SetBenchmark(pyBenchmark));
+
+ // The value rendering is case-insensitive to support both pythonnet enum str() conventions ("Daily" and "DAILY")
+ Assert.That(exception.Message, Does.Contain("Daily").IgnoreCase);
+ Assert.That(exception.Message, Does.Contain("'Resolution'"));
+ Assert.That(exception.Message, Does.Contain("The following overloads are available:"));
+ Assert.That(exception.Message, Does.Contain("set_benchmark("));
+ // The PyObject overload that just rejected the value is not hinted
+ Assert.That(exception.Message, Does.Not.Contain("benchmark: Any"));
+ Assert.That(exception.InnerException, Is.TypeOf());
+ }
+ }
+
[Test]
public void BenchmarkIsNotInitializeWithCustomSecurityInitializer()
{
diff --git a/Tests/Algorithm/Framework/Alphas/BasePairsTradingAlphaModelTests.cs b/Tests/Algorithm/Framework/Alphas/BasePairsTradingAlphaModelTests.cs
index dac461d301de..4d7e70accf49 100644
--- a/Tests/Algorithm/Framework/Alphas/BasePairsTradingAlphaModelTests.cs
+++ b/Tests/Algorithm/Framework/Alphas/BasePairsTradingAlphaModelTests.cs
@@ -44,7 +44,7 @@ protected override void InitializeAlgorithm(QCAlgorithm algorithm)
protected override string GetExpectedModelName(IAlphaModel model)
{
- return $"{nameof(BasePairsTradingAlphaModel)}({_lookback},{_resolution},1)";
+ return $"{nameof(BasePairsTradingAlphaModel)}({_lookback},{GetEnumString(_resolution, model)},1)";
}
protected override IAlphaModel CreatePythonAlphaModel()
diff --git a/Tests/Algorithm/Framework/Alphas/CommonAlphaModelTests.cs b/Tests/Algorithm/Framework/Alphas/CommonAlphaModelTests.cs
index 10b47ca55b75..4db6e487bc66 100644
--- a/Tests/Algorithm/Framework/Alphas/CommonAlphaModelTests.cs
+++ b/Tests/Algorithm/Framework/Alphas/CommonAlphaModelTests.cs
@@ -26,6 +26,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Python;
using QuantConnect.Tests.Common.Data.UniverseSelection;
@@ -262,6 +263,17 @@ public void ModelNameTest(Language language)
///
protected abstract string GetExpectedModelName(IAlphaModel model);
+ ///
+ /// Gets the display string of the given enum value as rendered in the model's language:
+ /// Python models render enum values in the Python convention, e.g. Resolution.Daily as DAILY
+ ///
+ protected static string GetEnumString(Enum value, IAlphaModel model)
+ {
+ return model is AlphaModelPythonWrapper
+ ? value.ToString().ToSnakeCase(constant: true)
+ : value.ToString();
+ }
+
///
/// Provides derived types a chance to initialize anything special they require
///
diff --git a/Tests/Algorithm/Framework/Alphas/ConstantAlphaModelTests.cs b/Tests/Algorithm/Framework/Alphas/ConstantAlphaModelTests.cs
index e2c61f36225f..fb1b351fc91d 100644
--- a/Tests/Algorithm/Framework/Alphas/ConstantAlphaModelTests.cs
+++ b/Tests/Algorithm/Framework/Alphas/ConstantAlphaModelTests.cs
@@ -93,7 +93,7 @@ protected override IEnumerable ExpectedInsights()
protected override string GetExpectedModelName(IAlphaModel model)
{
- return Invariant($"{nameof(ConstantAlphaModel)}({_type},{_direction},{_period},{_magnitude})");
+ return Invariant($"{nameof(ConstantAlphaModel)}({GetEnumString(_type, model)},{GetEnumString(_direction, model)},{_period},{_magnitude})");
}
}
}
diff --git a/Tests/Algorithm/Framework/Alphas/EmaCrossAlphaModelTests.cs b/Tests/Algorithm/Framework/Alphas/EmaCrossAlphaModelTests.cs
index 452d106e807e..e7adf56c136b 100644
--- a/Tests/Algorithm/Framework/Alphas/EmaCrossAlphaModelTests.cs
+++ b/Tests/Algorithm/Framework/Alphas/EmaCrossAlphaModelTests.cs
@@ -53,7 +53,7 @@ protected override IEnumerable ExpectedInsights()
protected override string GetExpectedModelName(IAlphaModel model)
{
- return $"{nameof(EmaCrossAlphaModel)}(12,26,Daily)";
+ return $"{nameof(EmaCrossAlphaModel)}(12,26,{GetEnumString(Resolution.Daily, model)})";
}
[Test]
diff --git a/Tests/Algorithm/Framework/Alphas/MacdAlphaModelTests.cs b/Tests/Algorithm/Framework/Alphas/MacdAlphaModelTests.cs
index fc8ba2ef837f..b381d2269073 100644
--- a/Tests/Algorithm/Framework/Alphas/MacdAlphaModelTests.cs
+++ b/Tests/Algorithm/Framework/Alphas/MacdAlphaModelTests.cs
@@ -20,6 +20,7 @@
using System.Collections.Generic;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Algorithm.Framework.Selection;
+using QuantConnect.Indicators;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Util;
@@ -53,7 +54,7 @@ protected override IEnumerable ExpectedInsights()
protected override string GetExpectedModelName(IAlphaModel model)
{
- return $"{nameof(MacdAlphaModel)}(12,26,9,Exponential,Daily)";
+ return $"{nameof(MacdAlphaModel)}(12,26,9,{GetEnumString(MovingAverageType.Exponential, model)},{GetEnumString(Resolution.Daily, model)})";
}
[Test]
diff --git a/Tests/Algorithm/Framework/Alphas/RsiAlphaModelTests.cs b/Tests/Algorithm/Framework/Alphas/RsiAlphaModelTests.cs
index cb828c5a016b..f8e4acbdba11 100644
--- a/Tests/Algorithm/Framework/Alphas/RsiAlphaModelTests.cs
+++ b/Tests/Algorithm/Framework/Alphas/RsiAlphaModelTests.cs
@@ -47,7 +47,7 @@ protected override IEnumerable ExpectedInsights()
protected override string GetExpectedModelName(IAlphaModel model)
{
- return $"{nameof(RsiAlphaModel)}(14,Daily)";
+ return $"{nameof(RsiAlphaModel)}(14,{GetEnumString(Resolution.Daily, model)})";
}
}
}
diff --git a/Tests/Common/Data/DynamicDataTests.cs b/Tests/Common/Data/DynamicDataTests.cs
index 26d822f0b337..dbc3fbe877e7 100644
--- a/Tests/Common/Data/DynamicDataTests.cs
+++ b/Tests/Common/Data/DynamicDataTests.cs
@@ -16,6 +16,7 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
+using Python.Runtime;
using QuantConnect.Data;
namespace QuantConnect.Tests.Common.Data
@@ -69,6 +70,36 @@ public void StoresBaseDataValues_Using_BaseDataProperties()
Assert.AreEqual(symbol, data.Symbol);
}
+ [Test]
+ public void SettingReservedPropertyWithUnsupportedPythonValueThrowsDescriptiveError()
+ {
+ var data = new DataType();
+ using (Py.GIL())
+ {
+ using var pyString = "not a valid value".ToPython();
+ using var pyInt = 123.ToPython();
+
+ var exception = Assert.Throws(() => data.SetProperty("time", pyString));
+ Assert.That(exception.Message, Does.Contain("'time'"));
+ Assert.That(exception.Message, Does.Contain("'str'"));
+ Assert.That(exception.Message, Does.Contain(nameof(DateTime)));
+ Assert.That(exception.InnerException, Is.TypeOf());
+
+ exception = Assert.Throws(() => data.SetProperty("end_time", pyString));
+ Assert.That(exception.Message, Does.Contain("'end_time'"));
+ Assert.That(exception.Message, Does.Contain(nameof(DateTime)));
+
+ exception = Assert.Throws(() => data.SetProperty("value", pyString));
+ Assert.That(exception.Message, Does.Contain("'value'"));
+ Assert.That(exception.Message, Does.Contain(nameof(Decimal)));
+
+ exception = Assert.Throws(() => data.SetProperty("symbol", pyInt));
+ Assert.That(exception.Message, Does.Contain("'symbol'"));
+ Assert.That(exception.Message, Does.Contain("'int'"));
+ Assert.That(exception.Message, Does.Contain(nameof(Symbol)));
+ }
+ }
+
[Test]
public void AccessingPropertyThatDoesNotExist_ThrowsKeyNotFoundException()
{
diff --git a/Tests/Common/Data/PeriodCountConsolidatorTests.cs b/Tests/Common/Data/PeriodCountConsolidatorTests.cs
index c0a96c6b0e83..ba9984c3f891 100644
--- a/Tests/Common/Data/PeriodCountConsolidatorTests.cs
+++ b/Tests/Common/Data/PeriodCountConsolidatorTests.cs
@@ -17,6 +17,7 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
+using Python.Runtime;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Market;
@@ -379,6 +380,46 @@ public void ConsolidatorShouldConsolidateOnMaxCountAndUseLastEndTime(Type consol
Assert.AreEqual(expectedEndTime, consolidated.EndTime);
}
+ [Test]
+ public void PyObjectConstructorThrowsDescriptiveErrorForUnsupportedPeriodType()
+ {
+ using (Py.GIL())
+ {
+ // A Resolution enum value is neither a timedelta nor a callable returning CalendarInfo,
+ // so the consolidator cannot create a period specification from it
+ using var pyResolution = Resolution.Daily.ToPython();
+ var exception = Assert.Throws(() => new QuoteBarConsolidator(pyResolution));
+
+ // The error must state the source value and its type and list the available constructor overloads.
+ // The value rendering is case-insensitive to support both pythonnet enum str() conventions ("Daily" and "DAILY")
+ Assert.That(exception.Message, Does.Contain("Daily").IgnoreCase);
+ Assert.That(exception.Message, Does.Contain("Resolution"));
+ Assert.That(exception.Message, Does.Contain("The following overloads are available:"));
+ Assert.That(exception.Message, Does.Contain("QuoteBarConsolidator(period: timedelta"));
+ Assert.That(exception.Message, Does.Contain("QuoteBarConsolidator(max_count: int"));
+ // The PyObject overload that just rejected the value is not hinted
+ Assert.That(exception.Message, Does.Not.Contain("pyfuncobj"));
+ Assert.That(exception.InnerException, Is.TypeOf());
+ }
+ }
+
+ [Test]
+ public void QuoteBarConsolidatorFromResolutionCreatesConsolidatorWithExpectedPeriod()
+ {
+ var time = new DateTime(2015, 04, 13);
+ QuoteBar consolidated = null;
+ using var consolidator = QuoteBarConsolidator.FromResolution(Resolution.Minute);
+ consolidator.DataConsolidated += (sender, bar) => consolidated = bar;
+
+ consolidator.Update(new QuoteBar { Time = time, Period = Time.OneSecond });
+ Assert.IsNull(consolidated);
+
+ consolidator.Update(new QuoteBar { Time = time.AddMinutes(1), Period = Time.OneSecond });
+ Assert.IsNotNull(consolidated);
+ Assert.AreEqual(Time.OneMinute, consolidated.Period);
+ Assert.AreEqual(time, consolidated.Time);
+ }
+
private static void PushBarsThrough(int barCount, TimeSpan period, TradeBarConsolidator consolidator, ref DateTime time)
{
TradeBar bar;
diff --git a/Tests/Common/Data/UniverseSelection/ScheduledUniverseTests.cs b/Tests/Common/Data/UniverseSelection/ScheduledUniverseTests.cs
index 727d82d16373..64d862383051 100644
--- a/Tests/Common/Data/UniverseSelection/ScheduledUniverseTests.cs
+++ b/Tests/Common/Data/UniverseSelection/ScheduledUniverseTests.cs
@@ -18,6 +18,7 @@
using System.Linq;
using NodaTime;
using NUnit.Framework;
+using Python.Runtime;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
@@ -102,6 +103,21 @@ public void TimeTriggeredDoesNotReturnTimesAfterEndTime()
Assert.IsEmpty(triggerTimes);
}
+ [Test]
+ public void PythonConstructorThrowsDescriptiveErrorWhenSelectorIsNotAFunction()
+ {
+ using (Py.GIL())
+ {
+ using var pySelector = "not a function".ToPython();
+ var exception = Assert.Throws(() =>
+ new ScheduledUniverse(_dateRules.EveryDay(), _timeRules.At(12, 0), pySelector));
+
+ Assert.That(exception.Message, Does.Contain("'not a function'"));
+ Assert.That(exception.Message, Does.Contain("'str'"));
+ Assert.That(exception.Message, Does.Contain("it is not a function"));
+ }
+ }
+
[Test]
public void TriggerTimesNone()
{
diff --git a/Tests/Common/Securities/SecurityTests.cs b/Tests/Common/Securities/SecurityTests.cs
index fc5b37b309e4..fcea2d214590 100644
--- a/Tests/Common/Securities/SecurityTests.cs
+++ b/Tests/Common/Securities/SecurityTests.cs
@@ -460,6 +460,22 @@ public void SetsAndGetsDynamicCustomPropertiesUsingGenericInterface()
Assert.Throws(() => security.Get("EMA"));
}
+ [Test]
+ public void GettingPythonCustomPropertyWithIncompatibleTypeThrowsDescriptiveError()
+ {
+ var security = GetSecurity();
+ using (Py.GIL())
+ {
+ security.Set("StringProperty", "a string value".ToPython());
+
+ var exception = Assert.Throws(() => security.Get("StringProperty"));
+ Assert.That(exception.Message, Does.Contain("'StringProperty'"));
+ Assert.That(exception.Message, Does.Contain("'str'"));
+ Assert.That(exception.Message, Does.Contain(nameof(Decimal)));
+ Assert.That(exception.InnerException, Is.TypeOf());
+ }
+ }
+
[Test]
public void SetsAndGetsDynamicCustomPropertiesUsingIndexer()
{
diff --git a/Tests/QuantConnect.Tests.csproj b/Tests/QuantConnect.Tests.csproj
index 2d82e0578471..ecbde9c74c8f 100644
--- a/Tests/QuantConnect.Tests.csproj
+++ b/Tests/QuantConnect.Tests.csproj
@@ -31,7 +31,7 @@
-
+