Skip to content

Commit

Permalink
Issue #745 - The new OptionSnapshotRequest class added and used for…
Browse files Browse the repository at this point in the history
… the `ListSnapshotsAsync` method (breaking change).

(cherry picked from commit be7b14e)
  • Loading branch information
OlegRa committed May 10, 2024
1 parent 0161128 commit 2b8295d
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 26 deletions.
12 changes: 7 additions & 5 deletions Alpaca.Markets.Tests/AlpacaOptionsDataClientTest.Snapshots.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -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);
}
}
Expand Down
25 changes: 9 additions & 16 deletions Alpaca.Markets/AlpacaOptionsDataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,19 @@ public Task<IReadOnlyDictionary<String, ITrade>> ListLatestTradesAsync(
getLatestAsync<ITrade, JsonOptionTrade>(
request.EnsureNotNull().Validate(), "trades/latest", data => data.Trades, cancellationToken);

public Task<IReadOnlyDictionary<String, ISnapshot>> ListSnapshotsAsync(
LatestOptionsDataRequest request,
public async Task<IDictionaryPage<ISnapshot>> ListSnapshotsAsync(
OptionSnapshotRequest request,
CancellationToken cancellationToken = default) =>
getLatestAsync<ISnapshot, JsonOptionSnapshot>(
request.EnsureNotNull().Validate(), "snapshots", data => data.Snapshots, cancellationToken);
await HttpClient.GetAsync<IDictionaryPage<ISnapshot>, JsonOptionsSnapshotData>(
await request.GetUriBuilderAsync(HttpClient).ConfigureAwait(false),
RateLimitHandler, cancellationToken).ConfigureAwait(false);

public Task<IReadOnlyDictionary<String, ISnapshot>> GetOptionChainAsync(
public async Task<IDictionaryPage<ISnapshot>> GetOptionChainAsync(
OptionChainRequest request,
CancellationToken cancellationToken = default) =>
getLatestAsync<ISnapshot, JsonOptionSnapshot>(
request.EnsureNotNull().Validate(), data => data.Snapshots, cancellationToken);
await HttpClient.GetAsync<IDictionaryPage<ISnapshot>, JsonOptionsSnapshotData>(
await request.GetUriBuilderAsync(HttpClient).ConfigureAwait(false),
RateLimitHandler, cancellationToken).ConfigureAwait(false);

private async Task<IReadOnlyDictionary<String, TApi>> getLatestAsync<TApi, TJson>(
LatestOptionsDataRequest request,
Expand All @@ -56,15 +58,6 @@ await HttpClient.GetAsync(
await request.GetUriBuilderAsync(HttpClient, lastPathSegment).ConfigureAwait(false),
itemsSelector, withSymbol<TApi, TJson>, RateLimitHandler, cancellationToken).ConfigureAwait(false);

private async Task<IReadOnlyDictionary<String, TApi>> getLatestAsync<TApi, TJson>(
OptionChainRequest request,
Func<JsonLatestData, Dictionary<String, TJson>> itemsSelector,
CancellationToken cancellationToken)
where TJson : TApi, ISymbolMutable =>
await HttpClient.GetAsync(
await request.GetUriBuilderAsync(HttpClient).ConfigureAwait(false),
itemsSelector, withSymbol<TApi, TJson>, RateLimitHandler, cancellationToken).ConfigureAwait(false);

private static TApi withSymbol<TApi, TJson>(
KeyValuePair<String, TJson> kvp)
where TJson : TApi, ISymbolMutable
Expand Down
4 changes: 4 additions & 0 deletions Alpaca.Markets/Helpers/DebuggerDisplayExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ internal static String ToDebuggerDisplayString<TItem>(
this IMultiPage<TItem> page) =>
$"{nameof(IPage<TItem>)}<{typeof(TItem).Name}> {{ Count = {page.Items.Count}, NextPageToken = \"{page.NextPageToken}\" }}";

internal static String ToDebuggerDisplayString<TItem>(
this IDictionaryPage<TItem> page) =>
$"{nameof(IDictionaryPage<TItem>)}<{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} }}";
Expand Down
6 changes: 3 additions & 3 deletions Alpaca.Markets/Interfaces/IAlpacaOptionsDataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ Task<IReadOnlyDictionary<String, ITrade>> ListLatestTradesAsync(
/// </exception>
/// <returns>Read-only dictionary with the current snapshot information.</returns>
[UsedImplicitly]
Task<IReadOnlyDictionary<String, ISnapshot>> ListSnapshotsAsync(
LatestOptionsDataRequest request,
Task<IDictionaryPage<ISnapshot>> ListSnapshotsAsync(
OptionSnapshotRequest request,
CancellationToken cancellationToken = default);

/// <summary>
Expand Down Expand Up @@ -143,7 +143,7 @@ Task<IReadOnlyDictionary<String, ISnapshot>> ListSnapshotsAsync(
/// </exception>
/// <returns>Read-only dictionary with the current snapshot information.</returns>
[UsedImplicitly]
Task<IReadOnlyDictionary<String, ISnapshot>> GetOptionChainAsync(
Task<IDictionaryPage<ISnapshot>> GetOptionChainAsync(
OptionChainRequest request,
CancellationToken cancellationToken = default);
}
20 changes: 20 additions & 0 deletions Alpaca.Markets/Interfaces/IDictionaryPage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Alpaca.Markets;

/// <summary>
/// Encapsulates single page response in Alpaca Data API v2.
/// </summary>
/// <typeparam name="TItem">Type of paged item (bar, trade or quote)</typeparam>
public interface IDictionaryPage<TItem>
{
/// <summary>
/// Gets the next page token for continuation. If value of this property
/// equals to <c>null</c> this page is the last one and no more data is available.
/// </summary>
[UsedImplicitly]
public String? NextPageToken { get; }

/// <summary>
/// Gets list of items for this response grouped by asset symbols.
/// </summary>
public IReadOnlyDictionary<String, TItem> Items { get; }
}
45 changes: 45 additions & 0 deletions Alpaca.Markets/Messages/JsonOptionSnapshotsData.cs
Original file line number Diff line number Diff line change
@@ -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<ISnapshot>) + "<" + nameof(ISnapshot) + ">")]
internal sealed class JsonOptionsSnapshotData : IDictionaryPage<ISnapshot>
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[JsonProperty(PropertyName = "snapshots", Required = Required.Default)]
public Dictionary<String, JsonOptionSnapshot> ItemsList { get; [ExcludeFromCodeCoverage] set; } = new();

[JsonProperty(PropertyName = "next_page_token", Required = Required.Default)]
public String? NextPageToken { get; set; }

[JsonIgnore]
public IReadOnlyDictionary<String, ISnapshot> Items { get; [ExcludeFromCodeCoverage] private set; }
= new Dictionary<String, ISnapshot>();

[OnDeserialized]
[UsedImplicitly]
internal void OnDeserializedMethod(
StreamingContext _) =>
Items = (ItemsList ?? []).ToDictionary(
kvp => kvp.Key,
withSymbol<ISnapshot, JsonOptionSnapshot>,
StringComparer.Ordinal);

[ExcludeFromCodeCoverage]
public override String ToString() =>
JsonConvert.SerializeObject(this);

[ExcludeFromCodeCoverage]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private String DebuggerDisplay =>
this.ToDebuggerDisplayString();

private static TApi withSymbol<TApi, TJson>(
KeyValuePair<String, TJson> kvp)
where TJson : TApi, ISymbolMutable
{
kvp.Value.SetSymbol(kvp.Key);
return kvp.Value;
}
}
49 changes: 49 additions & 0 deletions Alpaca.Markets/Parameters/OptionSnapshotRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace Alpaca.Markets;

/// <summary>
/// Encapsulates data for latest options data requests on Alpaca Data API v2.
/// </summary>
public sealed class OptionSnapshotRequest : Validation.IRequest
{
private readonly HashSet<String> _symbols = new(StringComparer.Ordinal);

/// <summary>
/// Creates new instance of <see cref="OptionSnapshotRequest"/> object.
/// </summary>
/// <param name="symbols">Options symbols list for data retrieval.</param>
/// <exception cref="ArgumentNullException">
/// The <paramref name="symbols"/> argument is <c>null</c>.
/// </exception>
public OptionSnapshotRequest(
IEnumerable<String> symbols) =>
_symbols.UnionWith(symbols.EnsureNotNull());

/// <summary>
/// Gets options symbols list for data retrieval.
/// </summary>
[UsedImplicitly]
public IReadOnlyCollection<String> Symbols => _symbols;

/// <summary>
/// Gets options feed for data retrieval.
/// </summary>
[UsedImplicitly]
[ExcludeFromCodeCoverage]
public OptionsFeed? OptionsFeed { get; set; }

internal async ValueTask<UriBuilder> GetUriBuilderAsync(
HttpClient httpClient) =>
new UriBuilder(httpClient.BaseAddress!)
{
Query = await new QueryBuilder()
.AddParameter("symbols", Symbols.ToList())
.AddParameter("feed", OptionsFeed)
.AsStringAsync().ConfigureAwait(false)
}.AppendPath("snapshots");

IEnumerable<RequestValidationException?> Validation.IRequest.GetExceptions()
{
yield return Symbols.TryValidateSymbolsList();
yield return Symbols.TryValidateSymbolName();
}
}
2 changes: 0 additions & 2 deletions Alpaca.Markets/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -454,11 +454,9 @@ Alpaca.Markets.IAlpacaNewsStreamingClient
Alpaca.Markets.IAlpacaNewsStreamingClient.GetNewsSubscription() -> Alpaca.Markets.IAlpacaDataSubscription<Alpaca.Markets.INewsArticle!>!
Alpaca.Markets.IAlpacaNewsStreamingClient.GetNewsSubscription(string! symbol) -> Alpaca.Markets.IAlpacaDataSubscription<Alpaca.Markets.INewsArticle!>!
Alpaca.Markets.IAlpacaOptionsDataClient
Alpaca.Markets.IAlpacaOptionsDataClient.GetOptionChainAsync(Alpaca.Markets.OptionChainRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyDictionary<string!, Alpaca.Markets.ISnapshot!>!>!
Alpaca.Markets.IAlpacaOptionsDataClient.ListExchangesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyDictionary<string!, string!>!>!
Alpaca.Markets.IAlpacaOptionsDataClient.ListLatestQuotesAsync(Alpaca.Markets.LatestOptionsDataRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyDictionary<string!, Alpaca.Markets.IQuote!>!>!
Alpaca.Markets.IAlpacaOptionsDataClient.ListLatestTradesAsync(Alpaca.Markets.LatestOptionsDataRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyDictionary<string!, Alpaca.Markets.ITrade!>!>!
Alpaca.Markets.IAlpacaOptionsDataClient.ListSnapshotsAsync(Alpaca.Markets.LatestOptionsDataRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyDictionary<string!, Alpaca.Markets.ISnapshot!>!>!
Alpaca.Markets.IAlpacaScreenerClient
Alpaca.Markets.IAlpacaScreenerClient.GetTopMarketMoversAsync(int? numberOfLosersAndGainersInResponse = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Alpaca.Markets.IMarketMovers!>!
Alpaca.Markets.IAlpacaStreamingClient
Expand Down
10 changes: 10 additions & 0 deletions Alpaca.Markets/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#nullable enable
Alpaca.Markets.IAlpacaOptionsDataClient.GetOptionChainAsync(Alpaca.Markets.OptionChainRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Alpaca.Markets.IDictionaryPage<Alpaca.Markets.ISnapshot!>!>!
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.ISnapshot!>!>!
Alpaca.Markets.IAlpacaTradingClient.ExerciseOptionsPositionByIdAsync(System.Guid contractId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<bool>!
Alpaca.Markets.IAlpacaTradingClient.ExerciseOptionsPositionBySymbolAsync(string! symbol, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<bool>!
Alpaca.Markets.IDictionaryPage<TItem>
Alpaca.Markets.IDictionaryPage<TItem>.Items.get -> System.Collections.Generic.IReadOnlyDictionary<string!, TItem>!
Alpaca.Markets.IDictionaryPage<TItem>.NextPageToken.get -> string?
Alpaca.Markets.LatestOptionsDataRequest
Alpaca.Markets.LatestOptionsDataRequest.LatestOptionsDataRequest(System.Collections.Generic.IEnumerable<string!>! symbols) -> void
Alpaca.Markets.LatestOptionsDataRequest.OptionsFeed.get -> Alpaca.Markets.OptionsFeed?
Expand Down Expand Up @@ -32,4 +37,9 @@ Alpaca.Markets.OptionContractsRequest.UnderlyingSymbols.get -> System.Collection
Alpaca.Markets.OptionsFeed
Alpaca.Markets.OptionsFeed.Indicative = 1 -> Alpaca.Markets.OptionsFeed
Alpaca.Markets.OptionsFeed.Opra = 0 -> Alpaca.Markets.OptionsFeed
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<string!>! symbols) -> void
Alpaca.Markets.OptionSnapshotRequest.Symbols.get -> System.Collections.Generic.IReadOnlyCollection<string!>!
Alpaca.Markets.PortfolioHistoryRequest.WithInterval(Alpaca.Markets.Interval<System.DateTime> value) -> Alpaca.Markets.PortfolioHistoryRequest!

0 comments on commit 2b8295d

Please sign in to comment.