diff --git a/YahooFinanceApi.Tests/ProfileTests.cs b/YahooFinanceApi.Tests/ProfileTests.cs new file mode 100644 index 0000000..dfbbddc --- /dev/null +++ b/YahooFinanceApi.Tests/ProfileTests.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace YahooFinanceApi.Tests; + +public class ProfileTests +{ + [Fact] + public async Task TestProfileAsync() + { + const string AAPL = "AAPL"; + + var aaplProfile = await Yahoo.QueryProfileAsync(AAPL); + + Assert.NotNull(aaplProfile.Address1); + Assert.NotNull(aaplProfile.AuditRisk); + Assert.NotNull(aaplProfile.BoardRisk); + Assert.NotNull(aaplProfile.City); + Assert.NotNull(aaplProfile.CompanyOfficers); + Assert.NotNull(aaplProfile.CompensationAsOfEpochDate); + Assert.NotNull(aaplProfile.CompensationRisk); + Assert.NotNull(aaplProfile.Country); + Assert.NotNull(aaplProfile.FullTimeEmployees); + Assert.NotNull(aaplProfile.GovernanceEpochDate); + Assert.NotNull(aaplProfile.Industry); + Assert.NotNull(aaplProfile.IndustryDisp); + Assert.NotNull(aaplProfile.IndustryKey); + Assert.NotNull(aaplProfile.LongBusinessSummary); + Assert.NotNull(aaplProfile.MaxAge); + Assert.NotNull(aaplProfile.State); + Assert.NotNull(aaplProfile.Zip); + Assert.NotNull(aaplProfile.Phone); + Assert.NotNull(aaplProfile.Website); + Assert.NotNull(aaplProfile.Sector); + Assert.NotNull(aaplProfile.SectorKey); + Assert.NotNull(aaplProfile.SectorDisp); + Assert.NotNull(aaplProfile.ShareHolderRightsRisk); + Assert.NotNull(aaplProfile.OverallRisk); + } +} \ No newline at end of file diff --git a/YahooFinanceApi/ProfileFields.cs b/YahooFinanceApi/ProfileFields.cs new file mode 100644 index 0000000..06df773 --- /dev/null +++ b/YahooFinanceApi/ProfileFields.cs @@ -0,0 +1,29 @@ +namespace YahooFinanceApi; + +public enum ProfileFields +{ + Address1, + City, + State, + Zip, + Country, + Phone, + Website, + Industry, + IndustryKey, + IndustryDisp, + Sector, + SectorKey, + SectorDisp, + LongBusinessSummary, + FullTimeEmployees, + CompanyOfficers, + AuditRisk, + BoardRisk, + CompensationRisk, + ShareHolderRightsRisk, + OverallRisk, + GovernanceEpochDate, + CompensationAsOfEpochDate, + MaxAge, +} \ No newline at end of file diff --git a/YahooFinanceApi/SecurityProfile.cs b/YahooFinanceApi/SecurityProfile.cs new file mode 100644 index 0000000..1574011 --- /dev/null +++ b/YahooFinanceApi/SecurityProfile.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace YahooFinanceApi; + +public class SecurityProfile +{ + public IReadOnlyDictionary Fields { get; private set; } + + // ctor + internal SecurityProfile(IReadOnlyDictionary fields) => Fields = fields; + + public dynamic this[string fieldName] => Fields[fieldName]; + public dynamic this[ProfileFields field] => Fields[field.ToString()]; + + public string Address1 => this[ProfileFields.Address1]; + public string City => this[ProfileFields.City]; + public string State => this[ProfileFields.State]; + public string Zip => this[ProfileFields.Zip]; + public string Country => this[ProfileFields.Country]; + public string Phone => this[ProfileFields.Phone]; + public string Website => this[ProfileFields.Website]; + public string Industry => this[ProfileFields.Industry]; + public string IndustryKey => this[ProfileFields.IndustryKey]; + public string IndustryDisp => this[ProfileFields.IndustryDisp]; + public string Sector => this[ProfileFields.Sector]; + public string SectorKey => this[ProfileFields.SectorKey]; + public string SectorDisp => this[ProfileFields.SectorDisp]; + public string LongBusinessSummary => this[ProfileFields.LongBusinessSummary]; + public long FullTimeEmployees => this[ProfileFields.FullTimeEmployees]; + public List CompanyOfficers => this[ProfileFields.CompanyOfficers]; + public long AuditRisk => this[ProfileFields.AuditRisk]; + public long BoardRisk => this[ProfileFields.BoardRisk]; + public long CompensationRisk => this[ProfileFields.CompensationRisk]; + public long ShareHolderRightsRisk => this[ProfileFields.ShareHolderRightsRisk]; + public long OverallRisk => this[ProfileFields.OverallRisk]; + public DateTime GovernanceEpochDate => DateTimeOffset.FromUnixTimeSeconds((long)this[ProfileFields.GovernanceEpochDate]).LocalDateTime; + public DateTime CompensationAsOfEpochDate => DateTimeOffset.FromUnixTimeSeconds((long)this[ProfileFields.CompensationAsOfEpochDate]).LocalDateTime; + public long MaxAge => this[ProfileFields.MaxAge]; +} \ No newline at end of file diff --git a/YahooFinanceApi/Yahoo - Profile.cs b/YahooFinanceApi/Yahoo - Profile.cs new file mode 100644 index 0000000..5684378 --- /dev/null +++ b/YahooFinanceApi/Yahoo - Profile.cs @@ -0,0 +1,67 @@ +using Flurl; +using Flurl.Http; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace YahooFinanceApi +{ + public sealed partial class Yahoo + { + + public static async Task QueryProfileAsync(string symbol, CancellationToken token = default) + { + await YahooSession.InitAsync(token); + + var url = $"https://query2.finance.yahoo.com/v10/finance/quoteSummary/{symbol}" + .SetQueryParam("modules", "assetProfile,convert_dates") + .SetQueryParam("crumb", YahooSession.Crumb); + + // Invalid symbols as part of a request are ignored by Yahoo. + // So the number of symbols returned may be less than requested. + // If there are no valid symbols, an exception is thrown by Flurl. + // This exception is caught (below) and an empty dictionary is returned. + // There seems to be no easy way to reliably identify changed symbols. + + dynamic data = null; + + try + { + data = await url + .WithCookie(YahooSession.Cookie.Name, YahooSession.Cookie.Value) + .WithHeader(YahooSession.UserAgentKey, YahooSession.UserAgentValue) + .GetAsync(token) + .ReceiveJson() + .ConfigureAwait(false); + } + catch (FlurlHttpException ex) + { + if (ex.Call.Response.StatusCode == (int)System.Net.HttpStatusCode.NotFound) + { + return null; + } + else + { + throw; + } + } + + var response = data.quoteSummary; + + var error = response.error; + if (error != null) + { + throw new InvalidDataException($"An error was returned by Yahoo: {error}"); + } + + var result = response.result[0].assetProfile; + var pascalDictionary = ((IDictionary) result).ToDictionary(x => x.Key.ToPascal(), x => x.Value); + + + return new SecurityProfile(pascalDictionary); + } + } +}