Skip to content
Permalink
Browse files

Merge pull request #10 from FlashBoys/GOFIN-2

Change history and events to use the new API.
  • Loading branch information...
ackleymi committed Jun 10, 2017
2 parents f50b5ef + 7e1ea13 commit 65cfa107c234f30dfb4827807c798cca12d66c6f
Showing with 354 additions and 101 deletions.
  1. +0 −1 .gitignore
  2. +17 −10 README.md
  3. +95 −0 cmd/health/main.go
  4. +8 −0 datetime.go
  5. +19 −0 datetime_test.go
  6. +44 −5 fetch.go
  7. +65 −10 fetch_test.go
  8. +4 −4 fields.go
  9. +20 −13 fixtures/events_fixture.csv
  10. +3 −0 fixtures/yahoo_appl.html
  11. +44 −44 history.go
  12. +15 −7 history_test.go
  13. +1 −1 pairs.go
  14. +2 −2 quotes.go
  15. +13 −0 quotes_test.go
  16. +1 −1 symbols.go
  17. +1 −1 utils.go
  18. +2 −2 utils_test.go
@@ -1,3 +1,2 @@
.DS_Store
cmd/
finance
@@ -104,7 +104,7 @@ func main() {
// Request daily history for TWTR.
// IntervalDaily OR IntervalWeekly OR IntervalMonthly are supported.
bars, err := finance.GetHistory("TWTR", start, end, finance.IntervalDaily)
bars, err := finance.GetHistory("TWTR", start, end, finance.Day)
if err == nil {
fmt.Println(bars)
}
@@ -125,12 +125,12 @@ import (
func main() {
// Set time range from Jan 2010 up to the current date.
// This example will return a slice of both dividends and splits.
start, _ := finance.ParseDatetime("1/1/2010")
// This example will return a slice of either dividends or splits.
start := finance.ParseDatetime("1/1/2010")
end := finance.NewDatetime(time.Now())
// Request event history for AAPL.
events, err := finance.GetEventHistory("AAPL", start, end)
events, err := finance.GetEventHistory("AAPL", start, end, finance.Dividends)
if err == nil {
fmt.Println(events)
}
@@ -209,24 +209,31 @@ func main() {

The primary technical tenants of this project are:

* Make financial data easy and fun to work with in Go-lang.
* Make financial data easy and fun to work with in Go.
* Abstract the burden of non-sexy model serialization away from the end-user.
* Provide a mature framework where the end-user needs only be concerned with analysis instead of data sourcing.

There are several applications for this library. It's intentions are to be conducive to the following activities:

* Quantitative financial analysis in Golang.
* Quantitative financial analysis in Go.
* Academic study/comparison in a clean, easy language.
* Algorithmic/Statistical-based strategy implementation.

## To-do
## API Changes

Yahoo decided to deprecate the ichart API for historical data. A few things to note:

* Dividends and Splits got separated into their own calls, use `finance.Dividends` or `finance.Splits`.
* A cookie and a crumb are now needed in the new historical API. This requires 2 calls, slowing down the response time/quality.
* Continuation of the historical data funcs were made possible by the solution proposed by pandas contributors [here](https://github.com/pydata/pandas-datareader/pull/331), so thanks for the help!
* That PR is also reporting a degradation of data quality in the responses, so watch out for that.

You can use the new health checking command to determine if all the endpoints are responding appropriately. Run `go run main.go` in the `cmd/health` directory and report any failures!

- [ ] Add greeks calculations to options data
- [ ] Key stats (full profile) for securities

## Contributing

If you find this repo helpful, please give it a star! If you wish to discuss changes to it, please open an issue. This project is not as mature as it could be, and financial projects in Golang are in drastic need of some basic helpful dependencies.
If you find this repo helpful, please give it a star! If you wish to discuss changes to it, please open an issue. This project is not as mature as it could be, and financial projects in Go are in drastic need of some basic helpful dependencies.

## Similar Projects

@@ -0,0 +1,95 @@
package main

import (
"fmt"
"time"

"github.com/FlashBoys/go-finance"
)

// Checks the health of various endpoints through go-finance funcs.
func main() {

/*
Check snapshot quote.
*/
q, err := finance.GetQuote("TWTR")
if err != nil {
fmt.Print("Problem fetching quote snapshot:\n", err)
} else {
fmt.Print("Success fetching quote snapshot:\n", q)
}

/*
Check multiple snapshot quotes.
*/
symbols := []string{"AAPL", "TWTR", "FB"}
quotes, err := finance.GetQuotes(symbols)
if err != nil {
fmt.Print("\nProblem fetching snapshot quotes:\n", err)
} else {
fmt.Print("\nSuccess fetching snapshot quotes:\n", quotes)
}

/*
Check history.
*/
start := finance.ParseDatetime("1/1/2017")
end := finance.ParseDatetime("2/1/2017")
bars, err := finance.GetHistory("TWTR", start, end, finance.Day)
if err != nil {
fmt.Print("\nProblem fetching quote history:\n", err)
} else {
fmt.Print("\nSuccess fetching quote history:\n", bars)
}

/*
Check currency pair.
*/
pairquote, err := finance.GetCurrencyPairQuote(finance.USDJPY)
if err != nil {
fmt.Print("\nProblem fetching currency pair:\n", err)
} else {
fmt.Print("\nSuccess fetching currency pair:\n", pairquote)
}

/*
Check events history.
*/
start = finance.ParseDatetime("1/1/2010")
end = finance.NewDatetime(time.Now())
events, err := finance.GetEventHistory("AAPL", start, end, finance.Splits)
if err != nil {
fmt.Print("\nProblem fetching event history:\n", err)
} else {
fmt.Print("\nSuccess fetching event history:\n", events)
}

/*
Check symbols list.
*/
symbols, err = finance.GetUSEquitySymbols()
if err != nil {
fmt.Print("\nProblem fetching symbol list:\n", err)
} else {
fmt.Print("\nSuccess fetching symbol list:\n", symbols[:10])
}

/*
Check options.
*/
c, err := finance.NewCycle("AAPL")
if err != nil {
fmt.Print("\nProblem fetching option cycle:\n", err)
} else {
fmt.Print("\nSuccess fetching option cycle:\n", c)
}

calls, puts, err := c.GetFrontMonth()
if err != nil {
fmt.Print("\nProblem fetching option chain:\n", err)
} else {
fmt.Print("\nSuccess fetching option chain:\n", calls[:2], "\n", puts[:2])
}

}
@@ -1,6 +1,7 @@
package finance

import (
"fmt"
"log"
"time"
)
@@ -13,6 +14,7 @@ type Datetime struct {
Hour int `json:",omitempty"`
Minute int `json:",omitempty"`
Second int `json:",omitempty"`
t time.Time
}

// ParseDatetime creates a new instance of Datetime from a string.
@@ -41,6 +43,7 @@ func NewDatetime(t time.Time) Datetime {
Hour: hour,
Minute: min,
Second: sec,
t: t,
}
}

@@ -50,5 +53,10 @@ func NewDatetime(t time.Time) Datetime {
Month: int(month),
Day: day,
Year: year,
t: t,
}
}

func (d Datetime) unixTime() string {
return fmt.Sprintf("%v", d.t.Unix())
}
@@ -0,0 +1,19 @@
package finance

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_UnixTime(t *testing.T) {

// Given have have a time,
start := ParseDatetime("1/1/2017")

// When we convert it to a unix timestamp,
timestamp := start.unixTime()

// Then it should equal a string of the number of secs since Jan 1, 1970-
assert.Equal(t, "1483228800", timestamp)
}
@@ -3,6 +3,7 @@ package finance
import (
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
@@ -20,13 +21,13 @@ var (
// OptionsURL option chains
OptionsURL = "http://www.google.com/finance/option_chain?"
// HistoryURL quote history
HistoryURL = "http://ichart.finance.yahoo.com/table.csv"
// EventURL event history
EventURL = "http://ichart.finance.yahoo.com/x"
HistoryURL = "https://query1.finance.yahoo.com/v7/finance/download/"
// SymbolsURL symbols list
SymbolsURL = "http://www.batstrading.com/market_data/symbol_listing/csv/"
// QuoteURL stock quotes
QuoteURL = "http://download.finance.yahoo.com/d/quotes.csv"
// sessionURL cookie parsing
sessionURL = "https://finance.yahoo.com/quote/AAPL/history"
)

type optionsResponse struct {
@@ -38,9 +39,17 @@ type optionsResponse struct {
Puts []map[string]string `json:"puts,omitempty"`
}

func fetchCSV(url string) (table [][]string, err error) {
func fetchCSV(url string, cookie *http.Cookie) (table [][]string, err error) {

resp, err := http.Get(url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return
}
if cookie != nil {
req.AddCookie(cookie)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return
}
@@ -66,6 +75,36 @@ func buildURL(base string, params map[string]string) string {
return url.String()
}

// getsession retrieves a session cookie and crumb to validate the yhoo request.
func getsession() (*http.Cookie, string, error) {

req, err := http.NewRequest("GET", sessionURL, nil)
if err != nil {
return nil, "", err
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()

// Get cookies.
cookies := resp.Cookies()
if len(cookies) <= 0 {
return nil, "", errors.New("cookie unavailable")
}

// Get crumb.
b, err := ioutil.ReadAll(resp.Body)
match := rp.FindStringSubmatch(string(b))
if len(match) <= 1 {
return nil, "", errors.New("crumb unavailable")
}

return cookies[0], match[1], nil
}

// buildURL takes a base URL and parameters returns the full URL.
func buildOptionsURL(base string, symbol string, d Datetime) string {
return buildURL(base, map[string]string{
@@ -1,6 +1,7 @@
package finance

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
@@ -13,7 +14,7 @@ func Test_FetchCSV(t *testing.T) {
defer ts.Close()

// When we request the csv,
table, err := fetchCSV(ts.URL)
table, err := fetchCSV(ts.URL, &http.Cookie{})
assert.Nil(t, err)

// Then the returned table should have 1 row.
@@ -39,16 +40,70 @@ func Test_BuildURL(t *testing.T) {
assert.Equal(t, "http://example.com/d/quotes.csv?s=AAPL", url)
}

func Test_Fetch(t *testing.T) {
func Test_GetSession(t *testing.T) {

// Given we need a valid yhoo session,
cs := startCookieServer("yahoo_appl.html", true)
sessionURL = cs.URL

// When we make a request,
cookie, crumb, err := getsession()

// We should get valid params back.
assert.Nil(t, err)
assert.NotNil(t, cookie)
assert.Equal(t, "j\\u002FlphNGEHaA", crumb)
cs.Close()

// Test bad crumb handling-
cs = startCookieServer("", true)
sessionURL = cs.URL
cookie, crumb, err = getsession()
assert.Nil(t, cookie)
assert.Equal(t, "crumb unavailable", err.Error())
cs.Close()

// Test bad cookie handling-
cs = startCookieServer("yahoo_appl.html", false)
sessionURL = cs.URL
defer cs.Close()

cookie, crumb, err = getsession()
assert.Equal(t, "cookie unavailable", err.Error())

}

func Test_BuildOptionsURL(t *testing.T) {

// Given we have a set of params and a base url,
baseURL := "http://example.org"
sym := "TWTR"
dt := Datetime{Month: 5, Day: 30, Year: 2017}

// When we construct the options url,
optionsURL := buildOptionsURL(baseURL, sym, dt)

// It should equal-
assert.Equal(t, "http://example.org?expd=30&expm=5&expy=2017&output=json&q=TWTR", optionsURL)
}

func Test_FetchOptions(t *testing.T) {
// Given that we want to download options data
// ts := startTestServer("request_fixture.txt")
// defer ts.Close()
//
// // When we request the malformed json text,
// response, err := fetchOptions(ts.URL)
// assert.Nil(t, err)
ts := startTestServer("options_fixture.txt")

// Then the returned string should be-
//assert.Equal(t, "{\"test\":{\"foo\":bar}}\n\n", string(response))
// When we request the malformed json text,
or, err := fetchOptions(ts.URL)
assert.Nil(t, err)

// Then the returned response should be-
assert.Equal(t, "110.43", or.Price)
ts.Close()

// Given that we want to download options data
ts = startTestServer("request_fixture.txt")
defer ts.Close()

// When we request the malformed json text,
or, err = fetchOptions(ts.URL)
assert.NotNil(t, err)
}

0 comments on commit 65cfa10

Please sign in to comment.
You can’t perform that action at this time.