diff --git a/Alpaca.Markets.Tests/AlpacaTradingClientTest.Options.cs b/Alpaca.Markets.Tests/AlpacaTradingClientTest.Options.cs new file mode 100644 index 00000000..d8168cce --- /dev/null +++ b/Alpaca.Markets.Tests/AlpacaTradingClientTest.Options.cs @@ -0,0 +1,118 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Alpaca.Markets.Tests; + +public sealed partial class AlpacaTradingClientTest +{ + private static readonly DateOnly _today = DateOnly.FromDateTime(DateTime.Today); + + [Fact] + public async Task GetOptionContractByIdWorks() + { + using var mock = mockClientsFactory.GetAlpacaTradingClientMock(); + + var contractId = Guid.NewGuid(); + + mock.AddGet("/v2/options/contracts/*", createOptionContract(contractId, Stock)); + + var optionContract = await mock.Client.GetOptionContractByIdAsync(contractId); + + validateOptionContract(optionContract, contractId, Stock); + } + + [Fact] + public async Task GetOptionContractBySymbolWorks() + { + using var mock = mockClientsFactory.GetAlpacaTradingClientMock(); + + var contractId = Guid.NewGuid(); + + mock.AddGet("/v2/options/contracts/*", createOptionContract(contractId, Stock)); + + var optionContract = await mock.Client.GetOptionContractBySymbolAsync(Stock); + + validateOptionContract(optionContract, contractId, Stock); + } + + [Fact] + public async Task ListOptionContractsWorks() + { + using var mock = mockClientsFactory.GetAlpacaTradingClientMock(); + + var contractId = Guid.NewGuid(); + + mock.AddGet("/v2/options/contracts", createOptionContractsList(contractId, Stock)); + + var optionContracts = await mock.Client.ListOptionContractsAsync( + new OptionContractsRequest(Stock) + { + ExpirationDateGreaterThanOrEqualTo = _today, + ExpirationDateLessThanOrEqualTo = _today, + StrikePriceGreaterThanOrEqualTo = Price, + StrikePriceLessThanOrEqualTo = Price, + OptionStyle = OptionStyle.American, + AssetStatus = AssetStatus.Active, + OptionType = OptionType.Call, + RootSymbol = Stock, + PageNumber = 1, + PageSize = 100 + }); + + validateOptionContract(optionContracts.Single(), contractId, Stock); + } + + private static JObject createOptionContractsList( + Guid contractId, + String symbol) => + new(new JProperty("option_contracts", new JArray(createOptionContract(contractId, symbol)))); + + private static JObject createOptionContract( + Guid contractId, + String symbol) => + new( + new JProperty("open_interest_date", _today.ToString("O")), + new JProperty("close_price_date", _today.ToString("O")), + new JProperty("expiration_date", _today.ToString("O")), + new JProperty("underlying_asset_id", contractId), + new JProperty("style", OptionStyle.American), + new JProperty("status", AssetStatus.Active), + new JProperty("underlying_symbol", symbol), + new JProperty("type", OptionType.Call), + new JProperty("open_interest", Price), + new JProperty("root_symbol", symbol), + new JProperty("strike_price", Price), + new JProperty("close_price", Price), + new JProperty("tradable", true), + new JProperty("symbol", symbol), + new JProperty("id", contractId), + new JProperty("name", symbol), + new JProperty("size", 100)); + + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")] + private static void validateOptionContract( + IOptionContract optionContract, + Guid contractId, + String symbol) + { + Assert.True(optionContract.IsTradable); + + Assert.NotNull(optionContract.ClosePrice); + Assert.NotNull(optionContract.OpenInterest); + Assert.NotNull(optionContract.ClosePriceDate); + Assert.NotNull(optionContract.OpenInterestDate); + + Assert.Equal(100, optionContract.Size); + Assert.Equal(Price, optionContract.StrikePrice); + + Assert.Equal(symbol, optionContract.Name); + Assert.Equal(symbol, optionContract.Symbol); + Assert.Equal(symbol, optionContract.RootSymbol); + Assert.Equal(symbol, optionContract.UnderlyingSymbol); + + Assert.Equal(contractId, optionContract.ContractId); + Assert.Equal(contractId, optionContract.UnderlyingAssetId); + + Assert.Equal(OptionType.Call, optionContract.OptionType); + Assert.Equal(OptionStyle.American, optionContract.OptionStyle); + } +} \ No newline at end of file diff --git a/Alpaca.Markets/Helpers/AssetAttributesEnumConverter.cs b/Alpaca.Markets/Helpers/AssetAttributesEnumConverter.cs index 0964b635..6d719dfe 100644 --- a/Alpaca.Markets/Helpers/AssetAttributesEnumConverter.cs +++ b/Alpaca.Markets/Helpers/AssetAttributesEnumConverter.cs @@ -9,15 +9,6 @@ public override Object ReadJson( JsonReader reader, Type objectType, Object? existingValue, - JsonSerializer serializer) - { - try - { - return AssetAttributes.Unknown.FromEnumString(reader); - } - catch (JsonSerializationException) - { - return AssetAttributes.Unknown; - } - } + JsonSerializer serializer) => + reader.ReadEnumString(AssetAttributes.Unknown); } diff --git a/Alpaca.Markets/Helpers/CryptoExchangeEnumConverter.cs b/Alpaca.Markets/Helpers/CryptoExchangeEnumConverter.cs index be037b49..be397eae 100644 --- a/Alpaca.Markets/Helpers/CryptoExchangeEnumConverter.cs +++ b/Alpaca.Markets/Helpers/CryptoExchangeEnumConverter.cs @@ -9,15 +9,6 @@ public override Object ReadJson( JsonReader reader, Type objectType, Object? existingValue, - JsonSerializer serializer) - { - try - { - return CryptoExchange.Unknown.FromEnumString(reader); - } - catch (JsonSerializationException) - { - return CryptoExchange.Unknown; - } - } + JsonSerializer serializer) => + reader.ReadEnumString(CryptoExchange.Unknown); } diff --git a/Alpaca.Markets/Helpers/EnumExtensions.cs b/Alpaca.Markets/Helpers/EnumExtensions.cs index 2c95ccf3..6c086852 100644 --- a/Alpaca.Markets/Helpers/EnumExtensions.cs +++ b/Alpaca.Markets/Helpers/EnumExtensions.cs @@ -27,13 +27,13 @@ public static String ToEnumString( where T : struct, Enum => JsonConvert.SerializeObject(enumValue).Trim(_doubleQuotes); - public static T FromEnumString< + public static T ReadEnumString< #if NET6_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] #endif T>( - this T fallbackEnumValue, - JsonReader reader) + this JsonReader reader, + T fallbackEnumValue) where T : struct, Enum => reader.TokenType == JsonToken.String ? NamesHelper.ValuesByNames.GetValueOrDefault( diff --git a/Alpaca.Markets/Helpers/ExchangeEnumConverter.cs b/Alpaca.Markets/Helpers/ExchangeEnumConverter.cs index bd7155da..9c3e711e 100644 --- a/Alpaca.Markets/Helpers/ExchangeEnumConverter.cs +++ b/Alpaca.Markets/Helpers/ExchangeEnumConverter.cs @@ -9,15 +9,6 @@ public override Object ReadJson( JsonReader reader, Type objectType, Object? existingValue, - JsonSerializer serializer) - { - try - { - return Exchange.Unknown.FromEnumString(reader); - } - catch (JsonSerializationException) - { - return Exchange.Unknown; - } - } + JsonSerializer serializer) => + reader.ReadEnumString(Exchange.Unknown); } diff --git a/Alpaca.Markets/Helpers/OrderSideEnumConverter.cs b/Alpaca.Markets/Helpers/OrderSideEnumConverter.cs index 0574ac5c..a1eaed4d 100644 --- a/Alpaca.Markets/Helpers/OrderSideEnumConverter.cs +++ b/Alpaca.Markets/Helpers/OrderSideEnumConverter.cs @@ -9,15 +9,6 @@ public override Object ReadJson( JsonReader reader, Type objectType, Object? existingValue, - JsonSerializer serializer) - { - try - { - return OrderSide.Sell.FromEnumString(reader); - } - catch (JsonSerializationException) - { - return OrderSide.Sell; // Treat all unknown order types as sell orders - } - } + JsonSerializer serializer) => + reader.ReadEnumString(OrderSide.Sell); // Treat all unknown order types as sell orders } diff --git a/Alpaca.Markets/Messages/JsonOptionContractsPage.cs b/Alpaca.Markets/Messages/JsonOptionContractsPage.cs index ee80c8c1..1642956a 100644 --- a/Alpaca.Markets/Messages/JsonOptionContractsPage.cs +++ b/Alpaca.Markets/Messages/JsonOptionContractsPage.cs @@ -6,5 +6,5 @@ internal sealed class JsonOptionContractsPage { [JsonProperty(PropertyName = "option_contracts", Required = Required.Always)] - public List Contracts { get; set; } = []; + public List Contracts { get; [ExcludeFromCodeCoverage] set; } = []; } \ No newline at end of file