Des exercices à une bibliothèque
Vous avez écrit une centaine de petites fonctions. Une bibliothèque est l'agencement de ces fonctions pour que quelqu'un d'autre (vous dans six mois compris) puisse s'en servir en confiance : une interface publique claire, chaque pièce testée seule, les pièces composées en pipeline, le tout installable, documenté et vérifié automatiquement. Ce projet de synthèse est le modèle en miniature. Une bibliothèque de stratégie est le meilleur endroit pour s'y exercer : le signal transforme les prix en positions, le backtest transforme les positions en rendements après coûts, les métriques résument les rendements, et un exécuteur relie le tout. Chaque étape prend des tableaux simples et en renvoie, si bien que chacune peut être testée seule avec des nombres que l'on vérifie à la main.
# The shape of a small library: a package, its tests, its metadata and its automated checks
tree = """\
quantlib/
├── pyproject.toml name, version, dependencies (numpy)
├── src/quantlib/
│ ├── __init__.py the public API: sma_positions, strategy_returns, performance_summary, run_strategy
│ ├── signals.py prices -> positions
│ ├── backtest.py positions -> returns after costs
│ └── metrics.py returns -> total return, volatility, Sharpe, drawdown
├── tests/ one test file per module, fixed seeds, hand-checked cases
└── .github/workflows/ci.yml lint, types, tests on every push
"""
print(tree.splitlines()[0]) # quantlib/
print(sum(1 for line in tree.splitlines() if ".py" in line)) # 4 the four module filesSignal et backtest : pas d'anticipation, coûts inclus
Le signal est ici un croisement de moyennes mobiles : être long quand la moyenne rapide des prix est au-dessus de la lente, short quand elle est en dessous, neutre quand elles sont égales ou que la lente n'existe pas encore. Les deux règles qui décident si un backtest est honnête se voient dans le code. D'abord, pas d'anticipation : la position décidée à la clôture du jour t ne peut utiliser que les prix jusqu'au jour t, et elle gagne le rendement du jour t + 1, c'est pourquoi le rendement du pas t utilise positions[t - 1]. Un test le prouve : retirer les derniers prix ne doit pas changer les positions antérieures. Ensuite, les coûts : chaque changement de position paie cost * |changement|, si bien qu'une stratégie qui bascule sans cesse peut gagner avant coûts et perdre après.
# The signal and the honest backtest: positions decided at the close are earned on the NEXT step, and trading costs money
import numpy as np
def sma_positions(prices, fast, slow):
prices = np.asarray(prices, dtype=float)
positions = np.zeros(len(prices), dtype=int)
for t in range(slow - 1, len(prices)):
f = prices[t - fast + 1 : t + 1].mean()
s = prices[t - slow + 1 : t + 1].mean()
positions[t] = 1 if f > s else -1 if f < s else 0
return positions
def strategy_returns(prices, positions, cost):
returns = []
for t in range(1, len(prices)):
before = positions[t - 2] if t >= 2 else 0
returns.append(positions[t - 1] * (prices[t] / prices[t - 1] - 1) - cost * abs(positions[t - 1] - before))
return np.array(returns)
prices = np.array([100, 101, 103, 102, 105, 108, 107, 110, 112, 111, 115, 118], dtype=float)
positions = sma_positions(prices, 2, 4)
print(positions) # [0 0 0 1 1 1 1 1 1 1 1 1] flat until the 4-day average exists, then long
print(np.allclose(sma_positions(prices[:8], 2, 4), positions[:8])) # True later prices never change earlier positions
print(np.round(strategy_returns(prices, positions, 0.0).sum() - strategy_returns(prices, positions, 0.001).sum(), 3)) # 0.001 one purchase at 0.1%Les chiffres de performance, et l'exécuteur
Une série de rendements se résume en quelques chiffres. Le rendement total compose les périodes, prod(1 + r) - 1 ; le rendement annuel le ramène à un an, (1 + total)^(périodes_par_an / n) - 1 ; la volatilité annuelle est l'écart-type d'échantillon fois sqrt(périodes_par_an) ; le ratio de Sharpe divise le rendement moyen par cet écart-type, annualisé, et c'est la mesure phare du rendement par unité de risque (0 pour une série plate plutôt qu'une erreur de division) ; le drawdown maximal est la pire chute depuis un sommet de la courbe de capital, le chiffre qui dit combien de douleur la stratégie vous demande d'encaisser. Une fonction exécuteur relie le tout : positions, puis rendements, puis résumé. Elle ne doit que composer les autres fonctions, pour qu'un bug se retrouve toujours dans une étape.
# Performance statistics of a return series, and the runner that wires the three stages together
import math
import numpy as np
def performance_summary(returns, periods_per_year):
returns = np.asarray(returns, dtype=float)
n = len(returns)
equity = np.concatenate(([1.0], np.cumprod(1 + returns)))
total = float(equity[-1] - 1)
deviation = float(np.std(returns, ddof=1))
peak = np.maximum.accumulate(equity)
return {
"total_return": total,
"annual_return": float((1 + total) ** (periods_per_year / n) - 1),
"annual_vol": deviation * math.sqrt(periods_per_year),
"sharpe": float(returns.mean() / deviation * math.sqrt(periods_per_year)) if deviation > 0 else 0.0,
"max_drawdown": float(np.max((peak - equity) / peak)),
}
summary = performance_summary([0.10, -0.20, 0.05], 252)
print(round(summary["total_return"], 4), round(summary["max_drawdown"], 4)) # -0.076 0.2 equity 1 -> 1.1 -> 0.88 -> 0.924: a 20% fall from the peak
print(performance_summary([0.0, 0.0, 0.0], 252)["sharpe"]) # 0.0 no variation: a Sharpe of 0, not an error# The runner only composes: if a number looks wrong you can test each stage on its own
def run_strategy(prices, fast, slow, cost, periods_per_year=252):
positions = sma_positions(prices, fast, slow)
returns = strategy_returns(prices, positions, cost)
return performance_summary(returns, periods_per_year)
# (sma_positions, strategy_returns and performance_summary are the functions you wrote in this lab)
steps = ["sma_positions", "strategy_returns", "performance_summary"]
print(" -> ".join(steps)) # sma_positions -> strategy_returns -> performance_summary
print(len(steps), "stages, one result: a dict of five numbers") # 3 stages, one result: a dict of five numbersVotre projet de synthèse
Construisez maintenant le vôtre, dans votre propre dépôt. Choisissez une des deux directions. Une bibliothèque de valorisation : Black-Scholes et les grecques, un valorisateur Monte Carlo avec variables antithétiques, un solveur de volatilité implicite et la calibration d'un modèle sur un jeu de cotations. Ou une bibliothèque de stratégies : plusieurs signaux, un backtest avec coûts et limites de position, une évaluation walk-forward et un rapport. Dans les deux cas, livrez les mêmes choses : un paquet avec une API publique claire et des annotations de type ; des tests à graines fixées et cas vérifiés à la main ; un pyproject.toml et un README montrant comment installer et lancer un exemple ; un workflow de CI avec lint, types et tests ; une note de performance avec un goulot profilé ; et un court texte sur les hypothèses et ce qui casserait le modèle. Répartissez votre temps : concevez d'abord l'interface, construisez le plus petit pipeline qui marche, puis élargissez.
- Une bibliothèque est faite de petites fonctions testées composées en pipeline derrière une interface claire : signal -> positions -> rendements après coûts -> métriques, reliés par un exécuteur.
- Un backtest honnête n'a pas d'anticipation (la position à t utilise les prix jusqu'à t et gagne le rendement suivant) et facture des coûts à chaque changement de position.
- Résumez avec rendement total et annuel, volatilité, Sharpe et drawdown maximal ; puis livrez un paquet avec tests, CI, un README et un texte honnête.
Voilà qui termine le parcours. Vous avez maintenant le langage, les outils numériques, les briques de données de marché et d'exécution, les modèles et les habitudes d'ingénierie d'un développeur quant en activité. Gardez votre dépôt de synthèse : c'est la meilleure preuve des compétences acquises ici.
Lab : ce qui est noté
- Transformer une tendance en positions avec
sma_positions25 pts - Backtester avec coûts dans
strategy_returns25 pts - Juger le résultat avec
performance_summary25 pts - Tout relier dans
run_strategy25 pts