Skip to content

Commit

Permalink
Issue #745 - Added new methods GetOptionChainAsyncAsAsyncEnumerable
Browse files Browse the repository at this point in the history
… and `ListSnapshotsAsAsyncEnumerable` into the Extensions package.
  • Loading branch information
OlegRa committed Apr 28, 2024
1 parent f7334b0 commit 476e681
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,4 @@ private static JArray createBarsList() =>
new JProperty("h", Price),
new JProperty("c", Price),
new JProperty("v", Volume));

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace Alpaca.Markets.Extensions.Tests;

public sealed partial class AlpacaOptionsDataClientTest
{
[Fact]
public async Task GetOptionChainAsAsyncEnumerableWorks()
{
using var mock = mockClientsFactory.GetAlpacaOptionsDataClientMock();

addPaginatedResponses(mock, addSingleContractPageExpectation);

var counter = await validateList(
mock.Client.GetOptionChainAsyncAsAsyncEnumerable(
new OptionChainRequest(Stock)));

Assert.NotEqual(0, counter);
}

[Fact]
public async Task ListSnapshotsAsAsyncEnumerableWorks()
{
using var mock = mockClientsFactory.GetAlpacaOptionsDataClientMock();

addPaginatedResponses(mock, addSingleSnapshotPageExpectation);

var counter = await validateList(
mock.Client.ListSnapshotsAsAsyncEnumerable(
new OptionSnapshotRequest(_symbols)));

Assert.NotEqual(0, counter);
}

private static void addSingleContractPageExpectation(
MockClient<AlpacaOptionsDataClientConfiguration, IAlpacaOptionsDataClient> mock,
String? token = null) =>
addSingleContractOrSnapshotPageExpectation(mock, "/*", token);

private static void addSingleSnapshotPageExpectation(
MockClient<AlpacaOptionsDataClientConfiguration, IAlpacaOptionsDataClient> mock,
String? token = null) =>
addSingleContractOrSnapshotPageExpectation(mock, String.Empty, token);

private static void addSingleContractOrSnapshotPageExpectation(
MockClient<AlpacaOptionsDataClientConfiguration, IAlpacaOptionsDataClient> mock,
String urlPathLastSegment,
String? token = null) =>
mock.AddGet($"/v1beta1/options/snapshots{urlPathLastSegment}", new JObject(
new JProperty("snapshots", new JObject(
_symbols.Select(symbol => new JProperty(symbol, createSnapshot())).OfType<Object>().ToArray())),
new JProperty("next_page_token", token)));

private static JObject createSnapshot() => new(
new JProperty("impliedVolatility", Price));
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.Diagnostics.CodeAnalysis;

namespace Alpaca.Markets.Extensions.Tests;

[Collection("MockEnvironment")]
[SuppressMessage("Usage", "xUnit1047:Avoid using TheoryDataRow arguments that might not be serializable")]
public sealed partial class AlpacaOptionsDataClientTest(
MockClientsFactoryFixture mockClientsFactory)
{
Expand Down
2 changes: 1 addition & 1 deletion Alpaca.Markets.Extensions/Alpaca.Markets.Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" PrivateAssets="compile;contentfiles;build;analyzers" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" PrivateAssets="compile;contentfiles;build;analyzers" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" PrivateAssets="compile;contentfiles;build;analyzers" />
<PackageReference Include="Alpaca.Markets" Version="7.1.0-beta2" PrivateAssets="compile;contentfiles;build;analyzers;" />
<PackageReference Include="Alpaca.Markets" Version="7.1.0-beta4" PrivateAssets="compile;contentfiles;build;analyzers;" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net4'))">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
namespace Alpaca.Markets.Extensions;

/// <summary>
/// Set of extension methods for the <see cref="IAlpacaOptionsDataClient"/> interface.
/// </summary>
public static class AlpacaOptionsDataClientExtensions
{
/// <summary>
/// Gets all option snapshots from Alpaca REST API endpoint as async enumerable stream.
/// </summary>
/// <param name="client">The <see cref="IAlpacaOptionsDataClient"/> object instance.</param>
/// <param name="request">Account activities request parameters.</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>
/// <exception cref="RestClientErrorException">
/// The response contains an error message or the received response cannot be deserialized properly due to JSON schema mismatch.
/// </exception>
/// <exception cref="SocketException">
/// The initial TPC socket connection failed due to an underlying low-level network connectivity issue.
/// </exception>
/// <exception cref="TaskCanceledException">
/// .NET Core and .NET 5 and later only: The request failed due to timeout.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The <paramref name="client"/> or <paramref name="request"/> argument is <c>null</c>.
/// </exception>
/// <returns>Option contacts' snapshots obtained page by page.</returns>
[UsedImplicitly]
[CLSCompliant(false)]
public static IAsyncEnumerable<IOptionSnapshot> ListSnapshotsAsAsyncEnumerable(
this IAlpacaOptionsDataClient client,
OptionSnapshotRequest request) =>
ListSnapshotsAsAsyncEnumerable(client, request, CancellationToken.None);

/// <summary>
/// Gets all option snapshots from Alpaca REST API endpoint as async enumerable stream.
/// </summary>
/// <param name="client">The <see cref="IAlpacaOptionsDataClient"/> object instance.</param>
/// <param name="request">Account activities request parameters.</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>
/// <exception cref="RestClientErrorException">
/// The response contains an error message or the received response cannot be deserialized properly due to JSON schema mismatch.
/// </exception>
/// <exception cref="SocketException">
/// The initial TPC socket connection failed due to an underlying low-level network connectivity issue.
/// </exception>
/// <exception cref="TaskCanceledException">
/// .NET Core and .NET 5 and later only: The request failed due to timeout.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The <paramref name="client"/> or <paramref name="request"/> argument is <c>null</c>.
/// </exception>
/// <returns>Option contacts' snapshots obtained page by page.</returns>
[UsedImplicitly]
[CLSCompliant(false)]
public static IAsyncEnumerable<IOptionSnapshot> ListSnapshotsAsAsyncEnumerable(
this IAlpacaOptionsDataClient client,
OptionSnapshotRequest request,
CancellationToken cancellationToken) =>
getAllOptionSnapshotsPages(client.EnsureNotNull(),
getRequestWithoutPageToken(request.EnsureNotNull()), cancellationToken);

/// <summary>
/// Gets all option snapshots from Alpaca REST API endpoint as async enumerable stream.
/// </summary>
/// <param name="client">The <see cref="IAlpacaOptionsDataClient"/> object instance.</param>
/// <param name="request">Account activities request parameters.</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>
/// <exception cref="RestClientErrorException">
/// The response contains an error message or the received response cannot be deserialized properly due to JSON schema mismatch.
/// </exception>
/// <exception cref="SocketException">
/// The initial TPC socket connection failed due to an underlying low-level network connectivity issue.
/// </exception>
/// <exception cref="TaskCanceledException">
/// .NET Core and .NET 5 and later only: The request failed due to timeout.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The <paramref name="client"/> or <paramref name="request"/> argument is <c>null</c>.
/// </exception>
/// <returns>Option contacts' snapshots obtained page by page.</returns>
[UsedImplicitly]
[CLSCompliant(false)]
public static IAsyncEnumerable<IOptionSnapshot> GetOptionChainAsyncAsAsyncEnumerable(
this IAlpacaOptionsDataClient client,
OptionChainRequest request) =>
GetOptionChainAsyncAsAsyncEnumerable(client, request, CancellationToken.None);

/// <summary>
/// Gets all option snapshots from Alpaca REST API endpoint as async enumerable stream.
/// </summary>
/// <param name="client">The <see cref="IAlpacaOptionsDataClient"/> object instance.</param>
/// <param name="request">Account activities request parameters.</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>
/// <exception cref="RestClientErrorException">
/// The response contains an error message or the received response cannot be deserialized properly due to JSON schema mismatch.
/// </exception>
/// <exception cref="SocketException">
/// The initial TPC socket connection failed due to an underlying low-level network connectivity issue.
/// </exception>
/// <exception cref="TaskCanceledException">
/// .NET Core and .NET 5 and later only: The request failed due to timeout.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The <paramref name="client"/> or <paramref name="request"/> argument is <c>null</c>.
/// </exception>
/// <returns>Option contacts' snapshots obtained page by page.</returns>
[UsedImplicitly]
[CLSCompliant(false)]
public static IAsyncEnumerable<IOptionSnapshot> GetOptionChainAsyncAsAsyncEnumerable(
this IAlpacaOptionsDataClient client,
OptionChainRequest request,
CancellationToken cancellationToken) =>
getAllOptionSnapshotsPages(client.EnsureNotNull(),
getRequestWithoutPageToken(request.EnsureNotNull()), cancellationToken);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static OptionSnapshotRequest getRequestWithoutPageToken(
OptionSnapshotRequest request) =>
new(request.Symbols)
{
//Pagination = { Size = Pagination.MaxPageSize },
OptionsFeed = request.OptionsFeed
};

private static async IAsyncEnumerable<IOptionSnapshot> getAllOptionSnapshotsPages(
IAlpacaOptionsDataClient client,
OptionSnapshotRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
do
{
var page = await client.ListSnapshotsAsync(request, cancellationToken).ConfigureAwait(false);

foreach (var item in page.Items)
{
yield return item.Value;
}

request.Pagination.Token = page.NextPageToken ?? String.Empty;
} while (!String.IsNullOrEmpty(request.Pagination.Token));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static OptionChainRequest getRequestWithoutPageToken(
OptionChainRequest request) =>
new(request.UnderlyingSymbol)
{
ExpirationDateGreaterThanOrEqualTo = request.ExpirationDateGreaterThanOrEqualTo,
ExpirationDateLessThanOrEqualTo = request.ExpirationDateLessThanOrEqualTo,
StrikePriceGreaterThanOrEqualTo = request.StrikePriceGreaterThanOrEqualTo,
StrikePriceLessThanOrEqualTo = request.StrikePriceLessThanOrEqualTo,
ExpirationDateEqualTo = request.ExpirationDateEqualTo,
Pagination = { Size = Pagination.MaxPageSize },
OptionType = request.OptionType,
RootSymbol = request.RootSymbol
};

private static async IAsyncEnumerable<IOptionSnapshot> getAllOptionSnapshotsPages(
IAlpacaOptionsDataClient client,
OptionChainRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
do
{
var page = await client.GetOptionChainAsync(request, cancellationToken).ConfigureAwait(false);

foreach (var item in page.Items)
{
yield return item.Value;
}

request.Pagination.Token = page.NextPageToken ?? String.Empty;
} while (!String.IsNullOrEmpty(request.Pagination.Token));
}
}
5 changes: 5 additions & 0 deletions Alpaca.Markets.Extensions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Alpaca.Markets.Extensions.AlpacaDataClientExtensions
Alpaca.Markets.Extensions.AlpacaDataStreamingClientExtensions
Alpaca.Markets.Extensions.AlpacaDataSubscriptionExtensions
Alpaca.Markets.Extensions.AlpacaNewsStreamingClientExtensions
Alpaca.Markets.Extensions.AlpacaOptionsDataClientExtensions
Alpaca.Markets.Extensions.AlpacaServiceCollectionExtensions
Alpaca.Markets.Extensions.AlpacaStreamingClientExtensions
Alpaca.Markets.Extensions.AlpacaTradingClientExtensions
Expand Down Expand Up @@ -81,6 +82,10 @@ static Alpaca.Markets.Extensions.AlpacaNewsStreamingClientExtensions.SubscribeNe
static Alpaca.Markets.Extensions.AlpacaNewsStreamingClientExtensions.SubscribeNewsAsync(this Alpaca.Markets.IAlpacaNewsStreamingClient! client, System.Collections.Generic.IEnumerable<string!>! symbols) -> System.Threading.Tasks.ValueTask<Alpaca.Markets.Extensions.IDisposableAlpacaDataSubscription<Alpaca.Markets.INewsArticle!>!>
static Alpaca.Markets.Extensions.AlpacaNewsStreamingClientExtensions.WithReconnect(this Alpaca.Markets.IAlpacaNewsStreamingClient! client) -> Alpaca.Markets.IAlpacaNewsStreamingClient!
static Alpaca.Markets.Extensions.AlpacaNewsStreamingClientExtensions.WithReconnect(this Alpaca.Markets.IAlpacaNewsStreamingClient! client, Alpaca.Markets.Extensions.ReconnectionParameters! parameters) -> Alpaca.Markets.IAlpacaNewsStreamingClient!
static Alpaca.Markets.Extensions.AlpacaOptionsDataClientExtensions.GetOptionChainAsyncAsAsyncEnumerable(this Alpaca.Markets.IAlpacaOptionsDataClient! client, Alpaca.Markets.OptionChainRequest! request) -> System.Collections.Generic.IAsyncEnumerable<Alpaca.Markets.IOptionSnapshot!>!
static Alpaca.Markets.Extensions.AlpacaOptionsDataClientExtensions.GetOptionChainAsyncAsAsyncEnumerable(this Alpaca.Markets.IAlpacaOptionsDataClient! client, Alpaca.Markets.OptionChainRequest! request, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable<Alpaca.Markets.IOptionSnapshot!>!
static Alpaca.Markets.Extensions.AlpacaOptionsDataClientExtensions.ListSnapshotsAsAsyncEnumerable(this Alpaca.Markets.IAlpacaOptionsDataClient! client, Alpaca.Markets.OptionSnapshotRequest! request) -> System.Collections.Generic.IAsyncEnumerable<Alpaca.Markets.IOptionSnapshot!>!
static Alpaca.Markets.Extensions.AlpacaOptionsDataClientExtensions.ListSnapshotsAsAsyncEnumerable(this Alpaca.Markets.IAlpacaOptionsDataClient! client, Alpaca.Markets.OptionSnapshotRequest! request, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable<Alpaca.Markets.IOptionSnapshot!>!
static Alpaca.Markets.Extensions.AlpacaTradingClientExtensions.GetCalendarForSingleDayAsync(this Alpaca.Markets.IAlpacaTradingClient! client, System.DateOnly date, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Alpaca.Markets.IIntervalCalendar?>!
static Alpaca.Markets.Extensions.AlpacaTradingClientExtensions.GetCalendarForSingleDayAsync(this Alpaca.Markets.IAlpacaTradingClient! client, System.DateTime date, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Alpaca.Markets.ICalendar?>!
static Alpaca.Markets.Extensions.AlpacaTradingClientExtensions.GetClockCachedAsync(this Alpaca.Markets.IAlpacaTradingClient! client) -> System.Threading.Tasks.ValueTask<Alpaca.Markets.IClock!>
Expand Down
Loading

0 comments on commit 476e681

Please sign in to comment.