Lean  $LEAN_TAG$
SymbolRepresentation.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Linq;
18 using QuantConnect.Logging;
19 using System.Globalization;
21 using System.Collections.Generic;
25 using static QuantConnect.StringExtensions;
26 using System.Text.RegularExpressions;
28 
29 namespace QuantConnect
30 {
31  /// <summary>
32  /// Public static helper class that does parsing/generation of symbol representations (options, futures)
33  /// </summary>
34  public static class SymbolRepresentation
35  {
36  // Define the regex as a private readonly static field and compile it
37  private static readonly Regex _optionTickerRegex = new Regex(@"^([A-Z0-9]+)\s*(\d{6})([CP])(\d{8})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
38 
39  /// <summary>
40  /// Class contains future ticker properties returned by ParseFutureTicker()
41  /// </summary>
43  {
44  /// <summary>
45  /// Underlying name
46  /// </summary>
47  public string Underlying { get; set; }
48 
49  /// <summary>
50  /// Short expiration year
51  /// </summary>
52  public int ExpirationYearShort { get; set; }
53 
54  /// <summary>
55  /// Short expiration year digits
56  /// </summary>
57  public int ExpirationYearShortLength { get; set; }
58 
59  /// <summary>
60  /// Expiration month
61  /// </summary>
62  public int ExpirationMonth { get; set; }
63 
64  /// <summary>
65  /// Expiration day
66  /// </summary>
67  public int ExpirationDay { get; set; }
68  }
69 
70  /// <summary>
71  /// Class contains option ticker properties returned by ParseOptionTickerIQFeed()
72  /// </summary>
74  {
75  /// <summary>
76  /// Underlying name
77  /// </summary>
78  public string Underlying { get; set; }
79 
80  /// <summary>
81  /// Option right
82  /// </summary>
83  public OptionRight OptionRight { get; set; }
84 
85  /// <summary>
86  /// Option strike
87  /// </summary>
88  public decimal OptionStrike { get; set; }
89 
90  /// <summary>
91  /// Expiration date
92  /// </summary>
93  public DateTime ExpirationDate { get; set; }
94  }
95 
96 
97  /// <summary>
98  /// Function returns underlying name, expiration year, expiration month, expiration day for the future contract ticker. Function detects if
99  /// the format used is either 1 or 2 digits year, and if day code is present (will default to 1rst day of month). Returns null, if parsing failed.
100  /// Format [Ticker][2 digit day code OPTIONAL][1 char month code][2/1 digit year code]
101  /// </summary>
102  /// <param name="ticker"></param>
103  /// <returns>Results containing 1) underlying name, 2) short expiration year, 3) expiration month</returns>
104  public static FutureTickerProperties ParseFutureTicker(string ticker)
105  {
106  var doubleDigitYear = char.IsDigit(ticker.Substring(ticker.Length - 2, 1)[0]);
107  var doubleDigitOffset = doubleDigitYear ? 1 : 0;
108 
109  var expirationDayOffset = 0;
110  var expirationDay = 1;
111  if (ticker.Length > 4 + doubleDigitOffset)
112  {
113  var potentialExpirationDay = ticker.Substring(ticker.Length - 4 - doubleDigitOffset, 2);
114  var containsExpirationDay = char.IsDigit(potentialExpirationDay[0]) && char.IsDigit(potentialExpirationDay[1]);
115  expirationDayOffset = containsExpirationDay ? 2 : 0;
116  if (containsExpirationDay && !int.TryParse(potentialExpirationDay, out expirationDay))
117  {
118  return null;
119  }
120  }
121 
122  var expirationYearString = ticker.Substring(ticker.Length - 1 - doubleDigitOffset, 1 + doubleDigitOffset);
123  var expirationMonthString = ticker.Substring(ticker.Length - 2 - doubleDigitOffset, 1);
124  var underlyingString = ticker.Substring(0, ticker.Length - 2 - doubleDigitOffset - expirationDayOffset);
125 
126  int expirationYearShort;
127 
128  if (!int.TryParse(expirationYearString, out expirationYearShort))
129  {
130  return null;
131  }
132 
133  if (!FuturesMonthCodeLookup.ContainsKey(expirationMonthString))
134  {
135  return null;
136  }
137 
138  var expirationMonth = FuturesMonthCodeLookup[expirationMonthString];
139 
140  return new FutureTickerProperties
141  {
142  Underlying = underlyingString,
143  ExpirationYearShort = expirationYearShort,
144  ExpirationYearShortLength = expirationYearString.Length,
145  ExpirationMonth = expirationMonth,
146  ExpirationDay = expirationDay
147  };
148  }
149 
150  /// <summary>
151  /// Helper method to parse and generate a future symbol from a given user friendly representation
152  /// </summary>
153  /// <param name="ticker">The future ticker, for example 'ESZ1'</param>
154  /// <param name="futureYear">Clarifies the year for the current future</param>
155  /// <returns>The future symbol or null if failed</returns>
156  public static Symbol ParseFutureSymbol(string ticker, int? futureYear = null)
157  {
158  var parsed = ParseFutureTicker(ticker);
159  if (parsed == null)
160  {
161  return null;
162  }
163 
164  var underlying = parsed.Underlying;
165  var expirationMonth = parsed.ExpirationMonth;
166  var expirationYear = GetExpirationYear(futureYear, parsed);
167 
168  if (!SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(underlying, SecurityType.Future, out var market))
169  {
170  Log.Debug($@"SymbolRepresentation.ParseFutureSymbol(): {Messages.SymbolRepresentation.FailedToGetMarketForTickerAndUnderlying(ticker, underlying)}");
171  return null;
172  }
173 
174  var expiryFunc = FuturesExpiryFunctions.FuturesExpiryFunction(Symbol.Create(underlying, SecurityType.Future, market));
175  var expiryDate = expiryFunc(new DateTime(expirationYear, expirationMonth, 1));
176 
177  return Symbol.CreateFuture(underlying, market, expiryDate);
178  }
179 
180  /// <summary>
181  /// Creates a future option Symbol from the provided ticker
182  /// </summary>
183  /// <param name="ticker">The future option ticker, for example 'ESZ0 P3590'</param>
184  /// <param name="strikeScale">Optional the future option strike scale factor</param>
185  public static Symbol ParseFutureOptionSymbol(string ticker, int strikeScale = 1)
186  {
187  var split = ticker.Split(' ');
188  if (split.Length != 2)
189  {
190  return null;
191  }
192 
193  var parsed = ParseFutureTicker(split[0]);
194  if (parsed == null)
195  {
196  return null;
197  }
198  ticker = parsed.Underlying;
199 
200  OptionRight right;
201  if (split[1][0] == 'P' || split[1][0] == 'p')
202  {
203  right = OptionRight.Put;
204  }
205  else if (split[1][0] == 'C' || split[1][0] == 'c')
206  {
207  right = OptionRight.Call;
208  }
209  else
210  {
211  return null;
212  }
213  var strike = split[1].Substring(1);
214 
215  if (parsed.ExpirationYearShort < 10)
216  {
217  parsed.ExpirationYearShort += 20;
218  }
219  var expirationYearParsed = 2000 + parsed.ExpirationYearShort;
220 
221  var expirationDate = new DateTime(expirationYearParsed, parsed.ExpirationMonth, 1);
222 
223  var strikePrice = decimal.Parse(strike, NumberStyles.Any, CultureInfo.InvariantCulture);
224  var futureTicker = FuturesOptionsSymbolMappings.MapFromOption(ticker);
225 
226  if (!SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(futureTicker, SecurityType.Future, out var market))
227  {
228  Log.Debug($"SymbolRepresentation.ParseFutureOptionSymbol(): {Messages.SymbolRepresentation.NoMarketFound(futureTicker)}");
229  return null;
230  }
231 
232  var canonicalFuture = Symbol.Create(futureTicker, SecurityType.Future, market);
233  var futureExpiry = FuturesExpiryFunctions.FuturesExpiryFunction(canonicalFuture)(expirationDate);
234  var future = Symbol.CreateFuture(futureTicker, market, futureExpiry);
235 
237 
238  return Symbol.CreateOption(future,
239  market,
240  OptionStyle.American,
241  right,
242  strikePrice / strikeScale,
243  futureOptionExpiry);
244  }
245 
246  /// <summary>
247  /// Returns future symbol ticker from underlying and expiration date. Function can generate tickers of two formats: one and two digits year.
248  /// Format [Ticker][2 digit day code][1 char month code][2/1 digit year code], more information at http://help.tradestation.com/09_01/tradestationhelp/symbology/futures_symbology.htm
249  /// </summary>
250  /// <param name="underlying">String underlying</param>
251  /// <param name="expiration">Expiration date</param>
252  /// <param name="doubleDigitsYear">True if year should represented by two digits; False - one digit</param>
253  /// <param name="includeExpirationDate">True if expiration date should be included</param>
254  /// <returns>The user friendly future ticker</returns>
255  public static string GenerateFutureTicker(string underlying, DateTime expiration, bool doubleDigitsYear = true, bool includeExpirationDate = true)
256  {
257  var year = doubleDigitsYear ? expiration.Year % 100 : expiration.Year % 10;
258  var month = expiration.Month;
259 
260  var contractMonthDelta = FuturesExpiryUtilityFunctions.GetDeltaBetweenContractMonthAndContractExpiry(underlying, expiration.Date);
261  if (contractMonthDelta < 0)
262  {
263  // For futures that have an expiry after the contract month.
264  // This is for dairy contracts, which can and do expire after the contract month.
265  var expirationMonth = expiration.AddDays(-(expiration.Day - 1))
266  .AddMonths(contractMonthDelta);
267 
268  month = expirationMonth.Month;
269  year = doubleDigitsYear ? expirationMonth.Year % 100 : expirationMonth.Year % 10;
270  }
271  else
272  {
273  // These futures expire in the month before or in the contract month
274  month += contractMonthDelta;
275 
276  // Get the month back into the allowable range, allowing for a wrap
277  // Below is a little algorithm for wrapping numbers with a certain bounds.
278  // In this case, were dealing with months, wrapping to years once we get to January
279  // As modulo works for [0, x), it's best to subtract 1 (as months are [1, 12] to convert to [0, 11]),
280  // do the modulo/integer division, then add 1 back on to get into the correct range again
281  month--;
282  year += month / 12;
283  month %= 12;
284  month++;
285  }
286 
287  var expirationDay = includeExpirationDate ? $"{expiration.Day:00}" : string.Empty;
288 
289  return $"{underlying}{expirationDay}{FuturesMonthLookup[month]}{year}";
290  }
291 
292  /// <summary>
293  /// Returns option symbol ticker in accordance with OSI symbology
294  /// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
295  /// </summary>
296  /// <param name="symbol">Symbol object to create OSI ticker from</param>
297  /// <returns>The OSI ticker representation</returns>
298  public static string GenerateOptionTickerOSI(this Symbol symbol)
299  {
300  if (!symbol.SecurityType.IsOption())
301  {
302  throw new ArgumentException(
304  }
305 
306  return GenerateOptionTickerOSI(symbol.Underlying.Value, symbol.ID.OptionRight, symbol.ID.StrikePrice, symbol.ID.Date);
307  }
308 
309  /// <summary>
310  /// Returns option symbol ticker in accordance with OSI symbology
311  /// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
312  /// </summary>
313  /// <param name="underlying">Underlying string</param>
314  /// <param name="right">Option right</param>
315  /// <param name="strikePrice">Option strike</param>
316  /// <param name="expiration">Option expiration date</param>
317  /// <returns>The OSI ticker representation</returns>
318  public static string GenerateOptionTickerOSI(string underlying, OptionRight right, decimal strikePrice, DateTime expiration)
319  {
320  if (underlying.Length > 5) underlying += " ";
321  return Invariant($"{underlying,-6}{expiration.ToStringInvariant(DateFormat.SixCharacter)}{right.ToStringPerformance()[0]}{(strikePrice * 1000m):00000000}");
322  }
323 
324  /// <summary>
325  /// Returns option symbol ticker in accordance with OSI symbology
326  /// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
327  /// </summary>
328  /// <param name="symbol">Symbol object to create OSI ticker from</param>
329  /// <returns>The OSI ticker representation</returns>
330  public static string GenerateOptionTickerOSICompact(this Symbol symbol)
331  {
332  // First, validate that the symbol is of the correct security type
333  if (!symbol.SecurityType.IsOption())
334  {
335  throw new ArgumentException(
337  }
338  return GenerateOptionTickerOSICompact(symbol.Underlying.Value, symbol.ID.OptionRight, symbol.ID.StrikePrice, symbol.ID.Date);
339  }
340 
341  /// <summary>
342  /// Returns option symbol ticker in accordance with OSI symbology
343  /// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
344  /// </summary>
345  /// <param name="underlying">Underlying string</param>
346  /// <param name="right">Option right</param>
347  /// <param name="strikePrice">Option strike</param>
348  /// <param name="expiration">Option expiration date</param>
349  /// <returns>The OSI ticker representation</returns>
350  public static string GenerateOptionTickerOSICompact(string underlying, OptionRight right, decimal strikePrice, DateTime expiration)
351  {
352  return Invariant($"{underlying}{expiration.ToStringInvariant(DateFormat.SixCharacter)}{right.ToStringPerformance()[0]}{(strikePrice * 1000m):00000000}");
353  }
354 
355  /// <summary>
356  /// Parses the specified OSI options ticker into a Symbol object
357  /// </summary>
358  /// <param name="ticker">The OSI compliant option ticker string</param>
359  /// <param name="securityType">The security type</param>
360  /// <param name="market">The associated market</param>
361  /// <returns>Symbol object for the specified OSI option ticker string</returns>
362  public static Symbol ParseOptionTickerOSI(string ticker, SecurityType securityType = SecurityType.Option, string market = Market.USA)
363  {
364  return ParseOptionTickerOSI(ticker, securityType, OptionStyle.American, market);
365  }
366 
367  /// <summary>
368  /// Parses the specified OSI options ticker into a Symbol object
369  /// </summary>
370  /// <param name="ticker">The OSI compliant option ticker string</param>
371  /// <param name="securityType">The security type</param>
372  /// <param name="market">The associated market</param>
373  /// <param name="optionStyle">The option style</param>
374  /// <returns>Symbol object for the specified OSI option ticker string</returns>
375  public static Symbol ParseOptionTickerOSI(string ticker, SecurityType securityType, OptionStyle optionStyle, string market)
376  {
377  if (!TryDecomposeOptionTickerOSI(ticker, out var optionTicker, out var expiry, out var right, out var strike))
378  {
379  throw new FormatException(Messages.SymbolRepresentation.InvalidOSITickerFormat(ticker));
380  }
381 
382  SecurityIdentifier underlyingSid;
383  string underlyingSymbolValue;
384  if (securityType == SecurityType.Option)
385  {
386  underlyingSid = SecurityIdentifier.GenerateEquity(optionTicker, market);
387  // We have the mapped symbol in the OSI ticker
388  underlyingSymbolValue = optionTicker;
389  // let it fallback to it's default handling, which include mapping
390  optionTicker = null;
391  }
392  else if (securityType == SecurityType.IndexOption)
393  {
394  underlyingSid = SecurityIdentifier.GenerateIndex(OptionSymbol.MapToUnderlying(optionTicker, securityType), market);
395  underlyingSymbolValue = underlyingSid.Symbol;
396  }
397  else if (securityType == SecurityType.FutureOption)
398  {
399  var futureTickerInfo = ParseFutureTicker(optionTicker);
400  underlyingSid = SecurityIdentifier.GenerateFuture(expiry, futureTickerInfo.Underlying, market);
401  underlyingSymbolValue = underlyingSid.Symbol;
402  }
403  else
404  {
405  throw new NotImplementedException($"ParseOptionTickerOSI(): {Messages.SymbolRepresentation.SecurityTypeNotImplemented(securityType)}");
406  }
407  var sid = SecurityIdentifier.GenerateOption(expiry, underlyingSid, optionTicker, market, strike, right, optionStyle);
408  return new Symbol(sid, ticker, new Symbol(underlyingSid, underlyingSymbolValue));
409  }
410 
411  /// <summary>
412  /// Tries to decompose the specified OSI options ticker into its components
413  /// </summary>
414  /// <param name="ticker">The OSI option ticker</param>
415  /// <param name="optionTicker">The option ticker extracted from the OSI symbol</param>
416  /// <param name="expiry">The option contract expiry date</param>
417  /// <param name="right">The option contract right</param>
418  /// <param name="strike">The option contract strike price</param>
419  /// <returns>True if the OSI symbol was in the right format and could be decomposed</returns>
420  public static bool TryDecomposeOptionTickerOSI(string ticker, out string optionTicker, out DateTime expiry,
421  out OptionRight right, out decimal strike)
422  {
423  optionTicker = null;
424  expiry = default;
425  right = OptionRight.Call;
426  strike = decimal.Zero;
427 
428  if (string.IsNullOrEmpty(ticker))
429  {
430  return false;
431  }
432 
433  var match = _optionTickerRegex.Match(ticker);
434  if (!match.Success)
435  {
436  return false;
437  }
438 
439  optionTicker = match.Groups[1].Value;
440  expiry = DateTime.ParseExact(match.Groups[2].Value, DateFormat.SixCharacter, null);
441  right = match.Groups[3].Value.ToUpperInvariant() == "C" ? OptionRight.Call : OptionRight.Put;
442  strike = Parse.Decimal(match.Groups[4].Value) / 1000m;
443 
444  return true;
445  }
446 
447  /// <summary>
448  /// Tries to decompose the specified OSI options ticker into its components
449  /// </summary>
450  /// <param name="ticker">The OSI option ticker</param>
451  /// <param name="securityType">The option security type</param>
452  /// <param name="optionTicker">The option ticker extracted from the OSI symbol</param>
453  /// <param name="underlyingTicker">The underlying ticker</param>
454  /// <param name="expiry">The option contract expiry date</param>
455  /// <param name="right">The option contract right</param>
456  /// <param name="strike">The option contract strike price</param>
457  /// <returns>True if the OSI symbol was in the right format and could be decomposed</returns>
458  public static bool TryDecomposeOptionTickerOSI(string ticker, SecurityType securityType, out string optionTicker,
459  out string underlyingTicker, out DateTime expiry, out OptionRight right, out decimal strike)
460  {
461  optionTicker = null;
462  underlyingTicker = null;
463  expiry = default;
464  right = OptionRight.Call;
465  strike = decimal.Zero;
466 
467  if (!securityType.IsOption())
468  {
469  return false;
470  }
471 
472  var result = TryDecomposeOptionTickerOSI(ticker, out optionTicker, out expiry, out right, out strike);
473  underlyingTicker = securityType != SecurityType.IndexOption ? optionTicker : IndexOptionSymbol.MapToUnderlying(optionTicker);
474 
475  return result;
476  }
477 
478  /// <summary>
479  /// Function returns option ticker from IQFeed option ticker
480  /// For example CSCO1220V19 Cisco October Put at 19.00 Expiring on 10/20/12
481  /// Symbology details: http://www.iqfeed.net/symbolguide/index.cfm?symbolguide=guide&amp;displayaction=support%C2%A7ion=guide&amp;web=iqfeed&amp;guide=options&amp;web=IQFeed&amp;type=stock
482  /// </summary>
483  /// <param name="symbol">THe option symbol</param>
484  /// <returns>The option ticker</returns>
485  public static string GenerateOptionTicker(Symbol symbol)
486  {
487  var symbolTicker = symbol.SecurityType == SecurityType.IndexOption ? symbol.Canonical.Value.Replace("?", string.Empty) : SecurityIdentifier.Ticker(symbol.Underlying, symbol.ID.Date);
488  var letter = OptionCodeLookup.Where(x => x.Value.Item2 == symbol.ID.OptionRight && x.Value.Item1 == symbol.ID.Date.Month).Select(x => x.Key).Single();
489  var twoYearDigit = symbol.ID.Date.ToString("yy");
490  return $"{symbolTicker}{twoYearDigit}{symbol.ID.Date.Day:00}{letter}{symbol.ID.StrikePrice.ToStringInvariant()}";
491  }
492 
493  /// <summary>
494  /// Function returns option contract parameters (underlying name, expiration date, strike, right) from IQFeed option ticker
495  /// Symbology details: http://www.iqfeed.net/symbolguide/index.cfm?symbolguide=guide&amp;displayaction=support%C2%A7ion=guide&amp;web=iqfeed&amp;guide=options&amp;web=IQFeed&amp;type=stock
496  /// </summary>
497  /// <param name="ticker">IQFeed option ticker</param>
498  /// <returns>Results containing 1) underlying name, 2) option right, 3) option strike 4) expiration date</returns>
499  public static OptionTickerProperties ParseOptionTickerIQFeed(string ticker)
500  {
501  var letterRange = OptionCodeLookup.Keys
502  .Select(x => x[0])
503  .ToArray();
504  var optionTypeDelimiter = ticker.LastIndexOfAny(letterRange);
505  var strikePriceString = ticker.Substring(optionTypeDelimiter + 1, ticker.Length - optionTypeDelimiter - 1);
506 
507  var lookupResult = OptionCodeLookup[ticker[optionTypeDelimiter].ToStringInvariant()];
508  var month = lookupResult.Item1;
509  var optionRight = lookupResult.Item2;
510 
511  var dayString = ticker.Substring(optionTypeDelimiter - 2, 2);
512  var yearString = ticker.Substring(optionTypeDelimiter - 4, 2);
513  var underlying = ticker.Substring(0, optionTypeDelimiter - 4);
514 
515  // if we cannot parse strike price, we ignore this contract, but log the information.
516  Decimal strikePrice;
517  if (!Decimal.TryParse(strikePriceString, NumberStyles.Any, CultureInfo.InvariantCulture, out strikePrice))
518  {
519  return null;
520  }
521 
522  int day;
523 
524  if (!int.TryParse(dayString, out day))
525  {
526  return null;
527  }
528 
529  int year;
530 
531  if (!int.TryParse(yearString, out year))
532  {
533  return null;
534  }
535 
536  var expirationDate = new DateTime(2000 + year, month, day);
537 
538  return new OptionTickerProperties
539  {
540  Underlying = underlying,
541  OptionRight = optionRight,
542  OptionStrike = strikePrice,
543  ExpirationDate = expirationDate
544  };
545  }
546 
547 
548  /// <summary>
549  /// A dictionary that maps option symbols to a tuple containing the option series number and the option right (Call or Put).
550  /// The key represents a single character option symbol, and the value contains the series number and the associated option right.
551  /// </summary>
552  /// <remarks>
553  /// The dictionary is designed to map each option symbol (e.g., "A", "M", "B", etc.) to an option series number and
554  /// the corresponding option right (either a Call or Put). The series number determines the group of options the symbol belongs to,
555  /// and the option right indicates whether the option is a Call (buyer has the right to buy) or Put (buyer has the right to sell).
556  /// </remarks>
557  public static IReadOnlyDictionary<string, Tuple<int, OptionRight>> OptionCodeLookup { get; } = new Dictionary<string, Tuple<int, OptionRight>>
558  {
559  { "A", Tuple.Create(1, OptionRight.Call) }, { "M", Tuple.Create(1, OptionRight.Put) },
560  { "B", Tuple.Create(2, OptionRight.Call) }, { "N", Tuple.Create(2, OptionRight.Put) },
561  { "C", Tuple.Create(3, OptionRight.Call) }, { "O", Tuple.Create(3, OptionRight.Put) },
562  { "D", Tuple.Create(4, OptionRight.Call) }, { "P", Tuple.Create(4, OptionRight.Put) },
563  { "E", Tuple.Create(5, OptionRight.Call) }, { "Q", Tuple.Create(5, OptionRight.Put) },
564  { "F", Tuple.Create(6, OptionRight.Call) }, { "R", Tuple.Create(6, OptionRight.Put) },
565  { "G", Tuple.Create(7, OptionRight.Call) }, { "S", Tuple.Create(7, OptionRight.Put) },
566  { "H", Tuple.Create(8, OptionRight.Call) }, { "T", Tuple.Create(8, OptionRight.Put) },
567  { "I", Tuple.Create(9, OptionRight.Call) }, { "U", Tuple.Create(9, OptionRight.Put) },
568  { "J", Tuple.Create(10, OptionRight.Call) }, { "V", Tuple.Create(10, OptionRight.Put) },
569  { "K", Tuple.Create(11, OptionRight.Call) }, { "W", Tuple.Create(11, OptionRight.Put) },
570  { "L", Tuple.Create(12, OptionRight.Call) }, { "X", Tuple.Create(12, OptionRight.Put) },
571  };
572 
573 
574  /// <summary>
575  /// Provides a lookup dictionary for mapping futures month codes to their corresponding numeric values.
576  /// </summary>
577  public static IReadOnlyDictionary<string, int> FuturesMonthCodeLookup { get; } = new Dictionary<string, int>
578  {
579  { "F", 1 }, // January
580  { "G", 2 }, // February
581  { "H", 3 }, // March
582  { "J", 4 }, // April
583  { "K", 5 }, // May
584  { "M", 6 }, // June
585  { "N", 7 }, // July
586  { "Q", 8 }, // August
587  { "U", 9 }, // September
588  { "V", 10 }, // October
589  { "X", 11 }, // November
590  { "Z", 12 } // December
591  };
592 
593  /// <summary>
594  /// Provides a lookup dictionary for mapping numeric values to their corresponding futures month codes.
595  /// </summary>
596  public static IReadOnlyDictionary<int, string> FuturesMonthLookup { get; } = FuturesMonthCodeLookup.ToDictionary(kv => kv.Value, kv => kv.Key);
597 
598  /// <summary>
599  /// Get the expiration year from short year (two-digit integer).
600  /// Examples: NQZ23 and NQZ3 for Dec 2023
601  /// </summary>
602  /// <param name="futureYear">Clarifies the year for the current future</param>
603  /// <param name="parsed">Contains useful information about the future expiration year</param>
604  /// <remarks>Tickers from live trading may not provide the four-digit year.</remarks>
605  private static int GetExpirationYear(int? futureYear, FutureTickerProperties parsed)
606  {
607  if (futureYear.HasValue)
608  {
609  var referenceYear = 1900 + parsed.ExpirationYearShort;
610  while (referenceYear < futureYear.Value)
611  {
612  referenceYear += 10;
613  }
614 
615  return referenceYear;
616  }
617 
618  var currentYear = DateTime.UtcNow.Year;
619  if (parsed.ExpirationYearShortLength > 1)
620  {
621  // we are given a double digit year
622  return 2000 + parsed.ExpirationYearShort;
623  }
624 
625  var baseYear = ((int)Math.Round(currentYear / 10.0)) * 10 + parsed.ExpirationYearShort;
626  while (baseYear < currentYear)
627  {
628  baseYear += 10;
629  }
630  return baseYear;
631  }
632  }
633 }