Aller au contenu

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 :

  1. Le développement et les tests initiaux sont effectués dans l'environnement local.
  2. 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é.
  3. 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, sinon USD). 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 que an_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, valeur reste le prix natif et devise reflè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, ou NULL lorsqu'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 : BUY pour un achat ou SELL pour 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 SELL ne peut pas être ajoutée avec une date antérieure au premier BUY du titre;
  • une transaction BUY peut être ajoutée avec une date antérieure au premier BUY existant, 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 BUY ferait passer le total de Shares sous zéro à cause des SELL ultérieurs, la suppression est refusée et un message en explique précisément la raison;
  • si l'administrateur tente de supprimer l'unique BUY alors qu'une ou plusieurs transactions SELL existent encore, l'application lui demande de supprimer d'abord toutes les ventes;
  • si l'unique BUY est 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 dans logs/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.

  1. 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/eod pour récupérer les prix de clôture de date_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.
  2. 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_csv standardise 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) est USD alors que sa devise de portefeuille (déduite par devise_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é :

  1. 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.
  2. Appel à l'API Exchange Rates Data (APILayer) : effectué uniquement si le cache est absent ou expiré, via fetch_exchange_rate().
  3. 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 valeur usd_to_cad_rate du fichier config.ini.
  4. 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_rate déjà présente dans config.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 table historique est 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=False cré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 commande UPSERT ne 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 un Dockerfile, 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 push sur la branche main.
  • 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âche test réussit, le processus se connecte au VPS de production via SSH. Il exécute ensuite un script qui orchestre le déploiement :

    1. git pull pour récupérer le code validé.
    2. docker compose up -d --build pour 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-mock pour 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émentation pytest via 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é.