Definition du projet#

Rapport de synthùse — conception du backend, juin 2026.

1. Vision et périmÚtre#

Application personnelle de suivi de patrimoine avec valorisation automatique quotidienne, couvrant trois classes d’actifs : monĂ©taire (comptes courants, Livret A, PEL
), titres (actions, ETF
) et crypto.

Principes directeurs retenus au fil de la conception :

  • Tout est instrument, y compris la monnaie (modĂšle inspirĂ© de Beancount/Ledger). L’euro est un instrument dont le prix vaut toujours 1. Un compte courant est un compte ne contenant qu’une position EUR ; un PEA contient une position EUR (espĂšces) et des positions ETF ; un wallet contient des positions crypto. Bonus : les devises Ă©trangĂšres sont gĂ©rĂ©es nativement (USD = instrument cotĂ© en EUR via price_history).

  • PrĂ©-calcul systĂ©matique : la valeur de chaque position et de chaque compte est calculĂ©e et stockĂ©e jour par jour en base (snapshots). Toute lecture (graphique, historique) est un simple SELECT, jamais de recalcul Ă  la volĂ©e.

  • Projection vers le futur : les comptes sont aussi valorisĂ©s sur un horizon de J+X jours grĂące aux transactions futures, pour anticiper les Ă©chĂ©ances.

  • Pas de table d’état Ă  maintenir : les positions (quantitĂ©s, PRU) sont entiĂšrement dĂ©rivĂ©es des transactions et figĂ©es dans les snapshots.

2. ModĂšle de transaction#

Une transaction est un échange atomique sur une seule ligne, modÚle source/dest généralisé aux instruments :

transaction(
  id,
  date,
  account_source_id,    -- nullable
  instrument_source_id,
  quantite_source,
  account_dest_id,      -- nullable
  instrument_dest_id,
  quantite_dest,
  tiers,                -- nullable, pour les flux externes
  label,
  categorie,
  envelope_id,          -- nullable
  validated,            -- booléen
  recurring_rule_id     -- nullable, lien vers la rÚgle génératrice
)

Cas couverts par ce modĂšle unique :

  • Virement : instrument source = instrument dest = EUR, mĂȘmes quantitĂ©s, deux comptes renseignĂ©s.

  • Flux externe (salaire, dĂ©pense) : un seul cĂŽtĂ© renseignĂ©, l’autre entiĂšrement null, champ tiers rempli.

  • Achat de titres : source = (compte, EUR, 950), dest = (compte, ETF, 10). Le prix unitaire est implicite (quantite_source / quantite_dest). L’atomicitĂ© est structurelle : impossible d’avoir une jambe orpheline.

  • Swap crypto : source = (wallet, BTC, 0.01), dest = (wallet, ETH, 0.15).

Frais : pas de gestion dĂ©diĂ©e. Les frais sont absorbĂ©s dans les quantitĂ©s Ă©changĂ©es, donc dans le PRU — cohĂ©rent avec la convention fiscale française.

Contrainte CHECK : si les deux comptes sont renseignĂ©s, les deux triplets (instrument, quantitĂ©) le sont aussi ; si un seul cĂŽtĂ© existe, l’autre est entiĂšrement null.

Les quantités sont en NUMERIC à précision généreuse (crypto : 8+ décimales, parts fractionnées), jamais de float pour des valeurs financiÚres.

3. Cycle de vie des transactions : le flag validated#

MĂ©canisme central de l’application, type pointage bancaire :

  • Une saisie manuelle d’une opĂ©ration passĂ©e naĂźt avec validated = true.

  • Le moteur de rĂ©currence matĂ©rialise rĂ©ellement les occurrences futures en base avec validated = false. Chaque occurrence a une identitĂ© propre : elle peut ĂȘtre modifiĂ©e (date, montant), supprimĂ©e, puis validĂ©e individuellement selon les Ă©vĂ©nements rĂ©els.

  • Une seule colonne date, modifiable tant que la transaction n’est pas validĂ©e (pas de distinction date prĂ©vue / date effective — l’objectif est l’historique du compte, pas le suivi des Ă©carts).

  • Une transaction non validĂ©e dont la date est dĂ©passĂ©e ne disparaĂźt pas : elle reste comptĂ©e dans la projection et remonte dans une liste « Ă  traiter » jusqu’à validation ou suppression. Cette liste constituera l’écran de pointage quotidien cĂŽtĂ© frontend.

RĂšgle de calcul des snapshots :

  • Snapshots passĂ©s et du jour : transactions validated = true uniquement.

  • Snapshots projetĂ©s (J+1 → J+X) : transactions validĂ©es et non validĂ©es.

