package pipeline import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" ) // ResolveCryptoID résout un ticker crypto (ex: "BTC") en ID CoinGecko (ex: "bitcoin"). func ResolveCryptoID(ctx context.Context, apiKey, ticker string) (string, error) { url := fmt.Sprintf("https://api.coingecko.com/api/v3/search?query=%s", ticker) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", err } if apiKey != "" { req.Header.Set("x-cg-pro-api-key", apiKey) } resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() var result struct { Coins []struct { ID string `json:"id"` Symbol string `json:"symbol"` } `json:"coins"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", fmt.Errorf("decode coingecko search: %w", err) } // Le premier résultat dont le symbol correspond exactement est le bon upper := strings.ToUpper(ticker) for _, coin := range result.Coins { if strings.ToUpper(coin.Symbol) == upper { return coin.ID, nil } } return "", fmt.Errorf("no coingecko match for ticker %s", ticker) } // FetchCoinGeckoHistory récupère les prix EUR journaliers entre from et to pour un coin. func FetchCoinGeckoHistory(ctx context.Context, apiKey, coinID string, from, to time.Time) ([]PricePoint, error) { baseURL := "https://api.coingecko.com/api/v3" if apiKey != "" { baseURL = "https://pro-api.coingecko.com/api/v3" } url := fmt.Sprintf("%s/coins/%s/market_chart/range?vs_currency=eur&from=%d&to=%d", baseURL, coinID, from.Unix(), to.Unix()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } if apiKey != "" { req.Header.Set("x-cg-pro-api-key", apiKey) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // Réponse : {"prices": [[timestamp_ms, price], ...]} var raw struct { Prices [][2]float64 `json:"prices"` } if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { return nil, fmt.Errorf("decode coingecko history: %w", err) } points := make([]PricePoint, 0, len(raw.Prices)) for _, p := range raw.Prices { points = append(points, PricePoint{ At: time.UnixMilli(int64(p[0])).UTC(), Price: p[1], }) } return points, nil } // FetchCoinGeckoPrices récupère les prix EUR pour une liste d'IDs CoinGecko. // Le ticker stocké dans instrument_ticker_cache doit être l'ID CoinGecko (ex: "bitcoin", "ethereum"). func FetchCoinGeckoPrices(ctx context.Context, apiKey string, coinIDs []string) (map[string]float64, error) { if len(coinIDs) == 0 { return map[string]float64{}, nil } baseURL := "https://api.coingecko.com/api/v3/simple/price" if apiKey != "" { baseURL = "https://pro-api.coingecko.com/api/v3/simple/price" } url := fmt.Sprintf("%s?ids=%s&vs_currencies=eur", baseURL, strings.Join(coinIDs, ",")) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } if apiKey != "" { req.Header.Set("x-cg-pro-api-key", apiKey) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // Réponse : {"bitcoin": {"eur": 45000.0}, "ethereum": {"eur": 2500.0}} var raw map[string]map[string]float64 if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { return nil, fmt.Errorf("decode coingecko response: %w", err) } prices := make(map[string]float64, len(raw)) for id, currencies := range raw { prices[id] = currencies["eur"] } return prices, nil }