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 @@ - +