Files
StockRadar/internal/yahoo/client.go
T
2026-04-20 21:29:22 +02:00

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
}