ajoute snapshot
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
c73021a14f
commit
0f2d7126eb
11 changed files with 862 additions and 19 deletions
212
internal/snapshot/engine.go
Normal file
212
internal/snapshot/engine.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
package snapshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.g3e.fr/H6N/account/internal/store"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
store *store.Store
|
||||
logger *slog.Logger
|
||||
horizonDays int
|
||||
}
|
||||
|
||||
func New(st *store.Store, logger *slog.Logger, horizonDays int) *Engine {
|
||||
return &Engine{store: st, logger: logger, horizonDays: horizonDays}
|
||||
}
|
||||
|
||||
func (e *Engine) Store() *store.Store { return e.store }
|
||||
|
||||
// RecomputeDay calcule les snapshots de tous les comptes pour une date donnée.
|
||||
// Toutes les transactions (validées ou non) sont incluses.
|
||||
func (e *Engine) RecomputeDay(ctx context.Context, date time.Time) error {
|
||||
date = date.Truncate(24 * time.Hour)
|
||||
|
||||
accountIDs, err := e.store.ListAllAccountIDs(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list accounts: %w", err)
|
||||
}
|
||||
|
||||
for _, accountID := range accountIDs {
|
||||
if err := e.recomputeAccount(ctx, accountID, date); err != nil {
|
||||
e.logger.Error("snapshot: account failed", "account_id", accountID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
e.logger.Info("snapshot: day computed", "date", date.Format("2006-01-02"), "accounts", len(accountIDs))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecomputeAccount recalcule les snapshots d'un compte depuis son invalidation
|
||||
// jusqu'à aujourd'hui + horizon. Efface l'invalidation une fois terminé.
|
||||
// Retourne (zero, zero, nil) si aucune invalidation n'est en attente.
|
||||
func (e *Engine) RecomputeAccount(ctx context.Context, accountID int32) (from, to time.Time, err error) {
|
||||
inv, found, err := e.store.GetInvalidation(ctx, accountID)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("get invalidation: %w", err)
|
||||
}
|
||||
if !found {
|
||||
return time.Time{}, time.Time{}, nil
|
||||
}
|
||||
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
horizon := today.AddDate(0, 0, e.horizonDays)
|
||||
|
||||
if err := e.backfillAccount(ctx, accountID, inv.RecomputeFrom, horizon); err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
|
||||
if err := e.store.ClearInvalidation(ctx, accountID); err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("clear invalidation: %w", err)
|
||||
}
|
||||
|
||||
return inv.RecomputeFrom, horizon, nil
|
||||
}
|
||||
|
||||
// DailySnapshot :
|
||||
// 1. Traite les invalidations en attente (jusqu'à today + horizon)
|
||||
// 2. Recalcule hier (consolide les transactions de la veille)
|
||||
// 3. Calcule le nouveau jour entrant dans la fenêtre (today + horizonDays)
|
||||
func (e *Engine) DailySnapshot(ctx context.Context) error {
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
yesterday := today.AddDate(0, 0, -1)
|
||||
newHorizonDay := today.AddDate(0, 0, e.horizonDays)
|
||||
|
||||
// 1. Invalidations
|
||||
if err := e.processInvalidations(ctx, newHorizonDay); err != nil {
|
||||
e.logger.Error("snapshot: invalidation processing failed", "error", err)
|
||||
}
|
||||
|
||||
// 2. Hier
|
||||
if err := e.RecomputeDay(ctx, yesterday); err != nil {
|
||||
e.logger.Error("snapshot: yesterday recompute failed", "error", err)
|
||||
}
|
||||
|
||||
// 3. Nouveau jour entrant dans la fenêtre
|
||||
if err := e.RecomputeDay(ctx, newHorizonDay); err != nil {
|
||||
e.logger.Error("snapshot: horizon day failed", "error", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) processInvalidations(ctx context.Context, until time.Time) error {
|
||||
invalidations, err := e.store.GetInvalidations(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get invalidations: %w", err)
|
||||
}
|
||||
if len(invalidations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.logger.Info("snapshot: processing invalidations", "count", len(invalidations))
|
||||
|
||||
for _, inv := range invalidations {
|
||||
if err := e.backfillAccount(ctx, inv.AccountID, inv.RecomputeFrom, until); err != nil {
|
||||
e.logger.Error("snapshot: backfill failed", "account_id", inv.AccountID, "error", err)
|
||||
continue
|
||||
}
|
||||
if err := e.store.ClearInvalidation(ctx, inv.AccountID); err != nil {
|
||||
e.logger.Error("snapshot: clear invalidation failed", "account_id", inv.AccountID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) backfillAccount(ctx context.Context, accountID int32, from, to time.Time) error {
|
||||
from = from.Truncate(24 * time.Hour)
|
||||
to = to.Truncate(24 * time.Hour)
|
||||
|
||||
if from.After(to) {
|
||||
return fmt.Errorf("from (%s) after to (%s)", from.Format("2006-01-02"), to.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
for d := from; !d.After(to); d = d.AddDate(0, 0, 1) {
|
||||
if err := e.recomputeAccount(ctx, accountID, d); err != nil {
|
||||
return fmt.Errorf("recompute %s: %w", d.Format("2006-01-02"), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) recomputeAccount(ctx context.Context, accountID int32, date time.Time) error {
|
||||
baseDate, hasBase, err := e.store.GetLatestSnapshotDate(ctx, accountID, date)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get latest snapshot date: %w", err)
|
||||
}
|
||||
|
||||
positions := map[int32]store.PositionRow{}
|
||||
if hasBase {
|
||||
base, err := e.store.GetBasePositions(ctx, accountID, baseDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get base positions: %w", err)
|
||||
}
|
||||
for _, p := range base {
|
||||
positions[p.InstrumentID] = p
|
||||
}
|
||||
}
|
||||
|
||||
deltas, err := e.store.ComputeDeltas(ctx, accountID, baseDate, date)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compute deltas: %w", err)
|
||||
}
|
||||
|
||||
for _, d := range deltas {
|
||||
p := positions[d.InstrumentID]
|
||||
p.InstrumentID = d.InstrumentID
|
||||
p.InstrumentType = d.InstrumentType
|
||||
p.Quantite += d.Quantite
|
||||
positions[d.InstrumentID] = p
|
||||
}
|
||||
|
||||
var totalValeur float64
|
||||
|
||||
for _, pos := range positions {
|
||||
if pos.Quantite <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
prix, found, err := e.store.GetPriceAt(ctx, pos.InstrumentID, date)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
valeur := pos.Quantite * prix
|
||||
totalValeur += valeur
|
||||
|
||||
snap := store.PositionSnapshot{
|
||||
Date: date,
|
||||
AccountID: accountID,
|
||||
InstrumentID: pos.InstrumentID,
|
||||
Quantite: pos.Quantite,
|
||||
PrixCloture: &prix,
|
||||
Valeur: &valeur,
|
||||
}
|
||||
if err := e.store.UpsertPositionSnapshot(ctx, snap); err != nil {
|
||||
return fmt.Errorf("upsert position snapshot: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return e.store.UpsertAccountSnapshot(ctx, store.AccountSnapshot{
|
||||
Date: date,
|
||||
AccountID: accountID,
|
||||
Valeur: totalValeur,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue