From 8e0e616ebe29b0f948f20542dfecfdd9daeca4bd Mon Sep 17 00:00:00 2001 From: Konstantin Ivaschenko Date: Fri, 26 Jul 2024 18:09:02 +0300 Subject: [PATCH] [MDAPI-42] [.NET] Implement OptionChain --- dxfeed-graal-net-api.sln | 7 + .../DXFeedOptionChain.csproj | 27 ++ samples/DXFeedOptionChain/Program.cs | 103 +++++ .../Ipf/Option/OptionChain.cs | 63 +++ .../Ipf/Option/OptionChainsBuilder.cs | 238 ++++++++++++ .../Ipf/Option/OptionSeries.cs | 359 ++++++++++++++++++ 6 files changed, 797 insertions(+) create mode 100644 samples/DXFeedOptionChain/DXFeedOptionChain.csproj create mode 100644 samples/DXFeedOptionChain/Program.cs create mode 100644 src/DxFeed.Graal.Net/Ipf/Option/OptionChain.cs create mode 100644 src/DxFeed.Graal.Net/Ipf/Option/OptionChainsBuilder.cs create mode 100644 src/DxFeed.Graal.Net/Ipf/Option/OptionSeries.cs diff --git a/dxfeed-graal-net-api.sln b/dxfeed-graal-net-api.sln index b8d5c8bb..27ff5382 100644 --- a/dxfeed-graal-net-api.sln +++ b/dxfeed-graal-net-api.sln @@ -48,6 +48,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipleMarketDepthSample", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UI", "UI", "{5F74BD34-C2D4-436B-8243-FB0F3BB9F0AC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DXFeedOptionChain", "samples\DXFeedOptionChain\DXFeedOptionChain.csproj", "{7E7BF3A7-C564-4B82-AAD6-6C1D1BCE3F19}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -138,6 +140,10 @@ Global {C8F5013F-7F40-46D2-92AD-6B593524A1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8F5013F-7F40-46D2-92AD-6B593524A1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8F5013F-7F40-46D2-92AD-6B593524A1D0}.Release|Any CPU.Build.0 = Release|Any CPU + {7E7BF3A7-C564-4B82-AAD6-6C1D1BCE3F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E7BF3A7-C564-4B82-AAD6-6C1D1BCE3F19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E7BF3A7-C564-4B82-AAD6-6C1D1BCE3F19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E7BF3A7-C564-4B82-AAD6-6C1D1BCE3F19}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {73597E04-D8A8-4991-A759-7F886CBE2A8F} = {C4490D74-2970-4A1B-8178-A724A06B140A} @@ -160,5 +166,6 @@ Global {5F74BD34-C2D4-436B-8243-FB0F3BB9F0AC} = {C4490D74-2970-4A1B-8178-A724A06B140A} {B74E8A86-1AB7-4B36-AED3-292CDD95BF90} = {5F74BD34-C2D4-436B-8243-FB0F3BB9F0AC} {930B1039-B76C-42C5-AD0F-9FA1A1FC9D84} = {5F74BD34-C2D4-436B-8243-FB0F3BB9F0AC} + {7E7BF3A7-C564-4B82-AAD6-6C1D1BCE3F19} = {C4490D74-2970-4A1B-8178-A724A06B140A} EndGlobalSection EndGlobal diff --git a/samples/DXFeedOptionChain/DXFeedOptionChain.csproj b/samples/DXFeedOptionChain/DXFeedOptionChain.csproj new file mode 100644 index 00000000..678b85a0 --- /dev/null +++ b/samples/DXFeedOptionChain/DXFeedOptionChain.csproj @@ -0,0 +1,27 @@ + + + + Exe + DxFeed.Graal.Net.Samples + net6.0 + + + + ../../artifacts/Debug/Samples/ + + + + ../../artifacts/Release/Samples/ + + + + + + + + + Always + + + + diff --git a/samples/DXFeedOptionChain/Program.cs b/samples/DXFeedOptionChain/Program.cs new file mode 100644 index 00000000..3bc3936a --- /dev/null +++ b/samples/DXFeedOptionChain/Program.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DxFeed.Graal.Net.Api; +using DxFeed.Graal.Net.Events.Market; +using DxFeed.Graal.Net.Ipf; +using DxFeed.Graal.Net.Ipf.Option; + +namespace DxFeed.Graal.Net.Samples; + +abstract class Program +{ + public static async Task Main(string[] args) + { + if (args.Length != 4) + { + Console.WriteLine("usage: DXFeedOptionChain "); + Console.WriteLine(" is name of instrument profiles file"); + Console.WriteLine(" is the product or underlying symbol"); + Console.WriteLine(" number of strikes to print for each series"); + Console.WriteLine(" number of months to print"); + return; + } + + SystemProperty.SetProperty("dxfeed.address", "demo.dxfeed.com:7300"); + + var argIpfFile = args[0]; + var argSymbol = args[1]; + var nStrikes = int.Parse(args[2]); + var nMonths = int.Parse(args[3]); + + var feed = DXFeed.GetInstance(); + + // subscribe to trade to learn instrument last price + Console.WriteLine($"Waiting for price of {argSymbol} ..."); + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(1)); + var trade = await feed.GetLastEventAsync(argSymbol, cts.Token); + + var price = trade.Price; + Console.WriteLine($"Price of {argSymbol} is {price.ToString(CultureInfo.InvariantCulture)}"); + + Console.WriteLine($"Reading instruments from {argIpfFile} ..."); + var instruments = new InstrumentProfileReader().ReadFromFile(argIpfFile).ToList(); + + Console.WriteLine("Building option chains ..."); + var chains = OptionChainsBuilder.Build(instruments).GetChains(); + if (!chains.TryGetValue(argSymbol, out var chain)) + { + Console.WriteLine($"No chain found for symbol {argSymbol}"); + return; + } + + nMonths = Math.Min(nMonths, chain.GetSeries().Count); + var seriesList = chain.GetSeries().Take(nMonths).ToList(); + + Console.WriteLine("Requesting option quotes ..."); + var quotes = new Dictionary>(); + + foreach (var series in seriesList) + { + var strikes = series.GetNStrikesAround(nStrikes, price); + foreach (var strike in strikes) + { + if (series.Calls.TryGetValue(strike, out var call)) + { + quotes[call] = feed.GetLastEventAsync(call.Symbol); + } + + if (series.Puts.TryGetValue(strike, out var put)) + { + quotes[put] = feed.GetLastEventAsync(put.Symbol); + } + } + } + + await Task.WhenAll(quotes.Values); + + Console.WriteLine("Printing option series ..."); + foreach (var series in seriesList) + { + Console.WriteLine($"Option series {series}"); + var strikes = series.GetNStrikesAround(nStrikes, price); + Console.WriteLine($"{"C.BID",10} {"C.ASK",10} {"STRIKE",10} {"P.BID",10} {"P.ASK",10}"); + foreach (var strike in strikes) + { + series.Calls.TryGetValue(strike, out var call); + series.Puts.TryGetValue(strike, out var put); + var callQuote = call != null && quotes.TryGetValue(call, out var callTask) + ? callTask.Result + : new Quote(); + var putQuote = put != null && quotes.TryGetValue(put, out var putTask) ? putTask.Result : new Quote(); + Console.WriteLine( + $"{callQuote.BidPrice,10:F3} {callQuote.AskPrice,10:F3} {strike,10:F3} {putQuote.BidPrice,10:F3} {putQuote.AskPrice,10:F3}"); + } + } + + Environment.Exit(0); // shutdown when done + } +} diff --git a/src/DxFeed.Graal.Net/Ipf/Option/OptionChain.cs b/src/DxFeed.Graal.Net/Ipf/Option/OptionChain.cs new file mode 100644 index 00000000..797dc798 --- /dev/null +++ b/src/DxFeed.Graal.Net/Ipf/Option/OptionChain.cs @@ -0,0 +1,63 @@ +// +// Copyright © 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +using System; +using System.Collections.Generic; + +namespace DxFeed.Graal.Net.Ipf.Option; + +public sealed class OptionChain : ICloneable +{ + private readonly string _symbol; + + private readonly SortedDictionary, OptionSeries> seriesMap = new(); + + public OptionChain(string symbol) => this._symbol = symbol; + + /// + /// Returns a shallow copy of this option chain. + /// All series are copied (cloned) themselves, but option instrument instances are shared with the original. + /// + /// A shallow copy of this option chain. + public object Clone() + { + OptionChain clone = new OptionChain(_symbol); + foreach (OptionSeries series in seriesMap.Values) + { + OptionSeries seriesClone = (OptionSeries)series.Clone(); + clone.seriesMap.Add(seriesClone, seriesClone); + } + return clone; + } + + /// + /// Returns symbol (product or underlying) of this option chain. + /// + /// Symbol (product or underlying) of this option chain. + public string GetSymbol() + { + return _symbol; + } + + /// + /// Returns a sorted set of option series of this option chain. + /// + /// Sorted set of option series of this option chain. + public SortedSet> GetSeries() + { + return new SortedSet>(seriesMap.Keys); + } + + internal void AddOption(OptionSeries series, bool isCall, double strike, T option) + { + if (!seriesMap.TryGetValue(series, out var os)) + { + os = new OptionSeries(series); + seriesMap[os] = os; + } + os.AddOption(isCall, strike, option); + } +} diff --git a/src/DxFeed.Graal.Net/Ipf/Option/OptionChainsBuilder.cs b/src/DxFeed.Graal.Net/Ipf/Option/OptionChainsBuilder.cs new file mode 100644 index 00000000..2bd00c37 --- /dev/null +++ b/src/DxFeed.Graal.Net/Ipf/Option/OptionChainsBuilder.cs @@ -0,0 +1,238 @@ +// +// Copyright © 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +namespace DxFeed.Graal.Net.Ipf.Option; + +/* + * QDS - Quick Data Signalling Library + * Copyright (C) 2002 - 2021 Devexperts LLC + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + */ +using System.Collections.Generic; + +/// +/// Builder class for a set of option chains grouped by product or underlying symbol. +/// +///

