diff --git a/Algorithm.CSharp/ContractDaysToExpiryRegressionAlgorithm.cs b/Algorithm.CSharp/ContractDaysToExpiryRegressionAlgorithm.cs
new file mode 100644
index 000000000000..4afad3cfc586
--- /dev/null
+++ b/Algorithm.CSharp/ContractDaysToExpiryRegressionAlgorithm.cs
@@ -0,0 +1,140 @@
+/*
+ * 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 System.Collections.Generic;
+using QuantConnect.Data;
+using QuantConnect.Data.Market;
+using QuantConnect.Interfaces;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Regression algorithm asserting the behavior of and ,
+ /// the supported alternative to manual expiry math mixing datetime and date values.
+ /// The Python version also asserts that the reference argument accepts both datetime and date instances.
+ ///
+ public class ContractDaysToExpiryRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ private Symbol _optionSymbol;
+ private int _contractsValidated;
+
+ public override void Initialize()
+ {
+ SetStartDate(2015, 12, 24);
+ SetEndDate(2015, 12, 24);
+ SetCash(100000);
+
+ var option = AddOption("GOOG");
+ option.SetFilter(u => u.Strikes(-2, +2).Expiration(0, 180));
+ _optionSymbol = option.Symbol;
+ }
+
+ public override void OnData(Slice slice)
+ {
+ OptionChain chain;
+ if (!slice.OptionChains.TryGetValue(_optionSymbol, out chain))
+ {
+ return;
+ }
+
+ foreach (var contract in chain)
+ {
+ var expected = (contract.Expiry.Date - Time.Date).Days;
+ if (contract.DaysToExpiry() != expected)
+ {
+ throw new RegressionTestException($"Expected DaysToExpiry() to be {expected} but was {contract.DaysToExpiry()}");
+ }
+ if (contract.DTE != expected)
+ {
+ throw new RegressionTestException($"Expected DTE to be {expected} but was {contract.DTE}");
+ }
+ if (contract.DaysToExpiry(Time.AddDays(-10)) != expected + 10)
+ {
+ throw new RegressionTestException($"Expected DaysToExpiry(reference) to be {expected + 10} but was {contract.DaysToExpiry(Time.AddDays(-10))}");
+ }
+ _contractsValidated++;
+ }
+ }
+
+ public override void OnEndOfAlgorithm()
+ {
+ if (_contractsValidated == 0)
+ {
+ throw new RegressionTestException("No contracts were validated");
+ }
+ }
+
+ ///
+ /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
+ ///
+ public bool CanRunLocally { get; } = true;
+
+ ///
+ /// This is used by the regression test system to indicate which languages this algorithm is written in.
+ ///
+ public List Languages { get; } = new() { Language.CSharp, Language.Python };
+
+ ///
+ /// Data Points count of all timeslices of algorithm
+ ///
+ public long DataPoints => 37131;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => 0;
+
+ ///
+ /// Final status of the algorithm
+ ///
+ public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
+
+ ///
+ /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
+ ///
+ public Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "0"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "0%"},
+ {"Drawdown", "0%"},
+ {"Expectancy", "0"},
+ {"Start Equity", "100000"},
+ {"End Equity", "100000"},
+ {"Net Profit", "0%"},
+ {"Sharpe Ratio", "0"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "0%"},
+ {"Loss Rate", "0%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "0"},
+ {"Beta", "0"},
+ {"Annual Standard Deviation", "0"},
+ {"Annual Variance", "0"},
+ {"Information Ratio", "0"},
+ {"Tracking Error", "0"},
+ {"Treynor Ratio", "0"},
+ {"Total Fees", "$0.00"},
+ {"Estimated Strategy Capacity", "$0"},
+ {"Lowest Capacity Asset", ""},
+ {"Portfolio Turnover", "0%"},
+ {"Drawdown Recovery", "0"},
+ {"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
+ };
+ }
+}
diff --git a/Algorithm.Python/ContractDaysToExpiryRegressionAlgorithm.py b/Algorithm.Python/ContractDaysToExpiryRegressionAlgorithm.py
new file mode 100644
index 000000000000..fb5f344476c5
--- /dev/null
+++ b/Algorithm.Python/ContractDaysToExpiryRegressionAlgorithm.py
@@ -0,0 +1,55 @@
+# 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.
+
+from AlgorithmImports import *
+
+###
+### Regression algorithm asserting the behavior of 'days_to_expiry' and 'dte' on option/future contracts,
+### the supported alternative to manual expiry math mixing datetime and date values.
+### It also asserts that the reference argument accepts both datetime and date instances.
+###
+class ContractDaysToExpiryRegressionAlgorithm(QCAlgorithm):
+ def initialize(self):
+ self.set_start_date(2015, 12, 24)
+ self.set_end_date(2015, 12, 24)
+ self.set_cash(100000)
+
+ option = self.add_option("GOOG")
+ option.set_filter(lambda u: u.strikes(-2, +2).expiration(0, 180))
+ self._option_symbol = option.symbol
+ self._contracts_validated = 0
+
+ def on_data(self, slice):
+ chain = slice.option_chains.get(self._option_symbol)
+ if not chain:
+ return
+
+ for contract in chain:
+ # The manual shape, with the operand types correctly aligned
+ expected = (contract.expiry.date() - self.time.date()).days
+ if contract.days_to_expiry() != expected:
+ raise AssertionError(f"Expected days_to_expiry() to be {expected} but was {contract.days_to_expiry()}")
+ if contract.dte != expected:
+ raise AssertionError(f"Expected dte to be {expected} but was {contract.dte}")
+ # The reference argument accepts both datetime and date instances
+ if contract.days_to_expiry(self.time) != expected:
+ raise AssertionError(f"Expected days_to_expiry(datetime) to be {expected} but was {contract.days_to_expiry(self.time)}")
+ if contract.days_to_expiry(self.time.date()) != expected:
+ raise AssertionError(f"Expected days_to_expiry(date) to be {expected} but was {contract.days_to_expiry(self.time.date())}")
+ if contract.days_to_expiry(reference=self.time.date() - timedelta(days=10)) != expected + 10:
+ raise AssertionError(f"Expected days_to_expiry(reference=date) to be {expected + 10}")
+ self._contracts_validated += 1
+
+ def on_end_of_algorithm(self):
+ if self._contracts_validated == 0:
+ raise AssertionError("No contracts were validated")
diff --git a/Common/AlgorithmImports.py b/Common/AlgorithmImports.py
index 35cda517a1ca..33cf2ab5f3ae 100644
--- a/Common/AlgorithmImports.py
+++ b/Common/AlgorithmImports.py
@@ -95,6 +95,7 @@
pass
from datetime import date, time, datetime, timedelta
+from zoneinfo import ZoneInfo
from typing import *
import math
import json
diff --git a/Common/Data/Market/BaseContract.cs b/Common/Data/Market/BaseContract.cs
index 19110435d8f0..a573b57ec014 100644
--- a/Common/Data/Market/BaseContract.cs
+++ b/Common/Data/Market/BaseContract.cs
@@ -48,6 +48,34 @@ public Symbol Symbol
///
public DateTime Expiry => Symbol.ID.Date;
+ ///
+ /// Gets the number of whole days until the contract expires, based on the contract's current time.
+ /// Shorthand alias of
+ ///
+ [PandasIgnore]
+ public int DTE => DaysToExpiry();
+
+ ///
+ /// Gets the number of whole days between the contract's current time () and
+ ///
+ /// The number of whole days until the contract expires
+ public int DaysToExpiry()
+ {
+ return DaysToExpiry(Time);
+ }
+
+ ///
+ /// Gets the number of whole days between the given reference and .
+ /// From Python, the reference accepts both datetime and date instances, avoiding the
+ /// TypeError users hit when manually mixing them, e.g. (contract.expiry - self.time.date()).days
+ ///
+ /// The date to measure from. Only the date part is used
+ /// The number of whole days from the given reference until the contract expires
+ public int DaysToExpiry(DateTime reference)
+ {
+ return (Expiry.Date - reference.Date).Days;
+ }
+
///
/// Gets the local date time this contract's data was last updated
///
diff --git a/Common/Exceptions/GenericTypeParameterPythonExceptionInterpreter.cs b/Common/Exceptions/GenericTypeParameterPythonExceptionInterpreter.cs
new file mode 100644
index 000000000000..f6b6d4f2a55c
--- /dev/null
+++ b/Common/Exceptions/GenericTypeParameterPythonExceptionInterpreter.cs
@@ -0,0 +1,62 @@
+/*
+ * 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 the TypeError exceptions pythonnet raises when a .NET generic type or Array is indexed with
+ /// something that is not a .NET type, e.g. 'RollingWindow[datetime](10)' with Python's datetime type
+ ///
+ public class GenericTypeParameterPythonExceptionInterpreter : 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)
+ {
+ // "type(s) expected" is raised for generic types, "type expected" for Array
+ return base.CanInterpret(exception) &&
+ (exception.Message.Contains(Messages.GenericTypeParameterPythonExceptionInterpreter.TypesExpectedSubstring) ||
+ exception.Message.Contains(Messages.GenericTypeParameterPythonExceptionInterpreter.TypeExpectedSubstring));
+ }
+
+ ///
+ /// 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.GenericTypeParameterPythonExceptionInterpreter.InvalidGenericTypeParameter;
+ message += PythonUtil.PythonExceptionStackParser(pe.StackTrace);
+
+ return new Exception(message, pe);
+ }
+ }
+}
diff --git a/Common/Exceptions/TzInfoPythonExceptionInterpreter.cs b/Common/Exceptions/TzInfoPythonExceptionInterpreter.cs
new file mode 100644
index 000000000000..36e1f2d23313
--- /dev/null
+++ b/Common/Exceptions/TzInfoPythonExceptionInterpreter.cs
@@ -0,0 +1,63 @@
+/*
+ * 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 caused by passing a Lean time zone (a NodaTime DateTimeZone, like the
+ /// values) where Python expects a tzinfo instance, e.g. 'datetime.now(TimeZones.NEW_YORK)'
+ ///
+ public class TzInfoPythonExceptionInterpreter : 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)
+ {
+ // CPython raises "tzinfo argument must be None or of a tzinfo subclass, not type 'CachedDateTimeZone'".
+ // Only interpret it when the offending type is a NodaTime time zone, so the hint below is accurate
+ return base.CanInterpret(exception) &&
+ exception.Message.Contains(Messages.TzInfoPythonExceptionInterpreter.TzInfoSubclassExpectedSubstring) &&
+ exception.Message.Contains(Messages.TzInfoPythonExceptionInterpreter.DateTimeZoneTypeSubstring);
+ }
+
+ ///
+ /// 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.TzInfoPythonExceptionInterpreter.LeanTimeZoneUsedAsTzInfo;
+ message += PythonUtil.PythonExceptionStackParser(pe.StackTrace);
+
+ return new Exception(message, pe);
+ }
+ }
+}
diff --git a/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreter.cs b/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreter.cs
index ee788db9a3e5..4080f8851583 100644
--- a/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreter.cs
+++ b/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreter.cs
@@ -14,6 +14,7 @@
*/
using System;
+using System.Text.RegularExpressions;
using Python.Runtime;
using QuantConnect.Util;
@@ -36,8 +37,15 @@ public class UnsupportedOperandPythonExceptionInterpreter : PythonExceptionInter
/// True if the exception can be interpreted, false otherwise
public override bool CanInterpret(Exception exception)
{
- return base.CanInterpret(exception) &&
- exception.Message.Contains(Messages.UnsupportedOperandPythonExceptionInterpreter.UnsupportedOperandTypeExpectedSubstring);
+ if (!base.CanInterpret(exception))
+ {
+ return false;
+ }
+ return exception.Message.Contains(Messages.UnsupportedOperandPythonExceptionInterpreter.UnsupportedOperandTypeExpectedSubstring) ||
+ // "can't compare datetime.datetime to datetime.date", the ordering flavor of mixing datetimes and dates,
+ // e.g. 'self.time.date() <= some_stored_datetime'
+ (exception.Message.Contains(Messages.UnsupportedOperandPythonExceptionInterpreter.CannotCompareTypesSubstring) &&
+ HasDatetimeAndDateOperands(exception.Message));
}
///
@@ -50,11 +58,37 @@ public override Exception Interpret(Exception exception, IExceptionInterpreter i
{
var pe = (PythonException)exception;
- var types = pe.Message.Split(':')[1].Trim();
- var message = Messages.UnsupportedOperandPythonExceptionInterpreter.InvalidObjectTypesForOperation(types);
+ string message;
+ if (pe.Message.Contains(Messages.UnsupportedOperandPythonExceptionInterpreter.UnsupportedOperandTypeExpectedSubstring))
+ {
+ var types = pe.Message.Split(':')[1].Trim();
+ message = Messages.UnsupportedOperandPythonExceptionInterpreter.InvalidObjectTypesForOperation(types);
+ }
+ else
+ {
+ // "can't compare {left} to {right}"
+ var match = Regex.Match(pe.Message, @"can't compare (?\S+) to (?\S+)");
+ var types = match.Success
+ ? $"'{match.Groups["left"].Value}' and '{match.Groups["right"].Value}'"
+ : "'datetime.datetime' and 'datetime.date'";
+ message = Messages.UnsupportedOperandPythonExceptionInterpreter.InvalidObjectTypesForComparison(types);
+ }
+
+ if (HasDatetimeAndDateOperands(pe.Message))
+ {
+ // The single most common shape of this error is expiry math like '(contract.id.date - self.time.date()).days',
+ // so point users at the supported alternatives
+ message += Messages.UnsupportedOperandPythonExceptionInterpreter.DatetimeAndDateOperandsHint;
+ }
message += PythonUtil.PythonExceptionStackParser(pe.StackTrace);
return new Exception(message, pe);
}
+
+ private static bool HasDatetimeAndDateOperands(string message)
+ {
+ // "datetime.date" is a prefix of "datetime.datetime", so require a word boundary after ".date"
+ return Regex.IsMatch(message, @"datetime\.datetime\b") && Regex.IsMatch(message, @"datetime\.date\b");
+ }
}
}
diff --git a/Common/Messages/Messages.Exceptions.cs b/Common/Messages/Messages.Exceptions.cs
index 1cda211c8f05..815b0ccb84a8 100644
--- a/Common/Messages/Messages.Exceptions.cs
+++ b/Common/Messages/Messages.Exceptions.cs
@@ -194,6 +194,19 @@ public static class UnsupportedOperandPythonExceptionInterpreter
///
public static string UnsupportedOperandTypeExpectedSubstring = "unsupported operand type";
+ ///
+ /// Substring of the TypeError CPython raises when ordering datetime.datetime against datetime.date,
+ /// e.g. "can't compare datetime.datetime to datetime.date"
+ ///
+ public static string CannotCompareTypesSubstring = "can't compare";
+
+ ///
+ /// Additional guidance appended when the offending operands are datetime.datetime and datetime.date
+ ///
+ public static string DatetimeAndDateOperandsHint =
+ " When mixing datetime and date values, align the types first, e.g. 'self.time.date()'." +
+ " For expiry math, option and future contracts provide 'contract.days_to_expiry(reference)', which accepts both types, and 'contract.dte'.";
+
///
/// Returns a message for invalid object types for operation
///
@@ -203,6 +216,65 @@ 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.";
}
+
+ ///
+ /// Returns a message for an invalid comparison between two object types
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string InvalidObjectTypesForComparison(string types)
+ {
+ return $"Trying to compare {types} objects throws a TypeError exception. To prevent the exception, ensure that both values share the same type.";
+ }
+ }
+
+ ///
+ /// Provides user-facing messages for the class and its consumers or related classes
+ ///
+ public static class TzInfoPythonExceptionInterpreter
+ {
+ ///
+ /// Substring of the TypeError CPython raises when a non-tzinfo object is passed as a tzinfo argument
+ ///
+ public static string TzInfoSubclassExpectedSubstring = "of a tzinfo subclass";
+
+ ///
+ /// Substring identifying NodaTime time zone types, e.g. 'CachedDateTimeZone', the type of the TimeZones values
+ ///
+ public static string DateTimeZoneTypeSubstring = "DateTimeZone";
+
+ ///
+ /// Message explaining that Lean time zones are not Python tzinfo instances and pointing at zoneinfo
+ ///
+ public static string LeanTimeZoneUsedAsTzInfo =
+ "Trying to use a Lean time zone like 'TimeZones.NEW_YORK' where Python expects a tzinfo instance throws a TypeError exception," +
+ " because Lean time zones are NodaTime DateTimeZone objects intended for Lean APIs." +
+ " Use Python's zoneinfo module instead, e.g. 'datetime.now(ZoneInfo(\"America/New_York\"))'." +
+ " 'ZoneInfo' is imported by AlgorithmImports, and 'ZoneInfo(str(TimeZones.NEW_YORK))' converts any Lean time zone.";
+ }
+
+ ///
+ /// Provides user-facing messages for the class and its consumers or related classes
+ ///
+ public static class GenericTypeParameterPythonExceptionInterpreter
+ {
+ ///
+ /// The TypeError message pythonnet raises when a generic type is indexed with something that is not a .NET type
+ ///
+ public static string TypesExpectedSubstring = "type(s) expected";
+
+ ///
+ /// The TypeError message pythonnet raises when the Array type is indexed with something that is not a .NET type
+ ///
+ public static string TypeExpectedSubstring = "type expected";
+
+ ///
+ /// Message explaining that Python types cannot parameterize .NET generic types and pointing at the alternatives
+ ///
+ public static string InvalidGenericTypeParameter =
+ "Trying to parameterize a .NET generic type with a Python type throws a TypeError exception." +
+ " Python types like 'datetime' cannot be used as generic type parameters: use a .NET type, e.g. 'from System import DateTime'" +
+ " then 'RollingWindow[DateTime](10)', or one of the supported aliases int, float, bool and str." +
+ " The untyped 'RollingWindow(10)' also accepts values of any type.";
}
}
}
diff --git a/Tests/Common/Data/Market/FuturesContractTests.cs b/Tests/Common/Data/Market/FuturesContractTests.cs
index 0c8dba318459..6b0c390348f3 100644
--- a/Tests/Common/Data/Market/FuturesContractTests.cs
+++ b/Tests/Common/Data/Market/FuturesContractTests.cs
@@ -111,6 +111,19 @@ public void PriceValueAndCloseAliasLastPrice()
Assert.AreEqual(futureContract.LastPrice, futureContract.Close);
}
+ [Test]
+ public void DaysToExpiryFromContractTimeAndExplicitReference()
+ {
+ // Future_CLF19_Jan2019 expires on 2018-12-19
+ var futureContract = new FuturesContract(Symbols.Future_CLF19_Jan2019) { Time = new DateTime(2018, 12, 10, 17, 0, 0) };
+
+ Assert.AreEqual(9, futureContract.DaysToExpiry());
+ Assert.AreEqual(futureContract.DaysToExpiry(), futureContract.DTE);
+
+ Assert.AreEqual(1, futureContract.DaysToExpiry(new DateTime(2018, 12, 18)));
+ Assert.AreEqual(-1, futureContract.DaysToExpiry(new DateTime(2018, 12, 20, 23, 59, 59)));
+ }
+
[Test]
public void OpenInterest()
{
diff --git a/Tests/Common/Data/Market/OptionContractTests.cs b/Tests/Common/Data/Market/OptionContractTests.cs
index 024821ba548c..3bb6ccaa05f4 100644
--- a/Tests/Common/Data/Market/OptionContractTests.cs
+++ b/Tests/Common/Data/Market/OptionContractTests.cs
@@ -15,6 +15,7 @@
using System;
using NUnit.Framework;
+using Python.Runtime;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Securities;
@@ -69,5 +70,46 @@ public void PriceValueAndCloseAliasLastPrice()
Assert.AreEqual(contract.LastPrice, contract.Value);
Assert.AreEqual(contract.LastPrice, contract.Close);
}
+
+ [Test]
+ public void DaysToExpiryFromContractTimeAndExplicitReference()
+ {
+ // SPY_C_192_Feb19_2016 expires on 2016-02-19
+ var symbol = Symbols.SPY_C_192_Feb19_2016;
+ var contract = new OptionContract(CreateOption(symbol)) { Time = new DateTime(2016, 02, 16, 9, 30, 0) };
+
+ // Default reference is the contract's current time, only the date parts matter
+ Assert.AreEqual(3, contract.DaysToExpiry());
+ Assert.AreEqual(contract.DaysToExpiry(), contract.DTE);
+
+ Assert.AreEqual(30, contract.DaysToExpiry(new DateTime(2016, 01, 20)));
+ Assert.AreEqual(0, contract.DaysToExpiry(new DateTime(2016, 02, 19, 23, 59, 59)));
+ Assert.AreEqual(-2, contract.DaysToExpiry(new DateTime(2016, 02, 21)));
+ }
+
+ [Test]
+ public void DaysToExpiryAcceptsPythonDateAndDatetimeReferences()
+ {
+ var symbol = Symbols.SPY_C_192_Feb19_2016;
+ var contract = new OptionContract(CreateOption(symbol)) { Time = new DateTime(2016, 02, 16, 9, 30, 0) };
+
+ using (Py.GIL())
+ {
+ dynamic getDte = PyModule.FromString("OptionContractTests_DaysToExpiry", @"
+from datetime import date, datetime
+
+def get_dte(contract):
+ return (contract.days_to_expiry(), contract.dte, contract.days_to_expiry(date(2016, 1, 20)),
+ contract.days_to_expiry(datetime(2016, 2, 19, 23, 59)), contract.days_to_expiry(reference=date(2016, 2, 21)))
+").GetAttr("get_dte");
+
+ var result = getDte(contract);
+ Assert.AreEqual(3, (int)result[0]);
+ Assert.AreEqual(3, (int)result[1]);
+ Assert.AreEqual(30, (int)result[2]);
+ Assert.AreEqual(0, (int)result[3]);
+ Assert.AreEqual(-2, (int)result[4]);
+ }
+ }
}
}
diff --git a/Tests/Common/Exceptions/GenericTypeParameterPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/GenericTypeParameterPythonExceptionInterpreterTests.cs
new file mode 100644
index 000000000000..4adf361e8d4f
--- /dev/null
+++ b/Tests/Common/Exceptions/GenericTypeParameterPythonExceptionInterpreterTests.cs
@@ -0,0 +1,60 @@
+/*
+ * 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 Python.Runtime;
+using QuantConnect.Exceptions;
+using System;
+using System.Collections.Generic;
+
+namespace QuantConnect.Tests.Common.Exceptions
+{
+ [TestFixture]
+ public class GenericTypeParameterPythonExceptionInterpreterTests
+ {
+ private PythonException _pythonException;
+
+ [OneTimeSetUp]
+ public void Setup()
+ {
+ // x = RollingWindow[datetime](10)
+ _pythonException = UnsupportedOperandPythonExceptionInterpreterTests.CreatePythonException("python_type_as_generic_type_parameter");
+ }
+
+ [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 CanInterpretReturnsTrueForOnlyGenericTypeParameterPythonExceptionType(Type exceptionType)
+ {
+ var exception = CreateExceptionFromType(exceptionType);
+ return new GenericTypeParameterPythonExceptionInterpreter().CanInterpret(exception);
+ }
+
+ [Test]
+ public void InterpretedMessagePointsAtNetTypesAndUntypedRollingWindow()
+ {
+ var interpreted = new GenericTypeParameterPythonExceptionInterpreter().Interpret(_pythonException, NullExceptionInterpreter.Instance);
+ Assert.True(interpreted.Message.Contains("RollingWindow[DateTime](10)"), interpreted.Message);
+ Assert.True(interpreted.Message.Contains("RollingWindow(10)"), interpreted.Message);
+ // The stack trace should point at the offending line
+ Assert.True(interpreted.Message.Contains("RollingWindow[datetime](10)"), interpreted.Message);
+ }
+
+ private Exception CreateExceptionFromType(Type type) => type == typeof(PythonException) ? _pythonException : (Exception)Activator.CreateInstance(type);
+ }
+}
diff --git a/Tests/Common/Exceptions/TzInfoPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/TzInfoPythonExceptionInterpreterTests.cs
new file mode 100644
index 000000000000..574d3f0aa0bf
--- /dev/null
+++ b/Tests/Common/Exceptions/TzInfoPythonExceptionInterpreterTests.cs
@@ -0,0 +1,81 @@
+/*
+ * 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 Python.Runtime;
+using QuantConnect.Exceptions;
+using System;
+using System.Collections.Generic;
+
+namespace QuantConnect.Tests.Common.Exceptions
+{
+ [TestFixture]
+ public class TzInfoPythonExceptionInterpreterTests
+ {
+ private PythonException _pythonException;
+
+ [OneTimeSetUp]
+ public void Setup()
+ {
+ // x = datetime.now(TimeZones.NEW_YORK)
+ _pythonException = UnsupportedOperandPythonExceptionInterpreterTests.CreatePythonException("lean_time_zone_as_tzinfo");
+ }
+
+ [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 CanInterpretReturnsTrueForOnlyTzInfoPythonExceptionType(Type exceptionType)
+ {
+ var exception = CreateExceptionFromType(exceptionType);
+ return new TzInfoPythonExceptionInterpreter().CanInterpret(exception);
+ }
+
+ [Test]
+ public void InterpretedMessagePointsAtZoneInfo()
+ {
+ var interpreted = new TzInfoPythonExceptionInterpreter().Interpret(_pythonException, NullExceptionInterpreter.Instance);
+ Assert.True(interpreted.Message.Contains("zoneinfo"), interpreted.Message);
+ Assert.True(interpreted.Message.Contains("ZoneInfo(\"America/New_York\")"), interpreted.Message);
+ // The stack trace should point at the offending line
+ Assert.True(interpreted.Message.Contains("datetime.now(TimeZones.NEW_YORK)"), interpreted.Message);
+ }
+
+ [Test]
+ public void DoesNotInterpretOtherTzInfoTypeErrors()
+ {
+ // A non-Lean type as tzinfo should not get the Lean-specific hint
+ PythonException exception = null;
+ using (Py.GIL())
+ {
+ try
+ {
+ PythonEngine.Exec("from datetime import datetime\ndatetime.now('America/New_York')");
+ }
+ catch (PythonException pythonException)
+ {
+ exception = pythonException;
+ }
+ }
+
+ Assert.IsNotNull(exception);
+ Assert.False(new TzInfoPythonExceptionInterpreter().CanInterpret(exception));
+ }
+
+ private Exception CreateExceptionFromType(Type type) => type == typeof(PythonException) ? _pythonException : (Exception)Activator.CreateInstance(type);
+ }
+}
diff --git a/Tests/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreterTests.cs b/Tests/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreterTests.cs
index 94211cfd8396..5a2eb9b2114e 100644
--- a/Tests/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreterTests.cs
+++ b/Tests/Common/Exceptions/UnsupportedOperandPythonExceptionInterpreterTests.cs
@@ -83,6 +83,84 @@ public void VerifyMessageContainsStackTraceInformation()
Assert.True(exception.Message.Contains("x = None + \"Pepe Grillo\""));
}
+ [Test]
+ public void NonDatetimeOperandsDoNotGetTheDatetimeHint()
+ {
+ var interpreted = new UnsupportedOperandPythonExceptionInterpreter()
+ .Interpret(_pythonException, NullExceptionInterpreter.Instance);
+ Assert.False(interpreted.Message.Contains("days_to_expiry"));
+ }
+
+ [Test]
+ public void DatetimeAndDateSubtractionGetsTheDatetimeHint()
+ {
+ // (contract.id.date - self.time.date()).days, the most common fleet shape of this error
+ var exception = CreatePythonException("datetime_and_date_subtraction");
+ var interpreter = new UnsupportedOperandPythonExceptionInterpreter();
+ Assert.True(interpreter.CanInterpret(exception));
+
+ var interpreted = interpreter.Interpret(exception, NullExceptionInterpreter.Instance);
+ Assert.True(interpreted.Message.Contains("'datetime.datetime' and 'datetime.date'"), interpreted.Message);
+ Assert.True(interpreted.Message.Contains("days_to_expiry"), interpreted.Message);
+ Assert.True(interpreted.Message.Contains(".date()"), interpreted.Message);
+ }
+
+ [Test]
+ public void DatetimeAndDateComparisonIsInterpretedWithTheDatetimeHint()
+ {
+ // "can't compare datetime.datetime to datetime.date", e.g. self.time.date() <= some_stored_datetime
+ var exception = CreatePythonException("datetime_and_date_comparison");
+ var interpreter = new UnsupportedOperandPythonExceptionInterpreter();
+ Assert.True(interpreter.CanInterpret(exception));
+
+ var interpreted = interpreter.Interpret(exception, NullExceptionInterpreter.Instance);
+ Assert.True(interpreted.Message.Contains("Trying to compare"), interpreted.Message);
+ Assert.True(interpreted.Message.Contains("'datetime.datetime'"), interpreted.Message);
+ Assert.True(interpreted.Message.Contains("'datetime.date'"), interpreted.Message);
+ Assert.True(interpreted.Message.Contains("days_to_expiry"), interpreted.Message);
+ }
+
+ [Test]
+ public void OtherComparisonTypeErrorsAreNotInterpreted()
+ {
+ // "can't compare offset-naive and offset-aware datetimes" is not the datetime-vs-date shape
+ PythonException exception = null;
+ using (Py.GIL())
+ {
+ try
+ {
+ PythonEngine.Exec("from datetime import datetime, timezone\ndatetime.now() < datetime.now(timezone.utc)");
+ }
+ catch (PythonException pythonException)
+ {
+ exception = pythonException;
+ }
+ }
+
+ Assert.IsNotNull(exception);
+ Assert.False(new UnsupportedOperandPythonExceptionInterpreter().CanInterpret(exception));
+ }
+
+ internal static PythonException CreatePythonException(string methodName)
+ {
+ using (Py.GIL())
+ {
+ var module = Py.Import("Test_PythonExceptionInterpreter");
+ dynamic algorithm = module.GetAttr("Test_PythonExceptionInterpreter").Invoke();
+
+ try
+ {
+ algorithm.InvokeMethod(methodName);
+ }
+ catch (PythonException pythonException)
+ {
+ return pythonException;
+ }
+ }
+
+ throw new InvalidOperationException($"Expected '{methodName}' to throw a PythonException");
+ }
+
private Exception CreateExceptionFromType(Type type) => type == typeof(PythonException) ? _pythonException : (Exception)Activator.CreateInstance(type);
}
}
diff --git a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
index de3b05ca8faf..3e17b4ff807b 100644
--- a/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
+++ b/Tests/RegressionAlgorithms/Test_PythonExceptionInterpreter.py
@@ -32,6 +32,18 @@ def no_method_match_rsi(self):
def unsupported_operand(self):
x = None + "Pepe Grillo"
+ def datetime_and_date_subtraction(self):
+ x = datetime.now() - datetime.now().date()
+
+ def datetime_and_date_comparison(self):
+ x = datetime.now() <= datetime.now().date()
+
+ def lean_time_zone_as_tzinfo(self):
+ x = datetime.now(TimeZones.NEW_YORK)
+
+ def python_type_as_generic_type_parameter(self):
+ x = RollingWindow[datetime](10)
+
def module_not_found(self):
from MissingClrNamespace.Distributions import Normal