diff --git a/README.md b/README.md index be13f9fd..a14a892f 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,8 @@ sudo /usr/bin/xattr -r -d com.apple.quarantine is a simple demonstration of how to subscribe to the Order event and handle snapshots and updates. - [x] [MultipleMarketDepthSample](https://github.com/dxFeed/dxfeed-graal-net-api/tree/main/samples/MultipleMarketDepthSample) is a simple demonstration of how to use the `MarketDepthModel` to manage and display order books for multiple symbols. +- [x] [DXFeedOptionChain](https://github.com/dxFeed/dxfeed-graal-net-api/tree/main/samples/DXFeedOptionChain) + how to build option chains, and prints quotes for nearby option strikes. ## Current State diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 9cd4b324..98222699 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -1,3 +1,5 @@ +* [MDAPI-42] [.NET] Implement OptionChain + ## Version 2.3.0 * [MDAPI-115] [.NET] Add IncOrderSnapshot sample 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/dxfeed-graal-net-api.sln.DotSettings b/dxfeed-graal-net-api.sln.DotSettings index 745c42d9..6c049c83 100644 --- a/dxfeed-graal-net-api.sln.DotSettings +++ b/dxfeed-graal-net-api.sln.DotSettings @@ -23,6 +23,7 @@ True True True + True True True True diff --git a/samples/DXFeedOptionChain/DXFeedOptionChain.csproj b/samples/DXFeedOptionChain/DXFeedOptionChain.csproj new file mode 100644 index 00000000..84084982 --- /dev/null +++ b/samples/DXFeedOptionChain/DXFeedOptionChain.csproj @@ -0,0 +1,21 @@ + + + + Exe + DxFeed.Graal.Net.Samples + net6.0 + + + + ../../artifacts/Debug/Samples/ + + + + ../../artifacts/Release/Samples/ + + + + + + + diff --git a/samples/DXFeedOptionChain/Program.cs b/samples/DXFeedOptionChain/Program.cs new file mode 100644 index 00000000..cd870382 --- /dev/null +++ b/samples/DXFeedOptionChain/Program.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +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.Options; + +namespace DxFeed.Graal.Net.Samples; + +/// +/// A simple sample that shows how to build option chains, and prints quotes for nearby option strikes. +/// +[SuppressMessage("ReSharper", "MethodSupportsCancellation")] +internal abstract class Program +{ + public static async Task Main(string[] args) + { + if (args.Length != 5) + { + Console.WriteLine("usage: DXFeedOptionChain
"); + Console.WriteLine(" is endpoint address"); + 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; + } + + var argAddress = args[0]; + var argIpfFile = args[1]; + var argSymbol = args[2]; + var nStrikes = int.Parse(args[3], CultureInfo.InvariantCulture); + var nMonths = int.Parse(args[4], CultureInfo.InvariantCulture); + + var feed = DXEndpoint.Create().Connect(argAddress).GetFeed(); + + // 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).Chains; + 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}"); + } + } + } +} diff --git a/src/DxFeed.Graal.Net/Ipf/Options/OptionChain.cs b/src/DxFeed.Graal.Net/Ipf/Options/OptionChain.cs new file mode 100644 index 00000000..cc0051e5 --- /dev/null +++ b/src/DxFeed.Graal.Net/Ipf/Options/OptionChain.cs @@ -0,0 +1,78 @@ +// +// 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.Options; + +/// +/// Represents a set of option series for a single product or underlying symbol. +/// +///

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 OptionChain : ICloneable +{ + private readonly SortedDictionary, OptionSeries> _seriesMap = new(); + + /// + /// Initializes a new instance of the class with the specified symbol. + /// + /// The symbol (product or underlying) of this option chain. + internal OptionChain(string symbol) => + Symbol = symbol; + + /// + /// Gets the symbol (product or underlying) of this option chain. + /// + public string Symbol { get; } + + /// + /// Returns a sorted set of option series in this option chain. + /// + /// A sorted set of option series in this option chain. + public SortedSet> GetSeries() => + new(_seriesMap.Keys); + + /// + /// 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() + { + var clone = new OptionChain(Symbol); + foreach (var series in _seriesMap.Values) + { + var seriesClone = (OptionSeries)series.Clone(); + clone._seriesMap.Add(seriesClone, seriesClone); + } + + return clone; + } + + /// + /// Adds an option to the specified series in this option chain. + /// If the series does not exist, it is created. + /// + /// The option series to which the option will be added. + /// Indicates whether the option is a call option. + /// The strike price of the option. + /// The option to add. + 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/Options/OptionChainsBuilder.cs b/src/DxFeed.Graal.Net/Ipf/Options/OptionChainsBuilder.cs new file mode 100644 index 00000000..4a69372e --- /dev/null +++ b/src/DxFeed.Graal.Net/Ipf/Options/OptionChainsBuilder.cs @@ -0,0 +1,276 @@ +// +// 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 DxFeed.Graal.Net.Utils; + +namespace DxFeed.Graal.Net.Ipf.Options; + +/// +/// 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 +{ + private readonly OptionSeries _series = new(); + private string _product = string.Empty; + private string _underlying = string.Empty; + private string _cfi = string.Empty; + + /// + /// Gets or sets the product for futures and options on futures (underlying asset name). + /// Example: "/YG". + /// + public string Product + { + get => _product; + set => _product = string.IsNullOrEmpty(value) ? string.Empty : value; + } + + /// + /// Gets or sets primary underlying symbol for options. + /// + /// "C", "/YGM9". + public string Underlying + { + get => _underlying; + set => _underlying = string.IsNullOrEmpty(value) ? string.Empty : value; + } + + /// + /// Gets or sets day id of expiration. + /// + /// DayUtil.GetYearMonthDayByDayId(20090117). + public int Expiration + { + get => _series.Expiration; + set => _series.Expiration = value; + } + + /// + /// Gets or sets day id of last trading day. + /// + /// DayUtil.GetYearMonthDayByDayId(20090117). + public int LastTrade + { + get => _series.LastTrade; + set => _series.LastTrade = value; + } + + /// + /// Gets or sets market value multiplier. + /// + /// "100", "33.2". + public double Multiplier + { + get => _series.Multiplier; + set => _series.Multiplier = value; + } + + /// + /// Gets or sets shares per contract for options. + /// + /// "1", "100". + public double SPC + { + get => _series.SPC; + set => _series.SPC = value; + } + + /// + /// Gets or sets 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 => _series.AdditionalUnderlyings; + set => _series.AdditionalUnderlyings = string.IsNullOrEmpty(value) ? string.Empty : value; + } + + /// + /// Gets or sets 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 => _series.MMY; + set => _series.MMY = string.IsNullOrEmpty(value) ? string.Empty : value; + } + + /// + /// Gets or sets 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 => _series.OptionType; + set => _series.OptionType = string.IsNullOrEmpty(value) ? string.Empty : value; + } + + /// + /// Gets or sets expiration cycle style, such as "Weeklys", "Quarterlys". + /// + public string ExpirationStyle + { + get => _series.ExpirationStyle; + set => _series.ExpirationStyle = string.IsNullOrEmpty(value) ? string.Empty : value; + } + + /// + /// Gets or sets settlement price determination style, such as "Open", "Close". + /// + public string SettlementStyle + { + get => _series.SettlementStyle; + set => _series.SettlementStyle = string.IsNullOrEmpty(value) ? string.Empty : value; + } + + /// + /// Gets or sets 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 => _cfi; + set + { + _cfi = string.IsNullOrEmpty(value) ? string.Empty : value; + _series.CFI = _cfi.Length < 2 ? _cfi : _cfi[0] + "X" + _cfi.Substring(2); + } + } + + /// + /// Gets or sets strike price for options. + /// + /// "80", "22.5". + public double Strike { get; set; } + + /// + /// Gets a view of chains created by this builder. + /// + public Dictionary> Chains { get; } = new(); + + /// + /// Builds option chains for all options from the given collections of . + /// + /// Collection of instrument profiles. + /// The builder with all the options from the instruments collection. + public static OptionChainsBuilder Build(IEnumerable instruments) + { + var ocb = new OptionChainsBuilder(); + foreach (var ip in instruments) + { + if (!ip.Type.Equals("OPTION", StringComparison.Ordinal)) + { + continue; + } + + ocb.Product = ip.Product; + ocb.Underlying = ip.Underlying; + ocb.Expiration = ip.Expiration; + ocb.LastTrade = ip.LastTrade; + ocb.Multiplier = ip.Multiplier; + ocb.SPC = ip.SPC; + ocb.AdditionalUnderlyings = ip.AdditionalUnderlyings; + ocb.MMY = ip.MMY; + ocb.OptionType = ip.OptionType; + ocb.ExpirationStyle = ip.ExpirationStyle; + ocb.SettlementStyle = ip.SettlementStyle; + ocb.CFI = ip.CFI; + ocb.Strike = ip.Strike; + ocb.AddOption(ip); + } + + return ocb; + } + + /// + /// Adds an option instrument to this builder. + /// Option is added to chains for the currently set and/or + /// to the that correspond + /// to all other currently set attributes. This method is safe in the sense that it ignores + /// illegal state of the builder. It only adds an option when all the following conditions are met: + ///
    + ///
  • is set and starts with either "OC" for call or "OP" for put.
  • + ///
  • is set and is not zero.
  • + ///
  • is set and is not nor .
  • + ///
  • or are set.
  • + ///
+ /// All the attributes remain set as before after the call to this method, but + /// are updated correspondingly. + ///
+ /// Option to add. + public void AddOption(T option) + { + var isCall = _cfi.StartsWith("OC", StringComparison.Ordinal); + if (!isCall && !_cfi.StartsWith("OP", StringComparison.Ordinal)) + { + return; + } + + if (_series.Expiration == 0) + { + return; + } + + if (double.IsNaN(Strike) || double.IsInfinity(Strike)) + { + return; + } + + if (!string.IsNullOrEmpty(_product)) + { + GetOrCreateChain(_product).AddOption(_series, isCall, Strike, option); + } + + if (!string.IsNullOrEmpty(_underlying)) + { + 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; + } +} diff --git a/src/DxFeed.Graal.Net/Ipf/Options/OptionSeries.cs b/src/DxFeed.Graal.Net/Ipf/Options/OptionSeries.cs new file mode 100644 index 00000000..34605509 --- /dev/null +++ b/src/DxFeed.Graal.Net/Ipf/Options/OptionSeries.cs @@ -0,0 +1,466 @@ +// +// 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.Options; + +/// +/// Represents a series of call and put options with different strike sharing the same attributes +/// such as 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; + + internal OptionSeries() + { + } + + internal 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 the day id of expiration. + /// + /// DayUtil.GetYearMonthDayByDayId(20090117). + public int Expiration { get; internal set; } + + /// + /// Gets the day id of the last trading day. + /// + /// DayUtil.GetYearMonthDayByDayId(20090117). + public int LastTrade { get; internal set; } + + /// + /// Gets the market value multiplier. + /// + /// "100", "33.2". + public double Multiplier { get; internal set; } + + /// + /// Gets the 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. + /// + public SortedDictionary Calls { get; } = new(); + + /// + /// Gets 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. + /// + public List Strikes + { + get + { + if (_strikes == null) + { + var strikesSet = new SortedSet(Calls.Keys); + strikesSet.UnionWith(Puts.Keys); + _strikes = strikesSet.ToList(); + } + + return _strikes; + } + } + + /// + /// Determines whether two specified instances of are equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are equal; otherwise, false. + public static bool operator ==(OptionSeries left, OptionSeries right) => + Equals(left, right); + + /// + /// Determines whether two specified instances of are not equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are not equal; otherwise, false. + public static bool operator !=(OptionSeries left, OptionSeries right) => + !Equals(left, right); + + /// + /// Determines whether one specified is less than another. + /// + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than the second instance; otherwise, false. + public static bool operator <(OptionSeries left, OptionSeries right) => + left.CompareTo(right) < 0; + + /// + /// Determines whether one specified is less than or equal to another. + /// + /// The first instance to compare. + /// The second instance to compare. + /// + /// true if the first instance is less than or equal to the second instance; otherwise, false. + /// + public static bool operator <=(OptionSeries left, OptionSeries right) => + left.CompareTo(right) <= 0; + + /// + /// Determines whether one specified is greater than another. + /// + /// The first instance to compare. + /// The second instance to compare. + /// + /// true if the first instance is greater than the second instance; otherwise, false. + /// + public static bool operator >(OptionSeries left, OptionSeries right) => + left.CompareTo(right) > 0; + + /// + /// Determines whether one specified is greater than or equal to another. + /// + /// The first instance to compare. + /// The second instance to compare. + /// + /// true if the first instance is greater than or equal to the second instance; otherwise, false. + /// + public static bool operator >=(OptionSeries left, OptionSeries right) => + left.CompareTo(right) >= 0; + + /// + /// Gets n strikes centered around a specified strike value. + /// + /// The maximum number of strikes to return. + /// The center strike. + /// A list of n strikes centered around the specified strike value. + /// If n < 0. + public List GetNStrikesAround(int n, double strike) + { + if (n < 0) + { + throw new ArgumentException("Must not be less than zero.", 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); + } + + /// + /// Creates 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. + /// + /// The other option series to compare with. + /// The result of the comparison. + public int CompareTo(OptionSeries? other) + { + if (other is null) + { + return 1; + } + + if (Expiration < other.Expiration) + { + return -1; + } + + if (Expiration > other.Expiration) + { + return 1; + } + + if (LastTrade < other.LastTrade) + { + return -1; + } + + if (LastTrade > other.LastTrade) + { + return 1; + } + + var 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); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals(object? obj) + { + if (ReferenceEquals(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.Equals(other.AdditionalUnderlyings, StringComparison.Ordinal) && + ExpirationStyle.Equals(other.ExpirationStyle, StringComparison.Ordinal) && + MMY.Equals(other.MMY, StringComparison.Ordinal) && + OptionType.Equals(other.OptionType, StringComparison.Ordinal) && + CFI.Equals(other.CFI, StringComparison.Ordinal) && + SettlementStyle.Equals(other.SettlementStyle, StringComparison.Ordinal); + } + + /// + /// Returns a hash code value for this object. + /// + /// A hash code value for this object. + [SuppressMessage( + "ReSharper", + "NonReadonlyMemberInGetHashCode", + Justification = "Setters are only called in the OptionChainsBuilder")] + public override int GetHashCode() + { + var result = Expiration; + result = (31 * result) + LastTrade; + var temp = !Multiplier.Equals(0) ? BitConverter.DoubleToInt64Bits(Multiplier) : 0L; + result = (31 * result) + temp.GetHashCode(); + temp = !SPC.Equals(0) ? BitConverter.DoubleToInt64Bits(SPC) : 0L; + result = (31 * result) + temp.GetHashCode(); + 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(); + } + + /// + /// Adds an option to the series. + /// + /// Indicates whether the option is a call. + /// The strike price of the option. + /// The option to add. + 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; + } +} diff --git a/tests/DxFeed.Graal.Net.Tests/Ipf/Options/OptionChainTest.cs b/tests/DxFeed.Graal.Net.Tests/Ipf/Options/OptionChainTest.cs new file mode 100644 index 00000000..8a9d6fee --- /dev/null +++ b/tests/DxFeed.Graal.Net.Tests/Ipf/Options/OptionChainTest.cs @@ -0,0 +1,55 @@ +// +// 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 DxFeed.Graal.Net.Ipf.Options; + +namespace DxFeed.Graal.Net.Tests.Ipf.Options; + +[TestFixture] +public class OptionChainTests +{ + [Test] + public void TestAddOption() + { + var chain = new OptionChain("AAPL"); + var series = new OptionSeries { Expiration = 20240101, LastTrade = 20231231 }; + chain.AddOption(series, true, 100, "CallOption1"); + + var seriesSet = chain.GetSeries(); + Assert.That(seriesSet, Has.Count.EqualTo(1)); + + var retrievedSeries = seriesSet.Min; + Assert.That(retrievedSeries?.Calls[100], Is.EqualTo("CallOption1")); + } + + [Test] + public void TestClone() + { + var chain = new OptionChain("AAPL"); + var series = new OptionSeries { Expiration = 20240101, LastTrade = 20231231 }; + chain.AddOption(series, true, 100, "CallOption1"); + + var clone = (OptionChain)chain.Clone(); + var seriesSet = clone.GetSeries(); + Assert.That(seriesSet, Has.Count.EqualTo(1)); + + var retrievedSeries = seriesSet.Min; + Assert.That(retrievedSeries?.Calls[100], Is.EqualTo("CallOption1")); + } + + [Test] + public void TestGetSeries() + { + var chain = new OptionChain("AAPL"); + var series1 = new OptionSeries { Expiration = 20240101, LastTrade = 20231231 }; + var series2 = new OptionSeries { Expiration = 20240201, LastTrade = 20231231 }; + chain.AddOption(series1, true, 100, "CallOption1"); + chain.AddOption(series2, false, 95, "PutOption1"); + + var seriesSet = chain.GetSeries(); + Assert.That(seriesSet, Has.Count.EqualTo(2)); + } +} diff --git a/tests/DxFeed.Graal.Net.Tests/Ipf/Options/OptionChainsBuilderTest.cs b/tests/DxFeed.Graal.Net.Tests/Ipf/Options/OptionChainsBuilderTest.cs new file mode 100644 index 00000000..e16468d5 --- /dev/null +++ b/tests/DxFeed.Graal.Net.Tests/Ipf/Options/OptionChainsBuilderTest.cs @@ -0,0 +1,121 @@ +// +// 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 DxFeed.Graal.Net.Ipf; +using DxFeed.Graal.Net.Ipf.Options; + +namespace DxFeed.Graal.Net.Tests.Ipf.Options; + +[TestFixture] +public class OptionChainsBuilderTests +{ + [Test] + public void TestBuildOptionChains() + { + var profiles = new List + { + new() + { + Type = "OPTION", + Product = "AAPL", + Underlying = "", + Expiration = 20240101, + LastTrade = 20231231, + Multiplier = 100, + SPC = 1, + AdditionalUnderlyings = "US$ 50", + MMY = "202401", + OptionType = "STAN", + ExpirationStyle = "Weeklys", + SettlementStyle = "Close", + CFI = "OCXXXX", + Strike = 100 + }, + new() + { + Type = "OPTION", + Product = "", + Underlying = "AAPL", + Expiration = 20240101, + LastTrade = 20231231, + Multiplier = 100, + SPC = 1, + AdditionalUnderlyings = "US$ 50", + MMY = "202401", + OptionType = "STAN", + ExpirationStyle = "Weeklys", + SettlementStyle = "Close", + CFI = "OPXXXX", + Strike = 105 + } + }; + + var builder = OptionChainsBuilder.Build(profiles); + var chains = builder.Chains; + + Assert.That(chains.ContainsKey("AAPL"), Is.True); + var chain = chains["AAPL"]; + var seriesSet = chain.GetSeries(); + Assert.That(seriesSet, Has.Count.EqualTo(1)); + + var series = seriesSet.Min; + Assert.Multiple(() => + { + Assert.That(series?.Calls.ContainsKey(100), Is.True); + Assert.That(series?.Puts.ContainsKey(105), Is.True); + }); + } + + [Test] + public void TestAddOption() + { + var builder = new OptionChainsBuilder + { + Product = "AAPL", + Underlying = "AAPL", + Expiration = 20240101, + LastTrade = 20231231, + Multiplier = 100, + SPC = 1, + AdditionalUnderlyings = "US$ 50", + MMY = "202401", + OptionType = "STAN", + ExpirationStyle = "Weeklys", + SettlementStyle = "Close", + CFI = "OCXXXX", + Strike = 100 + }; + + var option = new InstrumentProfile + { + Type = "OPTION", + Product = "AAPL", + Underlying = "AAPL", + Expiration = 20240101, + LastTrade = 20231231, + Multiplier = 100, + SPC = 1, + AdditionalUnderlyings = "US$ 50", + MMY = "202401", + OptionType = "STAN", + ExpirationStyle = "Weeklys", + SettlementStyle = "Close", + CFI = "OCXXXX", + Strike = 100 + }; + + builder.AddOption(option); + var chains = builder.Chains; + + Assert.That(chains.ContainsKey("AAPL"), Is.True); + var chain = chains["AAPL"]; + var seriesSet = chain.GetSeries(); + Assert.That(seriesSet, Has.Count.EqualTo(1)); + + var series = seriesSet.Min; + Assert.That(series?.Calls.ContainsKey(100), Is.True); + } +} diff --git a/tests/DxFeed.Graal.Net.Tests/Ipf/Options/OptionSeriesTests.cs b/tests/DxFeed.Graal.Net.Tests/Ipf/Options/OptionSeriesTests.cs new file mode 100644 index 00000000..fe2a8c57 --- /dev/null +++ b/tests/DxFeed.Graal.Net.Tests/Ipf/Options/OptionSeriesTests.cs @@ -0,0 +1,192 @@ +// +// 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 DxFeed.Graal.Net.Ipf.Options; + +// Use EqualConstraint for better assertion messages in case of failure. +#pragma warning disable NUnit2010 + +//Use ComparisonConstraint for better assertion messages in case of failure +#pragma warning disable NUnit2043 + +namespace DxFeed.Graal.Net.Tests.Ipf.Options; + +[TestFixture] +public class OptionSeriesTests +{ + [Test] + public void TestStrikes() + { + var series = new OptionSeries(); + series.AddOption(true, 100, "CallOption1"); + series.AddOption(false, 95, "PutOption1"); + series.AddOption(true, 105, "CallOption2"); + + var strikes = series.Strikes; + Assert.That(strikes, Is.EquivalentTo(new List { 95, 100, 105 })); + } + + [Test] + public void TestGetNStrikesAround() + { + var series = new OptionSeries(); + series.AddOption(true, 100, "CallOption1"); + series.AddOption(false, 95, "PutOption1"); + series.AddOption(true, 105, "CallOption2"); + + var centeredStrikes = series.GetNStrikesAround(2, 100); + Assert.That(centeredStrikes, Is.EquivalentTo(new List { 95, 100 })); + } + + [Test] + public void TestGetNStrikesAround_ThrowsException_WhenNIsNegative() + { + var series = new OptionSeries(); + + Assert.Throws(() => series.GetNStrikesAround(-1, 100), "Must not be less than zero."); + } + + [Test] + public void TestGetNStrikesAround_ScenarioWhenISmallerThanZero() + { + var series = new OptionSeries(); + series.AddOption(true, 100, "CallOption1"); + series.AddOption(false, 95, "PutOption1"); + series.AddOption(true, 105, "CallOption2"); + + var centeredStrikes = series.GetNStrikesAround(2, 97); // Strike 97 doesn't exist. + Assert.That(centeredStrikes, Is.EquivalentTo(new List { 95, 100 })); + } + + [Test] + public void TestClone() + { + var series = new OptionSeries(); + series.AddOption(true, 100, "CallOption1"); + series.AddOption(false, 95, "PutOption1"); + + var clone = (OptionSeries)series.Clone(); + Assert.Multiple(() => + { + Assert.That(clone.Strikes, Is.EquivalentTo(series.Strikes)); + Assert.That(clone.Calls[100], Is.EqualTo("CallOption1")); + Assert.That(clone.Puts[95], Is.EqualTo("PutOption1")); + }); + } + + [Test] + public void TestEquality() + { + var series1 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series2 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + + Assert.That(series1, Is.EqualTo(series2)); + } + + [Test] + public void TestInequality() + { + var series1 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series2 = + new OptionSeries { Expiration = 20240102, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + + Assert.That(series1, Is.Not.EqualTo(series2)); + } + + [Test] + public void TestAddOption() + { + var series = new OptionSeries(); + series.AddOption(true, 100, "CallOption1"); + + Assert.Multiple(() => + { + Assert.That(series.Calls.ContainsKey(100), Is.True); + Assert.That(series.Calls[100], Is.EqualTo("CallOption1")); + }); + } + + [Test] + public void TestEqualityOperator() + { + var series1 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series2 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + + Assert.That(series1 == series2, Is.True); + } + + [Test] + public void TestInequalityOperator() + { + var series1 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series2 = + new OptionSeries { Expiration = 20240102, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + + Assert.That(series1 != series2, Is.True); + } + + [Test] + public void TestLessThanOperator() + { + var series1 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series2 = + new OptionSeries { Expiration = 20240102, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + + Assert.That(series1 < series2, Is.True); + } + + [Test] + public void TestGreaterThanOperator() + { + var series1 = + new OptionSeries { Expiration = 20240102, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series2 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + + Assert.That(series1 > series2, Is.True); + } + + [Test] + public void TestLessThanOrEqualOperator() + { + var series1 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series2 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series3 = + new OptionSeries { Expiration = 20240102, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + + Assert.Multiple(() => + { + Assert.That(series1 <= series2, Is.True); + Assert.That(series1 <= series3, Is.True); + }); + } + + [Test] + public void TestGreaterThanOrEqualOperator() + { + var series1 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series2 = + new OptionSeries { Expiration = 20240101, LastTrade = 20231231, Multiplier = 100, SPC = 1 }; + var series3 = + new OptionSeries { Expiration = 20231231, LastTrade = 20231230, Multiplier = 100, SPC = 1 }; + + Assert.Multiple(() => + { + Assert.That(series1 >= series2, Is.True); + Assert.That(series1 >= series3, Is.True); + }); + } +}