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