Skip to content

Commit

Permalink
Issue #731 - The OptionFeed enum added and now it can be specified …
Browse files Browse the repository at this point in the history
…for latest/snapshot requests.

(cherry picked from commit bdce3fd)
  • Loading branch information
OlegRa committed Apr 14, 2024
1 parent 89ce624 commit d16daf3
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dotnet_naming_symbols.private_static_methods.required_modifiers = static
# Define the 'private_methods_style' naming style
dotnet_naming_style.private_methods_style.capitalization = camel_case

# Define the 'private_methods_underscored' naming rule
# Define the 'private_methods_rule' naming rule
dotnet_naming_rule.private_methods_rule.symbols = private_methods
dotnet_naming_rule.private_methods_rule.style = private_methods_style
dotnet_naming_rule.private_methods_rule.severity = error
Expand Down
6 changes: 4 additions & 2 deletions Alpaca.Markets.Tests/AlpacaOptionsDataClientTest.Snapshots.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public async Task ListSnapshotsAsyncWorks()

mock.AddCryptoSnapshotsExpectation(PathPrefix, _symbols);

var snapshots = await mock.Client.ListSnapshotsAsync(_symbols);
var snapshots = await mock.Client.ListSnapshotsAsync(
new LatestOptionsDataRequest(_symbols));

Assert.NotNull(snapshots);
Assert.NotEmpty(snapshots);
Expand All @@ -28,7 +29,8 @@ public async Task GetOptionChainAsyncWorks()

mock.AddOptionChainExpectation(PathPrefix, _symbols);

var snapshots = await mock.Client.GetOptionChainAsync("AAPL");
var snapshots = await mock.Client.GetOptionChainAsync(
new OptionChainRequest("AAPL"));

Assert.NotNull(snapshots);
Assert.NotEmpty(snapshots);
Expand Down
6 changes: 4 additions & 2 deletions Alpaca.Markets.Tests/AlpacaOptionsDataClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public async Task ListLatestQuotesAsyncWorks()

mock.AddLatestQuotesExpectation(PathPrefix, _symbols);

var quotes = await mock.Client.ListLatestQuotesAsync(_symbols);
var quotes = await mock.Client.ListLatestQuotesAsync(
new LatestOptionsDataRequest(_symbols));

Assert.NotNull(quotes);
Assert.NotEmpty(quotes);
Expand All @@ -77,7 +78,8 @@ public async Task ListLatestTradesAsyncWorks()

mock.AddLatestTradesExpectation(PathPrefix, _symbols);

var trades = await mock.Client.ListLatestTradesAsync(_symbols);
var trades = await mock.Client.ListLatestTradesAsync(
new LatestOptionsDataRequest(_symbols));

Assert.NotNull(trades);
Assert.NotEmpty(trades);
Expand Down
1 change: 1 addition & 0 deletions Alpaca.Markets.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=marginable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Numerics/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Onboarding/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=OPRA/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=orderbooks/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=OTOCO/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ratelimit/@EntryIndexedValue">True</s:Boolean>
Expand Down
40 changes: 19 additions & 21 deletions Alpaca.Markets/AlpacaOptionsDataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,49 +23,47 @@ public Task<IReadOnlyDictionary<String, String>> ListExchangesAsync(
"meta/exchanges", RateLimitHandler, cancellationToken);

public Task<IReadOnlyDictionary<String, IQuote>> ListLatestQuotesAsync(
IEnumerable<String> symbols,
LatestOptionsDataRequest request,
CancellationToken cancellationToken = default) =>
getLatestAsync<IQuote, JsonOptionQuote>(
symbols.EnsureNotNull(), "quotes/latest", data => data.Quotes, cancellationToken);
request.EnsureNotNull().Validate(), "quotes/latest", data => data.Quotes, cancellationToken);

public Task<IReadOnlyDictionary<String, ITrade>> ListLatestTradesAsync(
IEnumerable<String> symbols,
LatestOptionsDataRequest request,
CancellationToken cancellationToken = default) =>
getLatestAsync<ITrade, JsonOptionTrade>(
symbols.EnsureNotNull(), "trades/latest", data => data.Trades, cancellationToken);
request.EnsureNotNull().Validate(), "trades/latest", data => data.Trades, cancellationToken);

