From be7b14e183537f62796f8d07e9a3fdc56a1357ca Mon Sep 17 00:00:00 2001 From: Oleg Rakhmatulin Date: Fri, 26 Apr 2024 10:26:42 +0200 Subject: [PATCH] Issue #745 - The new `OptionSnapshotRequest` class added and used for the `ListSnapshotsAsync` method (breaking change). --- .../AlpacaOptionsDataClientTest.Snapshots.cs | 12 +++-- Alpaca.Markets/AlpacaOptionsDataClient.cs | 25 ++++------ .../Helpers/DebuggerDisplayExtensions.cs | 4 ++ .../Interfaces/IAlpacaOptionsDataClient.cs | 6 +-- Alpaca.Markets/Interfaces/IDictionaryPage.cs | 20 ++++++++ .../Messages/JsonOptionSnapshotsData.cs | 45 +++++++++++++++++ .../Parameters/OptionSnapshotRequest.cs | 49 +++++++++++++++++++ Alpaca.Markets/PublicAPI.Shipped.txt | 2 - Alpaca.Markets/PublicAPI.Unshipped.txt | 10 ++++ 9 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 Alpaca.Markets/Interfaces/IDictionaryPage.cs create mode 100644 Alpaca.Markets/Messages/JsonOptionSnapshotsData.cs create mode 100644 Alpaca.Markets/Parameters/OptionSnapshotRequest.cs diff --git a/Alpaca.Markets.Tests/AlpacaOptionsDataClientTest.Snapshots.cs b/Alpaca.Markets.Tests/AlpacaOptionsDataClientTest.Snapshots.cs index ecf9decc..0449986d 100644 --- a/Alpaca.Markets.Tests/AlpacaOptionsDataClientTest.Snapshots.cs +++ b/Alpaca.Markets.Tests/AlpacaOptionsDataClientTest.Snapshots.cs @@ -10,14 +10,15 @@ public async Task ListSnapshotsAsyncWorks() mock.AddCryptoSnapshotsExpectation(PathPrefix, _symbols); var snapshots = await mock.Client.ListSnapshotsAsync( - new LatestOptionsDataRequest(_symbols)); + new OptionSnapshotRequest(_symbols)); Assert.NotNull(snapshots); - Assert.NotEmpty(snapshots); + Assert.NotNull(snapshots.Items); + Assert.NotEmpty(snapshots.Items); foreach (var symbol in _symbols) { - var snapshot = snapshots[symbol]; + var snapshot = snapshots.Items[symbol]; validate(snapshot, symbol); } } @@ -33,11 +34,12 @@ public async Task GetOptionChainAsyncWorks() new OptionChainRequest("AAPL")); Assert.NotNull(snapshots); - Assert.NotEmpty(snapshots); + Assert.NotNull(snapshots.Items); + Assert.NotEmpty(snapshots.Items); foreach (var symbol in _symbols) { - var snapshot = snapshots[symbol]; + var snapshot = snapshots.Items[symbol]; validate(snapshot, symbol); } } diff --git a/Alpaca.Markets/AlpacaOptionsDataClient.cs b/Alpaca.Markets/AlpacaOptionsDataClient.cs index 51945f88..9896eb03 100644 --- a/Alpaca.Markets/AlpacaOptionsDataClient.cs +++ b/Alpaca.Markets/AlpacaOptionsDataClient.cs @@ -34,17 +34,19 @@ public Task> ListLatestTradesAsync( getLatestAsync( request.EnsureNotNull().Validate(), "trades/latest", data => data.Trades, cancellationToken); - public Task> ListSnapshotsAsync( - LatestOptionsDataRequest request, + public async Task> ListSnapshotsAsync( + OptionSnapshotRequest request, CancellationToken cancellationToken = default) => - getLatestAsync( - request.EnsureNotNull().Validate(), "snapshots", data => data.Snapshots, cancellationToken); + await HttpClient.GetAsync, JsonOptionsSnapshotData>( + await request.GetUriBuilderAsync(HttpClient).ConfigureAwait(false), + RateLimitHandler, cancellationToken).ConfigureAwait(false); - public Task> GetOptionChainAsync( + public async Task> GetOptionChainAsync( OptionChainRequest request, CancellationToken cancellationToken = default) => - getLatestAsync( - request.EnsureNotNull().Validate(), data => data.Snapshots, cancellationToken); + await HttpClient.GetAsync, JsonOptionsSnapshotData>( + await request.GetUriBuilderAsync(HttpClient).ConfigureAwait(false), + RateLimitHandler, cancellationToken).ConfigureAwait(false); private async Task> getLatestAsync( LatestOptionsDataRequest request, @@ -56,15 +58,6 @@ await HttpClient.GetAsync( await request.GetUriBuilderAsync(HttpClient, lastPathSegment).ConfigureAwait(false), itemsSelector, withSymbol, RateLimitHandler, cancellationToken).ConfigureAwait(false); - private async Task> getLatestAsync( - OptionChainRequest request, - Func> itemsSelector, - CancellationToken cancellationToken) - where TJson : TApi, ISymbolMutable => - await HttpClient.GetAsync( - await request.GetUriBuilderAsync(HttpClient).ConfigureAwait(false), - itemsSelector, withSymbol, RateLimitHandler, cancellationToken).ConfigureAwait(false); - private static TApi withSymbol( KeyValuePair kvp) where TJson : TApi, ISymbolMutable diff --git a/Alpaca.Markets/Helpers/DebuggerDisplayExtensions.cs b/Alpaca.Markets/Helpers/DebuggerDisplayExtensions.cs index ebbab89e..98ea1dfd 100644 --- a/Alpaca.Markets/Helpers/DebuggerDisplayExtensions.cs +++ b/Alpaca.Markets/Helpers/DebuggerDisplayExtensions.cs @@ -11,6 +11,10 @@ internal static String ToDebuggerDisplayString( this IMultiPage page) => $"{nameof(IPage)}<{typeof(TItem).Name}> {{ Count = {page.Items.Count}, NextPageToken = \"{page.NextPageToken}\" }}"; + internal static String ToDebuggerDisplayString( + this IDictionaryPage page) => + $"{nameof(IDictionaryPage)}<{typeof(TItem).Name}> {{ Count = {page.Items.Count}, NextPageToken = \"{page.NextPageToken}\" }}"; + internal static String ToDebuggerDisplayString( this IBar bar) => $"{nameof(IBar)} {{ TimeUtc = {bar.TimeUtc:O}, Symbol = \"{bar.Symbol}\", Open = {bar.Open}, High = {bar.High}, Low = {bar.Low}, Close = {bar.Close} }}"; diff --git a/Alpaca.Markets/Interfaces/IAlpacaOptionsDataClient.cs b/Alpaca.Markets/Interfaces/IAlpacaOptionsDataClient.cs index 367159e7..a20444c8 100644 --- a/Alpaca.Markets/Interfaces/IAlpacaOptionsDataClient.cs +++ b/Alpaca.Markets/Interfaces/IAlpacaOptionsDataClient.cs @@ -114,8 +114,8 @@ Task> ListLatestTradesAsync( /// /// Read-only dictionary with the current snapshot information. [UsedImplicitly] - Task> ListSnapshotsAsync( - LatestOptionsDataRequest request, + Task> ListSnapshotsAsync( + OptionSnapshotRequest request, CancellationToken cancellationToken = default); /// @@ -143,7 +143,7 @@ Task> ListSnapshotsAsync( /// /// Read-only dictionary with the current snapshot information. [UsedImplicitly] - Task> GetOptionChainAsync( + Task> GetOptionChainAsync( OptionChainRequest request, CancellationToken cancellationToken = default); } diff --git a/Alpaca.Markets/Interfaces/IDictionaryPage.cs b/Alpaca.Markets/Interfaces/IDictionaryPage.cs new file mode 100644 index 00000000..74944fd8 --- /dev/null +++ b/Alpaca.Markets/Interfaces/IDictionaryPage.cs @@ -0,0 +1,20 @@ +namespace Alpaca.Markets; + +/// +/// Encapsulates single page response in Alpaca Data API v2. +/// +/// Type of paged item (bar, trade or quote) +public interface IDictionaryPage +{ + /// + /// Gets the next page token for continuation. If value of this property + /// equals to null this page is the last one and no more data is available. + /// + [UsedImplicitly] + public String? NextPageToken { get; } + + /// + /// Gets list of items for this response grouped by asset symbols. + /// + public IReadOnlyDictionary Items { get; } +} diff --git a/Alpaca.Markets/Messages/JsonOptionSnapshotsData.cs b/Alpaca.Markets/Messages/JsonOptionSnapshotsData.cs new file mode 100644 index 00000000..49b38ac8 --- /dev/null +++ b/Alpaca.Markets/Messages/JsonOptionSnapshotsData.cs @@ -0,0 +1,45 @@ +namespace Alpaca.Markets; + +[SuppressMessage( + "Microsoft.Performance", "CA1812:Avoid uninstantiated internal classes", + Justification = "Object instances of this class will be created by Newtonsoft.JSON library.")] +[DebuggerDisplay("{DebuggerDisplay,nq}", Type = nameof(IDictionaryPage) + "<" + nameof(ISnapshot) + ">")] +internal sealed class JsonOptionsSnapshotData : IDictionaryPage +{ + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + [JsonProperty(PropertyName = "snapshots", Required = Required.Default)] + public Dictionary ItemsList { get; [ExcludeFromCodeCoverage] set; } = new(); + + [JsonProperty(PropertyName = "next_page_token", Required = Required.Default)] + public String? NextPageToken { get; set; } + + [JsonIgnore] + public IReadOnlyDictionary Items { get; [ExcludeFromCodeCoverage] private set; } + = new Dictionary(); + + [OnDeserialized] + [UsedImplicitly] + internal void OnDeserializedMethod( + StreamingContext _) => + Items = (ItemsList ?? []).ToDictionary( + kvp => kvp.Key, + withSymbol, + StringComparer.Ordinal); + + [ExcludeFromCodeCoverage] + public override String ToString() => + JsonConvert.SerializeObject(this); + + [ExcludeFromCodeCoverage] + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private String DebuggerDisplay => + this.ToDebuggerDisplayString(); + + private static TApi withSymbol( + KeyValuePair kvp) + where TJson : TApi, ISymbolMutable + { + kvp.Value.SetSymbol(kvp.Key); + return kvp.Value; + } +} diff --git a/Alpaca.Markets/Parameters/OptionSnapshotRequest.cs b/Alpaca.Markets/Parameters/OptionSnapshotRequest.cs new file mode 100644 index 00000000..e93984e0 --- /dev/null +++ b/Alpaca.Markets/Parameters/OptionSnapshotRequest.cs @@ -0,0 +1,49 @@ +namespace Alpaca.Markets; + +/// +/// Encapsulates data for latest options data requests on Alpaca Data API v2. +/// +public sealed class OptionSnapshotRequest : Validation.IRequest +{ + private readonly HashSet _symbols = new(StringComparer.Ordinal); + + /// + /// Creates new instance of object. + /// + /// Options symbols list for data retrieval. + /// + /// The argument is null. + /// + public OptionSnapshotRequest( + IEnumerable symbols) => + _symbols.UnionWith(symbols.EnsureNotNull()); + + /// + /// Gets options symbols list for data retrieval. + /// + [UsedImplicitly] + public IReadOnlyCollection Symbols => _symbols; + + /// + /// Gets options feed for data retrieval. + /// + [UsedImplicitly] + [ExcludeFromCodeCoverage] + public OptionsFeed? OptionsFeed { get; set; } + + internal async ValueTask GetUriBuilderAsync( + HttpClient httpClient) => + new UriBuilder(httpClient.BaseAddress!) + { + Query = await new QueryBuilder() + .AddParameter("symbols", Symbols.ToList()) + .AddParameter("feed", OptionsFeed) + .AsStringAsync().ConfigureAwait(false) + }.AppendPath("snapshots"); + + IEnumerable Validation.IRequest.GetExceptions() + { + yield return Symbols.TryValidateSymbolsList(); + yield return Symbols.TryValidateSymbolName(); + } +} diff --git a/Alpaca.Markets/PublicAPI.Shipped.txt b/Alpaca.Markets/PublicAPI.Shipped.txt index e7156270..1c50ca05 100644 --- a/Alpaca.Markets/PublicAPI.Shipped.txt +++ b/Alpaca.Markets/PublicAPI.Shipped.txt @@ -491,11 +491,9 @@ Alpaca.Markets.IAlpacaNewsStreamingClient Alpaca.Markets.IAlpacaNewsStreamingClient.GetNewsSubscription() -> Alpaca.Markets.IAlpacaDataSubscription! Alpaca.Markets.IAlpacaNewsStreamingClient.GetNewsSubscription(string! symbol) -> Alpaca.Markets.IAlpacaDataSubscription! Alpaca.Markets.IAlpacaOptionsDataClient -Alpaca.Markets.IAlpacaOptionsDataClient.GetOptionChainAsync(Alpaca.Markets.OptionChainRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Alpaca.Markets.IAlpacaOptionsDataClient.ListExchangesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Alpaca.Markets.IAlpacaOptionsDataClient.ListLatestQuotesAsync(Alpaca.Markets.LatestOptionsDataRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Alpaca.Markets.IAlpacaOptionsDataClient.ListLatestTradesAsync(Alpaca.Markets.LatestOptionsDataRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! -Alpaca.Markets.IAlpacaOptionsDataClient.ListSnapshotsAsync(Alpaca.Markets.LatestOptionsDataRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Alpaca.Markets.IAlpacaScreenerClient Alpaca.Markets.IAlpacaScreenerClient.GetTopMarketMoversAsync(int? numberOfLosersAndGainersInResponse = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Alpaca.Markets.IAlpacaStreamingClient diff --git a/Alpaca.Markets/PublicAPI.Unshipped.txt b/Alpaca.Markets/PublicAPI.Unshipped.txt index fc91bc24..7a9fb493 100644 --- a/Alpaca.Markets/PublicAPI.Unshipped.txt +++ b/Alpaca.Markets/PublicAPI.Unshipped.txt @@ -1,4 +1,9 @@ #nullable enable +Alpaca.Markets.IAlpacaOptionsDataClient.GetOptionChainAsync(Alpaca.Markets.OptionChainRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Alpaca.Markets.IAlpacaOptionsDataClient.ListSnapshotsAsync(Alpaca.Markets.OptionSnapshotRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Alpaca.Markets.IDictionaryPage +Alpaca.Markets.IDictionaryPage.Items.get -> System.Collections.Generic.IReadOnlyDictionary! +Alpaca.Markets.IDictionaryPage.NextPageToken.get -> string? Alpaca.Markets.OptionChainRequest.ExpirationDateEqualTo.get -> System.DateOnly? Alpaca.Markets.OptionChainRequest.ExpirationDateEqualTo.set -> void Alpaca.Markets.OptionChainRequest.ExpirationDateGreaterThanOrEqualTo.get -> System.DateOnly? @@ -14,3 +19,8 @@ Alpaca.Markets.OptionChainRequest.StrikePriceGreaterThanOrEqualTo.get -> decimal Alpaca.Markets.OptionChainRequest.StrikePriceGreaterThanOrEqualTo.set -> void Alpaca.Markets.OptionChainRequest.StrikePriceLessThanOrEqualTo.get -> decimal? Alpaca.Markets.OptionChainRequest.StrikePriceLessThanOrEqualTo.set -> void +Alpaca.Markets.OptionSnapshotRequest +Alpaca.Markets.OptionSnapshotRequest.OptionsFeed.get -> Alpaca.Markets.OptionsFeed? +Alpaca.Markets.OptionSnapshotRequest.OptionsFeed.set -> void +Alpaca.Markets.OptionSnapshotRequest.OptionSnapshotRequest(System.Collections.Generic.IEnumerable! symbols) -> void +Alpaca.Markets.OptionSnapshotRequest.Symbols.get -> System.Collections.Generic.IReadOnlyCollection!