4. Récurrences#

recurring_rule(
  id,
  account_source_id, instrument_source_id, quantite_source,
  account_dest_id, instrument_dest_id, quantite_dest,
  tiers, label, categorie, envelope_id,
  frequence,            -- idéalement RRULE (RFC 5545) : couvre hebdo, bimensuel,
                        -- mensuel, trimestriel, semestriel, annuel, fins de mois...
  date_debut, date_fin,
  generated_until       -- curseur d'idempotence
)

Le job de gĂ©nĂ©ration maintient un horizon : il s’assure qu’il existe toujours des transactions gĂ©nĂ©rĂ©es jusqu’à J+X, et ne crĂ©e que les occurrences au-delĂ  de generated_until. Ce curseur garantit l’idempotence sans jamais regarder les transactions existantes : une fois gĂ©nĂ©rĂ©es, elles vivent leur vie (modifiables, supprimables) sans que le job s’en soucie.

Conséquence assumée : modifier une rÚgle ne régénÚre pas automatiquement les occurrences déjà créées. Prévoir une action explicite « régénérer les occurrences futures non validées de cette rÚgle » (suppression + recréation). Comportement prévisible, pas de magie.

5. Comptes et enveloppes#

account(id, nom, type, devise_reference, plafond?, taux?)
envelope(id, account_id, nom, objectif, montant_alloue)
  • Un compte ne porte aucun solde : c’est un pur conteneur de positions, sa valeur est entiĂšrement dĂ©rivĂ©e.

  • Les enveloppes sont strictement monĂ©taires : ventilation logique de la position EUR d’un compte pour prĂ©parer des projets. Jamais de positions titres/crypto en enveloppe (ventiler des parts fluctuantes serait un cauchemar comptable). Validation applicative : la somme des enveloppes ne dĂ©passe pas le solde du compte.

  • Pas de contrainte ``allowed_instrument_types`` pour l’instant (YAGNI) : usage mono-utilisateur, garde-fou ajoutable plus tard par migration triviale si besoin. Si elle est ajoutĂ©e un jour, ce sera une liste de types par type de compte (un PEA autorise {devise, action, ETF}) — l’euro dans un compte titres reste toujours possible.

6. Instruments et prix#

instrument(id, type, code, devise_cotation)
  -- type : devise, action, etf, crypto...
  -- code : EUR, ISIN, ticker...

price_history(instrument_id, date, prix)

Pipeline de prix (rĂ©utilise l’existant du projet de portfolio) :

  1. RĂ©solution ISIN → ticker via OpenFIGI, avec cache (les mappings ne changent pas).

  2. Cours actions/ETF via Yahoo Finance.

  3. Cours crypto via une API type CoinGecko.

  4. EUR : prix = 1, en dur ou ligne fixe.

Jours sans cotation (week-ends, fériés) : jours normaux sans mouvement. Le job de snapshot tourne tous les jours et utilise le dernier prix connu (SELECT prix WHERE date <= jour ORDER BY date DESC LIMIT 1, ou window function en backfill). Les quantités, elles, suivent normalement les transactions (un virement ou un trade crypto peut exister un dimanche).

7. Snapshots Ă  deux niveaux#

position_snapshot(date, account_id, instrument_id, quantite, pru, prix_cloture, valeur)
account_snapshot(date, account_id, valeur)   -- ÎŁ des position_snapshots du jour
  • Le grain fin (position_snapshot) donne l’historique de chaque ligne par construction : quantitĂ© de BTC, valeur de la ligne MSCI World jour par jour.

  • Stocker quantitĂ© et valeur est volontairement redondant avec price_history : la valorisation du jour est figĂ©e, insensible aux corrections ultĂ©rieures de prix, et lisible sans jointure.

  • account_snapshot rend la lecture des graphiques et de l’agrĂ©gat patrimoine total triviale.

  • Pour la projection (J+1 → J+X), le prix utilisĂ© pour les titres/crypto est simplement le dernier connu.

PRU#

HistorisĂ© jour par jour dans position_snapshot → la plus-value latente quotidienne s’obtient sans jointure : valeur − quantite × pru.

Calcul en moyenne pondérée :

  • À chaque achat : PRU = (qte_avant × PRU_avant + qte_achetee × prix_implicite) / qte_totale, avec prix implicite = quantite_source / quantite_dest de la transaction.

  • À chaque vente : le PRU ne bouge pas, seule la quantitĂ© diminue.

  • Position revenue Ă  zĂ©ro puis rachetĂ©e : le PRU repart de zĂ©ro (pas de moyenne avec l’historique mort).

