diff --git a/src/main/java/com/crazzyghost/alphavantage/AlphaVantage.java b/src/main/java/com/crazzyghost/alphavantage/AlphaVantage.java index ef35d98..ef75644 100644 --- a/src/main/java/com/crazzyghost/alphavantage/AlphaVantage.java +++ b/src/main/java/com/crazzyghost/alphavantage/AlphaVantage.java @@ -29,6 +29,7 @@ import com.crazzyghost.alphavantage.fundamentaldata.FundamentalData; import com.crazzyghost.alphavantage.indicator.Indicator; import com.crazzyghost.alphavantage.marketstatus.MarketStatus; +import com.crazzyghost.alphavantage.news.News; import com.crazzyghost.alphavantage.search.Search; import com.crazzyghost.alphavantage.sector.Sector; import com.crazzyghost.alphavantage.technicalindicator.TechnicalIndicator; @@ -173,5 +174,8 @@ public Search search() { return new Search(config); } - + //News symbols + public News News() { + return new News(config); + } } diff --git a/src/main/java/com/crazzyghost/alphavantage/news/News.java b/src/main/java/com/crazzyghost/alphavantage/news/News.java new file mode 100644 index 0000000..ed164e9 --- /dev/null +++ b/src/main/java/com/crazzyghost/alphavantage/news/News.java @@ -0,0 +1,123 @@ +package com.crazzyghost.alphavantage.news; + +import com.crazzyghost.alphavantage.AlphaVantageException; +import com.crazzyghost.alphavantage.Config; +import com.crazzyghost.alphavantage.Fetcher; +import com.crazzyghost.alphavantage.UrlExtractor; +import com.crazzyghost.alphavantage.news.request.NewsRequest; +import com.crazzyghost.alphavantage.news.response.NewsResponse; +import com.crazzyghost.alphavantage.parser.Parser; +import java.io.IOException; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.NotNull; + +public final class News implements Fetcher { + private final Config config; + private final NewsRequest.Builder builder; + private SuccessCallback successCallback; + private FailureCallback failureCallback; + + public News(Config config) { + this.config = config; + this.builder = new NewsRequest.Builder(); + } + + public News setTickers(String tickers) { + this.builder.tickers(tickers); + return this; + } + + public News setTopics(String topics) { + this.builder.topics(topics); + return this; + } + + public News setTimeFrom(String timeFrom) { + this.builder.timeFrom(timeFrom); + return this; + } + + public News setTimeTo(String timeTo) { + this.builder.timeTo(timeTo); + return this; + } + + public News setSort(String sort) { + this.builder.sort(sort); + return this; + } + + public News setLimit(int limit) { + this.builder.limit(limit); + return this; + } + + public News onSuccess(SuccessCallback callback) { + this.successCallback = callback; + return this; + } + + public News onFailure(FailureCallback callback) { + this.failureCallback = callback; + return this; + } + + public NewsResponse fetchSync() throws AlphaVantageException { + + Config.checkNotNullOrKeyEmpty(config); + + this.successCallback = null; + this.failureCallback = null; + OkHttpClient client = config.getOkHttpClient(); + + try (Response response = client.newCall(UrlExtractor.extract(builder.build(), config.getKey())) + .execute()) { + return NewsResponse.of( + Parser.parseJSON(response.body() != null ? response.body().string() : null)); + } catch (IOException e) { + throw new AlphaVantageException(e.getMessage()); + } + } + + @Override + public void fetch() { + + Config.checkNotNullOrKeyEmpty(config); + + config.getOkHttpClient().newCall(UrlExtractor.extract(builder.build(), config.getKey())) + .enqueue(new Callback() { + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + if (failureCallback != null) { + failureCallback.onFailure(new AlphaVantageException()); + } + } + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) + throws IOException { + if (response.isSuccessful()) { + try (ResponseBody body = response.body()) { + NewsResponse newsResponse = + NewsResponse.of(Parser.parseJSON(body != null ? body.string() : null)); + if (newsResponse.getErrorMessage() != null && failureCallback != null) { + failureCallback.onFailure( + new AlphaVantageException(newsResponse.getErrorMessage())); + } + if (successCallback != null) { + successCallback.onSuccess(newsResponse); + } + } + } else { + if (failureCallback != null) { + failureCallback.onFailure(new AlphaVantageException()); + } + } + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/crazzyghost/alphavantage/news/request/NewsRequest.java b/src/main/java/com/crazzyghost/alphavantage/news/request/NewsRequest.java new file mode 100644 index 0000000..8cf4317 --- /dev/null +++ b/src/main/java/com/crazzyghost/alphavantage/news/request/NewsRequest.java @@ -0,0 +1,78 @@ +package com.crazzyghost.alphavantage.news.request; + +import com.crazzyghost.alphavantage.parameters.Function; + +public class NewsRequest { + + protected Function function; + protected String tickers; // Optional: Filter by tickers + protected String topics; // Optional: Filter by topics + protected String timeFrom; // Optional: Filter by starting time + protected String timeTo; // Optional: Filter by ending time + protected String sort; // Optional: Sorting options + protected int limit; // Optional: Limit of results + + protected NewsRequest(Builder builder) { + this.function = builder.function; + this.tickers = builder.tickers; + this.topics = builder.topics; + this.timeFrom = builder.timeFrom; + this.timeTo = builder.timeTo; + this.sort = builder.sort; + this.limit = builder.limit; + } + + public static class Builder { + + public Function function; + protected String tickers; + protected String topics; + protected String timeFrom; + protected String timeTo; + protected String sort; + protected int limit = 50; // Default limit + + public Builder() { + this.function = Function.NEWS_SENTIMENT; + } + + public Builder function(Function function) { + this.function = function; + return this; + } + + public Builder tickers(String tickers) { + this.tickers = tickers; + return this; + } + + public Builder topics(String topics) { + this.topics = topics; + return this; + } + + public Builder timeFrom(String timeFrom) { + this.timeFrom = timeFrom; + return this; + } + + public Builder timeTo(String timeTo) { + this.timeTo = timeTo; + return this; + } + + public Builder sort(String sort) { + this.sort = sort; + return this; + } + + public Builder limit(int limit) { + this.limit = limit; + return this; + } + + public NewsRequest build() { + return new NewsRequest(this); // Use constructor to create instance + } + } +} \ No newline at end of file diff --git a/src/main/java/com/crazzyghost/alphavantage/news/response/NewsResponse.java b/src/main/java/com/crazzyghost/alphavantage/news/response/NewsResponse.java new file mode 100644 index 0000000..13a245d --- /dev/null +++ b/src/main/java/com/crazzyghost/alphavantage/news/response/NewsResponse.java @@ -0,0 +1,157 @@ +package com.crazzyghost.alphavantage.news.response; + +import com.crazzyghost.alphavantage.parser.Parser; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class NewsResponse { + private final String errorMessage; + private final List newsItems; + + private NewsResponse(List newsItems, String error) { + this.errorMessage = error; + this.newsItems = newsItems != null ? newsItems : new ArrayList<>(); + } + + public static NewsResponse of(Map stringObjectMap) { + Parser parser = new NewsParser(); + return parser.parse(stringObjectMap); + } + + public List getNewsItems() { + return newsItems; + } + + public String getErrorMessage() { + return errorMessage; + } + + @Override + public String toString() { + return newsItems.toString(); + } + + public static class NewsItem { + private final String title; + private final String url; + private final String timePublished; + private final List authors; + private final String summary; + private final String source; + private final double overallSentimentScore; + private final String overallSentimentLabel; + private final List tickerSentiment; + + public NewsItem(String title, String url, String timePublished, List authors, + String summary, + String source, double overallSentimentScore, String overallSentimentLabel, + List tickerSentiment) { + this.title = title; + this.url = url; + this.timePublished = timePublished; + this.authors = authors != null ? authors : new ArrayList<>(); + this.summary = summary; + this.source = source; + this.overallSentimentScore = overallSentimentScore; + this.overallSentimentLabel = overallSentimentLabel; + this.tickerSentiment = tickerSentiment != null ? tickerSentiment : new ArrayList<>(); + } + + @Override + public String toString() { + return title + " - " + summary + " (" + overallSentimentLabel + ")"; + } + + public String getTitle() { + return title; + } + + public String getSummary() { + return summary; + } + + public String getUrl() { + return url; + } + } + + public static class TickerSentiment { + private final String ticker; + private final double relevanceScore; + private final double sentimentScore; + private final String sentimentLabel; + + public TickerSentiment(String ticker, double relevanceScore, double sentimentScore, + String sentimentLabel) { + this.ticker = ticker; + this.relevanceScore = relevanceScore; + this.sentimentScore = sentimentScore; + this.sentimentLabel = sentimentLabel; + } + + @Override + public String toString() { + return ticker + ": " + sentimentLabel + " (" + sentimentScore + ")"; + } + } + + public static class NewsParser extends Parser { + + @SuppressWarnings("unchecked") + @Override + public NewsResponse parse(Map stringObjectMap) { + List newsItems = new ArrayList<>(); + String errorMessage = null; + + // Check if the response is empty + if (stringObjectMap.isEmpty()) { + return onParseError("Empty JSON returned by the API, no news items found."); + } + + try { + List> feed = (List>) stringObjectMap.get("feed"); + if (feed != null) { + for (Map item : feed) { + // Parse the ticker sentiment + List tickerSentiments = new ArrayList<>(); + List> tickerSentimentList = + (List>) item.get("ticker_sentiment"); + if (tickerSentimentList != null) { + for (Map tickerSentimentMap : tickerSentimentList) { + tickerSentiments.add(new TickerSentiment( + (String) tickerSentimentMap.get("ticker"), + Double.parseDouble(tickerSentimentMap.get("relevance_score").toString()), + Double.parseDouble(tickerSentimentMap.get("ticker_sentiment_score").toString()), + (String) tickerSentimentMap.get("ticker_sentiment_label") + )); + } + } + + // Create a NewsItem and add it to the list + newsItems.add(new NewsItem( + (String) item.get("title"), + (String) item.get("url"), + (String) item.get("time_published"), + (List) item.get("authors"), + (String) item.get("summary"), + (String) item.get("source"), + Double.parseDouble(item.get("overall_sentiment_score").toString()), + (String) item.get("overall_sentiment_label"), + tickerSentiments + )); + } + } + } catch (ClassCastException | NullPointerException e) { + errorMessage = "Error parsing news sentiment results."; + } + + return new NewsResponse(newsItems, errorMessage); + } + + @Override + public NewsResponse onParseError(String error) { + return new NewsResponse(null, error); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/crazzyghost/alphavantage/parameters/Function.java b/src/main/java/com/crazzyghost/alphavantage/parameters/Function.java index 7c455df..0a8ac5f 100644 --- a/src/main/java/com/crazzyghost/alphavantage/parameters/Function.java +++ b/src/main/java/com/crazzyghost/alphavantage/parameters/Function.java @@ -103,5 +103,5 @@ public enum Function { DURABLES, UNEMPLOYMENT, NONFARM_PAYROLL, - + NEWS_SENTIMENT, }