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.

(cherry picked from commit 476e681)
  • Loading branch information
OlegRa committed May 10, 2024
1 parent d9091ba commit 936cdee
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 3 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
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 @@ -80,6 +81,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.GetClockCachedAsync(this Alpaca.Markets.IAlpacaTradingClient! client) -> System.Threading.Tasks.ValueTask<Alpaca.Markets.IClock!>
static Alpaca.Markets.Extensions.AlpacaTradingClientExtensions.GetClockCachedAsync(this Alpaca.Markets.IAlpacaTradingClient! client, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask<Alpaca.Markets.IClock!>
Expand Down
1 change: 1 addition & 0 deletions Alpaca.Markets.Tests/AlpacaTradingClientTest.Orders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public async Task PostBuyOrderAsyncWorks()
var order = await mock.Client.PostOrderAsync(
MarketOrder.Buy(Stock, OrderQuantity.Fractional(FractionalQuantity))
.WithClientOrderId(Guid.NewGuid().ToString("D"))
.WithPositionIntent(PositionIntent.BuyToOpen)
.WithDuration(TimeInForce.Gtc)
.WithExtendedHours(true));

Expand Down
4 changes: 2 additions & 2 deletions Alpaca.Markets.Tests/RequestValidationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public void OptionContactsRequestExpirationDateValidationWorks() =>
{
ExpirationDateGreaterThanOrEqualTo = DateOnly.FromDateTime(DateTime.Today),
ExpirationDateLessThanOrEqualTo = DateOnly.FromDateTime(DateTime.Today),
ExpirationDateEqualTo = DateOnly.FromDateTime(DateTime.Today),
ExpirationDateEqualTo = DateOnly.FromDateTime(DateTime.Today)
});

[Fact]
Expand All @@ -114,7 +114,7 @@ public void OptionChainRequestExpirationDateValidationWorks() =>
{
ExpirationDateGreaterThanOrEqualTo = DateOnly.FromDateTime(DateTime.Today),
ExpirationDateLessThanOrEqualTo = DateOnly.FromDateTime(DateTime.Today),
ExpirationDateEqualTo = DateOnly.FromDateTime(DateTime.Today),
ExpirationDateEqualTo = DateOnly.FromDateTime(DateTime.Today)
});

[Fact]
Expand Down

0 comments on commit 936cdee

Please sign in to comment.