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 
27 namespace QuantConnect
28 {
29  /// <summary>
30  /// Public static helper class that does parsing/generation of symbol representations (options, futures)
31  /// </summary>
32  public static class SymbolRepresentation
33  {
34  private static DateTime TodayUtc = DateTime.UtcNow;
35 
36  /// <summary>
37  /// Class contains future ticker properties returned by ParseFutureTicker()
38  /// </summary>
40  {
41  /// <summary>
42  /// Underlying name
43  /// </summary>
44  public string Underlying { get; set; }
45 
46  /// <summary>
47  /// Short expiration year
48  /// </summary>
49  public int ExpirationYearShort { get; set; }
50 
51  /// <summary>
52  /// Short expiration year digits
53  /// </summary>
54  public int ExpirationYearShortLength { get; set; }
55 
56  /// <summary>
57  /// Expiration month
58  /// </summary>
59  public int ExpirationMonth { get; set; }
60 
61  /// <summary>
62  /// Expiration day
63  /// </summary>
64  public int ExpirationDay { get; set; }
65  }
66 
67  /// <summary>
68  /// Class contains option ticker properties returned by ParseOptionTickerIQFeed()
69  /// </summary>
71  {
72  /// <summary>
73  /// Underlying name
74  /// </summary>
75  public string Underlying { get; set; }
76 
77  /// <summary>
78  /// Option right
79  /// </summary>
80  public OptionRight OptionRight { get; set; }
81 
82  /// <summary>
83  /// Option strike
84  /// </summary>
85  public decimal OptionStrike { get; set; }
86 
87  /// <summary>
88  /// Expiration date
89  /// </summary>
90  public DateTime ExpirationDate { get; set; }
91  }
92 
93 
94  /// <summary>
95  /// Function returns underlying name, expiration year, expiration month, expiration day for the future contract ticker. Function detects if
96  /// 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.
97  /// Format [Ticker][2 digit day code OPTIONAL][1 char month code][2/1 digit year code]
98  /// </summary>
99  /// <param name="ticker"></param>
100  /// <returns>Results containing 1) underlying name, 2) short expiration year, 3) expiration month</returns>
101  public static FutureTickerProperties ParseFutureTicker(string ticker)
102  {
103  var doubleDigitYear = char.IsDigit(ticker.Substring(ticker.Length - 2, 1)[0]);
104  var doubleDigitOffset = doubleDigitYear ? 1 : 0;
105 
106  var expirationDayOffset = 0;
107  var expirationDay = 1;
108  if (ticker.Length > 4 + doubleDigitOffset)
109  {
110  var potentialExpirationDay = ticker.Substring(ticker.Length - 4 - doubleDigitOffset, 2);
111  var containsExpirationDay = char.IsDigit(potentialExpirationDay[0]) && char.IsDigit(potentialExpirationDay[1]);
112  expirationDayOffset = containsExpirationDay ? 2 : 0;
113  if (containsExpirationDay && !int.TryParse(potentialExpirationDay, out expirationDay))
114  {
115  return null;
116  }
117  }
118 
119  var expirationYearString = ticker.Substring(ticker.Length - 1 - doubleDigitOffset, 1 + doubleDigitOffset);
120  var expirationMonthString = ticker.Substring(ticker.Length - 2 - doubleDigitOffset, 1);
121  var underlyingString = ticker.Substring(0, ticker.Length - 2 - doubleDigitOffset - expirationDayOffset);
122 
123  int expirationYearShort;
124 
125  if (!int.TryParse(expirationYearString, out expirationYearShort))
126  {
127  return null;
128  }
129 
130  if (!_futuresMonthCodeLookup.ContainsKey(expirationMonthString))
131  {
132  return null;
133  }
134 
135  var expirationMonth = _futuresMonthCodeLookup[expirationMonthString];
136 
137  return new FutureTickerProperties
138  {
139  Underlying = underlyingString,
140  ExpirationYearShort = expirationYearShort,
141  ExpirationYearShortLength = expirationYearString.Length,
142  ExpirationMonth = expirationMonth,
143  ExpirationDay = expirationDay
144  };
145  }
146 
147  /// <summary>
148  /// Helper method to parse and generate a future symbol from a given user friendly representation
149  /// </summary>
150  /// <param name="ticker">The future ticker, for example 'ESZ1'</param>
151  /// <param name="futureYear">Clarifies the year for the current future</param>
152  /// <returns>The future symbol or null if failed</returns>
153  public static Symbol ParseFutureSymbol(string ticker, int? futureYear = null)
154  {
155  var parsed = ParseFutureTicker(ticker);
156  if (parsed == null)
157  {
158  return null;
159  }
160 
161  var underlying = parsed.Underlying;
162  var expirationMonth = parsed.ExpirationMonth;
163  var expirationYear = GetExpirationYear(futureYear, parsed);
164 
165  if (!SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(underlying, SecurityType.Future, out var market))
166  {
167  Log.Debug($@"SymbolRepresentation.ParseFutureSymbol(): {
168  Messages.SymbolRepresentation.FailedToGetMarketForTickerAndUnderlying(ticker, underlying)}");
169  return null;
170  }
171 
172  var expiryFunc = FuturesExpiryFunctions.FuturesExpiryFunction(Symbol.Create(underlying, SecurityType.Future, market));
173  var expiryDate = expiryFunc(new DateTime(expirationYear, expirationMonth, 1));
174 
175  return Symbol.CreateFuture(underlying, market, expiryDate);
176  }
177 
178  /// <summary>
179  /// Creates a future option Symbol from the provided ticker
180  /// </summary>
181  /// <param name="ticker">The future option ticker, for example 'ESZ0 P3590'</param>
182  /// <param name="strikeScale">Optional the future option strike scale factor</param>
183  public static Symbol ParseFutureOptionSymbol(string ticker, int strikeScale = 1)
184  {
185  var split = ticker.Split(' ');
186  if (split.Length != 2)
187  {
188  return null;
189  }
190 
191  var parsed = ParseFutureTicker(split[0]);
192  if (parsed == null)
193  {
194  return null;
195  }
196  ticker = parsed.Underlying;
197 
198  OptionRight right;
199  if (split[1][0] == 'P' || split[1][0] == 'p')
200  {
201  right = OptionRight.Put;
202  }
203  else if (split[1][0] == 'C' || split[1][0] == 'c')
204  {
205  right = OptionRight.Call;
206  }
207  else
208  {
209  return null;
210  }
211  var strike = split[1].Substring(1);
212 
213  if (parsed.ExpirationYearShort < 10)
214  {
215  parsed.ExpirationYearShort += 20;
216  }
217  var expirationYearParsed = 2000 + parsed.ExpirationYearShort;
218 
219  var expirationDate = new DateTime(expirationYearParsed, parsed.ExpirationMonth, 1);
220 
221  var strikePrice = decimal.Parse(strike, NumberStyles.Any, CultureInfo.InvariantCulture);
222  var futureTicker = FuturesOptionsSymbolMappings.MapFromOption(ticker);
223 
224  if (!SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(futureTicker, SecurityType.Future, out var market))
225  {
226  Log.Debug($"SymbolRepresentation.ParseFutureOptionSymbol(): {Messages.SymbolRepresentation.NoMarketFound(futureTicker)}");
227  return null;
228  }
229 
230  var canonicalFuture = Symbol.Create(futureTicker, SecurityType.Future, market);
231  var futureExpiry = FuturesExpiryFunctions.FuturesExpiryFunction(canonicalFuture)(expirationDate);
232  var future = Symbol.CreateFuture(futureTicker, market, futureExpiry);
233 
235 
236  return Symbol.CreateOption(future,
237  market,
238  OptionStyle.American,
239  right,
240  strikePrice / strikeScale,
241  futureOptionExpiry);
242  }
243 
244  /// <summary>
245  /// Returns future symbol ticker from underlying and expiration date. Function can generate tickers of two formats: one and two digits year.
246  /// 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
247  /// </summary>
248  /// <param name="underlying">String underlying</param>
249  /// <param name="expiration">Expiration date</param>
250  /// <param name="doubleDigitsYear">True if year should represented by two digits; False - one digit</param>
251  /// <param name="includeExpirationDate">True if expiration date should be included</param>
252  /// <returns>The user friendly future ticker</returns>
253  public static string GenerateFutureTicker(string underlying, DateTime expiration, bool doubleDigitsYear = true, bool includeExpirationDate = true)
254  {
255  var year = doubleDigitsYear ? expiration.Year % 100 : expiration.Year % 10;
256  var month = expiration.Month;
257 
258  var contractMonthDelta = FuturesExpiryUtilityFunctions.GetDeltaBetweenContractMonthAndContractExpiry(underlying, expiration.Date);
259  if (contractMonthDelta < 0)
260  {
261  // For futures that have an expiry after the contract month.
262  // This is for dairy contracts, which can and do expire after the contract month.
263  var expirationMonth = expiration.AddDays(-(expiration.Day - 1))
264  .AddMonths(contractMonthDelta);
265 
266  month = expirationMonth.Month;
267  year = doubleDigitsYear ? expirationMonth.Year % 100 : expirationMonth.Year % 10;
268  }
269  else {
270  // These futures expire in the month before or in the contract month
271  month += contractMonthDelta;
272 
273  // Get the month back into the allowable range, allowing for a wrap
274  // Below is a little algorithm for wrapping numbers with a certain bounds.
275  // In this case, were dealing with months, wrapping to years once we get to January
276  // As modulo works for [0, x), it's best to subtract 1 (as months are [1, 12] to convert to [0, 11]),
277  // do the modulo/integer division, then add 1 back on to get into the correct range again
278  month--;
279  year += month / 12;
280  month %= 12;
281  month++;
282  }
283 
284  var expirationDay = includeExpirationDate ? $"{expiration.Day:00}" : string.Empty;
285 
286  return $"{underlying}{expirationDay}{_futuresMonthLookup[month]}{year}";
287  }
288 
289  /// <summary>
290  /// Returns option symbol ticker in accordance with OSI symbology
291  /// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
292  /// </summary>
293  /// <param name="symbol">Symbol object to create OSI ticker from</param>
294  /// <returns>The OSI ticker representation</returns>
295  public static string GenerateOptionTickerOSI(this Symbol symbol)
296  {
297  if (!symbol.SecurityType.IsOption())
298  {
299  throw new ArgumentException(
300  Messages.SymbolRepresentation.UnexpectedSecurityTypeForMethod(nameof(GenerateOptionTickerOSI), symbol.SecurityType));
301  }
302 
303  return GenerateOptionTickerOSI(symbol.Underlying.Value, symbol.ID.OptionRight, symbol.ID.StrikePrice, symbol.ID.Date);
304  }
305 
306  /// <summary>
307  /// Returns option symbol ticker in accordance with OSI symbology
308  /// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
309  /// </summary>
310  /// <param name="underlying">Underlying string</param>
311  /// <param name="right">Option right</param>
312  /// <param name="strikePrice">Option strike</param>
313  /// <param name="expiration">Option expiration date</param>
314  /// <returns>The OSI ticker representation</returns>
315  public static string GenerateOptionTickerOSI(string underlying, OptionRight right, decimal strikePrice, DateTime expiration)
316  {
317  if (underlying.Length > 5) underlying += " ";
318  return Invariant($"{underlying,-6}{expiration.ToStringInvariant(DateFormat.SixCharacter)}{right.ToStringPerformance()[0]}{(strikePrice * 1000m):00000000}");
319  }
320 
321  /// <summary>
322  /// Parses the specified OSI options ticker into a Symbol object
323  /// </summary>
324  /// <param name="ticker">The OSI compliant option ticker string</param>
325  /// <param name="securityType">The security type</param>
326  /// <param name="market">The associated market</param>
327  /// <returns>Symbol object for the specified OSI option ticker string</returns>
328  public static Symbol ParseOptionTickerOSI(string ticker, SecurityType securityType = SecurityType.Option, string market = Market.USA)
329  {
330  return ParseOptionTickerOSI(ticker, securityType, OptionStyle.American, market);
331  }
332 
333  /// <summary>
334  /// Parses the specified OSI options ticker into a Symbol object
335  /// </summary>
336  /// <param name="ticker">The OSI compliant option ticker string</param>
337  /// <param name="securityType">The security type</param>
338  /// <param name="market">The associated market</param>
339  /// <param name="optionStyle">The option style</param>
340  /// <returns>Symbol object for the specified OSI option ticker string</returns>
341  public static Symbol ParseOptionTickerOSI(string ticker, SecurityType securityType, OptionStyle optionStyle, string market)
342  {
343  var optionTicker = ticker.Substring(0, 6).Trim();
344  var expiration = DateTime.ParseExact(ticker.Substring(6, 6), DateFormat.SixCharacter, null);
345  OptionRight right;
346  if (ticker[12] == 'C' || ticker[12] == 'c')
347  {
348  right = OptionRight.Call;
349  }
350  else if (ticker[12] == 'P' || ticker[12] == 'p')
351  {
352  right = OptionRight.Put;
353  }
354  else
355  {
356  throw new FormatException(Messages.SymbolRepresentation.UnexpectedOptionRightFormatForParseOptionTickerOSI(ticker));
357  }
358  var strike = Parse.Decimal(ticker.Substring(13, 8)) / 1000m;
359  SecurityIdentifier underlyingSid;
360  if (securityType == SecurityType.Option)
361  {
362  underlyingSid = SecurityIdentifier.GenerateEquity(optionTicker, market);
363  // let it fallback to it's default handling, which include mapping
364  optionTicker = null;
365  }
366  else if(securityType == SecurityType.IndexOption)
367  {
368  underlyingSid = SecurityIdentifier.GenerateIndex(OptionSymbol.MapToUnderlying(optionTicker, securityType), market);
369  }
370  else
371  {
372  throw new NotImplementedException($"ParseOptionTickerOSI(): {Messages.SymbolRepresentation.SecurityTypeNotImplemented(securityType)}");
373  }
374  var sid = SecurityIdentifier.GenerateOption(expiration, underlyingSid, optionTicker, market, strike, right, optionStyle);
375  return new Symbol(sid, ticker, new Symbol(underlyingSid, underlyingSid.Symbol));
376  }
377 
378  /// <summary>
379  /// Function returns option ticker from IQFeed option ticker
380  /// For example CSCO1220V19 Cisco October Put at 19.00 Expiring on 10/20/12
381  /// 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
382  /// </summary>
383  /// <param name="symbol">THe option symbol</param>
384  /// <returns>The option ticker</returns>
385  public static string GenerateOptionTicker(Symbol symbol)
386  {
387  var letter = _optionSymbology.Where(x => x.Value.Item2 == symbol.ID.OptionRight && x.Value.Item1 == symbol.ID.Date.Month).Select(x => x.Key).Single();
388  var twoYearDigit = symbol.ID.Date.ToString("yy");
389  return $"{SecurityIdentifier.Ticker(symbol.Underlying, symbol.ID.Date)}{twoYearDigit}{symbol.ID.Date.Day:00}{letter}{symbol.ID.StrikePrice.ToStringInvariant()}";
390  }
391 
392  /// <summary>
393  /// Function returns option contract parameters (underlying name, expiration date, strike, right) from IQFeed option ticker
394  /// 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
395  /// </summary>
396  /// <param name="ticker">IQFeed option ticker</param>
397  /// <returns>Results containing 1) underlying name, 2) option right, 3) option strike 4) expiration date</returns>
398  public static OptionTickerProperties ParseOptionTickerIQFeed(string ticker)
399  {
400  var letterRange = _optionSymbology.Keys
401  .Select(x => x[0])
402  .ToArray();
403  var optionTypeDelimiter = ticker.LastIndexOfAny(letterRange);
404  var strikePriceString = ticker.Substring(optionTypeDelimiter + 1, ticker.Length - optionTypeDelimiter - 1);
405 
406  var lookupResult = _optionSymbology[ticker[optionTypeDelimiter].ToStringInvariant()];
407  var month = lookupResult.Item1;
408  var optionRight = lookupResult.Item2;
409 
410  var dayString = ticker.Substring(optionTypeDelimiter - 2, 2);
411  var yearString = ticker.Substring(optionTypeDelimiter - 4, 2);
412  var underlying = ticker.Substring(0, optionTypeDelimiter - 4);
413 
414  // if we cannot parse strike price, we ignore this contract, but log the information.
415  Decimal strikePrice;
416  if (!Decimal.TryParse(strikePriceString, NumberStyles.Any, CultureInfo.InvariantCulture, out strikePrice))
417  {
418  return null;
419  }
420 
421  int day;
422 
423  if (!int.TryParse(dayString, out day))
424  {
425  return null;
426  }
427 
428  int year;
429 
430  if (!int.TryParse(yearString, out year))
431  {
432  return null;
433  }
434 
435  var expirationDate = new DateTime(2000 + year, month, day);
436 
437  return new OptionTickerProperties
438  {
439  Underlying = underlying,
440  OptionRight = optionRight,
441  OptionStrike = strikePrice,
442  ExpirationDate = expirationDate
443  };
444  }
445 
446 
447  // This table describes IQFeed option symbology
448  private static Dictionary<string, Tuple<int, OptionRight>> _optionSymbology = new Dictionary<string, Tuple<int, OptionRight>>
449  {
450  { "A", Tuple.Create(1, OptionRight.Call) }, { "M", Tuple.Create(1, OptionRight.Put) },
451  { "B", Tuple.Create(2, OptionRight.Call) }, { "N", Tuple.Create(2, OptionRight.Put) },
452  { "C", Tuple.Create(3, OptionRight.Call) }, { "O", Tuple.Create(3, OptionRight.Put) },
453  { "D", Tuple.Create(4, OptionRight.Call) }, { "P", Tuple.Create(4, OptionRight.Put) },
454  { "E", Tuple.Create(5, OptionRight.Call) }, { "Q", Tuple.Create(5, OptionRight.Put) },
455  { "F", Tuple.Create(6, OptionRight.Call) }, { "R", Tuple.Create(6, OptionRight.Put) },
456  { "G", Tuple.Create(7, OptionRight.Call) }, { "S", Tuple.Create(7, OptionRight.Put) },
457  { "H", Tuple.Create(8, OptionRight.Call) }, { "T", Tuple.Create(8, OptionRight.Put) },
458  { "I", Tuple.Create(9, OptionRight.Call) }, { "U", Tuple.Create(9, OptionRight.Put) },
459  { "J", Tuple.Create(10, OptionRight.Call) }, { "V", Tuple.Create(10, OptionRight.Put) },
460  { "K", Tuple.Create(11, OptionRight.Call) }, { "W", Tuple.Create(11, OptionRight.Put) },
461  { "L", Tuple.Create(12, OptionRight.Call) }, { "X", Tuple.Create(12, OptionRight.Put) },
462 
463  };
464 
465 
466  private static IReadOnlyDictionary<string, int> _futuresMonthCodeLookup = new Dictionary<string, int>
467  {
468  { "F", 1 },
469  { "G", 2 },
470  { "H", 3 },
471  { "J", 4 },
472  { "K", 5 },
473  { "M", 6 },
474  { "N", 7 },
475  { "Q", 8 },
476  { "U", 9 },
477  { "V", 10 },
478  { "X", 11 },
479  { "Z", 12 }
480  };
481 
482  private static IReadOnlyDictionary<int, string> _futuresMonthLookup = _futuresMonthCodeLookup.ToDictionary(kv => kv.Value, kv => kv.Key);
483 
484  /// <summary>
485  /// Get the expiration year from short year (two-digit integer).
486  /// Examples: NQZ23 and NQZ3 for Dec 2023
487  /// </summary>
488  /// <param name="futureYear">Clarifies the year for the current future</param>
489  /// <param name="shortYear">Year in 2 digits format (23 represents 2023)</param>
490  /// <returns>Tickers from live trading may not provide the four-digit year.</returns>
491  private static int GetExpirationYear(int? futureYear, FutureTickerProperties parsed)
492  {
493  if(futureYear.HasValue)
494  {
495  var referenceYear = 1900 + parsed.ExpirationYearShort;
496  while(referenceYear < futureYear.Value)
497  {
498  referenceYear += 10;
499  }
500 
501  return referenceYear;
502  }
503 
504  var currentYear = DateTime.UtcNow.Year;
505  if (parsed.ExpirationYearShortLength > 1)
506  {
507  // we are given a double digit year
508  return 2000 + parsed.ExpirationYearShort;
509  }
510 
511  var baseYear = ((int)Math.Round(currentYear / 10.0)) * 10 + parsed.ExpirationYearShort;
512  while (baseYear < currentYear)
513  {
514  baseYear += 10;
515  }
516  return baseYear;
517  }
518  }
519 }