From 04fcb1ffac949c76488f4f791cacc0a8b0fc3d52 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 10 Aug 2026 10:41:07 -0400 Subject: [PATCH 1/2] Improve error messages for datetime vs date operations in Python algorithms --- ...ateComparisonPythonExceptionInterpreter.cs | 59 +++++++++ ...portedOperandPythonExceptionInterpreter.cs | 4 + Common/Messages/Messages.Exceptions.cs | 24 ++++ ...mparisonPythonExceptionInterpreterTests.cs | 114 ++++++++++++++++++ ...dOperandPythonExceptionInterpreterTests.cs | 40 ++++++ .../Test_PythonExceptionInterpreter.py | 9 ++ 6 files changed, 250 insertions(+) create mode 100644 Common/Exceptions/DatetimeDateComparisonPythonExceptionInterpreter.cs create mode 100644 Tests/Common/Exceptions/DatetimeDateComparisonPythonExceptionInterpreterTests.cs diff --git a/Common/Exceptions/DatetimeDateComparisonPythonExceptionInterpreter.cs b/Common/Exceptions/DatetimeDateComparisonPythonExceptionInterpreter.cs new file mode 100644 index 000000000000..11874ff5961c --- /dev/null +++ b/Common/Exceptions/DatetimeDateComparisonPythonExceptionInterpreter.cs @@ -0,0 +1,59 @@ +/* + * 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 Python.Runtime; +using QuantConnect.Util; + +namespace QuantConnect.Exceptions +{ + /// + /// Interprets TypeError exceptions raised when comparing datetime.datetime and datetime.date objects + /// + public class DatetimeDateComparisonPythonExceptionInterpreter : PythonExceptionInterpreter + { + /// + /// Determines the order that an instance of this class should be called + /// + public override int Order => 0; + + /// + /// Determines if this interpreter should be applied to the specified exception. + /// + /// The exception to check + /// True if the exception can be interpreted, false otherwise + public override bool CanInterpret(Exception exception) + { + return base.CanInterpret(exception) && + exception.Message.Contains(Messages.DatetimeDateComparisonPythonExceptionInterpreter.CantCompareExpectedSubstring); + } + + /// + /// Interprets the specified exception into a new exception + /// + /// The exception to be interpreted + /// An interpreter that should be applied to the inner exception. + /// The interpreted exception + public override Exception Interpret(Exception exception, IExceptionInterpreter innerInterpreter) + { + var pe = (PythonException)exception; + + var message = Messages.DatetimeDateComparisonPythonExceptionInterpreter.InvalidDatetimeDateComparison; + message += PythonUtil.PythonExceptionStackParser(pe.StackTrace); + + return new Exception(message, pe); + } + } +} diff --git a/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreter.cs b/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreter.cs index ee788db9a3e5..a429bc0dd9cc 100644 --- a/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreter.cs +++ b/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreter.cs @@ -52,6 +52,10 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i var types = pe.Message.Split(':')[1].Trim(); var message = Messages.UnsupportedOperandPythonExceptionInterpreter.InvalidObjectTypesForOperation(types); + if (types.Contains("'datetime.datetime'") && types.Contains("'datetime.date'")) + { + message += Messages.UnsupportedOperandPythonExceptionInterpreter.DatetimeDateOperationHint; + } message += PythonUtil.PythonExceptionStackParser(pe.StackTrace); return new Exception(message, pe); diff --git a/Common/Messages/Messages.Exceptions.cs b/Common/Messages/Messages.Exceptions.cs index 1cda211c8f05..716d9eeef239 100644 --- a/Common/Messages/Messages.Exceptions.cs +++ b/Common/Messages/Messages.Exceptions.cs @@ -203,6 +203,30 @@ public static string InvalidObjectTypesForOperation(string types) return $@"Trying to perform a summation, subtraction, multiplication or division between { types} objects throws a TypeError exception. To prevent the exception, ensure that both values share the same type."; } + + /// + /// Hint appended when the invalid operands are a datetime.datetime and a datetime.date + /// + public static string DatetimeDateOperationHint = + " To operate between them, use the date part of the datetime value, e.g.: (expiry.date() - today).days or self.time.date()."; + } + + /// + /// Provides user-facing messages for the class and its consumers or related classes + /// + public static class DatetimeDateComparisonPythonExceptionInterpreter + { + /// + /// Expected substring of the TypeError raised when comparing a datetime.datetime with a datetime.date + /// + public static string CantCompareExpectedSubstring = "can't compare datetime.datetime to datetime.date"; + + /// + /// User-facing message for datetime.datetime vs datetime.date comparisons + /// + public static string InvalidDatetimeDateComparison = + "Trying to compare 'datetime.datetime' and 'datetime.date' objects throws a TypeError exception. " + + "To prevent the exception, compare using the date part of the datetime value, e.g.: self.time.date() <= some_date."; } } } diff --git a/Tests/Common/Exceptions/DatetimeDateComparisonPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/DatetimeDateComparisonPythonExceptionInterpreterTests.cs new file mode 100644 index 000000000000..c62acdd3fd99 --- /dev/null +++ b/Tests/Common/Exceptions/DatetimeDateComparisonPythonExceptionInterpreterTests.cs @@ -0,0 +1,114 @@ +/* + * 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 NUnit.Framework; +using NUnit.Framework.Constraints; +using Python.Runtime; +using QuantConnect.Exceptions; +using System; +using System.Collections.Generic; + +namespace QuantConnect.Tests.Common.Exceptions +{ + [TestFixture] + public class DatetimeDateComparisonPythonExceptionInterpreterTests + { + private PythonException _comparisonException; + private PythonException _unsupportedOperandException; + + [OneTimeSetUp] + public void Setup() + { + using (Py.GIL()) + { + var module = Py.Import("Test_PythonExceptionInterpreter"); + dynamic algorithm = module.GetAttr("Test_PythonExceptionInterpreter").Invoke(); + + try + { + // x = datetime(2020, 1, 2) < date(2020, 1, 1) + algorithm.datetime_date_comparison(); + } + catch (PythonException pythonException) + { + _comparisonException = pythonException; + } + + try + { + // x = None + "Pepe Grillo" + algorithm.unsupported_operand(); + } + catch (PythonException pythonException) + { + _unsupportedOperandException = pythonException; + } + } + } + + [Test] + public void StackInterpreterRewritesComparisonErrorWithDateHint() + { + var interpreter = StackExceptionInterpreter.CreateFromAssemblies(); + var interpreted = interpreter.Interpret(_comparisonException, NullExceptionInterpreter.Instance); + Assert.IsTrue(interpreted.Message.Contains(".date()"), interpreted.Message); + Assert.IsTrue(interpreted.Message.Contains("x = datetime(2020, 1, 2) < date(2020, 1, 1)"), interpreted.Message); + } + + [Test] + [TestCase(typeof(Exception), ExpectedResult = false)] + [TestCase(typeof(KeyNotFoundException), ExpectedResult = false)] + [TestCase(typeof(DivideByZeroException), ExpectedResult = false)] + [TestCase(typeof(InvalidOperationException), ExpectedResult = false)] + [TestCase(typeof(PythonException), ExpectedResult = true)] + public bool CanInterpretReturnsTrueForOnlyDatetimeDateComparisonPythonExceptionType(Type exceptionType) + { + var exception = CreateExceptionFromType(exceptionType); + return new DatetimeDateComparisonPythonExceptionInterpreter().CanInterpret(exception); + } + + [Test] + public void CanInterpretReturnsFalseForOtherTypeErrors() + { + Assert.IsFalse(new DatetimeDateComparisonPythonExceptionInterpreter().CanInterpret(_unsupportedOperandException)); + Assert.IsFalse(new UnsupportedOperandPythonExceptionInterpreter().CanInterpret(_comparisonException)); + } + + [Test] + [TestCase(typeof(Exception), true)] + [TestCase(typeof(KeyNotFoundException), true)] + [TestCase(typeof(DivideByZeroException), true)] + [TestCase(typeof(InvalidOperationException), true)] + [TestCase(typeof(PythonException), false)] + public void InterpretThrowsForNonDatetimeDateComparisonPythonExceptionTypes(Type exceptionType, bool expectThrow) + { + var exception = CreateExceptionFromType(exceptionType); + var interpreter = new DatetimeDateComparisonPythonExceptionInterpreter(); + var constraint = expectThrow ? (IResolveConstraint)Throws.Exception : Throws.Nothing; + Assert.That(() => interpreter.Interpret(exception, NullExceptionInterpreter.Instance), constraint); + } + + [Test] + public void InterpretedMessageContainsGuidanceAndStackInformation() + { + var interpreted = new DatetimeDateComparisonPythonExceptionInterpreter().Interpret(_comparisonException, NullExceptionInterpreter.Instance); + Assert.IsTrue(interpreted.Message.Contains( + Messages.DatetimeDateComparisonPythonExceptionInterpreter.InvalidDatetimeDateComparison), interpreted.Message); + Assert.IsTrue(interpreted.Message.Contains("x = datetime(2020, 1, 2) < date(2020, 1, 1)"), interpreted.Message); + } + + private Exception CreateExceptionFromType(Type type) => type == typeof(PythonException) ? _comparisonException : (Exception)Activator.CreateInstance(type); + } +} diff --git a/Tests/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreterTests.cs index 94211cfd8396..778221e44599 100644 --- a/Tests/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreterTests.cs +++ b/Tests/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreterTests.cs @@ -26,6 +26,8 @@ namespace QuantConnect.Tests.Common.Exceptions public class UnsupportedOperandPythonExceptionInterpreterTests { private PythonException _pythonException; + private PythonException _datetimeDateException; + private PythonException _dateDatetimeException; [OneTimeSetUp] public void Setup() @@ -44,6 +46,26 @@ public void Setup() { _pythonException = pythonException; } + + try + { + // x = datetime(2020, 1, 2) - date(2020, 1, 1) + algorithm.unsupported_operand_datetime_date(); + } + catch (PythonException pythonException) + { + _datetimeDateException = pythonException; + } + + try + { + // x = date(2020, 1, 1) - datetime(2020, 1, 2) + algorithm.unsupported_operand_date_datetime(); + } + catch (PythonException pythonException) + { + _dateDatetimeException = pythonException; + } } } @@ -73,6 +95,24 @@ public void InterpretThrowsForNonUnsupportedOperandPythonExceptionTypes(Type exc Assert.That(() => interpreter.Interpret(exception, NullExceptionInterpreter.Instance), constraint); } + [Test] + [TestCase(true)] + [TestCase(false)] + public void AppendsDateHintForDatetimeDateOperands(bool datetimeFirst) + { + var exception = datetimeFirst ? _datetimeDateException : _dateDatetimeException; + var interpreted = new UnsupportedOperandPythonExceptionInterpreter().Interpret(exception, NullExceptionInterpreter.Instance); + Assert.IsTrue(interpreted.Message.Contains("'datetime.datetime'"), interpreted.Message); + Assert.IsTrue(interpreted.Message.Contains(".date()"), interpreted.Message); + } + + [Test] + public void DoesNotAppendDateHintForOtherOperands() + { + var interpreted = new UnsupportedOperandPythonExceptionInterpreter().Interpret(_pythonException, NullExceptionInterpreter.Instance); + Assert.IsFalse(interpreted.Message.Contains(".date()"), interpreted.Message); + } + [Test] public void VerifyMessageContainsStackTraceInformation() { diff --git a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py index de3b05ca8faf..4d20ffc4cfbf 100644 --- a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py +++ b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py @@ -32,6 +32,15 @@ def no_method_match_rsi(self): def unsupported_operand(self): x = None + "Pepe Grillo" + def unsupported_operand_datetime_date(self): + x = datetime(2020, 1, 2) - date(2020, 1, 1) + + def unsupported_operand_date_datetime(self): + x = date(2020, 1, 1) - datetime(2020, 1, 2) + + def datetime_date_comparison(self): + x = datetime(2020, 1, 2) < date(2020, 1, 1) + def module_not_found(self): from MissingClrNamespace.Distributions import Normal From 49b22a21238abf4c8f26760cb234413e66520d72 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 10 Aug 2026 12:54:22 -0400 Subject: [PATCH 2/2] Update QuantConnect.pythonnet to 2.0.65 --- Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj | 2 +- Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj | 2 +- Algorithm.Python/QuantConnect.Algorithm.Python.csproj | 2 +- Algorithm/QuantConnect.Algorithm.csproj | 2 +- AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj | 2 +- Common/QuantConnect.csproj | 2 +- Engine/QuantConnect.Lean.Engine.csproj | 2 +- Indicators/QuantConnect.Indicators.csproj | 2 +- Report/QuantConnect.Report.csproj | 2 +- Research/QuantConnect.Research.csproj | 2 +- Tests/QuantConnect.Tests.csproj | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj index 1a9bd1c466e9..c22f4d262335 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 0c20783ae5f8..0131d30b2c21 100644 --- a/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj +++ b/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj @@ -29,7 +29,7 @@ LICENSE - + diff --git a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj index b5939b6a817d..9341efe4b8e2 100644 --- a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj +++ b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj @@ -37,7 +37,7 @@ - + diff --git a/Algorithm/QuantConnect.Algorithm.csproj b/Algorithm/QuantConnect.Algorithm.csproj index 0b2931f58763..0cc17213cef0 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 c4e5848c699b..9ff2db7fbe10 100644 --- a/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj +++ b/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj @@ -28,7 +28,7 @@ LICENSE - + diff --git a/Common/QuantConnect.csproj b/Common/QuantConnect.csproj index 2d9b796f8afe..a55aff0af6d6 100644 --- a/Common/QuantConnect.csproj +++ b/Common/QuantConnect.csproj @@ -35,7 +35,7 @@ - + diff --git a/Engine/QuantConnect.Lean.Engine.csproj b/Engine/QuantConnect.Lean.Engine.csproj index 42ca0fe600b5..f64896534a28 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 5d0d7d18056c..f8b9d4acdbfd 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 5643b8e99dcb..1b5bf132458f 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 43f1ec2b1b5b..e35e4d0b0ee6 100644 --- a/Research/QuantConnect.Research.csproj +++ b/Research/QuantConnect.Research.csproj @@ -34,7 +34,7 @@ - + diff --git a/Tests/QuantConnect.Tests.csproj b/Tests/QuantConnect.Tests.csproj index 5c558b6ef752..8ecf12b6f247 100644 --- a/Tests/QuantConnect.Tests.csproj +++ b/Tests/QuantConnect.Tests.csproj @@ -31,7 +31,7 @@ - +