Fun with Yahoo Finance API
Google Finance API No More!
As of March 2018, something happened to Google Finance - it got taken to the chopping board and is now a miserable husk of its former self! Long gone are the days where one could simply hook into the API and download a fat, juicy csv-file of historical stock price data… or a sensible JSON of option prices.
Thankfully, there are many alternatives out there.
YahooFinanceAPI
Since I’ve been accessing the API through Java, I’m taking a look at this unofficial library.
Getting Historical Data
You can construct a Stock
object such that it contains 5 years historical data.
Then use the getHistory()
method to return a collection of HistoricalQuote
elements.
Calendar from = Calendar.getInstance();
Calendar to = Calendar.getInstance();
from.add(Calendar.YEAR, -5);
Stock google = YahooFinance.get("GOOG", from, to, Interval.DAILY);
List<HistoricalQuote> historyGoogle = google.getHistory();
HistoricalQuote
To look at, say, the closing price on each day, we’ll have to traverse the List
and invoke the method getClose()
.
for(HistoricalQuote historicalQuote : historyGoogle)
System.out.println(historicalQuote.getClose());
Looking at the documentation, we can see that this method (and others like it) returns a BigDecimal
.
Stream()
If you want, you can stream()
instead of iterating through the collection.
BigDecimal totalClose = historyGoogle
.stream()
.map(HistoricalQuote::getClose)
.reduce( BigDecimal.ZERO // identity
, BigDecimal::add); // accumulator
System.out.printf("Total close: %s\n", totalClose);
All we are doing here is totalling every closing price.
In map
, we map from the stream to create a tuple.
In this case, we use the getClose()
method to emit a singleton of BigDecimal
.
In reduce
we aggregate.
And in this instance, we’re adding.
Our reduce
operation takes two arguments:
- Identity
- Accumulator
The former is both the initial value of the sum and the default value in case the mapping returns a null-value at any point in the stream. That is, zero.
The latter is the part that adds the BigDecimal
element to the running total of closing prices.
Average Closing Price
To calculate any average, we need both a total and a count.
We will again use stream()
and return both these values.
BigDecimal[] totalWithCount = historyGoogle.stream()
.map(HistoricalQuote -> new BigDecimal[]{HistoricalQuote.getClose(), BigDecimal.ONE})
.reduce( new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO}, // identity
(a,b) -> new BigDecimal[]{a[0].add(b[0]), a[1].add(b[1])}); // accumulator
BigDecimal mean = totalWithCount[0].divide(totalWithCount[1], RoundingMode.HALF_UP);
System.out.println(mean);
Stream.map
Now in the map
operation we have two values:
- a closing price
- unity
When we emit from map
to reduce
, they are simply added to a running total and running count.
Stream.reduce
As before, reduce
takes both identity and accumulator arguments.
But now, map
is sending a pair of values.
So in both reduce
arguments, we construct a BigDecimal[]
and give it two elements.
As before, we use zero in identity.
But now, the accumulator now has two parameters: (a,b)
.
Where a
is a two-element array which contains the running total and count in the first and second elements respectively.
Likewise, b
is a two element array, but it contains the next two elements to add to a
.
Equal Weighted Variance
How would we go about computing a simple variance from the stream?
Suppose our variance is defined as:
\[\sigma^2 = \frac{1}{n-1} \sum_{i=1}^n (x_i-\mu)^2\]Our method is defined as
public BigDecimal varianceEqualWeighted(List<HistoricalQuote> history, BigDecimal mean)
This requires that we have computed our mean in a prior step.
BigDecimal totalProduct = IntStream
.range(0, history.size())
This time our stream is an instantiation of an IntStream
, which we use to address the index of history
.
.mapToObj(i -> history.get(i).getClose().subtract(mean))
.map(bd -> bd.multiply(bd))
Now we map the difference between the mean and the closing price.
Then we emit a tuple containing the square of the previous map.
.reduce(BigDecimal.ZERO, BigDecimal::add);
Next, we aggregate the square. BigDecimal.ZERO
is our zero element, necessary for computing the partial total at the first element.
return totalProduct.divide(new BigDecimal(history.size() - 1), RoundingMode.HALF_UP);
When all is said and done, we return BigDecimal totalProduct
and divide it by the number of elements in the stream. We subtract 1 from this divisor to reduce error caused by bias.
Yahoo!
And so we have it - a quick introduction to YahooFinanceAPI and a quick spin with Java 8 streams! Yeah! MapReduce!
References
- YahooFinanceAPI
- Oracle Java Documentation: Reduction
- What Happened to Google Finance?
- YahooFinanceAPI Documentation
- How to average BigDecimals Streams: WillShackleford