Threads and clocks

+/// This class is NOT thread-safe and cannot be used from multiple threads without external synchronization. +///
+/// The type of option instrument instances. +public class OptionChainsBuilder +{ + /// + /// Builds options chains for all options from the given collections of instrument profiles. + /// + /// Collection of instrument profiles. + /// Builder with all the options from instruments collection. + public static OptionChainsBuilder Build(IEnumerable instruments) + { + var ocb = new OptionChainsBuilder(); + foreach (var ip in instruments) + { + if (ip.Type != "OPTION") + continue; + + ocb.SetProduct(ip.Product); + ocb.SetUnderlying(ip.Underlying); + ocb.SetExpiration(ip.Expiration); + ocb.SetLastTrade(ip.LastTrade); + ocb.SetMultiplier(ip.Multiplier); + ocb.SetSPC(ip.SPC); + ocb.SetAdditionalUnderlyings(ip.AdditionalUnderlyings); + ocb.SetMMY(ip.MMY); + ocb.SetOptionType(ip.OptionType); + ocb.SetExpirationStyle(ip.ExpirationStyle); + ocb.SetSettlementStyle(ip.SettlementStyle); + ocb.SetCFI(ip.CFI); + ocb.SetStrike(ip.Strike); + ocb.AddOption(ip); + } + + return ocb; + } + + // ---------------- instance ---------------- + + private string product = string.Empty; + private string underlying = string.Empty; + private OptionSeries series = new OptionSeries(); + private string cfi = string.Empty; + private double strike; + + private readonly Dictionary> chains = new Dictionary>(); + + /// + /// Creates new option chains builder. + /// + public OptionChainsBuilder() { } + + /// + /// Changes product for futures and options on futures (underlying asset name). + /// Example: "/YG". + /// + /// Product for futures and options on futures (underlying asset name). + public void SetProduct(string product) + { + this.product = string.IsNullOrEmpty(product) ? string.Empty : product; + } + + /// + /// Changes primary underlying symbol for options. + /// Example: "C", "/YGM9" + /// + /// Primary underlying symbol for options. + public void SetUnderlying(string underlying) + { + this.underlying = string.IsNullOrEmpty(underlying) ? string.Empty : underlying; + } + + /// + /// Changes day id of expiration. + /// Example: DayUtil.GetDayIdByYearMonthDay(20090117). + /// + /// Day id of expiration. + public void SetExpiration(int expiration) + { + series.Expiration = expiration; + } + + /// + /// Changes day id of last trading day. + /// Example: DayUtil.GetDayIdByYearMonthDay(20090116). + /// + /// Day id of last trading day. + public void SetLastTrade(int lastTrade) + { + series.LastTrade = lastTrade; + } + + /// + /// Changes market value multiplier. + /// Example: 100, 33.2. + /// + /// Market value multiplier. + public void SetMultiplier(double multiplier) + { + series.Multiplier = multiplier; + } + + /// + /// Changes shares per contract for options. + /// Example: 1, 100. + /// + /// Shares per contract for options. + public void SetSPC(double spc) + { + series.SPC = spc; + } + + /// + /// Changes additional underlyings for options, including additional cash. + /// + /// Additional underlyings for options, including additional cash. + public void SetAdditionalUnderlyings(string additionalUnderlyings) + { + series.AdditionalUnderlyings = + string.IsNullOrEmpty(additionalUnderlyings) ? string.Empty : additionalUnderlyings; + } + + /// + /// Changes maturity month-year as provided for corresponding FIX tag (200). + /// + /// Maturity month-year as provided for corresponding FIX tag (200). + public void SetMMY(string mmy) + { + series.MMY = string.IsNullOrEmpty(mmy) ? string.Empty : mmy; + } + + /// + /// Changes type of option. + /// + /// Type of option. + public void SetOptionType(string optionType) + { + series.OptionType = string.IsNullOrEmpty(optionType) ? string.Empty : optionType; + } + + /// + /// Returns expiration cycle style, such as "Weeklys", "Quarterlys". + /// + /// Expiration cycle style. + public void SetExpirationStyle(string expirationStyle) + { + series.ExpirationStyle = string.IsNullOrEmpty(expirationStyle) ? string.Empty : expirationStyle; + } + + /// + /// Changes settlement price determination style, such as "Open", "Close". + /// + /// Settlement price determination style. + public void SetSettlementStyle(string settlementStyle) + { + series.SettlementStyle = string.IsNullOrEmpty(settlementStyle) ? string.Empty : settlementStyle; + } + + /// + /// Changes Classification of Financial Instruments code. + /// + /// CFI code. + public void SetCFI(string cfi) + { + this.cfi = string.IsNullOrEmpty(cfi) ? string.Empty : cfi; + series.CFI = this.cfi.Length < 2 ? this.cfi : this.cfi[0] + "X" + this.cfi.Substring(2); + } + + /// + /// Changes strike price for options. + /// Example: 80, 22.5. + /// + /// Strike price for options. + public void SetStrike(double strike) + { + this.strike = strike; + } + + /// + /// Adds an option instrument to this builder. + /// + /// Option to add. + public void AddOption(T option) + { + bool isCall = cfi.StartsWith("OC"); + if (!isCall && !cfi.StartsWith("OP")) + return; + if (series.Expiration == 0) + return; + if (double.IsNaN(strike) || double.IsInfinity(strike)) + return; + if (product.Length > 0) + GetOrCreateChain(product).AddOption(series, isCall, strike, option); + if (underlying.Length > 0) + GetOrCreateChain(underlying).AddOption(series, isCall, strike, option); + } + + private OptionChain GetOrCreateChain(string symbol) + { + if (!chains.TryGetValue(symbol, out var chain)) + { + chain = new OptionChain(symbol); + chains[symbol] = chain; + } + + return chain; + } + + /// + /// Returns a view of chains created by this builder. + /// + /// View of chains created by this builder. + public IDictionary> GetChains() + { + return chains; + } +} diff --git a/src/DxFeed.Graal.Net/Ipf/Option/OptionSeries.cs b/src/DxFeed.Graal.Net/Ipf/Option/OptionSeries.cs new file mode 100644 index 00000000..83f10ab7 --- /dev/null +++ b/src/DxFeed.Graal.Net/Ipf/Option/OptionSeries.cs @@ -0,0 +1,359 @@ +// +// Copyright © 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using DxFeed.Graal.Net.Utils; + +namespace DxFeed.Graal.Net.Ipf.Option; + +/// +/// Series of call and put options with different strike sharing the same attributes of +/// expiration, last trading day, spc, multiplier, etc. +/// +///

