Prüfungen, die von selbst laufen
Tests schützen nur, wenn sie jedes Mal von etwas ausgeführt werden, das nicht Ihr Gedächtnis ist. Continuous Integration (CI) ist ein Server, der bei jedem Push und Pull Request den Code in einer sauberen Umgebung auscheckt und die definierten Prüfungen ausführt: Lint, Typprüfung, Tests, einen kleinen Backtest. Scheitert etwas, wird die Änderung blockiert. Continuous Delivery/Deployment (CD) geht weiter: Bestehen die Prüfungen auf dem Hauptzweig, wird die neue Version automatisch gebaut und veröffentlicht. In der Quant-Arbeit ist der Einsatz konkret: Ein Refactoring, das einen Preis still um einen Basispunkt verschiebt, sollte eine Maschine fangen, bevor es ein Portfolio erreicht.
# 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')Was eine Quant-Pipeline prüft
Eine typische Pipeline hat Stufen, die schnell scheitern: Lint (Stil und offensichtliche Fehler) und Typprüfung dauern Sekunden, dann die Unit-Tests, dann langsamere Prüfungen wie ein kleiner Regressions-Backtest, der mit einem gespeicherten Ergebnis verglichen wird. Machen Sie die Prüfungen deterministisch: feste Zufalls-Seeds, kein Netz, keine Abhängigkeit vom heutigen Datum und Daten aus einem kleinen eingecheckten Snapshot statt eines Live-Feeds. Führen Sie sie auf einer Matrix aus Python-Versionen und Betriebssystemen aus, die Sie zu unterstützen behaupten. Für schnelles Feedback führen Sie nur die Tests aus, die eine Änderung betreffen kann (Testauswahl), mit sicherem Rückfall auf die gesamte Suite, wenn man es nicht sagen kann.
# 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 anythingGates, Secrets und die Workflow-Datei
Die Pipeline wird in einer Datei im Repository beschrieben, etwa .github/workflows/ci.yml für GitHub Actions: wann sie läuft (on:), die Jobs, die Matrix und die Schritte, wobei jeder Schritt entweder eine fertige Action mit uses: nutzt oder einen Befehl mit run: ausführt. Behandeln Sie die Datei wie Code, der wie jeder andere geprüft wird. Fügen Sie ein Deploy-Gate hinzu: Nur der Zweig main deployt, und nur wenn jeder erforderliche Job erfolgreich war (ein übersprungener oder fehlender Job ist kein Erfolg). Schützen Sie den Zweig, damit er nicht ohne bestandene Prüfungen gemergt wird. Halten Sie API-Schlüssel und Passwörter in den Secrets des CI-Systems, nie im Repository, und geben Sie jedem Job nur die nötigen Rechte. Veröffentlichen Sie zuerst in einer Paper-Trading- oder Staging-Umgebung.
# 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- CI führt Lint, Typprüfung und Tests bei jedem Push in einer sauberen Umgebung aus; CD veröffentlicht nur automatisch, wenn sie auf
mainbestehen. - Machen Sie Prüfungen deterministisch (Seeds, Snapshots, kein Netz), führen Sie sie auf einer Matrix aus Versionen und Systemen aus und wählen Sie Tests nach dem Geänderten.
- Beschreiben Sie die Pipeline in einer geprüften Workflow-Datei, knüpfen Sie Deploys an
mainund erforderliche Jobs, halten Sie Secrets im CI-System und reparieren Sie instabile Tests.
Als Nächstes: geplante Pipelines, Daten- und Trading-Jobs zur richtigen Zeit in der richtigen Reihenfolge ausführen.
Lab: Was bewertet wird
- Die Testmatrix mit
matrix_jobsaufspannen25 Pkt. - Mit
select_testsnur Relevantes ausführen25 Pkt. - Das Release mit
can_deployabsichern25 Pkt. - Den Workflow mit
workflow_yamlerzeugen25 Pkt.