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 }