Skip to content

Commit

Permalink
Consume a different Yahoo! API
Browse files Browse the repository at this point in the history
The old one stopped working a few days ago.
  • Loading branch information
Silvio Böhler committed Sep 12, 2024
1 parent cf9dc8c commit f7557b8
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 5 deletions.
10 changes: 5 additions & 5 deletions cmd/commands/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"github.com/sboehler/knut/lib/model"
"github.com/sboehler/knut/lib/model/price"
"github.com/sboehler/knut/lib/model/registry"
"github.com/sboehler/knut/lib/quotes/yahoo"
"github.com/sboehler/knut/lib/quotes/yahoo2"
"github.com/sboehler/knut/lib/syntax"
"github.com/shopspring/decimal"
"github.com/sourcegraph/conc/pool"
Expand Down Expand Up @@ -62,7 +62,7 @@ func (r *fetchRunner) run(cmd *cobra.Command, args []string) {

const fetchConcurrency = 5

func (r *fetchRunner) execute(cmd *cobra.Command, args []string) error {
func (r *fetchRunner) execute(_ *cobra.Command, args []string) error {
reg := registry.New()
configs, err := r.readConfig(args[0])
if err != nil {
Expand Down Expand Up @@ -133,13 +133,13 @@ func (r *fetchRunner) readFile(ctx *registry.Registry, filepath string) (res map

func (r *fetchRunner) fetchPrices(reg *registry.Registry, cfg fetchConfig, t0, t1 time.Time, results map[time.Time]*model.Price) error {
var (
c = yahoo.New()
quotes []yahoo.Quote
c = yahoo2.New()
quotes []yahoo2.Quote
commodity, target *model.Commodity
err error
)
if quotes, err = c.Fetch(cfg.Symbol, t0, t1); err != nil {
return err
return fmt.Errorf("error fetching symbol %s: %v", cfg.Symbol, err)
}
if commodity, err = reg.Commodities().Get(cfg.Commodity); err != nil {
return err
Expand Down
3 changes: 3 additions & 0 deletions lib/quotes/yahoo/yahoo.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package yahoo implements fetching pricing data from Yahoo!. The method
// in this packages stopped working around 2024-09-11, see package yahoo2
// for an updated version that uses a different API.
package yahoo

import (
Expand Down
146 changes: 146 additions & 0 deletions lib/quotes/yahoo2/yahoo2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2021 Silvio Böhler
//
// 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 yahoo2

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"time"
)

const yahooURL string = "https://query2.finance.yahoo.com/v8/finance/chart"

// Quote represents a quote on a given day.
type Quote struct {
Date time.Time
Open float64
High float64
Low float64
Close float64
AdjClose float64
Volume int
}

// Client is a client for Yahoo! quotes.
type Client struct {
url string
}

// New creates a new client with the default URL.
func New() Client {
return Client{yahooURL}
}

// Fetch fetches a set of quotes
func (c *Client) Fetch(sym string, t0, t1 time.Time) ([]Quote, error) {
u, err := createURL(c.url, sym, t0, t1)
if err != nil {
return nil, fmt.Errorf("error creating URL for symbol %s: %w", sym, err)
}
resp, err := http.Get(u.String())
if err != nil {
return nil, fmt.Errorf("error fetching data from URL %s: %w", u.String(), err)
}
defer resp.Body.Close()
quote, err := decodeResponse(resp.Body)
if err != nil {
return nil, fmt.Errorf("error decoding response for symbol %s (url: %s): %w", sym, u, err)
}
return quote, nil
}

// createURL creates a URL for the given root URL and parameters.
func createURL(rootURL, sym string, t0, t1 time.Time) (*url.URL, error) {
u, err := url.Parse(rootURL)
if err != nil {
return u, err
}
u.Path = path.Join(u.Path, url.PathEscape(sym))
u.RawQuery = url.Values{
"events": {"history"},
"interval": {"1d"},
"period1": {fmt.Sprint(t0.Unix())},
"period2": {fmt.Sprint(t1.Unix())},
}.Encode()
return u, nil
}

// decodeResponse takes a reader for the response and returns
// the parsed quotes.
func decodeResponse(r io.Reader) ([]Quote, error) {
d := json.NewDecoder(r)
var body jbody
if err := d.Decode(&body); err != nil {
return nil, err
}
result := body.Chart.Result[0]
loc, err := time.LoadLocation(result.Meta.ExchangeTimezoneName)
if err != nil {
return nil, fmt.Errorf("unknown time zone: %s", result.Meta.ExchangeTimezoneName)
}
var res []Quote
for i, ts := range body.Chart.Result[0].Timestamp {
date := time.Unix(int64(ts), 0).In(loc)
q := Quote{
Date: time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC),
Open: result.Indicators.Quote[0].Open[i],
Close: result.Indicators.Quote[0].Close[i],
High: result.Indicators.Quote[0].High[i],
Low: result.Indicators.Quote[0].Low[i],
AdjClose: result.Indicators.Adjclose[0].Adjclose[i],
Volume: result.Indicators.Quote[0].Volume[i],
}
res = append(res, q)
}
return res, nil
}

type jbody struct {
Chart jchart `json:"chart"`
}
type jchart struct {
Result []jresult `json:"result"`
}

type jresult struct {
Meta jmeta `json:"meta"`
Timestamp []int `json:"timestamp"`
Indicators jindicators `json:"indicators"`
}

type jmeta struct {
ExchangeTimezoneName string `json:"exchangeTimezoneName"`
}

type jindicators struct {
Quote []jquote `json:"quote"`
Adjclose []jadjclose `json:"adjclose"`
}

type jquote struct {
Volume []int `json:"volume"`
High []float64 `json:"high"`
Close []float64 `json:"close"`
Low []float64 `json:"low"`
Open []float64 `json:"open"`
}

type jadjclose struct {
Adjclose []float64 `json:"adjclose"`
}

0 comments on commit f7557b8

Please sign in to comment.