Chapitre 2 : Architecture Technique
2.1. Vue d'ensemble de l'architecture
L'architecture du projet "Augmented Investor" organise le travail entre la personne qui définit le besoin, l'assistant IA, l'environnement local de vérification, les contrôles GitHub et la production. Elle combine des étapes humaines et automatiques; l'assistant ne travaille pas sans consignes ni sans vérification.
Pour une modification fonctionnelle, le parcours standard est le suivant :
- Le propriétaire décrit le besoin, les résultats attendus et les cas importants; les décisions métier sont clarifiées.
- Spec Kit, l'outil de préparation du projet, formalise le besoin, propose un plan et une liste de tâches, puis vérifie que ces documents concordent.
- L'assistant réalise le travail par étapes, avec les tests de comportement écrits avant le code (TDD).
- Les résultats sont contrôlés localement par des tests et par la démonstration des vrais parcours avec des données séparées de la production. Le propriétaire vérifie et valide la démonstration.
- Après l'accord du propriétaire et la revue, le changement peut être intégré à
main, la version de référence du projet. - Cette intégration déclenche les tests du processus GitHub actuel, puis le déploiement automatique si ses contrôles réussissent.
Le parcours fournit plusieurs occasions de détecter les écarts avant la production. Toutefois, l'approbation locale du propriétaire est aujourd'hui une règle de travail, pas un verrou de déploiement imposé par GitHub : le workflow de production se déclenche sur un changement de main et ne demande pas séparément une approbation au moment du déploiement. La distinction doit rester claire entre une règle demandée et une protection techniquement activée.
2.2. L'application principale (Backend)
Le cœur logique de l'application est un back-end développé en Python, orchestré par le micro-cadre applicatif Flask.
2.2.1. Cadre applicatif
L'application est construite avec Flask. Le code suit le design pattern de l'Application Factory (via la fonction create_app). Cette approche consiste à encapsuler la création et la configuration de l'application dans une fonction, ce qui offre plusieurs avantages :
- Testabilité : permet de créer différentes instances de l'application pour différents contextes (production, test) avec des configurations distinctes.
- Organisation : évite les variables d'application globales et favorise une structure de code plus propre et plus modulaire.
2.2.2. Modèles et base de données
L'interaction avec la base de données est gérée par l'ORM SQLAlchemy. Le schéma est composé de trois tables principales :
user: stocke les informations d'authentification.titres: table de référence contenant la liste des actifs. Elle inclut :ticker(VARCHAR(255)): symbole unique du titre (ex:AAPL,BRK.B). Dimensionné pour accueillir les tickers de marché actuels et futurs.devise_portefeuille: devise native du titre au sein du portefeuille, déduite du préfixe du ticker (TSE:=>CAD, sinonUSD). C'est la seule source de vérité utilisée par l'application pour l'affichage natif.- Colonnes de seuils sur 52 semaines :
an_haut,an_bas: stockent la valeur brute du seuil, telle que fournie par le fichier source, sans aucune conversion.an_haut_cad,an_bas_cad: colonnes conservées pour compatibilité de schéma ; elles contiennent la même valeur brute quean_haut/an_bas. Les seuils 52 semaines ne sont jamais convertis, quelle que soit la devise du titre.
historique: enregistre la "photographie" de chaque titre pour chaque jour. Elle contient :valeur,devise: le prix du titre tel qu'affiché à l'utilisateur, et sa devise réelle. La conversion USD→CAD n'est appliquée que si le titre est coté en USD alors que sa devise de portefeuille (devise_portefeuille) est différente d'USD ; dans tous les autres cas,valeurreste le prix natif etdevisereflète la devise de portefeuille.cad_value: équivalent CAD conservé avec chaque relevé. Il est utilisé uniquement pour les totaux consolidés en C$, le graphique d'évolution du portefeuille et la performance consolidée ; jamais pour le prix natif d'un titre ni ses seuils 52 semaines.fx_rate_used: taux de change explicitement appliqué lors d'une conversion ciblée de prix USD vers CAD, ouNULLlorsqu'aucune conversion ciblée n'était nécessaire pour cet enregistrement.quantite: les actions comptabilisées à la date du relevé. Pour les nouveaux imports, elles proviennent des achats moins les ventes enregistrés jusqu'à cette date, jamais de la quantité CSV. Les anciens relevés ne sont pas recalculés en masse.
transactions: registre séparé des décisions d'achat et de vente de l'administrateur. Chaque ligne est rattachée à un titre et garde une date, un type, des actions, un prix unitaire et un total à deux décimales. La base conserve jusqu'à quatre décimales pour fidéliser les anciens prix et quantités du fichier fourni; les nouvelles saisies restent limitées à deux décimales. Cette table ne modifie ni l'historique des prix ni les indicateurs de marché.
2.2.2bis. Transactions d'un titre
La fiche de chaque titre contient maintenant deux lectures complémentaires :
- Historique des relevés : ce tableau montre ce que le marché a indiqué au fil du temps, par exemple le prix observé chaque jour.
- Transactions : ce tableau montre ce que l'administrateur a réellement fait sur le titre, par exemple acheter ou vendre des actions à une date donnée.
Cette séparation est volontaire. Les prix de marché servent à analyser la performance du titre; les transactions servent à comprendre les décisions d'achat et de vente. Modifier une transaction ne réécrit donc jamais l'historique des prix et ne change pas les indicateurs de marché.
Ce qu'une transaction contient
Chaque transaction indique :
- une date d'opération;
- un type d'opération :
BUYpour un achat ouSELLpour une vente; - le nombre d'actions;
- le prix unitaire;
- le montant total.
Le montant total n'est pas saisi par l'utilisateur. Il est toujours calculé par l'application au moment de l'enregistrement : nombre d'actions × prix unitaire. Cela évite qu'une erreur de saisie crée un total incohérent avec les champs visibles.
Règles d'ajout et de modification
L'administrateur peut ajouter ou corriger une transaction depuis la fiche du titre, mais l'application garantie la cohérence des transactions :
- la première transaction en date doit toujours être un
BUY, car les transactions sur un titre ne peuvent pas commencer par une vente; - une transaction
SELLne peut pas être ajoutée avec une date antérieure au premierBUYdu titre; - une transaction
BUYpeut être ajoutée avec une date antérieure au premierBUYexistant, car cela peut représenter une correction légitime de l'historique; - une transaction est refusée si, à sa date, elle ferait passer le nombre d'actions détenues sous zéro;
- les saisies faites dans l'interface sont limitées à deux décimales et 12 chiffres pour les quantités, les prix unitaires et le montant total.
Ces règles s'appliquent même lorsqu'une opération est ajoutée après coup avec une date passée. L'application ne regarde donc pas seulement l'ordre de saisie; elle rejoue les transactions dans leur ordre réel de date.
Lorsque plusieurs opérations portent exactement la même date, les achats BUY sont comptés avant les ventes SELL. Un achat effectué dans la journée peut ainsi couvrir une vente de cette même journée, sans dépendre de l'ordre dans lequel les lignes ont été saisies.
Règles de suppression
Le tableau doit toujours conserver au moins une transaction BUY tant que le titre existe. Cette règle évite d'afficher un titre sans opération d'achat fondatrice.
Quand l'administrateur clique sur Delete :
- si la transaction supprimée n’est pas l’unique transaction BUY restante du titre, la suppression peut être confirmée normalement;
- si supprimer un
BUYferait passer le total de Shares sous zéro à cause desSELLultérieurs, la suppression est refusée et un message en explique précisément la raison; - si l'administrateur tente de supprimer l'unique
BUYalors qu'une ou plusieurs transactionsSELLexistent encore, l'application lui demande de supprimer d'abord toutes les ventes; - si l'unique
BUYest la seule opération structurante restante et qu'aucune vente n'existe, l'application avertit que le titre complet sera supprimé; - dans ce dernier cas, Cancel annule l'action et rien ne change; Confirm supprime le titre, son historique de relevés et sa transaction restante.
Si plusieurs demandes tentent de modifier les transactions d'un même titre en même temps, l'application les traite une par une. Chaque demande vérifie le solde laissé par la précédente, ce qui empêche leur combinaison de produire un nombre d'actions négatif.
Affichage dans l'interface
Les transactions sont affichées de la plus récente à la moins récente. Ce choix rend la fiche plus utile au quotidien : l'utilisateur voit d'abord les opérations les plus proches de sa décision actuelle, tout en conservant l'historique complet dans le même tableau.
Version démonstration
La démonstration publique propose la même logique, mais uniquement avec des données isolées dans la session du visiteur. Elle permet de consulter, ajouter, modifier et supprimer des transactions sans toucher aux données sécurisées.
Si le visiteur confirme la suppression du dernier BUY d'un titre de démonstration, le titre est masqué temporairement dans cette session. Le résultat est visible une fois, puis le prochain rafraîchissement restaure le jeu de démonstration initial. Aucune écriture n'est faite dans la base de données sécurisée.
2.2.3. Gestion de l'Authentification
La sécurité de l'accès s'appuie sur Flask : * Flask-Login : gère le cycle de vie de la session utilisateur (connexion, déconnexion, protection des routes "privées"). * Flask-Bcrypt : assure le stockage sécurisé des mots de passe en ne sauvegardant que leur "hash" cryptographique, jamais le mot de passe en clair.
Connexion renforcée avec TOTP
Le formulaire unique contient l'identifiant, le mot de passe et un code TOTP à six chiffres. Le code change toutes les trente secondes ; une période de décalage avant ou après est tolérée. Les zéros initiaux sont conservés. Une période déjà acceptée, ou antérieure à celle-ci, ne peut plus ouvrir de session.
PyOTP vérifie le code. Fernet chiffre le facteur en base avec TOTP_ENCRYPTION_KEY, indépendante de SECRET_KEY, qui signe les sessions. Les clés viennent de l'environnement ; l'authentification ne lit pas de fichier .env. Une protection illisible refuse l'accès sans retour au mot de passe seul.
La préparation administrative dure dix minutes. Seul un premier code correct active le facteur. Un remplacement conserve l'ancien facteur jusqu'à confirmation du nouveau. Chaque confirmation révoque les sessions précédentes : leur génération est contrôlée à la requête suivante. Il n'existe ni code de secours ni route publique de récupération.
Cinq échecs par compte ou vingt par origine dans les cinq dernières minutes provoquent une attente de cinq minutes. Une réussite ne supprime pas les échecs encore dans cette fenêtre ; les demandes bloquées ne prolongent pas l'attente et ne consomment pas de code. L'origine provient de l'adresse fournie par le serveur et est conservée sous forme de pseudonyme, jamais comme adresse brute dans les événements d'authentification.
La consommation du code et les limites sont enregistrées ensemble avant la création de la session. Le service utilise une transaction dédiée : verrou de l'origine puis du compte sur MariaDB/InnoDB ; verrou d'écriture avant lecture sur SQLite fichier. Les tests SQLite ne démontrent pas le comportement concurrent de MariaDB.
Les formulaires de connexion et de déconnexion exigent un jeton de sécurité CSRF. La déconnexion utilise POST ; GET ne change pas l'état. Les retours de connexion sont limités aux chemins internes non ambigus. Les réponses d'authentification ne sont pas mises en cache. La déconnexion privée ne supprime pas les données de démonstration.
Au démarrage, la migration additive 0005_add_totp_auth.sql et les invariants du facteur sont contrôlés avant Gunicorn, même si l'amorçage facultatif des transactions échoue. Le schéma du portefeuille et ses règles financières restent indépendants de cette protection.
2.3. Le pipeline de données (ETL)
En complément du back-end Flask, un pipeline ETL (Extract, Transform, Load) actualise les données de marché. Il est orchestré par pipeline.py et conçu pour être indépendant de son heure d'exécution.
2.3.1. Logique Temporelle : Veille de marché
Le pipeline traite la veille de marché par rapport à la date/heure courante de Montréal, afin que les exécutions matinales ciblent systématiquement la dernière clôture disponible.
get_current_montreal_datetime(): le script détermine d'abord la date et l'heure dans le fuseau de Montréal (America/Montreal).date_a_traiter = veille_ouvrée: cette date devient la source de vérité pour toutes les opérations du pipeline (appels API, insertions en base de données).- Garde-fous opérationnels :
- Import bloqué le dimanche.
- Import bloqué le lundi par défaut.
- Rattrapage autorisé le lundi matin uniquement si la précédente exécution enregistrée est en échec.
- État persistant d'exécution : chaque run enregistre un statut (
success/failure) et une raison danslogs/import_run_state.json(ou chemin configuré) pour piloter l'autorisation du rattrapage du lundi.
2.3.2. Orchestration via run_full_pipeline
Les sources ont des rôles distincts : le registre détermine les actions détenues; le CSV maintient les titres suivis et leurs métriques; les cours sont récupérés selon le mode ci-dessous. Modifier une quantité dans le CSV ne crée pas de transaction et ne remplace pas le solde du registre. Les petites positions utilisent également ce solde, tout en conservant les pourcentages importés du CSV. Les autres métriques restent celles importées du CSV.
Un titre absent du fichier est supprimé avec ses transactions et relevés. Un nouveau titre peut être ajouté au suivi, mais reste à zéro action tant qu'aucun achat n'est enregistré. Un fichier vide conserve la protection existante : il ne provoque pas de suppression générale.
Le point d'entrée du processus est la fonction run_full_pipeline, qui accepte un argument booléen fetch_market_data.
-
Mode
fetch_market_data=True(comportement pour le cronjob quotidien) :- Extraction : Le pipeline lit le CSV de référence et appelle l'endpoint Marketstack
/v2/eodpour récupérer les prix de clôture dedate_a_traiter. - Objectif : N'insérer que des prix validés par l'API pour la date d'exécution. Les prix API manquants ne sont pas insérés et déclenchent une alerte opérateur.
- Extraction : Le pipeline lit le CSV de référence et appelle l'endpoint Marketstack
-
Mode
fetch_market_data=False(pour la synchronisation de portefeuille) :- Extraction : Le pipeline lit uniquement le fichier CSV. Aucun appel à Marketstack n'est effectué.
- Objectif : Permet une synchronisation immédiate de la structure de la base de données après une modification du portefeuille (ajout/suppression de titres). Il peut insérer des enregistrements temporaires basés sur le CSV pour
date_a_traiter.
2.3.3. Transformation (Transform)
- La fonction
read_and_clean_csvstandardise les noms de colonnes, gère les fichiers vides ou corrompus, et parse les informations complexes (comme la colonne "52 Week Range"). - Règle métier de conversion : la conversion de devise est ciblée et limitée au prix courant du marché. Elle ne s'applique que lorsque la devise du titre (
marketstack_currency) estUSDalors que sa devise de portefeuille (déduite pardevise_portefeuille(ticker)) est différente d'USD. Dans tous les autres cas, le prix reste affiché dans sa devise native. Les seuils 52 semaines (an_haut,an_bas) ne sont jamais convertis : ils restent strictement fidèles au fichier source, quelle que soit la devise du titre.
2.3.3bis. Gestion robuste du taux de change (fail-open)
Le taux USD→CAD utilisé pour la conversion ciblée est obtenu via get_effective_fx_rate, selon une logique conçue pour respecter un quota d'appels API limité :
- Cache local : si un taux a déjà été obtenu récemment (état persisté dans
logs/fx_rate_state.json, ou chemin configuré), il est réutilisé sans appel API. - Appel à l'API Exchange Rates Data (APILayer) : effectué uniquement si le cache est absent ou expiré, via
fetch_exchange_rate(). - Mise à jour automatique de la configuration : à chaque appel API réussi, le taux obtenu est persisté à la fois dans l'état local (
fx_rate_state.json) et dans la valeurusd_to_cad_ratedu fichierconfig.ini. - Repli en cascade (fail-open) : en cas d'échec de l'API, le pipeline retombe sur le dernier taux connu, puis, en dernier recours, sur la valeur
usd_to_cad_ratedéjà présente dansconfig.ini. L'import n'est jamais bloqué par une indisponibilité du service de taux de change.
2.3.4. Chargement (Load)
Les données transformées sont insérées dans la base de données MariaDB via des requêtes UPSERT (INSERT ... ON DUPLICATE KEY UPDATE).
- La clé unique
(titre_id, date_releve)de la tablehistoriqueest fondamentale. Elle garantit qu'il ne peut y avoir qu'un seul enregistrement par titre et par jour. - Scénario de mise à jour : si une exécution en mode
fetch_market_data=Falsecrée une entrée pour une date avec un prix temporaire, puis qu'une exécution ultérieure cible cette même date avec un prix API officiel, la commandeUPSERTne créera pas de doublon. Elle mettra à jour l'entrée existante.
2.4. L'infrastructure de production (VPS)
Le back-end et le pipeline de données fonctionnent sur un serveur privé virtuel (VPS) sous Ubuntu, avec une architecture moderne basée sur la conteneurisation.
2.4.1. Conteneurisation (Docker)
- Service
app: un conteneur Docker, construit sur mesure via unDockerfile, qui encapsule l'application Python. L'application est servie par un serveur de production WSGI, Gunicorn, optimisé pour gérer plusieurs requêtes simultanées. - Service
db: un conteneur officiel MariaDB 10.6, assurant un environnement de base de données stable et isolé. - Orchestration : l'ensemble des services est défini, configuré et lié par Docker Compose (via le fichier
docker-compose.yml), qui agit comme le chef d'orchestre de l'infrastructure. - Persistance des données : pour garantir qu'aucune donnée de la base de données ne soit perdue lors des mises à jour ou des redémarrages, les fichiers de MariaDB sont stockés dans un volume Docker nommé, qui est indépendant du cycle de vie du conteneur.
2.4.2. Serveur Web
Nginx est utilisé comme serveur web principal. Il joue le rôle de reverse proxy : * Il reçoit toutes les requêtes web entrantes sur les ports 80 (HTTP) et 443 (HTTPS). * Il transmet les requêtes à l'application tournant dans le conteneur Docker sur le port 8000. * Il gère la terminaison SSL/TLS, en servant les certificats gérés par Certbot pour assurer une connexion sécurisée (HTTPS).
La configuration TOTP limite le port applicatif publié à 127.0.0.1:8000:8000. AUTH_PROXY_HOPS=0 ignore les en-têtes de proxy. La valeur 1 ne doit être utilisée qu'après vérification du Nginx effectif et de l'inaccessibilité distante du backend ; seuls l'origine et le protocole sont alors confiés au proxy, jamais l'hôte. Hors mode local isolé, les facteurs sont refusés sur HTTP et les cookies de session sont sécurisés, HttpOnly et SameSite=Lax. Ces règles de configuration ne constituent pas une attestation de l'état du VPS.
2.5. Le système de qualité et de déploiement (CI/CD)
GitHub Actions coordonne la validation et la livraison de l'environnement de production au moyen d'un pipeline de CI/CD.
2.5.1. Processus de déploiement (deploy.yml)
- Déclencheur : le processus est automatiquement lancé à chaque
pushsur la branchemain. - Contrôles automatiques : GitHub exécute la suite Pytest avec une base MariaDB temporaire, puis des tests de parcours dans Google Chrome. Les tests navigateur couvrent le portefeuille local isolé et un parcours Marketstack. L'échec d'un contrôle bloque la suite du workflow.
-
Tâche
deploy(déploiement continu) : uniquement si les contrôlestestete2eréussissent, le processus se connecte au VPS de production via SSH. Il exécute ensuite un script qui orchestre le déploiement :git pullpour récupérer le code validé.docker compose up -d --buildpour reconstruire l'image de l'application avec le nouveau code et redémarrer les services sans interruption majeure.
2.5.2. Stratégie de Tests
- Cadre de test : Pytest, pour sa simplicité et sa puissance, ainsi que
pytest-mockpour simuler les appels externes. - Environnements de Test : Utilisation d'une base de données SQLite en mémoire pour les tests locaux rapides, et d'une vraie base de données MariaDB en CI pour une fiabilité maximale.
- Philosophie TDD : Le développement suit une approche de Test-Driven Development.
-
Gestion et Traçabilité des Tests : Le projet inclut un système de gestion des tests "docs-as-code" :
- Chaque cas de test est un fichier Markdown stocké dans le répertoire
test_cases/. - Un script Python (
scripts/sync_tests.py) assure la traçabilité en liant chaque cas de test à son implémentationpytestvia un marqueur@pytest.mark.test_id. - Un site web de rapport de couverture fonctionnelle est généré automatiquement avec MkDocs, fournissant une vue claire du statut de validation de chaque fonctionnalité.
- Chaque cas de test est un fichier Markdown stocké dans le répertoire
2.5.3. Le cadre de travail de l'assistant IA (harness engineering)
Dans ce projet, le harness engineering désigne le cadre qui guide l'assistant IA et vérifie les résultats produits. Le terme est récent et peut être employé différemment selon les organisations; il est utilisé ici au sens présenté par Birgitta Böckeler sur le site de Martin Fowler pour les assistants de programmation. Il s'agit d'un ensemble cohérent de consignes, de documents, d'outils et de contrôles, pas d'un développeur autonome.
| Partie du cadre | Rôle dans le projet |
|---|---|
| Le besoin métier | Le propriétaire décrit le problème, les résultats attendus et les cas qui influencent la décision. Les questions ambiguës sont clarifiées avant la réalisation. |
| Les règles et la mémoire du projet | Les consignes de l'assistant, les principes du projet et sa documentation rappellent les décisions prises, les limites et les vérifications attendues. |
| La préparation du travail | Spec Kit formalise le besoin, le plan et les tâches, puis vérifie leur cohérence avant la réalisation. |
| Les retours automatiques | Des tests vérifient les règles de calcul et les données; d'autres parcourent l'application dans Google Chrome sur une copie locale séparée de la production. Les problèmes détectés orientent les corrections. GitHub relance les contrôles prévus avant la livraison. |
| La validation humaine | Le propriétaire arbitre les choix métier et examine la démonstration locale, notamment les écrans liés et la conservation des modifications après navigation. Les tests réussis ne remplacent pas cette validation. |
| La livraison | Après intégration à main, les contrôles GitHub réussis déclenchent le déploiement automatique configuré. |
Ce cadre réduit les erreurs répétitives et rend le travail plus vérifiable. Il ne garantit pas que toute attente métier a été comprise : une spécification ou des tests peuvent eux-mêmes être incomplets. La supervision reste donc dirigée vers la définition du besoin, les résultats locaux et les décisions de livraison.
État du contrôle de production : le propriétaire doit valider la version locale avant l'intégration à main, conformément au processus du projet. Cette validation est actuellement une règle opérationnelle; le workflow GitHub déploie automatiquement à la suite d'un changement sur main si ses contrôles passent, sans étape distincte d'approbation manuelle. L'ajout d'un verrou technique d'approbation dans l'environnement GitHub est une amélioration possible, mais n'est pas décrite comme déjà mise en place.
Références : Birgitta Böckeler, Harness engineering for coding agent users, publié sur martinfowler.com (2 avril 2026); Anthropic, Building effective agents, notamment la distinction entre un travail organisé à l'avance et un agent qui choisit lui-même les étapes à suivre.