Chapitre 2 : Architecture Technique
2.1. Vue d'ensemble de l'architecture
L'architecture du projet "Augmented Analyst" a été conçue pour garantir la robustesse, la fiabilité et l'automatisation. Elle repose sur une séparation claire des responsabilités entre trois environnements distincts : l'environnement de développement local, la plateforme d'intégration continue (CI/CD) et l'environnement de production.
Le flux de travail standard est le suivant :
- Le développement et les tests initiaux sont effectués dans l'environnement local.
- Chaque modification validée et "pushée" vers le dépôt Git déclenche le processus dans l'environnement de CI/CD, qui agit comme un portail de contrôle qualité.
- Si, et seulement si, la qualité est validée, le code est automatiquement déployé dans l'environnement de production, où il est accessible à l'utilisateur final.
Cette structure garantit qu'aucune modification ne peut atteindre la production sans avoir été testée et validée, assurant ainsi une stabilité maximale du service.
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: la quantité détenue.
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 est assurée par deux extensions Flask clés : * 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.
2.3. Le pipeline de données (ETL)
Le système est alimenté par un pipeline ETL (Extract, Transform, Load) robuste, orchestré par le script pipeline.py. Ce pipeline est 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
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)
L'application est hébergée 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).
2.5. Le système de qualité et de déploiement (CI/CD)
L'automatisation est au cœur du projet, gérée par un pipeline de CI/CD hébergé sur GitHub Actions.
2.5.1. Processus de déploiement (deploy.yml)
- Déclencheur : le processus est automatiquement lancé à chaque
pushsur la branchemain. - Job
test(intégration continue) : avant tout déploiement, le code est récupéré dans un environnement Linux éphémère. Une suite de tests complète est exécutée avec Pytest. Pour garantir une validation réaliste, ce job lance son propre service de base de données MariaDB temporaire. Si un seul test échoue, toute la pipeline s'arrête. -
Tâche
deploy(déploiement continu) : uniquement si la tâchetestréussit, 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