add correct backfile

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-06-14 13:41:42 +02:00
commit 28e082f0fc
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
8 changed files with 317 additions and 35 deletions

View file

@ -9,16 +9,28 @@ import (
"git.g3e.fr/H6N/account/internal/store"
)
// PriceBackfiller est implémenté par le pipeline pour récupérer l'historique de prix manquant.
type PriceBackfiller interface {
BackfillInstrument(ctx context.Context, instrumentID int32, from, to time.Time) error
}
type Engine struct {
store *store.Store
logger *slog.Logger
horizonDays int
backfiller PriceBackfiller
}
func New(st *store.Store, logger *slog.Logger, horizonDays int) *Engine {
return &Engine{store: st, logger: logger, horizonDays: horizonDays}
}
// WithBackfiller active le backfill automatique des prix historiques manquants.
func (e *Engine) WithBackfiller(b PriceBackfiller) *Engine {
e.backfiller = b
return e
}
func (e *Engine) Store() *store.Store { return e.store }
// RecomputeDay calcule les snapshots de tous les comptes pour une date donnée.
@ -126,8 +138,11 @@ func (e *Engine) backfillAccount(ctx context.Context, accountID int32, from, to
return fmt.Errorf("from (%s) after to (%s)", from.Format("2006-01-02"), to.Format("2006-01-02"))
}
// backfilledInstruments évite de re-fetcher le même instrument plusieurs fois dans ce run.
backfilledInstruments := make(map[int32]bool)
for d := from; !d.After(to); d = d.AddDate(0, 0, 1) {
if err := e.recomputeAccount(ctx, accountID, d); err != nil {
if err := e.recomputeAccountWithBackfill(ctx, accountID, d, backfilledInstruments); err != nil {
return fmt.Errorf("recompute %s: %w", d.Format("2006-01-02"), err)
}
}
@ -135,11 +150,20 @@ func (e *Engine) backfillAccount(ctx context.Context, accountID int32, from, to
}
func (e *Engine) recomputeAccount(ctx context.Context, accountID int32, date time.Time) error {
return e.recomputeAccountWithBackfill(ctx, accountID, date, nil)
}
func (e *Engine) recomputeAccountWithBackfill(ctx context.Context, accountID int32, date time.Time, backfilled map[int32]bool) error {
baseDate, hasBase, err := e.store.GetLatestSnapshotDate(ctx, accountID, date)
if err != nil {
return fmt.Errorf("get latest snapshot date: %w", err)
}
prus, err := e.store.ComputePRUs(ctx, accountID, date)
if err != nil {
return fmt.Errorf("compute PRUs: %w", err)
}
positions := map[int32]store.PositionRow{}
if hasBase {
base, err := e.store.GetBasePositions(ctx, accountID, baseDate)
@ -175,30 +199,53 @@ func (e *Engine) recomputeAccount(ctx context.Context, accountID int32, date tim
if err != nil {
return fmt.Errorf("get price instrument %d: %w", pos.InstrumentID, err)
}
if !found {
if pos.InstrumentType == "devise" {
prix = 1.0
} else {
e.logger.Warn("snapshot: no price, position skipped",
"account_id", accountID,
"instrument_id", pos.InstrumentID,
"date", date.Format("2006-01-02"),
)
continue
}
if !found && pos.InstrumentType == "devise" {
prix = 1.0
found = true
}
valeur := pos.Quantite * prix
totalValeur += valeur
// Backfill automatique si prix manquant et backfiller disponible.
// from = date - 1 jour (marge timezone), to = maintenant (inclut aujourd'hui).
if !found && e.backfiller != nil && (backfilled == nil || !backfilled[pos.InstrumentID]) {
from := date.AddDate(0, 0, -1)
now := time.Now().UTC()
if err := e.backfiller.BackfillInstrument(ctx, pos.InstrumentID, from, now); err != nil {
e.logger.Warn("snapshot: backfill échoué",
"instrument_id", pos.InstrumentID, "error", err)
} else {
if backfilled != nil {
backfilled[pos.InstrumentID] = true
}
// Retry après backfill
prix, found, err = e.store.GetPriceAt(ctx, pos.InstrumentID, date)
if err != nil {
return fmt.Errorf("get price instrument %d (post-backfill): %w", pos.InstrumentID, err)
}
}
}
snap := store.PositionSnapshot{
Date: date,
AccountID: accountID,
InstrumentID: pos.InstrumentID,
Quantite: pos.Quantite,
PrixCloture: &prix,
Valeur: &valeur,
}
if pru, ok := prus[pos.InstrumentID]; ok {
snap.PRU = &pru
}
if found {
valeur := pos.Quantite * prix
snap.PrixCloture = &prix
snap.Valeur = &valeur
totalValeur += valeur
} else {
e.logger.Warn("snapshot: no price, valeur non calculée",
"account_id", accountID,
"instrument_id", pos.InstrumentID,
"date", date.Format("2006-01-02"),
)
}
if err := e.store.UpsertPositionSnapshot(ctx, snap); err != nil {
return fmt.Errorf("upsert position snapshot: %w", err)
}