This repository has been archived by the owner on Apr 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhistorical.go
368 lines (313 loc) · 9.33 KB
/
historical.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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
package yfq
import (
"encoding/csv"
"errors"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
"time"
)
const (
// CRUMBURL is the URL to the Yahoo Finance webpage of the given symbol to scrap for the cookie and symbol
CRUMBURL = "https://finance.yahoo.com/quote/{symbol}/history?p={symbol}"
// HISTORYURL is the basic URL to download historic quotes
HISTORYURL = "https://query1.finance.yahoo.com/v7/finance/download/{symbol}?"
// CONFIGURL configurates the query to HISTORYURL
CONFIGURL = "period1={start}&period2={end}&interval=1d&events=history&crumb={crumb}"
)
// Historical represents the basic Query for historical quotes
type Historical struct {
StartDate string
EndDate string
Quotes []Quote
crumbURL string
crumb string
cookies []*http.Cookie
}
// Quote represents a single quote
type Quote struct {
Date time.Time
Symbol string
Open float64
High float64
Low float64
Close float64
AdjClose float64
Volume int64
}
// NewHistorical return a Historical struct ready to use.
func NewHistorical() *Historical {
return &Historical{}
}
// Query returns the parsed data string as a slice of []Quote
func (h *Historical) Query(symbol string) (quotes []Quote, err error) {
data, err := h.baseQuery(symbol)
quotes = parseHistoricalCSV(symbol, data)
return quotes, err
}
// QueryRaw returns the raw csv string
func (h *Historical) QueryRaw(symbol string) (data [][]string, err error) {
data, err = h.baseQuery(symbol)
return data, nil
}
// ResetDates rests the given start end end dates to nil.
func (h *Historical) ResetDates() error {
h.StartDate = ""
h.EndDate = ""
return nil
}
// RenewCrumb renews the crumb and cookie to be used in the main query.
func (h *Historical) RenewCrumb() error {
crumb, cookies, err := getCrumb(h.crumbURL)
if err != nil {
return err
}
h.crumb = crumb
h.cookies = cookies
return nil
}
// baseQuery fetches the latest historical quotes from finance.yahoo.com
func (h *Historical) baseQuery(symbol string) (data [][]string, err error) {
// validate symbol
if len(symbol) == 0 {
err := errors.New("No symbol provided")
return data, err
}
// build or update the crumb
err = h.buildCrumb(symbol)
if err != nil {
return data, err
}
// validate order of dates
h.StartDate, h.EndDate, err = orderDates(h.StartDate, h.EndDate)
if err != nil {
return data, err
}
// parse dates
start, end, err := parseDates(h.StartDate, h.EndDate)
if err != nil {
return data, err
}
// build url for the main historical query
historyURL := strings.Replace(HISTORYURL, "{symbol}", symbol, -1)
configURL := strings.Replace(CONFIGURL, "{start}", start, -1)
configURL = strings.Replace(configURL, "{end}", end, -1)
configURL = strings.Replace(configURL, "{crumb}", h.crumb, -1)
queryURL := historyURL + configURL
// query for csv
data, err = readCSVFromURL(queryURL, h.cookies)
if err != nil {
err := fmt.Errorf("could not establish new csv request: %v", err)
return data, err
}
// whats the csv like?
// fmt.Println(len(data))
// if len(data) > 0 {
// fmt.Println(data[0])
// fmt.Println(data[len(data)-1])
// fmt.Printf("%#v\n", data[len(data)-1])
// }
return data, nil
}
// readCSVFromURL fetches the csv file from the provided URL.
// Is uses the provided cookies for the request.
func readCSVFromURL(url string, cookies []*http.Cookie) ([][]string, error) {
// set client
client := &http.Client{
Timeout: time.Second * 10,
}
// define request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// add cookies to the request
for _, c := range cookies {
req.AddCookie(c)
}
// send request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// read csv from response
reader := csv.NewReader(resp.Body)
data, err := reader.ReadAll()
if err != nil {
return nil, err
}
return data, nil
}
// parseHistoricalCSV parses the returned CSV into single Quotes and adds these to []Quote.
// It also adds the symbol to each row.
func parseHistoricalCSV(symbol string, csv [][]string) (quotes []Quote) {
var csvHeader []string
// parse csv to map
for id, row := range csv {
if id == 0 {
csvHeader = row
continue
}
// rearange row to header
quote := make(map[string]string)
for i, v := range row {
quote[csvHeader[i]] = v
}
// create new Quote and populate
q := Quote{}
q.Date, _ = time.Parse("2006-01-02", quote["Date"])
q.Symbol = strings.ToUpper(symbol)
q.Open, _ = strconv.ParseFloat(quote["Open"], 64)
q.High, _ = strconv.ParseFloat(quote["High"], 64)
q.Low, _ = strconv.ParseFloat(quote["Low"], 64)
q.Close, _ = strconv.ParseFloat(quote["Close"], 64)
q.AdjClose, _ = strconv.ParseFloat(quote["Adj Close"], 64)
q.Volume, _ = strconv.ParseInt(quote["Volume"], 10, 64)
quotes = append(quotes, q)
}
return
}
// buildCrumb build the necassary url and sets the crumb and cookies
func (h *Historical) buildCrumb(symbol string) error {
// check for available crumb and cookies and query these first if needed
// fmt.Printf("before\nh.crumb: %#v\nh.cookies: %#v\n", h.crumb, h.cookies)
if (h.crumb == "") || (h.cookies == nil) {
// build url for retrieving the crumb
crumbURL, err := buildCrumbURL(symbol)
if err != nil {
return err
}
h.crumbURL = crumbURL
crumb, cookies, err := getCrumb(crumbURL)
if err != nil {
return err
}
h.crumb = crumb
h.cookies = cookies
}
// fmt.Printf("after\nh.crumb: %#v\nh.cookies: %#v\n", h.crumb, h.cookies)
return nil
}
// buildCrumbURL builds the URL to request the crumb and cookies
func buildCrumbURL(symbol string) (url string, err error) {
if symbol == "" {
err := errors.New("could not build crumb URL, empty string given")
return url, err
}
// build url for retrieving the crumb
url = strings.Replace(CRUMBURL, "{symbol}", symbol, -1)
return url, nil
}
// getCrumb scraps the neccessary json and cookie from the yahoo finance page
// and returns the crumb string with the cookies.
func getCrumb(url string) (crumb string, cookies []*http.Cookie, err error) {
// set client with a timeout
client := &http.Client{
Timeout: time.Second * 10,
}
// define request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
err := fmt.Errorf("could not establish new crumb request: %v", err)
return crumb, cookies, err
}
// fmt.Printf("url:%#v\n req: %#v\n", req.URL, req)
// issue request
resp, err := client.Do(req)
if err != nil {
err := fmt.Errorf("could not query page: %v", err)
return crumb, cookies, err
}
defer resp.Body.Close()
// fmt.Printf("resp: %#v\n", resp)
// collect the received cookies
cookies = resp.Cookies()
// read response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
err := fmt.Errorf("could not read response body: %v", err)
return crumb, cookies, err
}
// search for crumb in body
crumb, err = parseCrumb(string(body))
if err != nil {
err := fmt.Errorf("could not parse response body for crumb: %v", err)
return crumb, cookies, err
}
return crumb, cookies, nil
}
// parseCrumb searches for a crumb within a string.
func parseCrumb(s string) (crumb string, err error) {
// search for crumb in body
re := regexp.MustCompile(`(?P<CrumbStore>"CrumbStore"\s?:\s?{"crumb"\s?:\s?"(?P<crumb>.*?)"})`)
matches := re.FindStringSubmatch(s)
if matches == nil {
err := errors.New("could not find crumb")
return crumb, err
}
// rearrange submatches to map
matchMap := make(map[string]string)
for i, name := range re.SubexpNames() {
if i != 0 {
matchMap[name] = matches[i]
}
}
crumb = matchMap["crumb"]
return crumb, nil
}
// orderDates validates the correct order of start to end date.
// Start must be earlier than end. If necassary, the method reorders the dates.
func orderDates(start, end string) (s, e string, err error) {
s = start
e = end
if (start > end) && ((start != "") && (end != "")) {
tmp := start
s = end
e = tmp
}
return s, e, err
}
// parseDates parses the start and end date and converts these to an UNIX time string.
// Start and end must be a date formated according to ISO 8601: yyyy-mm-dd or an empty string.
// There are three different cases for the start and end date:
// - start date empty, valid or invalid
// - end date empty, valid or invalid
// If both start and end date are valid, the date range will be parsed as specified.
// If both start and end are empty or invalid, the date range is set to max start (unix 0) and end to unix now.
// If start date is empty/invalid and end date is valid, date range is set to max start (unix 0) and end to specified date.
// If start date is valid and end date is empty/invalid, date range is set to start date and end date to unix now.
func parseDates(start, end string) (s, e string, err error) {
if len(start) == 0 {
// set to min
s = "0"
} else {
s, err = parseDateStringToUnix(start)
if err != nil {
s = "0"
}
}
if len(end) == 0 {
// set to min
e = strconv.Itoa(int(time.Now().Unix()))
} else {
e, err = parseDateStringToUnix(end)
if err != nil {
e = strconv.Itoa(int(time.Now().Unix()))
}
}
return s, e, nil
}
// parseDate parses a single date string to an unix string.
func parseDateStringToUnix(s string) (unix string, err error) {
date, err := time.Parse("2006-01-02", s)
if err != nil {
err = fmt.Errorf("Could not parse string \"%s\" to time: %v", s, err)
return s, err
}
unix = strconv.Itoa(int(date.Unix()))
return unix, nil
}