Moteur de recalcul unique#

Le PRU et les quantitĂ©s Ă©tant des Ă©tats cumulatifs, toute modification (INSERT/UPDATE/DELETE) d’une transaction validĂ©e antidatĂ©e invalide les snapshots de la position concernĂ©e depuis la date de la transaction modifiĂ©e. Une fonction unique recompute_position(account_id, instrument_id, from_date) rejoue les transactions chronologiquement, réécrit les position_snapshot puis les account_snapshot en cascade. Le mĂȘme moteur sert au backfill initial et aux corrections. Les snapshots projetĂ©s sont invalidĂ©s et recalculĂ©s Ă  chaque changement de rĂšgle ou de transaction future.

8. Job quotidien (pipeline en trois passes)#

  1. Ingestion des prix de clĂŽture dans price_history.

  2. Calcul des positions : quantitĂ©s et PRU par compte/instrument depuis les transactions validĂ©es → Ă©criture des position_snapshot du jour, puis recalcul de la fenĂȘtre de projection J+1 → J+X (transactions validĂ©es + non validĂ©es).

  3. Agrégation en account_snapshot.

Plus le job de gĂ©nĂ©ration des rĂ©currences (maintien de l’horizon generated_until) et la matĂ©rialisation dĂ©jĂ  couverte par le modĂšle validated.

9. Schéma final : 8 tables#

instrument, price_history, account, envelope, transaction, recurring_rule, position_snapshot, account_snapshot.

ChaĂźne logique : recurring_rule → transaction (Ă©change source/dest, cycle validated) → positions calculĂ©es → position_snapshot (valorisĂ© via price_history) → account_snapshot.

10. Stack et étapes de mise en place#

Socle : PostgreSQL (TimescaleDB prématuré pour du quotidien mono-utilisateur), API REST ou GraphQL (FastAPI, Go, NestJS
 au choix), docker-compose dÚs le départ, outil de migration (Alembic, Flyway
).

Ordre de réalisation recommandé :

  1. Socle : repo, docker-compose, base PostgreSQL, framework, migrations.

  2. Schéma et migrations : les 8 tables, contraintes CHECK (rÚgle source/dest), NUMERIC partout pour les montants, index sur (account_id, instrument_id, date) des snapshots et sur la date des transactions.

  3. CRUD comptes, instruments, transactions, enveloppes + validations applicatives (enveloppes ≀ solde, cohĂ©rence source/dest). → À ce stade, base fonctionnelle en saisie manuelle.

  4. Pipeline de prix : OpenFIGI (cache) + Yahoo Finance + CoinGecko → price_history.

  5. Moteur de snapshots : fonction recompute_position, job quotidien trois passes, mode backfill, gestion du dernier prix connu.

  6. Moteur de récurrence : génération idempotente via generated_until, RRULE (lib type dateutil.rrule), action de régénération explicite, liste « à traiter » des transactions échues non validées.

  7. Endpoints de restitution : historique par compte/position/agrĂ©gĂ©, projection, dĂ©tail enveloppes — de simples SELECT sur les snapshots.

  8. Tests et durcissement : tests prioritaires sur le moteur de rĂ©currence (idempotence, bimensuel, fins de mois, annĂ©es bissextiles, prĂ©servation des occurrences Ă©ditĂ©es) et sur les recalculs rĂ©troactifs (PRU, position Ă  zĂ©ro puis rachat, transaction antidatĂ©e). SĂ©curitĂ© : donnĂ©es financiĂšres personnelles → authentification dĂšs le backend mĂȘme mono-utilisateur, pas de clĂ©s API en dur (architecture Bitwarden/Vault existante), chiffrement au repos si hĂ©bergement externe.

Le frontend sera traité ultérieurement ; le modÚle validated fournit déjà nativement le futur écran de pointage.

11. Décisions actées et questions ouvertes#

ActĂ© : modĂšle Ă©change source/dest une ligne ; pas de gestion des frais ; une seule colonne date ; PRU historisĂ© dans les snapshots avec recalcul sur correction ; pas de contrainte de types d’instruments par compte ; jours sans cotation = jours normaux, dernier prix connu ; enveloppes strictement monĂ©taires ; idempotence par generated_until.

À trancher au dĂ©marrage : choix du framework backend ; valeur de l’horizon X de projection ; format exact des frĂ©quences (RRULE complet vs enum simple) ; politique de purge Ă©ventuelle des snapshots projetĂ©s.