/*
* 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.Linq;
using QuantConnect.Logging;
using System.Globalization;
using QuantConnect.Securities;
using System.Collections.Generic;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.FutureOption;
using static QuantConnect.StringExtensions;
using System.Text.RegularExpressions;
using QuantConnect.Securities.IndexOption;
namespace QuantConnect
{
///
/// Public static helper class that does parsing/generation of symbol representations (options, futures)
///
public static class SymbolRepresentation
{
// Define the regex as a private readonly static field and compile it
private static readonly Regex _optionTickerRegex = new Regex(@"^([A-Z0-9]+)\s*(\d{6})([CP])(\d{8})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
///
/// Class contains future ticker properties returned by ParseFutureTicker()
///
public class FutureTickerProperties
{
///
/// Underlying name
///
public string Underlying { get; set; }
///
/// Short expiration year
///
public int ExpirationYearShort { get; set; }
///
/// Short expiration year digits
///
public int ExpirationYearShortLength { get; set; }
///
/// Expiration month
///
public int ExpirationMonth { get; set; }
///
/// Expiration day
///
public int ExpirationDay { get; set; }
}
///
/// Class contains option ticker properties returned by ParseOptionTickerIQFeed()
///
public class OptionTickerProperties
{
///
/// Underlying name
///
public string Underlying { get; set; }
///
/// Option right
///
public OptionRight OptionRight { get; set; }
///
/// Option strike
///
public decimal OptionStrike { get; set; }
///
/// Expiration date
///
public DateTime ExpirationDate { get; set; }
}
///
/// Function returns underlying name, expiration year, expiration month, expiration day for the future contract ticker. Function detects if
/// 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.
/// Format [Ticker][2 digit day code OPTIONAL][1 char month code][2/1 digit year code]
///
///
/// Results containing 1) underlying name, 2) short expiration year, 3) expiration month
public static FutureTickerProperties ParseFutureTicker(string ticker)
{
var doubleDigitYear = char.IsDigit(ticker.Substring(ticker.Length - 2, 1)[0]);
var doubleDigitOffset = doubleDigitYear ? 1 : 0;
var expirationDayOffset = 0;
var expirationDay = 1;
if (ticker.Length > 4 + doubleDigitOffset)
{
var potentialExpirationDay = ticker.Substring(ticker.Length - 4 - doubleDigitOffset, 2);
var containsExpirationDay = char.IsDigit(potentialExpirationDay[0]) && char.IsDigit(potentialExpirationDay[1]);
expirationDayOffset = containsExpirationDay ? 2 : 0;
if (containsExpirationDay && !int.TryParse(potentialExpirationDay, out expirationDay))
{
return null;
}
}
var expirationYearString = ticker.Substring(ticker.Length - 1 - doubleDigitOffset, 1 + doubleDigitOffset);
var expirationMonthString = ticker.Substring(ticker.Length - 2 - doubleDigitOffset, 1);
var underlyingString = ticker.Substring(0, ticker.Length - 2 - doubleDigitOffset - expirationDayOffset);
int expirationYearShort;
if (!int.TryParse(expirationYearString, out expirationYearShort))
{
return null;
}
if (!FuturesMonthCodeLookup.ContainsKey(expirationMonthString))
{
return null;
}
var expirationMonth = FuturesMonthCodeLookup[expirationMonthString];
return new FutureTickerProperties
{
Underlying = underlyingString,
ExpirationYearShort = expirationYearShort,
ExpirationYearShortLength = expirationYearString.Length,
ExpirationMonth = expirationMonth,
ExpirationDay = expirationDay
};
}
///
/// Helper method to parse and generate a future symbol from a given user friendly representation
///
/// The future ticker, for example 'ESZ1'
/// Clarifies the year for the current future
/// The future symbol or null if failed
public static Symbol ParseFutureSymbol(string ticker, int? futureYear = null)
{
var parsed = ParseFutureTicker(ticker);
if (parsed == null)
{
return null;
}
var underlying = parsed.Underlying;
var expirationMonth = parsed.ExpirationMonth;
var expirationYear = GetExpirationYear(futureYear, parsed);
if (!SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(underlying, SecurityType.Future, out var market))
{
Log.Debug($@"SymbolRepresentation.ParseFutureSymbol(): {Messages.SymbolRepresentation.FailedToGetMarketForTickerAndUnderlying(ticker, underlying)}");
return null;
}
var expiryFunc = FuturesExpiryFunctions.FuturesExpiryFunction(Symbol.Create(underlying, SecurityType.Future, market));
var expiryDate = expiryFunc(new DateTime(expirationYear, expirationMonth, 1));
return Symbol.CreateFuture(underlying, market, expiryDate);
}
///
/// Creates a future option Symbol from the provided ticker
///
/// The future option ticker, for example 'ESZ0 P3590'
/// Optional the future option strike scale factor
public static Symbol ParseFutureOptionSymbol(string ticker, int strikeScale = 1)
{
var split = ticker.Split(' ');
if (split.Length != 2)
{
return null;
}
var parsed = ParseFutureTicker(split[0]);
if (parsed == null)
{
return null;
}
ticker = parsed.Underlying;
OptionRight right;
if (split[1][0] == 'P' || split[1][0] == 'p')
{
right = OptionRight.Put;
}
else if (split[1][0] == 'C' || split[1][0] == 'c')
{
right = OptionRight.Call;
}
else
{
return null;
}
var strike = split[1].Substring(1);
if (parsed.ExpirationYearShort < 10)
{
parsed.ExpirationYearShort += 20;
}
var expirationYearParsed = 2000 + parsed.ExpirationYearShort;
var expirationDate = new DateTime(expirationYearParsed, parsed.ExpirationMonth, 1);
var strikePrice = decimal.Parse(strike, NumberStyles.Any, CultureInfo.InvariantCulture);
var futureTicker = FuturesOptionsSymbolMappings.MapFromOption(ticker);
if (!SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(futureTicker, SecurityType.Future, out var market))
{
Log.Debug($"SymbolRepresentation.ParseFutureOptionSymbol(): {Messages.SymbolRepresentation.NoMarketFound(futureTicker)}");
return null;
}
var canonicalFuture = Symbol.Create(futureTicker, SecurityType.Future, market);
var futureExpiry = FuturesExpiryFunctions.FuturesExpiryFunction(canonicalFuture)(expirationDate);
var future = Symbol.CreateFuture(futureTicker, market, futureExpiry);
var futureOptionExpiry = FuturesOptionsExpiryFunctions.GetFutureOptionExpiryFromFutureExpiry(future);
return Symbol.CreateOption(future,
market,
OptionStyle.American,
right,
strikePrice / strikeScale,
futureOptionExpiry);
}
///
/// Returns future symbol ticker from underlying and expiration date. Function can generate tickers of two formats: one and two digits year.
/// 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
///
/// String underlying
/// Expiration date
/// True if year should represented by two digits; False - one digit
/// True if expiration date should be included
/// The user friendly future ticker
public static string GenerateFutureTicker(string underlying, DateTime expiration, bool doubleDigitsYear = true, bool includeExpirationDate = true)
{
var year = doubleDigitsYear ? expiration.Year % 100 : expiration.Year % 10;
var month = expiration.Month;
var contractMonthDelta = FuturesExpiryUtilityFunctions.GetDeltaBetweenContractMonthAndContractExpiry(underlying, expiration.Date);
if (contractMonthDelta < 0)
{
// For futures that have an expiry after the contract month.
// This is for dairy contracts, which can and do expire after the contract month.
var expirationMonth = expiration.AddDays(-(expiration.Day - 1))
.AddMonths(contractMonthDelta);
month = expirationMonth.Month;
year = doubleDigitsYear ? expirationMonth.Year % 100 : expirationMonth.Year % 10;
}
else
{
// These futures expire in the month before or in the contract month
month += contractMonthDelta;
// Get the month back into the allowable range, allowing for a wrap
// Below is a little algorithm for wrapping numbers with a certain bounds.
// In this case, were dealing with months, wrapping to years once we get to January
// As modulo works for [0, x), it's best to subtract 1 (as months are [1, 12] to convert to [0, 11]),
// do the modulo/integer division, then add 1 back on to get into the correct range again
month--;
year += month / 12;
month %= 12;
month++;
}
var expirationDay = includeExpirationDate ? $"{expiration.Day:00}" : string.Empty;
return $"{underlying}{expirationDay}{FuturesMonthLookup[month]}{year}";
}
///
/// Returns option symbol ticker in accordance with OSI symbology
/// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
///
/// Symbol object to create OSI ticker from
/// The OSI ticker representation
public static string GenerateOptionTickerOSI(this Symbol symbol)
{
if (!symbol.SecurityType.IsOption())
{
throw new ArgumentException(
Messages.SymbolRepresentation.UnexpectedSecurityTypeForMethod(nameof(GenerateOptionTickerOSI), symbol.SecurityType));
}
return GenerateOptionTickerOSI(symbol.Underlying.Value, symbol.ID.OptionRight, symbol.ID.StrikePrice, symbol.ID.Date);
}
///
/// Returns option symbol ticker in accordance with OSI symbology
/// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
///
/// Underlying string
/// Option right
/// Option strike
/// Option expiration date
/// The OSI ticker representation
public static string GenerateOptionTickerOSI(string underlying, OptionRight right, decimal strikePrice, DateTime expiration)
{
if (underlying.Length > 5) underlying += " ";
return Invariant($"{underlying,-6}{expiration.ToStringInvariant(DateFormat.SixCharacter)}{right.ToStringPerformance()[0]}{(strikePrice * 1000m):00000000}");
}
///
/// Returns option symbol ticker in accordance with OSI symbology
/// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
///
/// Symbol object to create OSI ticker from
/// The OSI ticker representation
public static string GenerateOptionTickerOSICompact(this Symbol symbol)
{
// First, validate that the symbol is of the correct security type
if (!symbol.SecurityType.IsOption())
{
throw new ArgumentException(
Messages.SymbolRepresentation.UnexpectedSecurityTypeForMethod(nameof(GenerateOptionTickerOSICompact), symbol.SecurityType));
}
return GenerateOptionTickerOSICompact(symbol.Underlying.Value, symbol.ID.OptionRight, symbol.ID.StrikePrice, symbol.ID.Date);
}
///
/// Returns option symbol ticker in accordance with OSI symbology
/// More information can be found at http://www.optionsclearing.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
///
/// Underlying string
/// Option right
/// Option strike
/// Option expiration date
/// The OSI ticker representation
public static string GenerateOptionTickerOSICompact(string underlying, OptionRight right, decimal strikePrice, DateTime expiration)
{
return Invariant($"{underlying}{expiration.ToStringInvariant(DateFormat.SixCharacter)}{right.ToStringPerformance()[0]}{(strikePrice * 1000m):00000000}");
}
///
/// Parses the specified OSI options ticker into a Symbol object
///
/// The OSI compliant option ticker string
/// The security type
/// The associated market
/// Symbol object for the specified OSI option ticker string
public static Symbol ParseOptionTickerOSI(string ticker, SecurityType securityType = SecurityType.Option, string market = Market.USA)
{
return ParseOptionTickerOSI(ticker, securityType, OptionStyle.American, market);
}
///
/// Parses the specified OSI options ticker into a Symbol object
///
/// The OSI compliant option ticker string
/// The security type
/// The associated market
/// The option style
/// Symbol object for the specified OSI option ticker string
public static Symbol ParseOptionTickerOSI(string ticker, SecurityType securityType, OptionStyle optionStyle, string market)
{
if (!TryDecomposeOptionTickerOSI(ticker, out var optionTicker, out var expiry, out var right, out var strike))
{
throw new FormatException(Messages.SymbolRepresentation.InvalidOSITickerFormat(ticker));
}
SecurityIdentifier underlyingSid;
string underlyingSymbolValue;
if (securityType == SecurityType.Option)
{
underlyingSid = SecurityIdentifier.GenerateEquity(optionTicker, market);
// We have the mapped symbol in the OSI ticker
underlyingSymbolValue = optionTicker;
// let it fallback to it's default handling, which include mapping
optionTicker = null;
}
else if (securityType == SecurityType.IndexOption)
{
underlyingSid = SecurityIdentifier.GenerateIndex(OptionSymbol.MapToUnderlying(optionTicker, securityType), market);
underlyingSymbolValue = underlyingSid.Symbol;
}
else if (securityType == SecurityType.FutureOption)
{
var futureTickerInfo = ParseFutureTicker(optionTicker);
underlyingSid = SecurityIdentifier.GenerateFuture(expiry, futureTickerInfo.Underlying, market);
underlyingSymbolValue = underlyingSid.Symbol;
}
else
{
throw new NotImplementedException($"ParseOptionTickerOSI(): {Messages.SymbolRepresentation.SecurityTypeNotImplemented(securityType)}");
}
var sid = SecurityIdentifier.GenerateOption(expiry, underlyingSid, optionTicker, market, strike, right, optionStyle);
return new Symbol(sid, ticker, new Symbol(underlyingSid, underlyingSymbolValue));
}
///
/// Tries to decompose the specified OSI options ticker into its components
///
/// The OSI option ticker
/// The option ticker extracted from the OSI symbol
/// The option contract expiry date
/// The option contract right
/// The option contract strike price
/// True if the OSI symbol was in the right format and could be decomposed
public static bool TryDecomposeOptionTickerOSI(string ticker, out string optionTicker, out DateTime expiry,
out OptionRight right, out decimal strike)
{
optionTicker = null;
expiry = default;
right = OptionRight.Call;
strike = decimal.Zero;
if (string.IsNullOrEmpty(ticker))
{
return false;
}
var match = _optionTickerRegex.Match(ticker);
if (!match.Success)
{
return false;
}
optionTicker = match.Groups[1].Value;
expiry = DateTime.ParseExact(match.Groups[2].Value, DateFormat.SixCharacter, null);
right = match.Groups[3].Value.ToUpperInvariant() == "C" ? OptionRight.Call : OptionRight.Put;
strike = Parse.Decimal(match.Groups[4].Value) / 1000m;
return true;
}
///
/// Tries to decompose the specified OSI options ticker into its components
///
/// The OSI option ticker
/// The option security type
/// The option ticker extracted from the OSI symbol
/// The underlying ticker
/// The option contract expiry date
/// The option contract right
/// The option contract strike price
/// True if the OSI symbol was in the right format and could be decomposed
public static bool TryDecomposeOptionTickerOSI(string ticker, SecurityType securityType, out string optionTicker,
out string underlyingTicker, out DateTime expiry, out OptionRight right, out decimal strike)
{
optionTicker = null;
underlyingTicker = null;
expiry = default;
right = OptionRight.Call;
strike = decimal.Zero;
if (!securityType.IsOption())
{
return false;
}
var result = TryDecomposeOptionTickerOSI(ticker, out optionTicker, out expiry, out right, out strike);
underlyingTicker = securityType != SecurityType.IndexOption ? optionTicker : IndexOptionSymbol.MapToUnderlying(optionTicker);
return result;
}
///
/// Function returns option ticker from IQFeed option ticker
/// For example CSCO1220V19 Cisco October Put at 19.00 Expiring on 10/20/12
/// Symbology details: http://www.iqfeed.net/symbolguide/index.cfm?symbolguide=guide&displayaction=support%C2%A7ion=guide&web=iqfeed&guide=options&web=IQFeed&type=stock
///
/// THe option symbol
/// The option ticker
public static string GenerateOptionTicker(Symbol symbol)
{
var symbolTicker = symbol.SecurityType == SecurityType.IndexOption ? symbol.Canonical.Value.Replace("?", string.Empty) : SecurityIdentifier.Ticker(symbol.Underlying, symbol.ID.Date);
var letter = OptionCodeLookup.Where(x => x.Value.Item2 == symbol.ID.OptionRight && x.Value.Item1 == symbol.ID.Date.Month).Select(x => x.Key).Single();
var twoYearDigit = symbol.ID.Date.ToString("yy");
return $"{symbolTicker}{twoYearDigit}{symbol.ID.Date.Day:00}{letter}{symbol.ID.StrikePrice.ToStringInvariant()}";
}
///
/// Function returns option contract parameters (underlying name, expiration date, strike, right) from IQFeed option ticker
/// Symbology details: http://www.iqfeed.net/symbolguide/index.cfm?symbolguide=guide&displayaction=support%C2%A7ion=guide&web=iqfeed&guide=options&web=IQFeed&type=stock
///
/// IQFeed option ticker
/// Results containing 1) underlying name, 2) option right, 3) option strike 4) expiration date
public static OptionTickerProperties ParseOptionTickerIQFeed(string ticker)
{
var letterRange = OptionCodeLookup.Keys
.Select(x => x[0])
.ToArray();
var optionTypeDelimiter = ticker.LastIndexOfAny(letterRange);
var strikePriceString = ticker.Substring(optionTypeDelimiter + 1, ticker.Length - optionTypeDelimiter - 1);
var lookupResult = OptionCodeLookup[ticker[optionTypeDelimiter].ToStringInvariant()];
var month = lookupResult.Item1;
var optionRight = lookupResult.Item2;
var dayString = ticker.Substring(optionTypeDelimiter - 2, 2);
var yearString = ticker.Substring(optionTypeDelimiter - 4, 2);
var underlying = ticker.Substring(0, optionTypeDelimiter - 4);
// if we cannot parse strike price, we ignore this contract, but log the information.
Decimal strikePrice;
if (!Decimal.TryParse(strikePriceString, NumberStyles.Any, CultureInfo.InvariantCulture, out strikePrice))
{
return null;
}
int day;
if (!int.TryParse(dayString, out day))
{
return null;
}
int year;
if (!int.TryParse(yearString, out year))
{
return null;
}
var expirationDate = new DateTime(2000 + year, month, day);
return new OptionTickerProperties
{
Underlying = underlying,
OptionRight = optionRight,
OptionStrike = strikePrice,
ExpirationDate = expirationDate
};
}
///
/// A dictionary that maps option symbols to a tuple containing the option series number and the option right (Call or Put).
/// The key represents a single character option symbol, and the value contains the series number and the associated option right.
///
///
/// The dictionary is designed to map each option symbol (e.g., "A", "M", "B", etc.) to an option series number and
/// the corresponding option right (either a Call or Put). The series number determines the group of options the symbol belongs to,
/// 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).
///
public static IReadOnlyDictionary> OptionCodeLookup { get; } = new Dictionary>
{
{ "A", Tuple.Create(1, OptionRight.Call) }, { "M", Tuple.Create(1, OptionRight.Put) },
{ "B", Tuple.Create(2, OptionRight.Call) }, { "N", Tuple.Create(2, OptionRight.Put) },
{ "C", Tuple.Create(3, OptionRight.Call) }, { "O", Tuple.Create(3, OptionRight.Put) },
{ "D", Tuple.Create(4, OptionRight.Call) }, { "P", Tuple.Create(4, OptionRight.Put) },
{ "E", Tuple.Create(5, OptionRight.Call) }, { "Q", Tuple.Create(5, OptionRight.Put) },
{ "F", Tuple.Create(6, OptionRight.Call) }, { "R", Tuple.Create(6, OptionRight.Put) },
{ "G", Tuple.Create(7, OptionRight.Call) }, { "S", Tuple.Create(7, OptionRight.Put) },
{ "H", Tuple.Create(8, OptionRight.Call) }, { "T", Tuple.Create(8, OptionRight.Put) },
{ "I", Tuple.Create(9, OptionRight.Call) }, { "U", Tuple.Create(9, OptionRight.Put) },
{ "J", Tuple.Create(10, OptionRight.Call) }, { "V", Tuple.Create(10, OptionRight.Put) },
{ "K", Tuple.Create(11, OptionRight.Call) }, { "W", Tuple.Create(11, OptionRight.Put) },
{ "L", Tuple.Create(12, OptionRight.Call) }, { "X", Tuple.Create(12, OptionRight.Put) },
};
///
/// Provides a lookup dictionary for mapping futures month codes to their corresponding numeric values.
///
public static IReadOnlyDictionary FuturesMonthCodeLookup { get; } = new Dictionary
{
{ "F", 1 }, // January
{ "G", 2 }, // February
{ "H", 3 }, // March
{ "J", 4 }, // April
{ "K", 5 }, // May
{ "M", 6 }, // June
{ "N", 7 }, // July
{ "Q", 8 }, // August
{ "U", 9 }, // September
{ "V", 10 }, // October
{ "X", 11 }, // November
{ "Z", 12 } // December
};
///
/// Provides a lookup dictionary for mapping numeric values to their corresponding futures month codes.
///
public static IReadOnlyDictionary FuturesMonthLookup { get; } = FuturesMonthCodeLookup.ToDictionary(kv => kv.Value, kv => kv.Key);
///
/// Get the expiration year from short year (two-digit integer).
/// Examples: NQZ23 and NQZ3 for Dec 2023
///
/// Clarifies the year for the current future
/// Contains useful information about the future expiration year
/// Tickers from live trading may not provide the four-digit year.
private static int GetExpirationYear(int? futureYear, FutureTickerProperties parsed)
{
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;
}
}
}