Lean  $LEAN_TAG$
OptionSymbol.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.Collections.Generic;
18 using System.Runtime.CompilerServices;
21 
23 {
24  /// <summary>
25  /// Static class contains common utility methods specific to symbols representing the option contracts
26  /// </summary>
27  public static class OptionSymbol
28  {
29  private static readonly Dictionary<string, byte> _optionExpirationErrorLog = new();
30 
31  /// <summary>
32  /// Returns true if the option is a standard contract that expires 3rd Friday of the month
33  /// </summary>
34  /// <param name="symbol">Option symbol</param>
35  /// <returns></returns>
36  public static bool IsStandardContract(Symbol symbol)
37  {
38  return IsStandard(symbol);
39  }
40 
41  /// <summary>
42  /// Returns true if the option is a standard contract that expires 3rd Friday of the month
43  /// </summary>
44  /// <param name="symbol">Option symbol</param>
45  /// <returns></returns>
46  public static bool IsStandard(Symbol symbol)
47  {
48  var date = symbol.ID.Date;
49 
50  // first we find out the day of week of the first day in the month
51  var firstDayOfMonth = new DateTime(date.Year, date.Month, 1).DayOfWeek;
52 
53  // find out the day of first Friday in this month
54  var firstFriday = firstDayOfMonth == DayOfWeek.Saturday ? 7 : 6 - (int)firstDayOfMonth;
55 
56  // check if the expiration date is within the week containing 3rd Friday
57  // we exclude monday, wednesday, and friday weeklys
58  return firstFriday + 7 + 5 /*sat -> wed */ < date.Day && date.Day < firstFriday + 2 * 7 + 2 /* sat, sun*/;
59  }
60 
61  /// <summary>
62  /// Returns true if the option is a weekly contract that expires on Friday , except 3rd Friday of the month
63  /// </summary>
64  /// <param name="symbol">Option symbol</param>
65  /// <returns></returns>
66  public static bool IsWeekly(Symbol symbol)
67  {
68  return !IsStandard(symbol) && symbol.ID.Date.DayOfWeek == DayOfWeek.Friday;
69  }
70 
71  /// <summary>
72  /// Maps the option ticker to it's underlying
73  /// </summary>
74  /// <param name="optionTicker">The option ticker to map</param>
75  /// <param name="securityType">The security type of the option or underlying</param>
76  /// <returns>The underlying ticker</returns>
77  public static string MapToUnderlying(string optionTicker, SecurityType securityType)
78  {
79  if(securityType == SecurityType.FutureOption || securityType == SecurityType.Future)
80  {
81  return FuturesOptionsSymbolMappings.MapFromOption(optionTicker);
82  }
83  else if (securityType == SecurityType.IndexOption || securityType == SecurityType.Index)
84  {
85  return IndexOptionSymbol.MapToUnderlying(optionTicker);
86  }
87 
88  return optionTicker;
89  }
90 
91  /// <summary>
92  /// Returns the last trading date for the option contract
93  /// </summary>
94  /// <param name="symbol">Option symbol</param>
95  /// <returns></returns>
96  public static DateTime GetLastDayOfTrading(Symbol symbol)
97  {
98  // The OCC proposed rule change: starting from 1 Feb 2015 standard monthly contracts
99  // expire on 3rd Friday, not Saturday following 3rd Friday as it was before.
100  // More details: https://www.sec.gov/rules/sro/occ/2013/34-69480.pdf
101 
102  int daysBefore = 0;
103  var symbolDateTime = symbol.ID.Date;
104 
105  if (IsStandard(symbol) &&
106  symbolDateTime.DayOfWeek == DayOfWeek.Saturday &&
107  symbolDateTime < new DateTime(2015, 2, 1))
108  {
109  daysBefore--;
110  }
111 
112  var exchangeHours = MarketHoursDatabase.FromDataFolder()
113  .GetEntry(symbol.ID.Market, symbol, symbol.SecurityType)
114  .ExchangeHours;
115 
116  while (!exchangeHours.IsDateOpen(symbolDateTime.AddDays(daysBefore)))
117  {
118  daysBefore--;
119  }
120 
121  return symbolDateTime.AddDays(daysBefore).Date;
122  }
123 
124  /// <summary>
125  /// Returns the settlement date time of the option contract.
126  /// </summary>
127  /// <param name="symbol">The option contract symbol</param>
128  /// <returns>The settlement date time</returns>
129  public static DateTime GetSettlementDateTime(Symbol symbol)
130  {
131  if (!TryGetExpirationDateTime(symbol, out var expiryTime, out var exchangeHours))
132  {
133  throw new ArgumentException($"The symbol {symbol} is not an option type");
134  }
135 
136  // Standard index options are AM-settled, which means they settle on market open of the expiration date
137  if (expiryTime.Date == symbol.ID.Date.Date && symbol.SecurityType == SecurityType.IndexOption && IsStandard(symbol))
138  {
139  expiryTime = exchangeHours.GetNextMarketOpen(expiryTime.Date, false);
140  }
141 
142  return expiryTime;
143  }
144 
145  /// <summary>
146  /// Returns true if the option contract is expired at the specified time
147  /// </summary>
148  /// <param name="symbol">The option contract symbol</param>
149  /// <param name="currentTimeUtc">The current time (UTC)</param>
150  /// <returns>True if the option contract is expired at the specified time, false otherwise</returns>
151  public static bool IsOptionContractExpired(Symbol symbol, DateTime currentTimeUtc)
152  {
153  if (TryGetExpirationDateTime(symbol, out var expiryTime, out var exchangeHours))
154  {
155  var currentTime = currentTimeUtc.ConvertFromUtc(exchangeHours.TimeZone);
156  return currentTime >= expiryTime;
157  }
158 
159  return false;
160  }
161 
162  [MethodImpl(MethodImplOptions.AggressiveInlining)]
163  private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryTime, out SecurityExchangeHours exchangeHours)
164  {
165  if (!symbol.SecurityType.IsOption())
166  {
167  expiryTime = default;
168  exchangeHours = null;
169  return false;
170  }
171 
172  exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
173 
174  // Ideally we can calculate expiry on the date of the symbol ID, but if that exchange is not open on that day we
175  // will consider expired on the last trading day close before this; Example in AddOptionContractExpiresRegressionAlgorithm
176  var lastTradingDay = exchangeHours.IsDateOpen(symbol.ID.Date)
177  ? symbol.ID.Date
178  : exchangeHours.GetPreviousTradingDay(symbol.ID.Date);
179 
180  expiryTime = exchangeHours.GetNextMarketClose(lastTradingDay, false);
181 
182  // Once bug 6189 was solved in ´GetNextMarketClose()´ there was found possible bugs on some futures symbol.ID.Date or delisting/liquidation handle event.
183  // Specifically see 'DelistingFutureOptionRegressionAlgorithm' where Symbol.ID.Date: 4/1/2012 00:00 ExpiryTime: 4/2/2012 16:00 for Milk 3 futures options.
184  // See 'bug-milk-class-3-future-options-expiration' branch. So let's limit the expiry time to up to end of day of expiration
185  if (expiryTime >= symbol.ID.Date.AddDays(1).Date)
186  {
187  lock (_optionExpirationErrorLog)
188  {
189  if (symbol.ID.Underlying != null
190  // let's log this once per underlying and expiration date: avoiding the same log for multiple option contracts with different strikes/rights
191  && _optionExpirationErrorLog.TryAdd($"{symbol.ID.Underlying}-{symbol.ID.Date}", 1))
192  {
193  Logging.Log.Error($"OptionSymbol.IsOptionContractExpired(): limiting unexpected option expiration time for symbol {symbol.ID}. Symbol.ID.Date {symbol.ID.Date}. ExpiryTime: {expiryTime}");
194  }
195  }
196  expiryTime = symbol.ID.Date.AddDays(1).Date;
197  }
198 
199  return true;
200  }
201  }
202 }