From 988ead9953fbdbb442028de25e4fd270ff2b2aa4 Mon Sep 17 00:00:00 2001 From: Oleg Rakhmatulin Date: Fri, 12 Apr 2024 09:33:35 +0200 Subject: [PATCH] Issue #472 - Added support for the new portfolio history endpoint using the existing `IAlpacaTradingClient.GetPortfolioHistoryAsync` method. (cherry picked from commit 361e0f0db2692cc0b817a6cdc08a7f587908dfb7) --- .../AlpacaTradingClientTest.Account.cs | 7 +-- Alpaca.Markets.sln.DotSettings | 2 + Alpaca.Markets/Alpaca.Markets.csproj | 2 +- Alpaca.Markets/AlpacaTradingClient.General.cs | 2 +- Alpaca.Markets/CompatibilitySuppressions.xml | 28 --------- Alpaca.Markets/Enums/IntradayProfitLoss.cs | 22 +++++++ Alpaca.Markets/Enums/IntradayReporting.cs | 31 ++++++++++ .../Interfaces/IAlpacaTradingClient.cs | 3 + .../Parameters/PortfolioHistoryRequest.cs | 59 +++++++++++++++++-- Alpaca.Markets/PublicAPI.Shipped.txt | 12 ++++ Alpaca.Markets/PublicAPI.Unshipped.txt | 1 + 11 files changed, 128 insertions(+), 41 deletions(-) create mode 100644 Alpaca.Markets/Enums/IntradayProfitLoss.cs create mode 100644 Alpaca.Markets/Enums/IntradayReporting.cs diff --git a/Alpaca.Markets.Tests/AlpacaTradingClientTest.Account.cs b/Alpaca.Markets.Tests/AlpacaTradingClientTest.Account.cs index c85f135a..e60df5f0 100644 --- a/Alpaca.Markets.Tests/AlpacaTradingClientTest.Account.cs +++ b/Alpaca.Markets.Tests/AlpacaTradingClientTest.Account.cs @@ -165,7 +165,6 @@ public async Task GetPortfolioHistoryAsyncWorks() using var mock = mockClientsFactory.GetAlpacaTradingClientMock(); var today = DateTime.UtcNow.Date; - var todayDateOnly = DateOnly.FromDateTime(today); mock.AddGet("/v2/account/portfolio/history", new JObject( new JProperty("timestamp", new JArray( @@ -180,10 +179,8 @@ public async Task GetPortfolioHistoryAsyncWorks() var history = await mock.Client.GetPortfolioHistoryAsync( new PortfolioHistoryRequest { - Period = new HistoryPeriod(5, HistoryPeriodUnit.Day), - TimeFrame = TimeFrame.FifteenMinutes, - ExtendedHours = true - }.WithInterval(new Interval(todayDateOnly, todayDateOnly))); + TimeFrame = TimeFrame.FifteenMinutes + }.WithInterval(new Interval(today, today))); Assert.Equal(Price, history.BaseValue); diff --git a/Alpaca.Markets.sln.DotSettings b/Alpaca.Markets.sln.DotSettings index fe4c46bd..fe6abfa7 100644 --- a/Alpaca.Markets.sln.DotSettings +++ b/Alpaca.Markets.sln.DotSettings @@ -1,7 +1,9 @@  False <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="Private methods"><ElementKinds><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + True False True True diff --git a/Alpaca.Markets/Alpaca.Markets.csproj b/Alpaca.Markets/Alpaca.Markets.csproj index ebedd628..63e30c6a 100644 --- a/Alpaca.Markets/Alpaca.Markets.csproj +++ b/Alpaca.Markets/Alpaca.Markets.csproj @@ -48,7 +48,7 @@ true - true + 7.0.0 true true diff --git a/Alpaca.Markets/AlpacaTradingClient.General.cs b/Alpaca.Markets/AlpacaTradingClient.General.cs index e4166262..0275c788 100644 --- a/Alpaca.Markets/AlpacaTradingClient.General.cs +++ b/Alpaca.Markets/AlpacaTradingClient.General.cs @@ -31,7 +31,7 @@ public async Task GetPortfolioHistoryAsync( PortfolioHistoryRequest request, CancellationToken cancellationToken = default) => await _httpClient.GetAsync( - await request.EnsureNotNull() + await request.EnsureNotNull().Validate() .GetUriBuilderAsync(_httpClient).ConfigureAwait(false), _rateLimitHandler, cancellationToken).ConfigureAwait(false); diff --git a/Alpaca.Markets/CompatibilitySuppressions.xml b/Alpaca.Markets/CompatibilitySuppressions.xml index 8c521d14..c05ec260 100644 --- a/Alpaca.Markets/CompatibilitySuppressions.xml +++ b/Alpaca.Markets/CompatibilitySuppressions.xml @@ -575,13 +575,6 @@ lib/net462/Alpaca.Markets.dll true - - CP0002 - M:Alpaca.Markets.PortfolioHistoryRequest.WithInterval(Alpaca.Markets.Interval{System.DateTime}) - lib/net462/Alpaca.Markets.dll - lib/net462/Alpaca.Markets.dll - true - CP0002 F:Alpaca.Markets.AccountActivityType.RefTafFee @@ -876,13 +869,6 @@ lib/net6.0/Alpaca.Markets.dll true - - CP0002 - M:Alpaca.Markets.PortfolioHistoryRequest.WithInterval(Alpaca.Markets.Interval{System.DateTime}) - lib/net6.0/Alpaca.Markets.dll - lib/net6.0/Alpaca.Markets.dll - true - CP0002 F:Alpaca.Markets.AccountActivityType.RefTafFee @@ -1177,13 +1163,6 @@ lib/netstandard2.0/Alpaca.Markets.dll true - - CP0002 - M:Alpaca.Markets.PortfolioHistoryRequest.WithInterval(Alpaca.Markets.Interval{System.DateTime}) - lib/netstandard2.0/Alpaca.Markets.dll - lib/netstandard2.0/Alpaca.Markets.dll - true - CP0002 F:Alpaca.Markets.AccountActivityType.RefTafFee @@ -1478,13 +1457,6 @@ lib/netstandard2.1/Alpaca.Markets.dll true - - CP0002 - M:Alpaca.Markets.PortfolioHistoryRequest.WithInterval(Alpaca.Markets.Interval{System.DateTime}) - lib/netstandard2.1/Alpaca.Markets.dll - lib/netstandard2.1/Alpaca.Markets.dll - true - CP0006 M:Alpaca.Markets.IAlpacaTradingClient.ExerciseOptionsPositionByIdAsync(System.Guid,System.Threading.CancellationToken) diff --git a/Alpaca.Markets/Enums/IntradayProfitLoss.cs b/Alpaca.Markets/Enums/IntradayProfitLoss.cs new file mode 100644 index 00000000..d0cb00ed --- /dev/null +++ b/Alpaca.Markets/Enums/IntradayProfitLoss.cs @@ -0,0 +1,22 @@ +namespace Alpaca.Markets; + +/// +/// Intraday profit/loss calculation for portfolio history in the Alpaca REST API. +/// +[JsonConverter(typeof(StringEnumConverter))] +public enum IntradayProfitLoss +{ + /// + /// Don't reset the profit/los value to the previous day's closing equity for each trading day. + /// + [UsedImplicitly] + [EnumMember(Value = "no_reset")] + NoReset, + + /// + /// Reset the profit/los value to the previous day's closing equity for each trading day. + /// + [UsedImplicitly] + [EnumMember(Value = "per_day")] + PerDay +} diff --git a/Alpaca.Markets/Enums/IntradayReporting.cs b/Alpaca.Markets/Enums/IntradayReporting.cs new file mode 100644 index 00000000..fc278bc0 --- /dev/null +++ b/Alpaca.Markets/Enums/IntradayReporting.cs @@ -0,0 +1,31 @@ +namespace Alpaca.Markets; + +/// +/// Intraday reporting styles for portfolio history in the Alpaca REST API. +/// +[JsonConverter(typeof(StringEnumConverter))] +public enum IntradayReporting +{ + /// + /// Only timestamps for the core equity trading hours are returned (usually 9:30am to 4:00pm, trading days only). + /// + [UsedImplicitly] + [EnumMember(Value = "market_hours")] + MarketHours, + + /// + /// Returns timestamps for the whole session including extended hours (usually 4:00am to 8:00pm, trading days only). + /// + [UsedImplicitly] + [EnumMember(Value = "extended_hours")] + ExtendedHours, + + /// + /// Returns price data points 24/7 (for off-session times too). To calculate the equity values we are using the following prices: + /// - Between 4:00am and 10:00pm on trading days the valuation will be calculated based on the last trade (extended hours and normal hours respectively). + /// - After 10:00pm, until the next session open the equities will be valued at their official closing price on the primary exchange. + /// + [UsedImplicitly] + [EnumMember(Value = "continuous")] + Continuous +} diff --git a/Alpaca.Markets/Interfaces/IAlpacaTradingClient.cs b/Alpaca.Markets/Interfaces/IAlpacaTradingClient.cs index c31d507d..af3d4de4 100644 --- a/Alpaca.Markets/Interfaces/IAlpacaTradingClient.cs +++ b/Alpaca.Markets/Interfaces/IAlpacaTradingClient.cs @@ -607,6 +607,9 @@ Task> ListAccountActivitiesAsync( /// /// Portfolio history request parameters. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// The argument contains invalid data or some required data is missing, unable to create a valid HTTP request. + /// /// /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. /// diff --git a/Alpaca.Markets/Parameters/PortfolioHistoryRequest.cs b/Alpaca.Markets/Parameters/PortfolioHistoryRequest.cs index 25fb3172..bcb2b3ac 100644 --- a/Alpaca.Markets/Parameters/PortfolioHistoryRequest.cs +++ b/Alpaca.Markets/Parameters/PortfolioHistoryRequest.cs @@ -4,13 +4,21 @@ /// Encapsulates request parameters for call. /// [UsedImplicitly] -public sealed class PortfolioHistoryRequest +public sealed class PortfolioHistoryRequest : Validation.IRequest { /// /// Gets inclusive date interval for filtering items in response. /// [UsedImplicitly] - public Interval DateInterval { get; private set; } + [ExcludeFromCodeCoverage] + [Obsolete("Use the Interval property instead of this one.", true)] + public Interval DateInterval => Interval.AsDateInterval(); + + /// + /// Gets inclusive date interval for filtering items in response. + /// + [UsedImplicitly] + public Interval Interval { get; private set; } /// /// Gets or sets the time frame value for desired history. Default value (if null) is 1 minute @@ -25,11 +33,24 @@ public sealed class PortfolioHistoryRequest [UsedImplicitly] public HistoryPeriod? Period { get; set; } + /// + /// Gets or sets intraday reporting style. Make sense only if are equal to . + /// + [UsedImplicitly] + public IntradayReporting? IntradayReporting { get; set; } + + /// + /// Gets or sets intraday profit/loss reset. Make sense only if are equal to . + /// + [UsedImplicitly] + public IntradayProfitLoss? IntradayProfitLoss { get; set; } + /// /// Gets or sets flags, indicating that include extended hours included in the result. /// This is effective only for time frame less than 1 day. /// [UsedImplicitly] + [Obsolete("Use the DateInterval property instead of this one.", false)] public Boolean? ExtendedHours { get; set; } internal async ValueTask GetUriBuilderAsync( @@ -38,12 +59,13 @@ internal async ValueTask GetUriBuilderAsync( { Path = "v2/account/portfolio/history", Query = await new QueryBuilder() - .AddParameter("start_date", DateInterval.From) - .AddParameter("end_date", DateInterval.Into) + .AddParameter("intraday_reporting", IntradayReporting) .AddParameter("period", Period?.ToString()) + .AddParameter("start", Interval.From, "O") + .AddParameter("end", Interval.Into, "O") + .AddParameter("pnl_reset", IntradayProfitLoss) // ReSharper disable once StringLiteralTypo .AddParameter("timeframe", TimeFrame) - .AddParameter("extended_hours", ExtendedHours) .AsStringAsync().ConfigureAwait(false) }; @@ -54,10 +76,35 @@ internal async ValueTask GetUriBuilderAsync( /// Request with applied filtering. [UsedImplicitly] [MethodImpl(MethodImplOptions.AggressiveInlining)] + public PortfolioHistoryRequest WithInterval( + Interval value) + { + Interval = value; + return this; + } + + /// + /// Sets time interval for filtering data returned by this request. + /// /// + /// New filtering interval. + /// Request with applied filtering. + [UsedImplicitly] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("Use the override that gets Interval instead of this one.", true)] public PortfolioHistoryRequest WithInterval( Interval value) { - DateInterval = value; + Interval = value.AsTimeInterval(); return this; } + + IEnumerable Validation.IRequest.GetExceptions() + { + yield return Interval.TryValidateInterval(); + if (Period.HasValue && !Interval.IsEmpty()) + { + yield return new RequestValidationException( + "Both `Period` and `Interval` are set.", nameof(Period)); + } + } } diff --git a/Alpaca.Markets/PublicAPI.Shipped.txt b/Alpaca.Markets/PublicAPI.Shipped.txt index 32479cca..d2a5380f 100644 --- a/Alpaca.Markets/PublicAPI.Shipped.txt +++ b/Alpaca.Markets/PublicAPI.Shipped.txt @@ -634,6 +634,13 @@ Alpaca.Markets.INewsArticle.Summary.get -> string! Alpaca.Markets.INewsArticle.Symbols.get -> System.Collections.Generic.IReadOnlyList! Alpaca.Markets.INewsArticle.ThumbImageUrl.get -> System.Uri? Alpaca.Markets.INewsArticle.UpdatedAtUtc.get -> System.DateTime +Alpaca.Markets.IntradayProfitLoss +Alpaca.Markets.IntradayProfitLoss.NoReset = 0 -> Alpaca.Markets.IntradayProfitLoss +Alpaca.Markets.IntradayProfitLoss.PerDay = 1 -> Alpaca.Markets.IntradayProfitLoss +Alpaca.Markets.IntradayReporting +Alpaca.Markets.IntradayReporting.Continuous = 2 -> Alpaca.Markets.IntradayReporting +Alpaca.Markets.IntradayReporting.ExtendedHours = 1 -> Alpaca.Markets.IntradayReporting +Alpaca.Markets.IntradayReporting.MarketHours = 0 -> Alpaca.Markets.IntradayReporting Alpaca.Markets.IOptionContract Alpaca.Markets.IOptionContract.ClosePrice.get -> decimal? Alpaca.Markets.IOptionContract.ClosePriceDate.get -> System.DateOnly? @@ -1049,6 +1056,11 @@ Alpaca.Markets.PortfolioHistoryRequest Alpaca.Markets.PortfolioHistoryRequest.ExtendedHours.get -> bool? Alpaca.Markets.PortfolioHistoryRequest.ExtendedHours.set -> void Alpaca.Markets.PortfolioHistoryRequest.DateInterval.get -> Alpaca.Markets.Interval +Alpaca.Markets.PortfolioHistoryRequest.Interval.get -> Alpaca.Markets.Interval +Alpaca.Markets.PortfolioHistoryRequest.IntradayProfitLoss.get -> Alpaca.Markets.IntradayProfitLoss? +Alpaca.Markets.PortfolioHistoryRequest.IntradayProfitLoss.set -> void +Alpaca.Markets.PortfolioHistoryRequest.IntradayReporting.get -> Alpaca.Markets.IntradayReporting? +Alpaca.Markets.PortfolioHistoryRequest.IntradayReporting.set -> void Alpaca.Markets.PortfolioHistoryRequest.Period.get -> Alpaca.Markets.HistoryPeriod? Alpaca.Markets.PortfolioHistoryRequest.Period.set -> void Alpaca.Markets.PortfolioHistoryRequest.PortfolioHistoryRequest() -> void diff --git a/Alpaca.Markets/PublicAPI.Unshipped.txt b/Alpaca.Markets/PublicAPI.Unshipped.txt index 487fdb67..726e8438 100644 --- a/Alpaca.Markets/PublicAPI.Unshipped.txt +++ b/Alpaca.Markets/PublicAPI.Unshipped.txt @@ -17,3 +17,4 @@ 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.PortfolioHistoryRequest.WithInterval(Alpaca.Markets.Interval value) -> Alpaca.Markets.PortfolioHistoryRequest!