-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathyahoofin.go
185 lines (149 loc) · 4.44 KB
/
yahoofin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package yahoofin
import (
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/http/cookiejar"
"regexp"
"strconv"
"time"
"github.com/gocarina/gocsv"
)
// NewClient creates a new Yahoo Finance client
func NewClient() (*Client, error) {
var seedTickers = []string{"AAPL", "GOOG", "MSFT"}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
httpClient := &http.Client{Jar: jar}
c := Client{httpClient: httpClient}
i := rand.Intn(len(seedTickers))
ticker := seedTickers[i]
crumb, err := getCrumb(c.httpClient, ticker)
if err != nil {
return nil, err
}
c.crumb = crumb
return &c, nil
}
// Client is a struct that represents a Yahoo Finance client
type Client struct {
// crumb is sent along with each request and is needed to make successful requests directly to the historical prices endpoint
crumb string
// httpClient is a persistent client used to store cookies after the initial request is sent
httpClient *http.Client
}
// Price represents a single datapoint returned by the yahoo api
type Price struct {
Date DateTime `csv:"Date"`
Open float64 `csv:"Open"`
High float64 `csv:"High"`
Low float64 `csv:"Low"`
Close float64 `csv:"Close"`
AdjClose float64 `csv:"Adj Close"`
Volume float64 `csv:"Volume"`
}
// Field represents the type of data that is being requested from the yahoo api
type Field string
const (
// History will return historical prices
History Field = "history"
// Dividend will return dividend payment history
Dividend Field = "dividend"
// Split will return stock split data
Split Field = "split"
)
func (c *Client) makeRequest(ticker string, startDate, endDate time.Time, field Field) (*http.Response, error) {
urlFmtStr := "https://query1.finance.yahoo.com/v7/finance/download/%s?period1=%d&period2=%d&interval=1d&events=%s&crumb=%s"
url := fmt.Sprintf(urlFmtStr, ticker, startDate.Unix(), endDate.Unix(), field, c.crumb)
return c.httpClient.Get(url)
}
func validateResponse(resp *http.Response) error {
if resp.StatusCode >= 300 {
se := ServerErrorRoot{}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if err := json.Unmarshal(body, &se); err != nil {
// Sometimes the server returns raw text instead of json...
return fmt.Errorf(string(body))
}
return fmt.Errorf("%v: %v", se.Chart.Error.Code, se.Chart.Error.Description)
}
return nil
}
// GetSecurityDataString returns the raw response data from the yahoo endpoint.
// This string will be CSV formatted if the request succeeds.
// In the event of a failed request, this string will be JSON-formatted
func (c *Client) GetSecurityDataString(ticker string, startDate, endDate time.Time, field Field) (string, error) {
resp, err := c.makeRequest(ticker, startDate, endDate, field)
if err != nil {
return "", err
}
if err := validateResponse(resp); err != nil {
return "", err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
// GetSecurityData returns a slice of pointers to Price structs, based on the data received from yahoo
func (c *Client) GetSecurityData(ticker string, startDate, endDate time.Time, field Field) ([]*Price, error) {
prices := []*Price{}
resp, err := c.makeRequest(ticker, startDate, endDate, field)
if err != nil {
return prices, err
}
if err := validateResponse(resp); err != nil {
return prices, err
}
if err := gocsv.Unmarshal(resp.Body, &prices); err != nil {
return prices, err
}
return prices, nil
}
// DateTime is a custom implementation of time.Time used to unmarshal yahoo csv data
type DateTime struct {
time.Time
}
// UnmarshalCSV converts the CSV string as internal date
func (date *DateTime) UnmarshalCSV(csv string) (err error) {
date.Time, err = time.Parse("2006-01-02", csv)
if err != nil {
return err
}
return nil
}
func getCrumb(client *http.Client, ticker string) (string, error) {
crumb := ""
url := fmt.Sprintf("https://finance.yahoo.com/quote/%s", ticker)
resp, err := client.Get(url)
if err != nil {
return crumb, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return crumb, err
}
crumbRe, err := regexp.Compile(`"CrumbStore":{"crumb":"([^"]+)"\}`)
if err != nil {
return crumb, err
}
matches := crumbRe.FindAllStringSubmatch(string(body), 1)
if len(matches) > 0 {
crumb = matches[0][1]
}
if len(crumb) > 0 {
crumb, err = strconv.Unquote(`"` + crumb + `"`)
if err != nil {
return crumb, err
}
}
return crumb, nil
}