public Task<IReadOnlyDictionary<String, ISnapshot>> ListSnapshotsAsync(
IEnumerable<String> symbols,
LatestOptionsDataRequest request,
CancellationToken cancellationToken = default) =>
getLatestAsync<ISnapshot, JsonOptionSnapshot>(
symbols.EnsureNotNull(), "snapshots", data => data.Snapshots, cancellationToken);
request.EnsureNotNull().Validate(), "snapshots", data => data.Snapshots, cancellationToken);

public Task<IReadOnlyDictionary<String, ISnapshot>> GetOptionChainAsync(
String underlyingSymbol,
OptionChainRequest request,
CancellationToken cancellationToken = default) =>
getLatestAsync<ISnapshot, JsonOptionSnapshot>(
[], $"snapshots/{underlyingSymbol.EnsureNotNull()}", data => data.Snapshots, cancellationToken);
request.EnsureNotNull().Validate(), data => data.Snapshots, cancellationToken);

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

private async ValueTask<UriBuilder> getUriBuilderAsync(
IEnumerable<String> symbols,
String lastPathSegment) =>
new UriBuilder(HttpClient.BaseAddress!)
{
Query = await new QueryBuilder()
.AddParameter("symbols", symbols.ToList())
.AsStringAsync().ConfigureAwait(false)
}.AppendPath(lastPathSegment);
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)
Expand Down
22 changes: 22 additions & 0 deletions Alpaca.Markets/Enums/OptionsFeed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Alpaca.Markets;

