246 lines
5.5 KiB
Go
246 lines
5.5 KiB
Go
package yahoo
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const baseURL = "https://query1.finance.yahoo.com/v8/finance/chart"
|
|
const summaryURL = "https://query1.finance.yahoo.com/v10/finance/quoteSummary"
|
|
|
|
type Client struct {
|
|
http *http.Client
|
|
}
|
|
|
|
type Bar struct {
|
|
Date time.Time
|
|
Open float64
|
|
High float64
|
|
Low float64
|
|
Close float64
|
|
Volume int64
|
|
}
|
|
|
|
type Quote struct {
|
|
Symbol string
|
|
Price float64
|
|
PrevClose float64
|
|
ChangePct float64
|
|
}
|
|
|
|
type chartResponse struct {
|
|
Chart struct {
|
|
Result []struct {
|
|
Meta struct {
|
|
Symbol string `json:"symbol"`
|
|
RegularMarketPrice float64 `json:"regularMarketPrice"`
|
|
PreviousClose float64 `json:"previousClose"`
|
|
} `json:"meta"`
|
|
Timestamps []int64 `json:"timestamp"`
|
|
Indicators struct {
|
|
Quote []struct {
|
|
Open []float64 `json:"open"`
|
|
High []float64 `json:"high"`
|
|
Low []float64 `json:"low"`
|
|
Close []float64 `json:"close"`
|
|
Volume []int64 `json:"volume"`
|
|
} `json:"quote"`
|
|
} `json:"indicators"`
|
|
} `json:"result"`
|
|
Error *struct {
|
|
Code string `json:"code"`
|
|
Description string `json:"description"`
|
|
} `json:"error"`
|
|
} `json:"chart"`
|
|
}
|
|
|
|
func New() *Client {
|
|
return &Client{
|
|
http: &http.Client{Timeout: 10 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (c *Client) History(symbol string, days int) ([]Bar, error) {
|
|
rangeStr := "3mo"
|
|
if days > 90 {
|
|
rangeStr = "6mo"
|
|
}
|
|
url := fmt.Sprintf("%s/%s?interval=1d&range=%s", baseURL, symbol, rangeStr)
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", "Mozilla/5.0")
|
|
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("yahoo: HTTP %d for %s", resp.StatusCode, symbol)
|
|
}
|
|
|
|
var data chartResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if data.Chart.Error != nil {
|
|
return nil, fmt.Errorf("yahoo: %s — %s", data.Chart.Error.Code, data.Chart.Error.Description)
|
|
}
|
|
if len(data.Chart.Result) == 0 {
|
|
return nil, fmt.Errorf("yahoo: no data for %s", symbol)
|
|
}
|
|
|
|
result := data.Chart.Result[0]
|
|
quotes := result.Indicators.Quote
|
|
if len(quotes) == 0 {
|
|
return nil, fmt.Errorf("yahoo: empty quotes for %s", symbol)
|
|
}
|
|
q := quotes[0]
|
|
|
|
bars := make([]Bar, 0, len(result.Timestamps))
|
|
for i, ts := range result.Timestamps {
|
|
if i >= len(q.Close) || q.Close[i] == 0 {
|
|
continue
|
|
}
|
|
bar := Bar{
|
|
Date: time.Unix(ts, 0).UTC(),
|
|
Close: safeFloat(q.Close, i),
|
|
Open: safeFloat(q.Open, i),
|
|
High: safeFloat(q.High, i),
|
|
Low: safeFloat(q.Low, i),
|
|
Volume: safeInt(q.Volume, i),
|
|
}
|
|
bars = append(bars, bar)
|
|
}
|
|
return bars, nil
|
|
}
|
|
|
|
func (c *Client) GetQuote(symbol string) (*Quote, error) {
|
|
url := fmt.Sprintf("%s/%s?interval=1d&range=5d", baseURL, symbol)
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", "Mozilla/5.0")
|
|
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var data chartResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(data.Chart.Result) == 0 {
|
|
return nil, fmt.Errorf("yahoo: no result for %s", symbol)
|
|
}
|
|
|
|
meta := data.Chart.Result[0].Meta
|
|
changePct := 0.0
|
|
if meta.PreviousClose > 0 {
|
|
changePct = (meta.RegularMarketPrice - meta.PreviousClose) / meta.PreviousClose * 100
|
|
}
|
|
|
|
return &Quote{
|
|
Symbol: meta.Symbol,
|
|
Price: meta.RegularMarketPrice,
|
|
PrevClose: meta.PreviousClose,
|
|
ChangePct: changePct,
|
|
}, nil
|
|
}
|
|
|
|
// MarketCapInfo contient les données fondamentales clés.
|
|
type MarketCapInfo struct {
|
|
MarketCap int64 // en USD
|
|
FloatShares int64
|
|
ShortRatio float64
|
|
ForwardPE float64
|
|
}
|
|
|
|
type quoteSummaryResponse struct {
|
|
QuoteSummary struct {
|
|
Result []struct {
|
|
SummaryDetail struct {
|
|
MarketCap struct {
|
|
Raw int64 `json:"raw"`
|
|
} `json:"marketCap"`
|
|
ForwardPE struct {
|
|
Raw float64 `json:"raw"`
|
|
} `json:"forwardPE"`
|
|
} `json:"summaryDetail"`
|
|
DefaultKeyStatistics struct {
|
|
FloatShares struct {
|
|
Raw int64 `json:"raw"`
|
|
} `json:"floatShares"`
|
|
ShortRatio struct {
|
|
Raw float64 `json:"raw"`
|
|
} `json:"shortRatio"`
|
|
} `json:"defaultKeyStatistics"`
|
|
} `json:"result"`
|
|
} `json:"quoteSummary"`
|
|
}
|
|
|
|
// GetMarketCap retourne les données fondamentales d'un ticker.
|
|
func (c *Client) GetMarketCap(symbol string) (*MarketCapInfo, error) {
|
|
url := fmt.Sprintf("%s/%s?modules=summaryDetail,defaultKeyStatistics", summaryURL, symbol)
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", "Mozilla/5.0")
|
|
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("yahoo quoteSummary: HTTP %d for %s", resp.StatusCode, symbol)
|
|
}
|
|
|
|
var data quoteSummaryResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results := data.QuoteSummary.Result
|
|
if len(results) == 0 {
|
|
return nil, fmt.Errorf("yahoo: no summary for %s", symbol)
|
|
}
|
|
|
|
r := results[0]
|
|
return &MarketCapInfo{
|
|
MarketCap: r.SummaryDetail.MarketCap.Raw,
|
|
FloatShares: r.DefaultKeyStatistics.FloatShares.Raw,
|
|
ShortRatio: r.DefaultKeyStatistics.ShortRatio.Raw,
|
|
ForwardPE: r.SummaryDetail.ForwardPE.Raw,
|
|
}, nil
|
|
}
|
|
|
|
func safeFloat(s []float64, i int) float64 {
|
|
if i < len(s) {
|
|
return s[i]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func safeInt(s []int64, i int) int64 {
|
|
if i < len(s) {
|
|
return s[i]
|
|
}
|
|
return 0
|
|
}
|