182 lines
5.3 KiB
Go
182 lines
5.3 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"git.g3e.fr/H6N/account/internal/store"
|
|
)
|
|
|
|
type Config struct {
|
|
OpenFIGIKey string
|
|
CoinGeckoKey string
|
|
}
|
|
|
|
type Pipeline struct {
|
|
store *store.Store
|
|
cfg Config
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func New(s *store.Store, cfg Config, logger *slog.Logger) *Pipeline {
|
|
return &Pipeline{store: s, cfg: cfg, logger: logger}
|
|
}
|
|
|
|
// FetchAll récupère les prix de tous les instruments et les écrit dans price_history.
|
|
func (p *Pipeline) FetchAll(ctx context.Context) error {
|
|
instruments, err := p.store.ListInstruments(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
|
|
// Séparer les instruments par type pour batch CoinGecko
|
|
var cryptoIDs []string
|
|
cryptoByID := make(map[string]store.Instrument)
|
|
|
|
for _, inst := range instruments {
|
|
switch inst.Type {
|
|
case "devise":
|
|
if err := p.store.UpsertPrice(ctx, inst.ID, now, 1.0); err != nil {
|
|
p.logger.Error("upsert devise price", "code", inst.Code, "error", err)
|
|
}
|
|
|
|
case "action", "etf":
|
|
ticker, err := p.resolveTicker(ctx, inst)
|
|
if err != nil {
|
|
p.logger.Warn("could not resolve ticker", "instrument", inst.Code, "error", err)
|
|
continue
|
|
}
|
|
price, err := FetchYahooPrice(ctx, ticker)
|
|
if err != nil {
|
|
p.logger.Warn("yahoo fetch failed", "ticker", ticker, "error", err)
|
|
continue
|
|
}
|
|
if err := p.store.UpsertPrice(ctx, inst.ID, now, price); err != nil {
|
|
p.logger.Error("upsert price", "instrument", inst.Code, "error", err)
|
|
}
|
|
p.logger.Info("price fetched", "instrument", inst.Code, "ticker", ticker, "price", price)
|
|
|
|
case "crypto":
|
|
coinID, err := p.resolveCoinID(ctx, inst)
|
|
if err != nil {
|
|
p.logger.Warn("could not resolve coingecko id", "instrument", inst.Code, "error", err)
|
|
continue
|
|
}
|
|
cryptoIDs = append(cryptoIDs, coinID)
|
|
cryptoByID[coinID] = inst
|
|
}
|
|
}
|
|
|
|
// Batch fetch crypto
|
|
if len(cryptoIDs) > 0 {
|
|
prices, err := FetchCoinGeckoPrices(ctx, p.cfg.CoinGeckoKey, cryptoIDs)
|
|
if err != nil {
|
|
p.logger.Error("coingecko fetch failed", "error", err)
|
|
} else {
|
|
for coinID, price := range prices {
|
|
inst := cryptoByID[coinID]
|
|
if err := p.store.UpsertPrice(ctx, inst.ID, now, price); err != nil {
|
|
p.logger.Error("upsert crypto price", "instrument", inst.Code, "error", err)
|
|
continue
|
|
}
|
|
p.logger.Info("price fetched", "instrument", inst.Code, "coin_id", coinID, "price", price)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
p.logger.Error("clean price history failed", "error", err)
|
|
return err
|
|
}
|
|
p.logger.Info("price history cleaned")
|
|
return nil
|
|
}
|
|
|
|
// resolveTicker retourne le ticker Yahoo Finance, en le résolvant via OpenFIGI si nécessaire.
|
|
func (p *Pipeline) resolveTicker(ctx context.Context, inst store.Instrument) (string, error) {
|
|
ticker, err := p.store.GetTicker(ctx, inst.ID)
|
|
if err == nil {
|
|
return ticker, nil
|
|
}
|
|
|
|
resolved, err := ResolveISIN(ctx, p.cfg.OpenFIGIKey, inst.Code)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_ = p.store.UpsertTicker(ctx, inst.ID, resolved)
|
|
return resolved, nil
|
|
}
|
|
|
|
// resolveCoinID retourne l'ID CoinGecko, en le résolvant via l'API search si nécessaire.
|
|
func (p *Pipeline) resolveCoinID(ctx context.Context, inst store.Instrument) (string, error) {
|
|
coinID, err := p.store.GetTicker(ctx, inst.ID)
|
|
if err == nil {
|
|
return coinID, nil
|
|
}
|
|
|
|
resolved, err := ResolveCryptoID(ctx, p.cfg.CoinGeckoKey, inst.Code)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_ = p.store.UpsertTicker(ctx, inst.ID, resolved)
|
|
return resolved, nil
|
|
}
|