/// <summary>
/// Supported options feed for Alpaca REST API.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum OptionsFeed
{
/// <summary>
/// Options Price Reporting Authority.
/// </summary>
[UsedImplicitly]
[EnumMember(Value = "opra")]
Opra,

/// <summary>
/// Indicative options data.
/// </summary>
[UsedImplicitly]
[EnumMember(Value = "indicative")]
Indicative
}
2 changes: 2 additions & 0 deletions Alpaca.Markets/Helpers/OpenClose.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ internal OpenClose(
/// <summary>
/// Gets open time in EST time zone.
/// </summary>
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global
public DateTimeOffset OpenEst { get; init; }

/// <summary>
/// Gets close time in EST time zone.
/// </summary>
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global
public DateTimeOffset CloseEst { get; init; }

/// <summary>
Expand Down
36 changes: 24 additions & 12 deletions Alpaca.Markets/Interfaces/IAlpacaOptionsDataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ Task<IReadOnlyDictionary<String, String>> ListExchangesAsync(
/// <summary>
/// Gets most recent quotes for several option contracts from Alpaca REST API endpoint.
/// </summary>
/// <param name="symbols">Option contracts symbol names list.</param>
/// <param name="request">Option contracts latest data request.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="RequestValidationException">
/// The <paramref name="request"/> argument contains invalid data or some required data is missing, unable to create a valid HTTP request.
/// </exception>
/// <exception cref="HttpRequestException">
/// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout.
/// </exception>
Expand All @@ -49,19 +52,22 @@ Task<IReadOnlyDictionary<String, String>> ListExchangesAsync(
/// .NET Core and .NET 5 and later only: The request failed due to timeout.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The <paramref name="symbols"/> argument is <c>null</c>.
/// The <paramref name="request"/> argument is <c>null</c>.
/// </exception>
/// <returns>Read-only dictionary with the latest quotes information.</returns>
[UsedImplicitly]
Task<IReadOnlyDictionary<String, IQuote>> ListLatestQuotesAsync(
IEnumerable<String> symbols,
LatestOptionsDataRequest request,
CancellationToken cancellationToken = default);

/// <summary>
/// Gets most recent trades for several option contracts from Alpaca REST API endpoint.
/// </summary>
/// <param name="symbols">Option contracts symbol names list.</param>
/// <param name="request">Option contracts latest data request.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="RequestValidationException">
/// The <paramref name="request"/> argument contains invalid data or some required data is missing, unable to create a valid HTTP request.
/// </exception>
/// <exception cref="HttpRequestException">
/// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout.
/// </exception>
Expand All @@ -75,19 +81,22 @@ Task<IReadOnlyDictionary<String, IQuote>> ListLatestQuotesAsync(
/// .NET Core and .NET 5 and later only: The request failed due to timeout.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The <paramref name="symbols"/> argument is <c>null</c>.
/// The <paramref name="request"/> argument is <c>null</c>.
/// </exception>
/// <returns>Read-only dictionary with the latest trades information.</returns>
[UsedImplicitly]
Task<IReadOnlyDictionary<String, ITrade>> ListLatestTradesAsync(
IEnumerable<String> symbols,
LatestOptionsDataRequest request,
CancellationToken cancellationToken = default);

/// <summary>
/// Gets current snapshot (latest trade/quote) for several option contracts from Alpaca REST API endpoint.
/// </summary>
/// <param name="symbols">Option contracts symbol names list.</param>
/// <param name="request">Option contracts latest data request.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="RequestValidationException">
/// The <paramref name="request"/> argument contains invalid data or some required data is missing, unable to create a valid HTTP request.
/// </exception>
/// <exception cref="HttpRequestException">
/// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout.
/// </exception>
Expand All @@ -101,19 +110,22 @@ Task<IReadOnlyDictionary<String, ITrade>> ListLatestTradesAsync(
/// .NET Core and .NET 5 and later only: The request failed due to timeout.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The <paramref name="symbols"/> argument is <c>null</c>.
/// The <paramref name="request"/> argument is <c>null</c>.
/// </exception>
/// <returns>Read-only dictionary with the current snapshot information.</returns>
[UsedImplicitly]
Task<IReadOnlyDictionary<String, ISnapshot>> ListSnapshotsAsync(
IEnumerable<String> symbols,
LatestOptionsDataRequest request,
CancellationToken cancellationToken = default);

/// <summary>
/// Gets option chain (snapshots list) for option contracts with same underlying symbol from Alpaca REST API endpoint.
/// </summary>
/// <param name="underlyingSymbol">The financial instrument on which returned option contracts are based or derived.</param>
/// <param name="request">Option contracts latest data request.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="RequestValidationException">
/// The <paramref name="request"/> argument contains invalid data or some required data is missing, unable to create a valid HTTP request.
/// </exception>
/// <exception cref="HttpRequestException">
/// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout.
/// </exception>
Expand All @@ -127,11 +139,11 @@ Task<IReadOnlyDictionary<String, ISnapshot>> ListSnapshotsAsync(
/// .NET Core and .NET 5 and later only: The request failed due to timeout.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The <paramref name="underlyingSymbol"/> argument is <c>null</c>.
/// The <paramref name="request"/> argument is <c>null</c>.
/// </exception>
/// <returns>Read-only dictionary with the current snapshot information.</returns>
[UsedImplicitly]
Task<IReadOnlyDictionary<String, ISnapshot>> GetOptionChainAsync(
String underlyingSymbol,
OptionChainRequest request,
CancellationToken cancellationToken = default);
}
50 changes: 50 additions & 0 deletions Alpaca.Markets/Parameters/LatestOptionsDataRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace Alpaca.Markets;

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

/// <summary>
/// Creates new instance of <see cref="LatestOptionsDataRequest"/> 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 LatestOptionsDataRequest(
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,
String lastPathSegment) =>
new UriBuilder(httpClient.BaseAddress!)
{
Query = await new QueryBuilder()
.AddParameter("symbols", Symbols.ToList())
.AddParameter("feed", OptionsFeed)
.AsStringAsync().ConfigureAwait(false)
}.AppendPath(lastPathSegment);

IEnumerable<RequestValidationException?> Validation.IRequest.GetExceptions()
{
yield return Symbols.TryValidateSymbolsList();
yield return Symbols.TryValidateSymbolName();
}
}
46 changes: 46 additions & 0 deletions Alpaca.Markets/Parameters/OptionChainRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace Alpaca.Markets;

/// <summary>
/// Encapsulates data for latest options data requests on Alpaca Data API v2.
/// </summary>
public sealed class OptionChainRequest : Validation.IRequest
{
/// <summary>
/// Creates new instance of <see cref="LatestOptionsDataRequest"/> object.
/// </summary>
/// <param name="underlyingSymbol">Option underlying symbol for data retrieval.</param>
/// <exception cref="ArgumentNullException">
/// The <paramref name="underlyingSymbol"/> argument is <c>null</c>.
/// </exception>
public OptionChainRequest(
String underlyingSymbol) =>
UnderlyingSymbol = underlyingSymbol.EnsureNotNull();

/// <summary>
/// Gets options symbols list for data retrieval.
/// </summary>
[UsedImplicitly]

public String UnderlyingSymbol { get; }

/// <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("feed", OptionsFeed)
.AsStringAsync().ConfigureAwait(false)
}.AppendPath($"snapshots/{UnderlyingSymbol}");

IEnumerable<RequestValidationException?> Validation.IRequest.GetExceptions()
{
yield return UnderlyingSymbol.TryValidateSymbolName();
}
}
Loading

0 comments on commit d16daf3

Please sign in to comment.