Threads and locks

+/// +/// This class is NOT thread-safe and cannot be used from multiple threads without external synchronization. +///
+/// The type of option instrument instances. +public sealed class OptionSeries : ICloneable, IComparable> +{ + private List? _strikes; + + public OptionSeries() + { + } + + public OptionSeries(OptionSeries other) + { + Expiration = other.Expiration; + LastTrade = other.LastTrade; + Multiplier = other.Multiplier; + SPC = other.SPC; + AdditionalUnderlyings = other.AdditionalUnderlyings; + MMY = other.MMY; + OptionType = other.OptionType; + ExpirationStyle = other.ExpirationStyle; + SettlementStyle = other.SettlementStyle; + CFI = other.CFI; + } + + /// + /// Gets day id of expiration. + /// + /// DayUtil.GetYearMonthDayByDayId(20090117). + public int Expiration { get; internal set; } + + /// + /// Gets day id of last trading day. + /// + /// DayUtil.GetYearMonthDayByDayId(20090117). + public int LastTrade { get; internal set; } + + /// + /// Gets market value multiplier. + /// + /// "100", "33.2". + public double Multiplier { get; internal set; } + + /// + /// Gets shares per contract for options. + /// + /// "1", "100". + public double SPC { get; internal set; } + + /// + /// Gets additional underlyings for options, including additional cash. + /// It shall use following format: + /// + /// <VALUE> ::= <empty> | <LIST> + /// <LIST> ::= <AU> | <AU> <semicolon> <space> <LIST> + /// <AU> ::= <UNDERLYING> <space> <SPC> + /// + /// the list shall be sorted by <UNDERLYING>. + /// + /// "SE 50", "FIS 53; US$ 45.46". + public string AdditionalUnderlyings { get; internal set; } = string.Empty; + + /// + /// Gets maturity month-year as provided for corresponding FIX tag (200). + /// It can use several different formats depending on data source. + ///
    + ///
  • YYYYMM – if only year and month are specified
  • + ///
  • YYYYMMDD – if full date is specified
  • + ///
  • YYYYMMwN – if week number (within a month) is specified
  • + ///
