diff --git a/Common/SymbolRepresentation.cs b/Common/SymbolRepresentation.cs
index b88c00e16be5..42e56c48f9fb 100644
--- a/Common/SymbolRepresentation.cs
+++ b/Common/SymbolRepresentation.cs
@@ -46,7 +46,12 @@ public class FutureTickerProperties
///
/// Short expiration year
///
- public int ExpirationYearShort { get; set; }
+ public int ExpirationYearShort { get; set; }
+
+ ///
+ /// Short expiration year digits
+ ///
+ public int ExpirationYearShortLength { get; set; }
///
/// Expiration month
@@ -133,6 +138,7 @@ public static FutureTickerProperties ParseFutureTicker(string ticker)
{
Underlying = underlyingString,
ExpirationYearShort = expirationYearShort,
+ ExpirationYearShortLength = expirationYearString.Length,
ExpirationMonth = expirationMonth,
ExpirationDay = expirationDay
};
@@ -153,9 +159,8 @@ public static Symbol ParseFutureSymbol(string ticker, int? futureYear = null)
}
var underlying = parsed.Underlying;
- var expirationYearShort = parsed.ExpirationYearShort;
var expirationMonth = parsed.ExpirationMonth;
- var expirationYear = futureYear ?? GetExpirationYear(expirationYearShort);
+ var expirationYear = GetExpirationYear(futureYear, parsed);
if (!SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(underlying, SecurityType.Future, out var market))
{
@@ -467,12 +472,35 @@ public static OptionTickerProperties ParseOptionTickerIQFeed(string ticker)
/// Get the expiration year from short year (two-digit integer).
/// Examples: NQZ23 and NQZ3 for Dec 2023
///
+ /// Clarifies the year for the current future
/// Year in 2 digits format (23 represents 2023)
/// Tickers from live trading may not provide the four-digit year.
- private static int GetExpirationYear(int shortYear)
+ private static int GetExpirationYear(int? futureYear, FutureTickerProperties parsed)
{
- var baseYear = shortYear > 9 ? 2000 : 10 * (int)Math.Floor(DateTime.UtcNow.Year / 10d);
- return baseYear + shortYear;
+ if(futureYear.HasValue)
+ {
+ var referenceYear = 1900 + parsed.ExpirationYearShort;
+ while(referenceYear < futureYear.Value)
+ {
+ referenceYear += 10;
+ }
+
+ return referenceYear;
+ }
+
+ var currentYear = DateTime.UtcNow.Year;
+ if (parsed.ExpirationYearShortLength > 1)
+ {
+ // we are given a double digit year
+ return 2000 + parsed.ExpirationYearShort;
+ }
+
+ var baseYear = ((int)Math.Round(currentYear / 10.0)) * 10 + parsed.ExpirationYearShort;
+ while (baseYear < currentYear)
+ {
+ baseYear += 10;
+ }
+ return baseYear;
}
}
}
diff --git a/Tests/Common/SymbolRepresentationTests.cs b/Tests/Common/SymbolRepresentationTests.cs
index 8966ce8747be..5701e6cb789c 100644
--- a/Tests/Common/SymbolRepresentationTests.cs
+++ b/Tests/Common/SymbolRepresentationTests.cs
@@ -211,13 +211,48 @@ public void GenerateFutureTickerExpiringInNextMonth(string ticker, int year, int
Assert.AreEqual(expectedValue, result);
}
- [TestCase("VXZ2", 2012, 19)]
- [TestCase("VXZ2", null, 21)]
- public void GenerateFutureSymbolFromTickerMissingDecadeInfo(string ticker, int? futureYear, int day)
+ [TestCase("CLU0", 2008, "2010-08-20")]
+ [TestCase("CLU1", 2008, "2011-08-22")]
+ [TestCase("CLU2", 2008, "2012-08-21")]
+ [TestCase("CLU8", 2008, "2008-08-20")]
+ [TestCase("CLU9", 2008, "2009-08-20")]
+ public void GenerateFutureSymbolFromTickerKnownYearSingleDigit(string ticker, int futureYear, DateTime expectedExpiration)
{
var result = SymbolRepresentation.ParseFutureSymbol(ticker, futureYear);
- // When the future year is not provided, we have an ambiguous case (2002, 2012, 1902, etc) and default current decade 2020
- Assert.AreEqual(new DateTime(futureYear ?? 2022, 12, day), result.ID.Date.Date);
+ Assert.AreEqual(expectedExpiration, result.ID.Date.Date);
+ }
+
+ [TestCase("CLU20", 2020, "2020-08-20")]
+ [TestCase("CLU21", 2020, "2021-08-20")]
+ [TestCase("CLU22", 2020, "2022-08-22")]
+ [TestCase("CLU28", 2020, "2028-08-22")]
+ [TestCase("CLU29", 2020, "2029-08-21")]
+ public void GenerateFutureSymbolFromTickerUnknownYearSingleDigit(string ticker, int futureYear, DateTime expectedExpiration)
+ {
+ var result = SymbolRepresentation.ParseFutureSymbol(ticker, futureYear);
+ Assert.AreEqual(expectedExpiration, result.ID.Date.Date);
+ }
+
+ [TestCase("CLU20", "2020-08-20")]
+ [TestCase("CLU21", "2021-08-20")]
+ [TestCase("CLU22", "2022-08-22")]
+ [TestCase("CLU28", "2028-08-22")]
+ [TestCase("CLU29", "2029-08-21")]
+ public void GenerateFutureSymbolFromTickerUnknownYearSingleDigit(string ticker, DateTime expectedExpiration)
+ {
+ var result = SymbolRepresentation.ParseFutureSymbol(ticker);
+ Assert.AreEqual(expectedExpiration, result.ID.Date.Date);
+ }
+
+ [TestCase("CLU0", "2030-08-20")]
+ [TestCase("CLU1", "2031-08-20")]
+ [TestCase("CLU2", "2032-08-20")]
+ [TestCase("CLU8", "2028-08-22")]
+ [TestCase("CLU9", "2029-08-21")]
+ public void GenerateFutureSymbolFromTickerUnknownYearDoubleDigit(string ticker, DateTime expectedExpiration)
+ {
+ var result = SymbolRepresentation.ParseFutureSymbol(ticker);
+ Assert.AreEqual(expectedExpiration, result.ID.Date.Date);
}
[TestCase("NQZ23")]