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.