+ ///
+ public string MMY { get; internal set; } = string.Empty; + + /// + /// Gets type of option. + /// It shall use one of following values. + ///
    + ///
  • STAN = Standard Options
  • + ///
  • LEAP = Long-term Equity AnticiPation Securities
  • + ///
  • SDO = Special Dated Options
  • + ///
  • BINY = Binary Options
  • + ///
  • FLEX = FLexible EXchange Options
  • + ///
  • VSO = Variable Start Options
  • + ///
  • RNGE = Range
  • + ///
+ ///
+ public string OptionType { get; internal set; } = string.Empty; + + /// + /// Gets expiration cycle style, such as "Weeklys", "Quarterlys". + /// + public string ExpirationStyle { get; internal set; } = string.Empty; + + /// + /// Gets settlement price determination style, such as "Open", "Close". + /// + public string SettlementStyle { get; internal set; } = string.Empty; + + /// + /// Gets classification of Financial Instruments code. + /// It is a mandatory field for OPTION instruments as it is the only way to distinguish Call/Put type, + /// American/European exercise, Cash/Physical delivery. + /// It shall use six-letter CFI code from ISO 10962 standard. + /// It is allowed to use 'X' extensively and to omit trailing letters (assumed to be 'X'). + /// See ISO 10962 on Wikipedia. + /// + /// "ESNTPB", "ESXXXX", "ES", "OPASPS". + public string CFI { get; internal set; } = string.Empty; + + /// + /// Gets a sorted map of all calls from strike to a corresponding option instrument. + /// + /// A sorted map of all calls from strike to a corresponding option instrument. + public SortedDictionary Calls { get; } = new(); + + /// + /// Gets a sorted map of all puts from strike to a corresponding option instrument. + /// + /// A sorted map of all puts from strike to a corresponding option instrument. + public SortedDictionary Puts { get; } = new(); + + /// + /// Gets a list of all strikes in ascending order. + /// + /// A list of all strikes in ascending order. + public List Strikes + { + get + { + if (_strikes == null) + { + var strikesSet = new SortedSet(Calls.Keys); + strikesSet.UnionWith(Puts.Keys); + _strikes = strikesSet.ToList(); + } + + return _strikes; + } + } + + /// + /// Gets n strikes are centered around a specified strike value. + /// + /// The maximal number of strikes to return. + /// The center strike. + /// n strikes are centered around a specified strike value. + /// When n < 0. + public List GetNStrikesAround(int n, double strike) + { + if (n < 0) + { + throw new ArgumentException(nameof(n)); + } + + var strikes = Strikes; + var i = strikes.BinarySearch(strike); + if (i < 0) + { + i = ~i; + } + + var from = Math.Max(0, i - (n / 2)); + var to = Math.Min(strikes.Count, from + n); + return strikes.GetRange(from, to - from); + } + + /// + /// Returns a shallow copy of this option series. + /// Collections of calls and puts are copied, but option instrument instances are shared with the original. + /// + /// A shallow copy of this option series. + public object Clone() + { + var clone = new OptionSeries(this); + foreach (var kvp in Calls) + { + clone.Calls.Add(kvp.Key, kvp.Value); + } + + foreach (var kvp in Puts) + { + clone.Puts.Add(kvp.Key, kvp.Value); + } + + return clone; + } + + /// + /// Compares this option series to another one by its attributes. + /// Expiration takes precedence in comparison. + /// + /// Another option series to compare with. + /// Result of comparison. + public int CompareTo(OptionSeries other) + { + if (Expiration < other.Expiration) return -1; + if (Expiration > other.Expiration) return 1; + if (LastTrade < other.LastTrade) return -1; + if (LastTrade > other.LastTrade) return 1; + + int i = Multiplier.CompareTo(other.Multiplier); + if (i != 0) return i; + + i = SPC.CompareTo(other.SPC); + if (i != 0) return i; + + i = string.Compare(AdditionalUnderlyings, other.AdditionalUnderlyings, StringComparison.Ordinal); + if (i != 0) return i; + + i = string.Compare(MMY, other.MMY, StringComparison.Ordinal); + if (i != 0) return i; + + i = string.Compare(OptionType, other.OptionType, StringComparison.Ordinal); + if (i != 0) return i; + + i = string.Compare(ExpirationStyle, other.ExpirationStyle, StringComparison.Ordinal); + if (i != 0) return i; + + i = string.Compare(SettlementStyle, other.SettlementStyle, StringComparison.Ordinal); + if (i != 0) return i; + + return string.Compare(CFI, other.CFI, StringComparison.Ordinal); + } + + /// + /// Indicates whether some other object is equal to this option series by its attributes. + /// + /// Another object to compare with. + /// True if the specified object is equal to this option series; otherwise, false. + public override bool Equals(object? obj) + { + if (this == obj) + { + return true; + } + + if (obj is not OptionSeries other) + { + return false; + } + + return Expiration == other.Expiration && + LastTrade == other.LastTrade && + Multiplier.Equals(other.Multiplier) && + SPC.Equals(other.SPC) && + AdditionalUnderlyings == other.AdditionalUnderlyings && + ExpirationStyle == other.ExpirationStyle && + MMY == other.MMY && + OptionType == other.OptionType && + CFI == other.CFI && + SettlementStyle == other.SettlementStyle; + } + + /// + /// Returns a hash code value for this option series. + /// + /// A hash code value. + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() + { + var result = Expiration; + result = (31 * result) + LastTrade; + var temp = Multiplier != +0.0d ? BitConverter.DoubleToInt64Bits(Multiplier) : 0L; + result = (31 * result) + (int)(temp ^ (temp >> 32)); + temp = SPC != +0.0d ? BitConverter.DoubleToInt64Bits(SPC) : 0L; + result = (31 * result) + (int)(temp ^ (temp >> 32)); + result = (31 * result) + AdditionalUnderlyings.GetHashCode(); + result = (31 * result) + MMY.GetHashCode(); + result = (31 * result) + OptionType.GetHashCode(); + result = (31 * result) + ExpirationStyle.GetHashCode(); + result = (31 * result) + SettlementStyle.GetHashCode(); + result = (31 * result) + CFI.GetHashCode(); + return result; + } + + /// + /// Returns a string representation of this series. + /// + /// The string representation of this series. + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("expiration=").Append(DayUtil.GetYearMonthDayByDayId(Expiration)); + if (LastTrade != 0) + { + sb.Append(", lastTrade=").Append(DayUtil.GetYearMonthDayByDayId(LastTrade)); + } + + if (!Multiplier.Equals(0)) + { + sb.Append(", multiplier=").Append(Multiplier); + } + + if (!SPC.Equals(0)) + { + sb.Append(", spc=").Append(SPC); + } + + if (!string.IsNullOrEmpty(AdditionalUnderlyings)) + { + sb.Append(", additionalUnderlyings=").Append(AdditionalUnderlyings); + } + + if (!string.IsNullOrEmpty(MMY)) + { + sb.Append(", mmy=").Append(MMY); + } + + if (!string.IsNullOrEmpty(OptionType)) + { + sb.Append(", optionType=").Append(OptionType); + } + + if (!string.IsNullOrEmpty(ExpirationStyle)) + { + sb.Append(", expirationStyle=").Append(ExpirationStyle); + } + + if (!string.IsNullOrEmpty(SettlementStyle)) + { + sb.Append(", settlementStyle=").Append(SettlementStyle); + } + + sb.Append(", cfi=").Append(CFI); + return sb.ToString(); + } + + internal void AddOption(bool isCall, double strike, T option) + { + var map = isCall ? Calls : Puts; + if (!map.ContainsKey(strike)) + { + _strikes = null; // Clear cached strikes list. + } + + map[strike] = option; + } +}