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")]