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
tiersrempli.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 = trueuniquement.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) :
RĂ©solution ISIN â ticker via OpenFIGI, avec cache (les mappings ne changent pas).
Cours actions/ETF via Yahoo Finance.
Cours crypto via une API type CoinGecko.
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_snapshotrend 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)#
Ingestion des prix de clĂŽture dans
price_history.Calcul des positions : quantitĂ©s et PRU par compte/instrument depuis les transactions validĂ©es â Ă©criture des
position_snapshotdu jour, puis recalcul de la fenĂȘtre de projection J+1 â J+X (transactions validĂ©es + non validĂ©es).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é :
Socle : repo, docker-compose, base PostgreSQL, framework, migrations.
Schéma et migrations : les 8 tables, contraintes CHECK (rÚgle source/dest),
NUMERICpartout pour les montants, index sur (account_id, instrument_id, date) des snapshots et sur la date des transactions.CRUD comptes, instruments, transactions, enveloppes + validations applicatives (enveloppes †solde, cohĂ©rence source/dest). â Ă ce stade, base fonctionnelle en saisie manuelle.
Pipeline de prix : OpenFIGI (cache) + Yahoo Finance + CoinGecko â
price_history.Moteur de snapshots : fonction
recompute_position, job quotidien trois passes, mode backfill, gestion du dernier prix connu.Moteur de récurrence : génération idempotente via
generated_until, RRULE (lib typedateutil.rrule), action de rĂ©gĂ©nĂ©ration explicite, liste « Ă traiter » des transactions Ă©chues non validĂ©es.Endpoints de restitution : historique par compte/position/agrĂ©gĂ©, projection, dĂ©tail enveloppes â de simples SELECT sur les snapshots.
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.