diff --git a/Polygon.Net.Tests/FunctionalTests/NewsTests.cs b/Polygon.Net.Tests/FunctionalTests/NewsTests.cs new file mode 100644 index 0000000..b53df0b --- /dev/null +++ b/Polygon.Net.Tests/FunctionalTests/NewsTests.cs @@ -0,0 +1,86 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Polygon.Net.Models; +using static Polygon.Net.Tests.TestManager; + +namespace Polygon.Net.Tests.FunctionalTests +{ + [TestClass] + public class NewsApiTests + { + private const string STATUS_OK = "OK"; + private static DateTime START_TIME = new DateTime(2023, 03, 05); // Start Specific Day + private static DateTime END_TIME = new DateTime(2023, 03, 06); // End Specific Day + + [TestMethod] + public async Task GetNewsSucceedsAsync() + { + var newsResponse = await PolygonTestClient.GetNewsAsync(); + + Assert.IsInstanceOfType(newsResponse.Results, typeof(List)); + Assert.IsNotNull(newsResponse); + Assert.AreEqual(STATUS_OK, newsResponse.Status); + } + + [TestMethod] + public async Task GetNewsWithParametersSucceedsAsync() + { + int countNews = 5; + String ticker = "GOOGL"; + DateTime dateTime = START_TIME; + + var newsResponse = await PolygonTestClient.GetNewsAsync(START_TIME, END_TIME, ticker, "asc", "published_utc", limit: countNews); + + Assert.IsInstanceOfType(newsResponse.Results, typeof(List)); + Assert.IsNotNull(newsResponse); + Assert.AreEqual(STATUS_OK, newsResponse.Status); + Assert.AreEqual(newsResponse.Count, countNews); + + foreach (var news in newsResponse.Results) + { + DateTime publishedNews = DateTime.Parse(news.PublishedUtc); + + Assert.AreEqual(START_TIME.ToString("yyyy-MM-dd"), publishedNews.ToString("yyyy-MM-dd")); + Assert.IsTrue(news.Tickers.Contains(ticker), "The ticker was not found inside tickers"); + Assert.IsTrue(publishedNews > dateTime, "Date current news is not greater than before date news"); + + dateTime = publishedNews; + } + } + + [TestMethod] + public async Task GetNewsEmptyAsync() + { + var newsResponse = await PolygonTestClient.GetNewsAsync(ticker: "ABCXYZ"); + + Assert.IsTrue(newsResponse.Count == 0, "the news count should be empty"); + Assert.AreEqual(STATUS_OK, newsResponse.Status); + } + + [TestMethod] + public async Task GetTodayNewsSucceedsAsync() + { + DateTime currentDate = DateTime.Now.Date; + var newsResponse = await PolygonTestClient.GetNewsAsync(currentDate); + + Assert.IsInstanceOfType(newsResponse.Results, typeof(List)); + Assert.IsNotNull(newsResponse); + Assert.AreEqual(STATUS_OK, newsResponse.Status); + + foreach (var news in newsResponse.Results) + { + Assert.AreEqual(currentDate.ToString("yyyy-MM-dd"), DateTime.Parse(news.PublishedUtc).ToString("yyyy-MM-dd")); + } + } + + [TestMethod] + public async Task GetNextPageNewsAsync() + { + var newsResponse = await PolygonTestClient.GetNewsAsync(START_TIME, END_TIME); + var nextPage = await PolygonTestClient.GetNewsAsync(nextPage: newsResponse.HashNextUrl); + + Assert.IsInstanceOfType(nextPage.Results, typeof(List)); + Assert.IsNotNull(nextPage); + Assert.AreEqual(STATUS_OK, nextPage.Status); + } + } +} diff --git a/Polygon.Net.Tests/Polygon.Net.Tests.csproj b/Polygon.Net.Tests/Polygon.Net.Tests.csproj index df0a273..51d3a0d 100644 --- a/Polygon.Net.Tests/Polygon.Net.Tests.csproj +++ b/Polygon.Net.Tests/Polygon.Net.Tests.csproj @@ -2,8 +2,10 @@ net7.0 - false + enable + enable + enable diff --git a/Polygon.Net/API/NewsApi.cs b/Polygon.Net/API/NewsApi.cs new file mode 100644 index 0000000..6415d14 --- /dev/null +++ b/Polygon.Net/API/NewsApi.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http.Extensions; +using Newtonsoft.Json; +using Polygon.Net.Http; +using Polygon.Net.Models; + +namespace Polygon.Net; + +public partial class PolygonClient +{ + private const string NEWS_ENDPOINT = "/v2/reference/news"; + + /// + /// Get the Polygon news. + /// + /// Return results published after this date + /// Return results published before this date + /// The polygon API ticker by default is desc + /// Order results based on the sort field + /// Limit the number of results returned, default is 10 and max is 1000 + /// Sort field used for ordering + /// next page + /// NewsResponse + /// + public async Task GetNewsAsync(DateTime? startTime = null, DateTime? endTime = null, string? ticker = null, string? order = null, string? sort = null, int limit = 0, string? nextPage = null) + { + var qb = new QueryBuilder(); + qb.AddIf(nextPage != null, "cursor", nextPage); + qb.AddIf(ticker != null, nameof(ticker), ticker); + qb.AddIf(order != null, nameof(order), order); + qb.AddIf(limit != 0, nameof(limit), limit + ""); + qb.AddIf(sort != null, nameof(sort), sort); + qb.AddIf(startTime != null, "published_utc.gte", startTime?.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss")); + qb.AddIf(endTime != null, "published_utc.lte", endTime?.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss")); + + string requestUrl = $"{_polygonSettings.ApiBaseUrl}{NEWS_ENDPOINT}{qb.ToString()}"; + string contentStr = await Get(requestUrl).ConfigureAwait(false); + + return String.IsNullOrEmpty(contentStr) ? new NewsResponse() : JsonConvert.DeserializeObject(contentStr); + } +} diff --git a/Polygon.Net/Client/IPolygonClient.cs b/Polygon.Net/Client/IPolygonClient.cs index 06c2e54..677afce 100644 --- a/Polygon.Net/Client/IPolygonClient.cs +++ b/Polygon.Net/Client/IPolygonClient.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Polygon.Net.Models; +using Polygon.Net.Models; namespace Polygon.Net { @@ -55,6 +53,16 @@ Task GetAggregatesBarsAsync( Task> GetMarketHolidaysAsync(); - Task> GetTickerTypesAsync(string assetClass = default, string locale = default); + Task> GetTickerTypesAsync(string? assetClass = default, string? locale = default); + + Task GetNewsAsync( + DateTime? startTime = null, + DateTime? endTime = null, + string? ticker = null, + string? order = null, + string? sort = null, + int limit = 0, + string? nextPage = null + ); } } diff --git a/Polygon.Net/Client/PolygonClient.cs b/Polygon.Net/Client/PolygonClient.cs index 91edde2..2936871 100644 --- a/Polygon.Net/Client/PolygonClient.cs +++ b/Polygon.Net/Client/PolygonClient.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; +using System.Text; using AutoMapper; using static Pineapple.Common.Preconditions; @@ -26,7 +22,7 @@ private async Task Get(string requestUrl) { var client = _dependencies.HttpClientFactory.CreateClient(_polygonSettings.HttpClientName); - requestUrl = $"{ requestUrl }{ (requestUrl.Contains("?") ? "&" : "?") }apikey={ _polygonSettings.ApiKey }"; + requestUrl = $"{requestUrl}{(requestUrl.Contains("?") ? "&" : "?")}apikey={_polygonSettings.ApiKey}"; var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); @@ -48,7 +44,7 @@ private string GetQueryParameterString(Dictionary queryParams) { if (qp.Value != null) { - sb.Append($"&{ qp.Key }={ qp.Value }"); + sb.Append($"&{qp.Key}={qp.Value}"); } } diff --git a/Polygon.Net/Http/QueryStringExtension.cs b/Polygon.Net/Http/QueryStringExtension.cs new file mode 100644 index 0000000..12d4d9d --- /dev/null +++ b/Polygon.Net/Http/QueryStringExtension.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Http.Extensions; + +namespace Polygon.Net.Http; + +internal static class QueryStringExtension +{ + public static void AddIf(this QueryBuilder query, bool conditional, string? name, string? value) + { + if (conditional) + { + query.Add(name, value); + } + } +} diff --git a/Polygon.Net/Models/NewsInfo.cs b/Polygon.Net/Models/NewsInfo.cs new file mode 100644 index 0000000..45dffe2 --- /dev/null +++ b/Polygon.Net/Models/NewsInfo.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace Polygon.Net.Models; + +public class NewsInfo +{ + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("author")] + public string Author { get; set; } + + [JsonProperty("published_utc")] + public string PublishedUtc { get; set; } + + [JsonProperty("article_url")] + public string ArticleUrl { get; set; } + + [JsonProperty("keywords")] + public List Keywords { get; set; } = new List() { }; + + [JsonProperty("tickers")] + public List Tickers { get; set; } = new List() { }; +} diff --git a/Polygon.Net/Models/NewsResponse.cs b/Polygon.Net/Models/NewsResponse.cs new file mode 100644 index 0000000..7943541 --- /dev/null +++ b/Polygon.Net/Models/NewsResponse.cs @@ -0,0 +1,39 @@ +using System.Web; +using Newtonsoft.Json; + +namespace Polygon.Net.Models; + +public class NewsResponse +{ + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("request_id")] + public string RequestId { get; set; } + + [JsonProperty("count")] + public int Count { get; set; } = 0; + + private string? _hashNextUrl; + + [JsonProperty("next_url")] + public string? HashNextUrl + { + get { return _hashNextUrl; } + set + { + string? hash = null; + if (value != null) + { + Uri uri = new Uri(value); + var query = HttpUtility.ParseQueryString(uri.Query); + hash = query.Get("cursor"); + } + + _hashNextUrl = hash != null ? hash : value; + } + } + + [JsonProperty("results")] + public List Results { get; set; } = new List() { }; +} diff --git a/Polygon.Net/Polygon.Net.csproj b/Polygon.Net/Polygon.Net.csproj index a6a8e5c..29c862f 100644 --- a/Polygon.Net/Polygon.Net.csproj +++ b/Polygon.Net/Polygon.Net.csproj @@ -2,9 +2,9 @@ net7.0 - 1.2.0 - 1.2.0.0 - 1.2.0.0 + 1.2.2 + 1.2.2.0 + 1.2.2.0 MILL5 A .NET class library for use against the Polygon APIs supporting .NET Standard, .NET Core, .NET 5.0, and the new .NET 6.0 Copyright © MILL5, LLC 2021 @@ -19,11 +19,16 @@ snupkg MIT polygon.png + e0e1f4de-446a-4f5c-879a-30ff77b4e606 + enable + enable + +