add correct backfile
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
f862abe5a4
commit
28e082f0fc
8 changed files with 317 additions and 35 deletions
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ResolveCryptoID résout un ticker crypto (ex: "BTC") en ID CoinGecko (ex: "bitcoin").
|
||||
|
|
@ -46,6 +47,48 @@ func ResolveCryptoID(ctx context.Context, apiKey, ticker string) (string, error)
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package pipeline
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
|
|
@ -90,6 +91,54 @@ func (p *Pipeline) FetchAll(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// BackfillInstrument récupère l'historique de prix d'un instrument entre from et to
|
||||
// et upserte chaque point dans price_history. Idempotent.
|
||||
func (p *Pipeline) BackfillInstrument(ctx context.Context, instrumentID int32, from, to time.Time) error {
|
||||
inst, err := p.store.GetInstrument(ctx, instrumentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get instrument %d: %w", instrumentID, err)
|
||||
}
|
||||
|
||||
switch inst.Type {
|
||||
case "devise":
|
||||
return nil // EUR toujours = 1, pas de backfill nécessaire
|
||||
|
||||
case "action", "etf":
|
||||
ticker, err := p.resolveTicker(ctx, inst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve ticker for %s: %w", inst.Code, err)
|
||||
}
|
||||
points, err := FetchYahooHistory(ctx, ticker, from, to)
|
||||
if err != nil {
|
||||
return fmt.Errorf("yahoo history %s: %w", ticker, err)
|
||||
}
|
||||
for _, pt := range points {
|
||||
if err := p.store.UpsertPrice(ctx, inst.ID, pt.At, pt.Price); err != nil {
|
||||
p.logger.Error("upsert historical price", "instrument", inst.Code, "date", pt.At, "error", err)
|
||||
}
|
||||
}
|
||||
p.logger.Info("backfill done", "instrument", inst.Code, "points", len(points))
|
||||
|
||||
case "crypto":
|
||||
coinID, err := p.resolveCoinID(ctx, inst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve coin id for %s: %w", inst.Code, err)
|
||||
}
|
||||
points, err := FetchCoinGeckoHistory(ctx, p.cfg.CoinGeckoKey, coinID, from, to)
|
||||
if err != nil {
|
||||
return fmt.Errorf("coingecko history %s: %w", coinID, err)
|
||||
}
|
||||
for _, pt := range points {
|
||||
if err := p.store.UpsertPrice(ctx, inst.ID, pt.At, pt.Price); err != nil {
|
||||
p.logger.Error("upsert historical price", "instrument", inst.Code, "date", pt.At, "error", err)
|
||||
}
|
||||
}
|
||||
p.logger.Info("backfill done", "instrument", inst.Code, "points", len(points))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanHistory supprime les prix intraday des jours passés, ne gardant que le dernier par instrument.
|
||||
func (p *Pipeline) CleanHistory(ctx context.Context) error {
|
||||
if err := p.store.CleanPastDays(ctx); err != nil {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type yahooChart struct {
|
||||
|
|
@ -14,6 +15,12 @@ type yahooChart struct {
|
|||
RegularMarketPrice float64 `json:"regularMarketPrice"`
|
||||
Currency string `json:"currency"`
|
||||
} `json:"meta"`
|
||||
Timestamps []int64 `json:"timestamp"`
|
||||
Indicators struct {
|
||||
Quote []struct {
|
||||
Close []*float64 `json:"close"`
|
||||
} `json:"quote"`
|
||||
} `json:"indicators"`
|
||||
} `json:"result"`
|
||||
Error *struct{ Description string } `json:"error"`
|
||||
} `json:"chart"`
|
||||
|
|
@ -23,33 +30,79 @@ type yahooChart struct {
|
|||
func FetchYahooPrice(ctx context.Context, ticker string) (float64, error) {
|
||||
url := fmt.Sprintf(
|
||||
"https://query1.finance.yahoo.com/v8/finance/chart/%s?interval=1d&range=1d", ticker)
|
||||
return fetchYahoo(ctx, url, ticker)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
// FetchYahooHistory récupère les prix de clôture journaliers entre from et to.
|
||||
// Retourne une slice de (time, price) pour chaque jour ayant une clôture non nulle.
|
||||
func FetchYahooHistory(ctx context.Context, ticker string, from, to time.Time) ([]PricePoint, error) {
|
||||
url := fmt.Sprintf(
|
||||
"https://query1.finance.yahoo.com/v8/finance/chart/%s?interval=1d&period1=%d&period2=%d",
|
||||
ticker, from.Unix(), to.Unix())
|
||||
|
||||
chart, err := fetchYahooChart(ctx, url, ticker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(chart.Chart.Result) == 0 || len(chart.Chart.Result[0].Indicators.Quote) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
res := chart.Chart.Result[0]
|
||||
closes := res.Indicators.Quote[0].Close
|
||||
var points []PricePoint
|
||||
for i, ts := range res.Timestamps {
|
||||
if i >= len(closes) || closes[i] == nil || *closes[i] == 0 {
|
||||
continue
|
||||
}
|
||||
points = append(points, PricePoint{
|
||||
At: time.Unix(ts, 0).UTC(),
|
||||
Price: *closes[i],
|
||||
})
|
||||
}
|
||||
return points, nil
|
||||
}
|
||||
|
||||
func fetchYahoo(ctx context.Context, url, ticker string) (float64, error) {
|
||||
chart, err := fetchYahooChart(ctx, url, ticker)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var chart yahooChart
|
||||
if err := json.NewDecoder(resp.Body).Decode(&chart); err != nil {
|
||||
return 0, fmt.Errorf("decode yahoo response: %w", err)
|
||||
}
|
||||
if chart.Chart.Error != nil {
|
||||
return 0, fmt.Errorf("yahoo error: %s", chart.Chart.Error.Description)
|
||||
}
|
||||
if len(chart.Chart.Result) == 0 {
|
||||
return 0, fmt.Errorf("no result for ticker %s", ticker)
|
||||
}
|
||||
|
||||
price := chart.Chart.Result[0].Meta.RegularMarketPrice
|
||||
if price == 0 {
|
||||
return 0, fmt.Errorf("zero price for ticker %s", ticker)
|
||||
}
|
||||
return price, nil
|
||||
}
|
||||
|
||||
func fetchYahooChart(ctx context.Context, url, ticker string) (*yahooChart, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var chart yahooChart
|
||||
if err := json.NewDecoder(resp.Body).Decode(&chart); err != nil {
|
||||
return nil, fmt.Errorf("decode yahoo response: %w", err)
|
||||
}
|
||||
if chart.Chart.Error != nil {
|
||||
return nil, fmt.Errorf("yahoo error: %s", chart.Chart.Error.Description)
|
||||
}
|
||||
return &chart, nil
|
||||
}
|
||||
|
||||
// PricePoint est un prix à un instant donné, partagé entre les providers.
|
||||
type PricePoint struct {
|
||||
At time.Time
|
||||
Price float64
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue