Des contrôles qui s'exécutent tout seuls
Les tests ne vous protègent que s'ils sont exécutés, à chaque fois, par autre chose que votre mémoire. L'intégration continue (CI) est un serveur qui, à chaque push et pull request, récupère le code dans un environnement propre et exécute les contrôles définis : lint, vérification des types, tests, petit backtest. Si quelque chose échoue, la modification est bloquée. La livraison/déploiement continu (CD) va plus loin : quand les contrôles passent sur la branche principale, la nouvelle version est construite et publiée automatiquement. En quant, l'enjeu est concret : un refactoring qui décale discrètement un prix d'un point de base doit être attrapé par une machine avant d'atteindre un portefeuille.
# A test matrix: every operating system with every Python version, minus the combinations you skip
from itertools import product
os_list = ["linux", "macos", "windows"]
py_versions = ["3.11", "3.12"]
exclude = {("windows", "3.11")}
jobs = [(os_name, version) for os_name, version in product(os_list, py_versions) if (os_name, version) not in exclude]
print(len(jobs)) # 5
print(jobs[0], jobs[-1]) # ('linux', '3.11') ('windows', '3.12')Ce que vérifie un pipeline quantitatif
Un pipeline typique a des étapes qui échouent vite : le lint (style et erreurs évidentes) et la vérification des types prennent quelques secondes, puis les tests unitaires, puis des contrôles plus lents comme un petit backtest de régression comparé à un résultat enregistré. Rendez les contrôles déterministes : graines aléatoires fixées, pas de réseau, pas de dépendance à la date du jour, et des données issues d'un petit instantané versionné plutôt que d'un flux en direct. Exécutez-les sur une matrice de versions de Python et de systèmes que vous prétendez prendre en charge. Pour un retour rapide, n'exécutez que les tests qu'une modification peut affecter (sélection de tests), avec un repli sûr sur toute la suite quand on ne sait pas.
# Test selection: run only the tests a change can affect; an unknown file means "run everything"
def select_tests(changed_files, mapping):
chosen = set()
for path in changed_files:
matches = [tests for prefix, tests in mapping.items() if path.startswith(prefix)]
if not matches:
return sorted({test for tests in mapping.values() for test in tests})
for tests in matches:
chosen.update(tests)
return sorted(chosen)
mapping = {"src/pricing/": ["tests/test_bs.py", "tests/test_greeks.py"], "src/risk/": ["tests/test_var.py"]}
print(select_tests(["src/risk/var.py"], mapping)) # ['tests/test_var.py']
print(select_tests(["src/risk/var.py", "setup.cfg"], mapping)) # all three tests: setup.cfg could affect anythingGarde-fous, secrets et fichier de workflow
Le pipeline est décrit dans un fichier conservé dans le dépôt, par exemple .github/workflows/ci.yml pour GitHub Actions : quand il s'exécute (on:), les tâches, la matrice et les étapes, chaque étape faisant soit uses: une action toute prête, soit run: une commande. Traitez le fichier comme du code, relu comme tout autre. Ajoutez un garde-fou de déploiement : seule la branche main déploie, et seulement quand chaque tâche requise a réussi (une tâche sautée ou absente n'est pas un succès). Protégez la branche pour qu'elle ne soit pas fusionnée sans contrôles réussis. Gardez clés d'API et mots de passe dans les secrets du système de CI, jamais dans le dépôt, et donnez à chaque tâche seulement les permissions nécessaires. Publiez d'abord vers un environnement de paper trading ou de préproduction.
# The deploy gate: main branch only, and every required job must have succeeded
def can_deploy(results, required, branch):
return branch == "main" and all(results.get(job) == "success" for job in required)
print(can_deploy({"lint": "success", "tests": "success"}, ["lint", "tests"], "main")) # True
print(can_deploy({"lint": "success", "tests": "skipped"}, ["lint", "tests"], "main")) # False skipped is not success
print(can_deploy({"lint": "success"}, ["lint", "tests"], "main")) # False a missing job is not success
print(can_deploy({"lint": "success", "tests": "success"}, ["lint", "tests"], "dev")) # False only main deploys# The workflow file is plain text, so it can be generated and checked like any other output
def workflow_yaml(name, python_versions, command):
versions = ", ".join(f'"{version}"' for version in python_versions)
lines = [f"name: {name}", "on: [push, pull_request]", "jobs:", " test:", " runs-on: ubuntu-latest",
" strategy:", " matrix:", f" python-version: [{versions}]", " steps:",
" - uses: actions/checkout@v4", " - uses: actions/setup-python@v5", " with:",
" python-version: ${{ matrix.python-version }}", " - run: pip install -e .[test]",
f" - run: {command}"]
return "\n".join(lines) + "\n"
text = workflow_yaml("ci", ["3.11", "3.12"], "pytest -q")
print(text.splitlines()[7]) # python-version: ["3.11", "3.12"]
print(text.splitlines()[-1]) # - run: pytest -q- La CI exécute lint, vérification des types et tests dans un environnement propre à chaque push ; la CD ne publie automatiquement que s'ils passent sur
main. - Rendez les contrôles déterministes (graines, instantanés, pas de réseau), exécutez-les sur une matrice de versions et de systèmes, et sélectionnez les tests selon ce qui a changé.
- Décrivez le pipeline dans un fichier de workflow relu, conditionnez les déploiements à
mainet aux tâches requises, gardez les secrets dans le système de CI et corrigez les tests instables.
À suivre : les pipelines planifiés, exécuter les tâches de données et de trading au bon moment, dans le bon ordre.
Lab : ce qui est noté
- Développer la matrice de tests avec
matrix_jobs25 pts - N'exécuter que l'utile avec
select_tests25 pts - Garder la mise en production avec
can_deploy25 pts - Générer le workflow avec
workflow_yaml25 pts