Java tutorial
/* * Copyright 2014 Scott J. Johnson (http://scottjjohnson.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.scottjjohnson.finance.analysis; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.scottjjohnson.finance.analysis.beans.DailyQuoteBean; import com.scottjjohnson.finance.analysis.beans.QueryBean; import com.scottjjohnson.finance.analysis.beans.QuoteHistoryBean; import com.scottjjohnson.finance.analysis.beans.ResultsBean; import com.scottjjohnson.util.DateUtils; /** * Methods for calling the Yahoo Finance YQL API. * * Example YQL URL: * * https://query.yahooapis.com/v1/public/yql?q=select+*+from+yahoo.finance.historicaldata+where+symbol+%3D+%22ACT%22+and * +startDate+%3D+%222013-10-01%22+and+endDate+%3D+%222014-07-25%22&format=json&env=store%3A%2F%2Fdatatables.org%2F * alltableswithkeys * * Example YQL JSON response: * * <PRE> * { * "query":{ * "count":201, * "created":"2014-07-30T03:07:26Z", * "lang":"en-US", * "results":{ * "quote":[ * { * "Symbol":"ACT", * "Date":"2014-07-25", * "Open":"218.15", * "High":"219.59", * "Low":"216.90", * "Close":"217.45", * "Volume":"1361900", * "Adj_Close":"217.45" * }, * { * "Symbol":"ACT", * "Date":"2014-07-24", * "Open":"219.41", * "High":"219.75", * "Low":"217.65", * "Close":"218.68", * "Volume":"1753500", * "Adj_Close":"218.68" * }, * . * . * . * { * "Symbol":"ACT", * "Date":"2013-10-08", * "Open":"143.71", * "High":"143.71", * "Low":"138.84", * "Close":"139.80", * "Volume":"2418200", * "Adj_Close":"139.80" * } * ] * } * } * } * </PRE> */ public class YahooFinanceClient { private static final Logger LOGGER = LoggerFactory.getLogger(YahooFinanceClient.class); private static final String BASE_URL = "http://query.yahooapis.com/v1/public/yql?"; private static final String HISTORICAL_DATA_QUERY_PATTERN = "select * from yahoo.finance.historicaldata where symbol = \"%s\" and startDate = \"%s\" and endDate = \"%s\""; private static final String RESPONSE_FORMAT = "json"; private static final String DATA_SOURCE_ENV = "store://datatables.org/alltableswithkeys"; private static final int MAX_RETRIES = 5; private static final long MILLISECONDS_TO_SLEEP_AFTER_YQL_FAILURE = 10000; // 10sec private HttpClient httpClient = null; /** * Constructor */ public YahooFinanceClient() { httpClient = new HttpClient(); } /** * Queries Yahoo Finance for historical stock quote data. * * @param symbol * stock ticker symbol to be queried * @param dateRangeInDays * number of days of history to be retrieved * @return list of quote beans, empty list if the ticker is not found * @throws SourceDataException * if the YQL query fails */ public List<DailyQuoteBean> getHistoricalDailyQuotes(String symbol, int dateRangeInDays) throws SourceDataException, InvalidSymbolException { List<DailyQuoteBean> quotes = null; String responseBody = null; int attempt = 0; String url = buildHistoricalDataURL(symbol, dateRangeInDays); // loop until successful or we exceed the maximum number of retries while (quotes == null && attempt < MAX_RETRIES) { attempt++; try { responseBody = httpClient.sendGetRequest(url); quotes = parseJson(responseBody); } catch (Exception e) { LOGGER.error("Encountered exception getting quotes.", e); } finally { if (quotes == null) { LOGGER.warn("Failed to get quotes for ticker {}, attempt #{}, response body = {}.", symbol, attempt, responseBody); responseBody = null; // delay the next attempt for a few milliseconds if (attempt < MAX_RETRIES) { try { Thread.sleep(MILLISECONDS_TO_SLEEP_AFTER_YQL_FAILURE); } catch (InterruptedException e) { // It's bad practice to swallow the interrupt so apply it to the current thread. Thread.currentThread().interrupt(); } } } else if (quotes.size() == 0) { // no retry for 'no data found'. Just log it. LOGGER.warn("No data found for symbol {}, attempt #{}, response body = {}.", symbol, attempt, responseBody); } } } if (quotes == null) { throw new SourceDataException("Failed to get quotes for symbol '" + symbol + "'"); } else if (quotes.size() == 0) throw new InvalidSymbolException("No data found for symbol '" + symbol + "'"); return quotes; } /** * Builds a url for extracting historical quotes data from Yahoo Finance. * * The date range ends on the Friday prior to the current date and begins DateRangeInDays days earlier. * * @param ticker * stock ticker symbol to be queried * @param dateRangeInDays * number of calendar days of history to be retrieved * @return url containing a YQL query string */ private String buildHistoricalDataURL(String ticker, int dateRangeInDays) { Calendar cal = DateUtils.getStockExchangeCalendar(); // today // ************* HOKEY WORKAROUND FOR YAHOO ISSUE -- Ask for extra day into future to bust cache cal.add(Calendar.DAY_OF_WEEK, 1); dateRangeInDays++; // ************* Date endDate = cal.getTime(); cal.add(Calendar.DAY_OF_WEEK, -dateRangeInDays); Date startDate = cal.getTime(); // SimpleDateFormat is not thread-safe so creating one as needed. SimpleDateFormat yqlDateFormat = new SimpleDateFormat("yyyy-MM-dd"); String query = String.format(HISTORICAL_DATA_QUERY_PATTERN, ticker, yqlDateFormat.format(startDate), yqlDateFormat.format(endDate)); List<NameValuePair> queryParams = new ArrayList<NameValuePair>(); queryParams.add(new BasicNameValuePair("q", query)); queryParams.add(new BasicNameValuePair("format", RESPONSE_FORMAT)); queryParams.add(new BasicNameValuePair("env", DATA_SOURCE_ENV)); StringBuilder sb = new StringBuilder(BASE_URL); sb.append(URLEncodedUtils.format(queryParams, "UTF-8")); LOGGER.debug("YQL query = {}.", sb.toString()); return sb.toString(); } /** * Extracts quote data from the YQL JSON response. * * @param json * YQL response body * @return list of quotes */ protected List<DailyQuoteBean> parseJson(String json) { List<DailyQuoteBean> quote = null; QuoteHistoryBean quoteHistory = new Gson().fromJson(json, QuoteHistoryBean.class); // unwrap the beans. We need only the list of quotes. if (quoteHistory != null) { QueryBean query = quoteHistory.getQuery(); if (query != null) { if (query.getCount() > 0) { ResultsBean results = query.getResults(); if (results != null) { quote = results.getQuotes(); if (quote != null) { if (quote.size() > 0) { LOGGER.trace("Quote size = {}, list = {}.", quote.size(), quote); // verify the first bean has a date. Sometimes Yahoo // is returning incorrect column names resulting in // GSON being unable to populate the beans. DailyQuoteBean firstQuote = quote.get(0); if (firstQuote == null || firstQuote.getDate() == null || firstQuote.getVolume() == 0) { quote = null; // in the event of a problem, destroy the list } } } } } else { quote = new ArrayList<DailyQuoteBean>(); // if Yahoo returned no quotes, return an empty list. } } } if (quote == null) { LOGGER.warn("Unable to extract quotes from JSON."); } return quote; } }