com.scottjjohnson.finance.analysis.YahooFinanceClient.java Source code

Java tutorial

Introduction

Here is the source code for com.scottjjohnson.finance.analysis.YahooFinanceClient.java

Source

/*
 * 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;
    }
}