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;
+ }
+}