/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.IndexOption;
namespace QuantConnect.Securities.Option
{
///
/// Static class contains common utility methods specific to symbols representing the option contracts
///
public static class OptionSymbol
{
private static readonly Dictionary _optionExpirationErrorLog = new();
///
/// Returns true if the option is a standard contract that expires 3rd Friday of the month
///
/// Option symbol
///
public static bool IsStandardContract(Symbol symbol)
{
return IsStandard(symbol);
}
///
/// Returns true if the option is a standard contract that expires 3rd Friday of the month
///
/// Option symbol
///
public static bool IsStandard(Symbol symbol)
{
var date = symbol.ID.Date;
// first we find out the day of week of the first day in the month
var firstDayOfMonth = new DateTime(date.Year, date.Month, 1).DayOfWeek;
// find out the day of first Friday in this month
var firstFriday = firstDayOfMonth == DayOfWeek.Saturday ? 7 : 6 - (int)firstDayOfMonth;
// check if the expiration date is within the week containing 3rd Friday
// we exclude monday, wednesday, and friday weeklys
return firstFriday + 7 + 5 /*sat -> wed */ < date.Day && date.Day < firstFriday + 2 * 7 + 2 /* sat, sun*/;
}
///
/// Returns true if the option is a weekly contract that expires on Friday , except 3rd Friday of the month
///
/// Option symbol
///
public static bool IsWeekly(Symbol symbol)
{
return !IsStandard(symbol) && symbol.ID.Date.DayOfWeek == DayOfWeek.Friday;
}
///
/// Maps the option ticker to it's underlying
///
/// The option ticker to map
/// The security type of the option or underlying
/// The underlying ticker
public static string MapToUnderlying(string optionTicker, SecurityType securityType)
{
if(securityType == SecurityType.FutureOption || securityType == SecurityType.Future)
{
return FuturesOptionsSymbolMappings.MapFromOption(optionTicker);
}
else if (securityType == SecurityType.IndexOption || securityType == SecurityType.Index)
{
return IndexOptionSymbol.MapToUnderlying(optionTicker);
}
return optionTicker;
}
///
/// Returns the last trading date for the option contract
///
/// Option symbol
///
public static DateTime GetLastDayOfTrading(Symbol symbol)
{
// The OCC proposed rule change: starting from 1 Feb 2015 standard monthly contracts
// expire on 3rd Friday, not Saturday following 3rd Friday as it was before.
// More details: https://www.sec.gov/rules/sro/occ/2013/34-69480.pdf
int daysBefore = 0;
var symbolDateTime = symbol.ID.Date;
if (IsStandard(symbol) &&
symbolDateTime.DayOfWeek == DayOfWeek.Saturday &&
symbolDateTime < new DateTime(2015, 2, 1))
{
daysBefore--;
}
var exchangeHours = MarketHoursDatabase.FromDataFolder()
.GetEntry(symbol.ID.Market, symbol, symbol.SecurityType)
.ExchangeHours;
while (!exchangeHours.IsDateOpen(symbolDateTime.AddDays(daysBefore)))
{
daysBefore--;
}
return symbolDateTime.AddDays(daysBefore).Date;
}
///
/// Returns the settlement date time of the option contract.
///
/// The option contract symbol
/// The settlement date time
public static DateTime GetSettlementDateTime(Symbol symbol)
{
if (!TryGetExpirationDateTime(symbol, out var expiryTime, out var exchangeHours))
{
throw new ArgumentException($"The symbol {symbol} is not an option type");
}
// Standard index options are AM-settled, which means they settle on market open of the expiration date
if (expiryTime.Date == symbol.ID.Date.Date && symbol.SecurityType == SecurityType.IndexOption && IsStandard(symbol))
{
expiryTime = exchangeHours.GetNextMarketOpen(expiryTime.Date, false);
}
return expiryTime;
}
///
/// Returns true if the option contract is expired at the specified time
///
/// The option contract symbol
/// The current time (UTC)
/// True if the option contract is expired at the specified time, false otherwise
public static bool IsOptionContractExpired(Symbol symbol, DateTime currentTimeUtc)
{
if (TryGetExpirationDateTime(symbol, out var expiryTime, out var exchangeHours))
{
var currentTime = currentTimeUtc.ConvertFromUtc(exchangeHours.TimeZone);
return currentTime >= expiryTime;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryTime, out SecurityExchangeHours exchangeHours)
{
if (!symbol.SecurityType.IsOption())
{
expiryTime = default;
exchangeHours = null;
return false;
}
exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
// Ideally we can calculate expiry on the date of the symbol ID, but if that exchange is not open on that day we
// will consider expired on the last trading day close before this; Example in AddOptionContractExpiresRegressionAlgorithm
var lastTradingDay = exchangeHours.IsDateOpen(symbol.ID.Date)
? symbol.ID.Date
: exchangeHours.GetPreviousTradingDay(symbol.ID.Date);
expiryTime = exchangeHours.GetLastDailyMarketClose(lastTradingDay, false);
// Once bug 6189 was solved in ´GetNextMarketClose()´ there was found possible bugs on some futures symbol.ID.Date or delisting/liquidation handle event.
// Specifically see 'DelistingFutureOptionRegressionAlgorithm' where Symbol.ID.Date: 4/1/2012 00:00 ExpiryTime: 4/2/2012 16:00 for Milk 3 futures options.
// See 'bug-milk-class-3-future-options-expiration' branch. So let's limit the expiry time to up to end of day of expiration
if (expiryTime >= symbol.ID.Date.AddDays(1).Date)
{
lock (_optionExpirationErrorLog)
{
if (symbol.ID.Underlying != null
// let's log this once per underlying and expiration date: avoiding the same log for multiple option contracts with different strikes/rights
&& _optionExpirationErrorLog.TryAdd($"{symbol.ID.Underlying}-{symbol.ID.Date}", 1))
{
Logging.Log.Error($"OptionSymbol.IsOptionContractExpired(): limiting unexpected option expiration time for symbol {symbol.ID}. Symbol.ID.Date {symbol.ID.Date}. ExpiryTime: {expiryTime}");
}
}
expiryTime = symbol.ID.Date.AddDays(1).Date;
}
return true;
}
}
}