Présentation
QuantFinance est une bibliothèque Python complète pour la finance quantitative. Elle couvre le pricing d'instruments financiers, la gestion des risques, l'optimisation de portefeuille et le backtesting de stratégies.
Portefeuille
- Markowitz
- Risk Parity
- Black-Litterman
- HRP
- Max Diversification
Pricing
- Black-Scholes
- Binomial Tree
- Monte Carlo
- Options exotiques
- Obligations
Risque
- VaR (4 méthodes)
- CVaR / ES
- Sharpe, Sortino, Calmar
- Max Drawdown
- Stress Testing
Utilitaires
- Yahoo Finance
- Données synthétiques
- RSI, MACD, Bollinger
- Rééquilibrage
Installation
pip install quantfinance
git clone https://github.com/Mafoya1er/quantfinance.git cd quantfinance pip install -e .
pip install quantfinance[data] # Analyse de données pip install quantfinance[dev] # Développement pip install quantfinance[all] # Tout installer
Démarrage rapide
from quantfinance.pricing.options import BlackScholes from quantfinance.portfolio.optimization import PortfolioOptimizer from quantfinance.risk.var import VaRCalculator from quantfinance.utils.data import DataLoader # Pricing d'une option call bs = BlackScholes(S=100, K=105, T=1, r=0.05, sigma=0.25, option_type='call') print(f"Prix: {bs.price():.2f}, Delta: {bs.delta():.4f}") # Optimisation de portefeuille prices = DataLoader.generate_synthetic_prices(n_assets=5, n_days=756) returns = prices.pct_change().dropna() optimizer = PortfolioOptimizer(returns, risk_free_rate=0.02) result = optimizer.maximize_sharpe() print(f"Sharpe: {result['sharpe_ratio']:.3f}") # Value at Risk var_95 = VaRCalculator.historical_var(returns.iloc[:, 0], 0.95) print(f"VaR 95%: {var_95:.2%}")
quantfinance.portfolio
Module d'optimisation de portefeuille, d'allocation d'actifs et de backtesting.
Initialise l'optimiseur avec les rendements historiques et le taux sans risque.
from quantfinance.portfolio.optimization import PortfolioOptimizer optimizer = PortfolioOptimizer(returns_df, risk_free_rate=0.03)
Crée un portefeuille équipondéré (poids égaux pour chaque actif).
result = optimizer.equal_weight() print("Poids:", result['weights']) print("Rendement:", result['return'])
Minimise la volatilité du portefeuille — portefeuille à variance minimale.
result = optimizer.minimize_volatility() print(f"Volatilité minimale: {result['volatility']:.2%}") print(f"Rendement associé: {result['return']:.2%}") print(result['weights'])
Maximise le ratio de Sharpe — point tangent de la frontière efficiente.
result = optimizer.maximize_sharpe() print(f"Sharpe max: {result['sharpe_ratio']:.3f}") print(f"Rendement: {result['return']:.2%}") print(f"Volatilité: {result['volatility']:.2%}")
Maximise le rendement sous contrainte de volatilité cible. Retourne un objet résultat scipy avec les poids optimaux dans result.x.
result = optimizer.maximize_return(target_volatility=0.18) weights = pd.Series(result.x, index=optimizer.assets) ret, vol, sharpe = optimizer.portfolio_performance(weights) print(f"Rendement max (vol≤18%): {ret:.2%}")
Portefeuille de parité de risque — chaque actif contribue également au risque total.
result = optimizer.risk_parity() print("Contribution au risque:", result['risk_contribution']) print("Poids:", result['weights'])
Calcule les métriques annualisées d'un portefeuille : rendement, volatilité et ratio de Sharpe.
import numpy as np weights = np.array([0.25, 0.25, 0.25, 0.25]) ret, vol, sharpe = optimizer.portfolio_performance(weights) print(f"Rendement: {ret:.2%} | Volatilité: {vol:.2%} | Sharpe: {sharpe:.3f}")
Utilitaires bas niveau — calcule séparément le rendement espéré ou la volatilité d'un portefeuille donné.
weights = np.array([0.4, 0.3, 0.2, 0.1]) print(f"Rendement: {optimizer.expected_return(weights):.2%}") print(f"Volatilité: {optimizer.portfolio_volatility(weights):.2%}")
from quantfinance.portfolio.optimization import EfficientFrontier frontier = EfficientFrontier(optimizer)
Calcule les portefeuilles optimaux sur la frontière efficiente.
frontier_data = frontier.calculate_frontier(n_points=50) print(frontier_data.head()) # columns: return, volatility, sharpe_ratio, weights...
Trace la frontière efficiente avec les actifs individuels et le portefeuille optimal.
import matplotlib.pyplot as plt fig = frontier.plot(show_assets=True, show_optimal=True) plt.show()
from quantfinance.portfolio.optimization import AssetAllocator # Internalement : cov_matrix et std_devs sont annualisés (×252) allocator = AssetAllocator(returns_df) # returns = rendements quotidiens
Allocation HRP — utilise la hiérarchie des corrélations pour diversifier sans inversion de matrice.
weights = allocator.hierarchical_risk_parity() print("Poids HRP:", weights)
Minimise la corrélation moyenne entre les actifs du portefeuille.
weights = allocator.minimum_correlation() print("Poids min corr:", weights)
Maximise le ratio de diversification — rapport volatilité pondérée / volatilité portefeuille.
weights = allocator.maximum_diversification() print("Poids max div:", weights)
from quantfinance.portfolio.rebalancing import Rebalancer rebalancer = Rebalancer(target_weights, prices_df, initial_capital=100000, transaction_cost=0.001)
Rééquilibre à intervalles fixes. Fréquences : 'daily', 'weekly' (lundi), 'monthly' (fin de mois), 'quarterly', 'yearly'. Retourne un DataFrame avec colonnes : portfolio_value, cash, holdings_value, is_rebalance, weight_<asset>.
results = rebalancer.periodic_rebalancing(frequency='quarterly') print(results.head())
Rééquilibre uniquement quand un poids dévie de plus de threshold par rapport à la cible. Retourne un DataFrame avec les mêmes colonnes que periodic_rebalancing, plus rebalance_count (compteur cumulatif de rééquilibrages).
# Rééquilibre si déviation > 3% results = rebalancer.threshold_rebalancing(threshold=0.03) print(results.head())
Génère les signaux de trading. Doit retourner une Series avec les valeurs : 1 (long), 0 (neutre), -1 (short).
from quantfinance.portfolio.backtesting import Strategy class MyStrategy(Strategy): def generate_signals(self, data): # Exemple : signal basé sur RSI personnalisé close = data['Close'] signal = pd.Series(0, index=data.index) signal[close > close.rolling(20).mean()] = 1 signal[close < close.rolling(20).mean()] = -1 return signal strategy = MyStrategy() bt = Backtester(data, strategy, initial_capital=10000) results = bt.run()
from quantfinance.portfolio.backtesting import Backtester, MovingAverageCrossover strategy = MovingAverageCrossover(short_window=20, long_window=50) backtester = Backtester(prices_df, strategy, initial_capital=10000, commission=0.0005)
Exécute le backtest et retourne les métriques de performance.
results = backtester.run() print(f"Capital final: {results['Final Value']:.2f}") print(f"Rendement total: {results['Total Return']:.2%}") print(f"Ratio de Sharpe: {results['Sharpe Ratio']:.3f}") print(f"Max Drawdown: {results['Max Drawdown']:.2%}") print(f"Nb transactions: {results['Number of Trades']}") print(f"Win Rate: {results['Win Rate']:.1%}")
Trace la courbe de capital, les signaux de trading et les drawdowns. Lève ValueError si run() n'a pas été appelé avant. Génère 3 sous-graphiques : prix + signaux, valeur du portefeuille, drawdown.
import matplotlib.pyplot as plt fig = backtester.plot_results() plt.show()
Achète au début et conserve jusqu'à la fin. Sert de benchmark de référence.
from quantfinance.portfolio.backtesting import BuyAndHoldStrategy strategy = BuyAndHoldStrategy() signals = strategy.generate_signals(prices_df) print(signals.head()) # Toujours 1 (long)
Signal d'achat quand la MA courte croise la MA longue à la hausse, et de vente à la baisse.
from quantfinance.portfolio.backtesting import MovingAverageCrossover strategy = MovingAverageCrossover(short_window=10, long_window=30) signals = strategy.generate_signals(prices_df) print(signals.head()) # 1 = long, -1 = short, 0 = neutre # Backtest complet bt = Backtester(prices_df, strategy, initial_capital=50000) results = bt.run() print(f"Rendement total: {results['Total Return']:.2%}")
quantfinance.pricing
Module de pricing d'options (vanille et exotiques) et d'obligations.
| Paramètre | Type | Description |
|---|---|---|
| S | float | Prix spot du sous-jacent |
| K | float | Prix d'exercice (strike) |
| T | float | Maturité en années |
| r | float | Taux sans risque annualisé |
| sigma | float | Volatilité annualisée |
| option_type | str | 'call' ou 'put' |
| q | float | Taux de dividende continu (défaut : 0.0) |
from quantfinance.pricing.options import BlackScholes # Option call européenne bs_call = BlackScholes(S=100, K=105, T=1, r=0.05, sigma=0.25, option_type='call') # Option put européenne bs_put = BlackScholes(S=100, K=95, T=0.5, r=0.03, sigma=0.20, option_type='put')
Calcule le prix théorique de l'option selon Black-Scholes.
prix = bs_call.price() print(f"Prix de l'option: {prix:.2f}") # ex: 10.45
Sensibilité du prix au prix du sous-jacent. Entre 0 et 1 pour un call, entre -1 et 0 pour un put.
print(f"Delta: {bs_call.delta():.4f}") # ex: 0.4523 print(f"Delta put: {bs_put.delta():.4f}") # ex: -0.3871
Convexité — taux de variation du delta par rapport au prix du sous-jacent. Toujours positif.
print(f"Gamma: {bs_call.gamma():.6f}") # ex: 0.018432
Sensibilité du prix à la volatilité. Exprimé pour une variation de 1% de sigma.
print(f"Vega: {bs_call.vega():.4f}") # ex: 0.3841
Décroissance temporelle — perte de valeur par jour écoulé. Généralement négatif.
print(f"Theta: {bs_call.theta():.4f}") # ex: -0.0152 (perte de 1.52 cts/jour)
Sensibilité au taux sans risque.
print(f"Rho: {bs_call.rho():.4f}") # ex: 0.2910
Calcule la volatilité implicite par la méthode de Newton-Raphson à partir du prix de marché observé.
| Paramètre | Type | Description |
|---|---|---|
| market_price | float | Prix de marché observé de l'option |
market_price = 8.50 implied_vol = bs_call.implied_volatility(market_price) print(f"Volatilité implicite: {implied_vol:.2%}") # ex: 21.34%
Dollar Value of a Basis Point — variation du prix pour un mouvement de 1bp (0.01%) du rendement.
dv01 = bond.dv01(ytm=0.045) print(f"DV01: {dv01:.4f}€") # variation de prix pour +1bp
Calcule les intérêts courus depuis le dernier paiement de coupon.
| Paramètre | Type | Description |
|---|---|---|
| days_since_last_coupon | int | Nombre de jours depuis le dernier coupon |
accrued = bond.accrued_interest(days_since_last_coupon=45) print(f"Intérêts courus: {accrued:.4f}€")
Prix sale = prix propre + intérêts courus. Représente le montant réellement payé à l'achat.
dirty = bond.dirty_price(ytm=0.045, days_since_last_coupon=45) clean = bond.price(ytm=0.045) print(f"Prix propre: {clean:.2f}€ | Prix sale: {dirty:.2f}€")
Génère le calendrier complet des flux de trésorerie de l'obligation.
cf = bond.cash_flows() print(cf) # Period Time (years) Cash Flow # 1 0.5 25.0 # 2 1.0 25.0 # ... ... ... # 20 10.0 1025.0
Approxime le changement de prix avec duration seule, puis avec duration + convexité.
# Impact d'une hausse de 50bp dur_approx, full_approx = bond.price_change_approximation(ytm=0.045, yield_change=0.005) print(f"Approx. duration : {dur_approx:.4f}€") print(f"Approx. dur+conv : {full_approx:.4f}€")
Calcule d1 et d2 de la formule de Black-Scholes. d1 = [ln(S/K) + (r − q + σ²/2)·T] / (σ√T), d2 = d1 − σ√T.
bs = BlackScholes(S=100, K=105, T=1, r=0.05, sigma=0.25) print(f"d1={bs.d1():.4f}, d2={bs.d2():.4f}")
Retourne toutes les grecques en une seule fois : {'delta', 'gamma', 'vega', 'theta', 'rho'}.
g = bs.greeks() print(f"Delta: {g['delta']:.4f} | Gamma: {g['gamma']:.4f}") print(f"Vega: {g['vega']:.4f} | Theta: {g['theta']:.4f} | Rho: {g['rho']:.4f}")
| Paramètre | Type | Description |
|---|---|---|
| N | int | Nombre de pas dans l'arbre (défaut : 100) |
| exercise_type | str | 'european' ou 'american' (défaut : 'european') |
| q | float | Taux de dividende continu (défaut : 0.0) |
from quantfinance.pricing.options import BinomialTree # Option américaine put (exercice anticipé possible) bt = BinomialTree(S=100, K=105, T=1, r=0.05, sigma=0.25, N=200, option_type='put', exercise_type='american') print(f"Prix option américaine: {bt.price():.2f}") # Comparaison européenne vs américaine bt_eu = BinomialTree(S=100, K=105, T=1, r=0.05, sigma=0.25, exercise_type='european') print(f"Prime d'exercice anticipé: {bt.price() - bt_eu.price():.2f}")
Prix standard par simulation de trajectoires du sous-jacent.
from quantfinance.pricing.options import MonteCarlo mc = MonteCarlo(S=100, K=105, T=1, r=0.05, sigma=0.25, n_simulations=50000) print(f"Prix MC: {mc.price():.2f}")
Prix d'une option asiatique basé sur la moyenne du sous-jacent sur la durée de vie.
# Moyenne arithmétique (standard) prix_asiatique = mc.price_asian_option(option_type='arithmetic') print(f"Option asiatique: {prix_asiatique:.2f}") # Moyenne géométrique prix_geo = mc.price_asian_option(option_type='geometric') print(f"Option asiatique géo: {prix_geo:.2f}")
Prix d'une option à barrière — s'active ou se désactive si le sous-jacent touche la barrière.
| barrier_type | Type | Description |
|---|---|---|
| 'up-and-out' | str | L'option disparaît si S dépasse la barrière |
| 'up-and-in' | str | L'option s'active si S dépasse la barrière |
| 'down-and-out' | str | L'option disparaît si S tombe sous la barrière |
| 'down-and-in' | str | L'option s'active si S tombe sous la barrière |
prix_barriere = mc.price_barrier_option(barrier=120, barrier_type='up-and-out') print(f"Option à barrière up-out: {prix_barriere:.2f}")
Calcule le prix avec intervalle de confiance. Retourne (prix, borne_inf, borne_sup).
price, low, high = mc.price_with_confidence_interval(confidence_level=0.95) print(f"Prix: {price:.2f} | IC 95%: [{low:.2f}, {high:.2f}]")
from quantfinance.pricing.bonds import Bond # Obligation 1000€, coupon 5%, maturité 10 ans, semi-annuel bond = Bond(face_value=1000, coupon_rate=0.05, maturity=10, frequency=2)
Prix théorique de l'obligation pour un taux de rendement donné.
prix = bond.price(ytm=0.04) print(f"Prix: {prix:.2f}€") # > 1000 car YTM < coupon
Yield to Maturity — rendement actuariel jusqu'à maturité.
ytm = bond.ytm(market_price=980) print(f"YTM: {ytm:.2%}") # ex: 5.24%
Mesures de sensibilité aux taux d'intérêt.
ytm = 0.045 print(f"Duration Macaulay: {bond.duration(ytm):.2f} ans") print(f"Duration modifiée: {bond.modified_duration(ytm):.2f}") print(f"Convexité: {bond.convexity(ytm):.2f}")
from quantfinance.pricing.bonds import ZeroCouponBond # Obligation zéro-coupon 1000€, maturité 5 ans zcb = ZeroCouponBond(face_value=1000, maturity=5) print(f"Prix (YTM 4%): {zcb.price(ytm=0.04):.2f}€") print(f"YTM (prix 820): {zcb.ytm(price=820):.2%}") print(f"Duration: {zcb.duration():.1f} ans") # = maturité pour un zéro-coupon
from quantfinance.pricing.bonds import YieldCurve maturities = [0.25, 0.5, 1, 2, 5, 10, 20, 30] rates = [0.04, 0.041, 0.043, 0.045, 0.047, 0.049, 0.050, 0.051] curve = YieldCurve(maturities, rates)
Interpole le taux pour une ou plusieurs maturités. Méthodes disponibles : 'linear', 'cubic', 'quadratic'.
r_3y = curve.interpolate(3, method='cubic') print(f"Taux 3 ans: {r_3y:.2%}")
Taux forward entre deux dates : f(t1, t2) = (r₂·t₂ − r₁·t₁) / (t₂ − t₁).
# Taux forward entre 1 an et 3 ans fwd = curve.forward_rate(t1=1, t2=3) print(f"Taux forward 1x3: {fwd:.2%}")
Facteur d'actualisation DF(T) = e^(−r·T).
df_5y = curve.discount_factor(5) print(f"Facteur d'actualisation 5 ans: {df_5y:.4f}")
Taux par — taux du coupon qui donne un prix de 100.
par = curve.par_rate(maturity=5, frequency=2) print(f"Taux par 5 ans: {par:.2%}")
Trace la courbe de taux interpolée avec les points de marché.
import matplotlib.pyplot as plt fig = curve.plot(method='cubic') plt.show()
Taux forward instantané f(t) = r(t) + t · dr/dt, calculé par différence finie.
ifr = curve.instantaneous_forward_rate(maturity=5, method='cubic') print(f"Taux forward instantané (5 ans): {ifr:.2%}")
from quantfinance.pricing.bonds import Bond, BondPortfolio b1 = Bond(face_value=1000, coupon_rate=0.05, maturity=5) b2 = Bond(face_value=1000, coupon_rate=0.03, maturity=10) b3 = Bond(face_value=1000, coupon_rate=0.04, maturity=2) portfolio = BondPortfolio(bonds=[b1, b2, b3], weights=[0.5, 0.3, 0.2])
Métriques agrégées du portefeuille. ytm peut être un scalaire (même taux pour toutes) ou un array (un taux par obligation).
ytm = 0.045 print(f"Prix: {portfolio.price(ytm):.2f}") print(f"Duration Macaulay: {portfolio.duration(ytm):.2f} ans") print(f"Duration modifiée: {portfolio.modified_duration(ytm):.2f}") print(f"Convexité: {portfolio.convexity(ytm):.2f}")
Tableau récapitulatif de toutes les métriques par obligation et pour le portefeuille total.
df = portfolio.summary(ytm=0.045) print(df) # Bond Weight Price Duration Modified Duration Convexity YTM # Bond 1 0.50 ... ... ... ... 0.045 # Bond 2 0.30 ... ... ... ... 0.045 # PORTFOLIO 1.00 ... ... ... ... 0.045
Calcule la volatilité implicite d'une option à partir de son prix de marché.
from quantfinance.pricing.options import ImpliedVolatility iv = ImpliedVolatility.calculate( market_price=8.50, S=100, K=105, T=1, r=0.05, option_type='call' ) print(f"Volatilité implicite: {iv:.2%}")
Calcule une surface de volatilité implicite complète à partir d'une matrice de prix de marché (n_strikes × n_maturities).
| Paramètre | Type | Description |
|---|---|---|
| market_prices | np.ndarray | Matrice (n_strikes × n_maturities) des prix observés |
| strikes | np.ndarray | Tableau des prix d'exercice |
| maturities | np.ndarray | Tableau des maturités |
import numpy as np strikes = np.array([90, 95, 100, 105, 110]) maturities = np.array([0.25, 0.5, 1.0, 2.0]) prices = np.random.uniform(1, 15, (5, 4)) # exemple surface = ImpliedVolatility.volatility_surface( market_prices=prices, S=100, strikes=strikes, maturities=maturities, r=0.05, option_type='call' ) print(surface) # Matrice 5x4 de vols implicites
quantfinance.risk
Module de mesure et d'analyse des risques de marché.
VaR historique non paramétrique — quantile empirique des pertes observées.
from quantfinance.risk.var import VaRCalculator var_95 = VaRCalculator.historical_var(returns, confidence=0.95) var_99 = VaRCalculator.historical_var(returns, confidence=0.99) print(f"VaR 95%: {var_95:.2%}") print(f"VaR 99%: {var_99:.2%}")
VaR paramétrique sous hypothèse de normalité des rendements.
var_param = VaRCalculator.parametric_var(returns, confidence=0.95) print(f"VaR paramétrique 95%: {var_param:.2%}")
VaR avec pondération exponentielle — donne plus de poids aux observations récentes (modèle RiskMetrics).
# lambda_ = 0.94 est le standard RiskMetrics pour données journalières var_ewma = VaRCalculator.ewma_var(returns, confidence=0.95, lambda_=0.94) print(f"VaR EWMA 95%: {var_ewma:.2%}")
VaR par simulation Monte Carlo de trajectoires futures.
var_mc = VaRCalculator.monte_carlo_var(returns, confidence=0.95, n_sim=100000) print(f"VaR Monte Carlo 95%: {var_mc:.2%}")
CVaR / Expected Shortfall — perte moyenne dans les scénarios pires que la VaR. Mesure cohérente du risque.
es_95 = VaRCalculator.expected_shortfall(returns, confidence=0.95) print(f"CVaR 95%: {es_95:.2%}") # toujours >= VaR # Comparaison des 4 méthodes for method, val in [ ("Historique", VaRCalculator.historical_var(returns, 0.95)), ("Paramétrique", VaRCalculator.parametric_var(returns, 0.95)), ("EWMA", VaRCalculator.ewma_var(returns, 0.95)), ("Monte Carlo", VaRCalculator.monte_carlo_var(returns, 0.95)), ]: print(f"{method:14s}: {val:.2%}")
Ratio rendement/risque annualisé. Mesure le rendement excédentaire par unité de volatilité totale.
from quantfinance.risk.metrics import RiskMetrics sharpe = RiskMetrics.sharpe_ratio(returns, risk_free_rate=0.02) print(f"Sharpe: {sharpe:.3f}") # > 1 = bon, > 2 = excellent
Comme Sharpe mais ne pénalise que la volatilité négative (downside deviation).
sortino = RiskMetrics.sortino_ratio(returns, risk_free_rate=0.02) print(f"Sortino: {sortino:.3f}")
Rendement annualisé divisé par le drawdown maximum. Mesure la récupération après perte.
calmar = RiskMetrics.calmar_ratio(returns) print(f"Calmar: {calmar:.3f}")
Perte maximale depuis un pic précédent, exprimée en pourcentage.
mdd = RiskMetrics.max_drawdown(returns) print(f"Max Drawdown: {mdd:.2%}") # ex: -23.45%
Alpha annualisé divisé par le tracking error — mesure la valeur ajoutée par rapport à un benchmark.
ir = RiskMetrics.information_ratio(portfolio_returns, benchmark_returns, periods_per_year=252) print(f"Information Ratio: {ir:.3f}")
Ratio Omega — rapport entre les gains au-dessus du seuil et les pertes en-dessous. Contrairement au Sharpe, ne suppose pas la normalité.
omega = RiskMetrics.omega_ratio(returns, threshold=0.0) print(f"Ratio Omega: {omega:.3f}") # > 1 = bonne performance
Sensibilité au marché. β = Cov(R, R_market) / Var(R_market). β > 1 = plus volatile que le marché.
b = RiskMetrics.beta(portfolio_returns, market_returns) print(f"Beta: {b:.3f}")
Alpha de Jensen annualisé — surperformance par rapport au CAPM : α = E[R] − (Rf + β·(E[Rm] − Rf)).
a = RiskMetrics.alpha(portfolio_returns, market_returns, risk_free_rate=0.02) print(f"Alpha annualisé: {a:.2%}")
Tracking error annualisée — écart-type des rendements actifs (R − R_benchmark).
te = RiskMetrics.tracking_error(portfolio_returns, benchmark_returns) print(f"Tracking Error: {te:.2%}")
Déviation à la baisse annualisée — racine de la moyenne des carrés des rendements inférieurs au seuil cible.
dd_dev = RiskMetrics.downside_deviation(returns, target_return=0.0) print(f"Downside deviation: {dd_dev:.2%}")
Série complète de drawdowns à chaque point dans le temps (valeurs entre -1 et 0).
dd_series = RiskMetrics.drawdown_series(returns) print(dd_series.describe())
Durée maximale du drawdown en nombre de périodes (du pic au creux).
duration = RiskMetrics.max_drawdown_duration(returns) print(f"Durée max drawdown: {duration} jours")
Statistiques de distribution. Skewness > 0 = queue droite. Kurtosis en excès > 0 = queues plus épaisses que la normale (leptokurtique).
print(f"Skewness: {RiskMetrics.skewness(returns):.3f}") print(f"Kurtosis (excès): {RiskMetrics.kurtosis(returns):.3f}") # excès = True par défaut print(f"Kurtosis (brut): {RiskMetrics.kurtosis(returns, excess=False):.3f}")
Variance ratio — test de l'hypothèse de marche aléatoire. VR = 1 si marche aléatoire, VR > 1 suggère une tendance, VR < 1 une mean-reversion.
vr = RiskMetrics.var_ratio(returns, q=5) print(f"Variance Ratio (q=5): {vr:.3f}") # 1.0 = random walk
from quantfinance.risk.metrics import PerformanceAnalyzer analyzer = PerformanceAnalyzer(returns, risk_free_rate=0.02)
Tableau complet de toutes les métriques de performance et de risque.
summary = analyzer.summary_statistics() print(summary) # Rendement annualisé : 12.34% # Volatilité annualisée : 8.92% # Ratio de Sharpe : 1.382 # Max Drawdown : -15.23% # VaR 95% : -1.45% # CVaR 95% : -2.10%
Métriques calculées sur une fenêtre glissante — utile pour suivre l'évolution dans le temps.
# Fenêtre de 252 jours (1 an) rolling = analyzer.rolling_metrics(window=252) print(rolling[['sharpe', 'volatility']].tail())
Graphique de la performance cumulée et des drawdowns.
import matplotlib.pyplot as plt fig = analyzer.plot_performance() plt.show()
VaR d'un portefeuille pondéré. Méthodes disponibles : 'historical', 'parametric', 'monte_carlo'.
| Paramètre | Type | Description |
|---|---|---|
| returns_matrix | pd.DataFrame | Matrice des rendements (colonnes = actifs) |
| weights | np.ndarray | Poids du portefeuille (normalisés automatiquement) |
import numpy as np weights = np.array([0.4, 0.3, 0.2, 0.1]) pvar = VaRCalculator.portfolio_var(returns_matrix, weights, confidence_level=0.95) print(f"VaR Portefeuille (95%): {pvar:.2%}")
VaR componentielle — contribution de chaque actif à la VaR totale du portefeuille. La somme des composantes est égale à la VaR totale.
cvar_comp = VaRCalculator.component_var(returns_matrix, weights) print(cvar_comp) # Contribution de chaque actif à la VaR totale print(f"Somme: {cvar_comp.sum():.4f}") # ≈ portfolio_var
VaR marginale — variation de VaR pour une petite augmentation de poids dans chaque actif. Utile pour décider où allouer ou retirer du capital.
mvar = VaRCalculator.marginal_var(returns_matrix, weights) print(mvar) # Actif avec mvar le + bas = meilleure diversification
Génère une table pivot des rendements mensuels — lignes = années, colonnes = mois (Jan–Déc) + colonne moyenne annuelle. Requiert un index DatetimeIndex.
table = analyzer.monthly_returns_table() print(table.applymap(lambda x: f"{x:.1%}" if pd.notna(x) else ""))
Applique des scénarios de crises historiques (2008, COVID-19, etc.) au portefeuille.
from quantfinance.risk.stress import StressTesting results = StressTesting.historical_scenarios(returns) print(results) # Crise 2008 : -38.2% # COVID 2020 : -12.4% # Dot-com 2000 : -25.1%
Applique des chocs personnalisés sur les facteurs de risque.
# Scénario : choc de -30% sur les actions, +200bps sur les taux shocks = {'equity': -0.30, 'rates': 0.02} pnl = StressTesting.custom_scenario(returns, shocks) print(f"P&L sous stress: {pnl:.2%}")
quantfinance.utils
Utilitaires pour le chargement de données et le calcul d'indicateurs techniques.
Charge un fichier CSV local de prix ou de rendements.
from quantfinance.utils.data import DataLoader prices = DataLoader.load_csv('data/prices.csv') print(prices.head())
Télécharge les données historiques depuis Yahoo Finance.
prices = DataLoader.load_yahoo( tickers=['AAPL', 'MSFT', 'GOOGL', 'AMZN'], start='2020-01-01', end='2024-01-01' ) returns = prices.pct_change().dropna() print(returns.describe())
Génère des prix synthétiques par mouvement brownien géométrique (GBM).
# 5 actifs sur 3 ans de données journalières prices = DataLoader.generate_synthetic_prices(n_assets=5, n_days=252*3) returns = prices.pct_change().dropna()
Génère des données OHLCV (Open, High, Low, Close, Volume) pour le backtesting.
data = DataLoader.generate_ohlcv_data(n_days=500) print(data.columns.tolist()) # ['Open', 'High', 'Low', 'Close', 'Volume']
Nettoie les données financières : valeurs manquantes, outliers, données incohérentes.
raw_data = DataLoader.load_csv('data/raw_prices.csv') clean = DataLoader.clean_data(raw_data) print(f"Lignes supprimées: {len(raw_data) - len(clean)}")
Moyennes mobiles simple (SMA) et exponentielle (EMA).
from quantfinance.utils.indicators import TechnicalIndicators close = prices['Close'] sma_20 = TechnicalIndicators.sma(close, window=20) ema_50 = TechnicalIndicators.ema(close, window=50) # Signal de croisement signal = (sma_20 > ema_50).astype(int)
Relative Strength Index — oscillateur entre 0 et 100. Suracheté > 70, survendu < 30.
rsi = TechnicalIndicators.rsi(close, window=14) print(f"RSI actuel: {rsi.iloc[-1]:.1f}") survendu = rsi[rsi <30] surachete = rsi[rsi >70]
MACD (12,26,9) — retourne la ligne MACD, la ligne signal et l'histogramme.
macd = TechnicalIndicators.macd(close) print(macd['macd'].tail()) print(macd['signal'].tail()) print(macd['histogram'].tail())
Bandes de Bollinger (±2 écarts-types autour de la SMA).
bands = TechnicalIndicators.bollinger_bands(close, window=20) print(f"Bande haute: {bands['upper'].iloc[-1]:.2f}") print(f"Bande centrale: {bands['middle'].iloc[-1]:.2f}") print(f"Bande basse: {bands['lower'].iloc[-1]:.2f}")
Génère des données OHLCV (Open, High, Low, Close, Volume) synthétiques via mouvement brownien géométrique.
ohlcv = DataLoader.generate_ohlcv_data(n_days=252, volatility=0.02, random_seed=42) print(ohlcv.head()) # Retourne DataFrame avec colonnes: Open, High, Low, Close, Volume
Télécharge des données via pandas-datareader. Nécessite pip install pandas-datareader. Sources disponibles : 'yahoo', 'fred', 'iex', etc.
# Données FRED (taux d'intérêt, indicateurs macro) gdp = DataLoader.download_pandas_datareader( tickers='GDP', start_date='2010-01-01', source='fred' )
Analyse de scénarios de stress définis manuellement. scenarios est un dict {nom: array_chocs} où chaque array contient un choc par actif.
| Paramètre | Type | Description |
|---|---|---|
| scenarios | Dict[str, np.ndarray] | Ex: {'Crise 2008': np.array([-0.4, -0.3, -0.1])} |
from quantfinance.risk.var import StressTesting import numpy as np scenarios = { 'Crise 2008': np.array([-0.40, -0.35, -0.10]), 'Covid 2020': np.array([-0.30, -0.25, -0.05]), 'Hausse taux': np.array([-0.05, -0.15, 0.02]), } result = StressTesting.scenario_analysis(returns_matrix, weights, scenarios) print(result) # Colonnes: Scenario, Portfolio Return, P&L, New Value
Stress test basé sur des périodes historiques réelles. stress_periods est un dict {nom: (date_debut, date_fin)}. Retourne rendement cumulé, max drawdown, pire/meilleur jour et volatilité pour chaque période.
stress_periods = {
'Crise financière': ('2008-09-01', '2009-03-31'),
'Covid crash': ('2020-02-19', '2020-03-23'),
}
result = StressTesting.historical_stress_test(returns_matrix, weights, stress_periods)
print(result)Stress test Monte Carlo — simule n_scenarios scénarios et retourne VaR, ES et distribution des pertes. Clés du dict retourné : scenarios, mean, std, min, max, VaR_95, ES_95, VaR_99, ES_99.
result = StressTesting.monte_carlo_stress_test( returns_matrix, weights, n_scenarios=5000, random_seed=42 ) print(f"VaR 95%: {result['VaR_95']:.2%} | ES 99%: {result['ES_99']:.2%}")
Gère les valeurs manquantes. Méthodes : 'ffill', 'bfill', 'interpolate', 'drop', 'mean', 'median'.
from quantfinance.utils.data import DataCleaner clean = DataCleaner.handle_missing_values(prices, method='ffill', limit=5) clean = DataCleaner.handle_missing_values(prices, method='interpolate')
Retire ou corrige les valeurs aberrantes. Méthodes : 'clip' (ajuste aux bornes ±n_std), 'remove' (supprime les lignes), 'winsorize' (clip aux percentiles 5%/95%).
clean = DataCleaner.remove_outliers(returns, n_std=3.0, method='clip')
Plafonne les valeurs extrêmes aux percentiles spécifiés.
winsorized = DataCleaner.winsorize(returns, lower_percentile=0.01, upper_percentile=0.99)
Calcule les rendements à partir des prix. Méthodes : 'simple' (arithmétique, pct_change) ou 'log' (logarithmique, ln(P_t/P_{t-1})). Supprime automatiquement les NaN.
returns = DataCleaner.calculate_returns(prices, method='log') returns_5d = DataCleaner.calculate_returns(prices, method='simple', periods=5)
align_data aligne plusieurs DataFrames sur les mêmes dates. resample_data rééchantillonne à une fréquence différente — méthodes : 'last', 'first', 'mean', 'sum'.
df1, df2 = DataCleaner.align_data(prices1, prices2, join='inner') monthly = DataCleaner.resample_data(daily_prices, frequency='M', method='last')
Normalise les données. Méthodes : 'zscore' (standardisation), 'minmax' (0–1), 'robust' (médiane/IQR).
from quantfinance.utils.data import DataTransformer normalized = DataTransformer.normalize(returns, method='robust')
Ajoute des indicateurs techniques à un DataFrame OHLCV. Indicateurs disponibles : 'SMA' (20/50/200), 'EMA' (12/26), 'RSI' (14), 'MACD' (12/26/9), 'BB' (Bollinger 20), 'ATR' (14). Par défaut : ['SMA', 'EMA'].
| Indicateur | Colonnes ajoutées | Description |
|---|---|---|
| SMA | SMA_20, SMA_50, SMA_200 | Moyennes mobiles simples |
| EMA | EMA_12, EMA_26 | Moyennes mobiles exponentielles |
| RSI | RSI | Relative Strength Index |
| MACD | MACD, MACD_Signal, MACD_Hist | MACD + signal + histogramme |
| BB | BB_Upper, BB_Middle, BB_Lower | Bandes de Bollinger |
| ATR | ATR | Average True Range (nécessite OHLC) |
data_with_indicators = DataTransformer.add_technical_indicators( ohlcv, indicators=['SMA', 'EMA', 'RSI', 'MACD', 'BB', 'ATR'] ) print(data_with_indicators.columns.tolist())
Crée des features retardées pour les modèles de machine learning. Les NaN initiaux sont supprimés.
features = DataTransformer.create_lagged_features(returns, lags=[1, 2, 5, 10]) # Ajoute colonnes: col_lag_1, col_lag_2, col_lag_5, col_lag_10
Calcule des statistiques roulantes (moyenne, écart-type, min, max) pour chaque colonne et chaque fenêtre. Les NaN initiaux sont supprimés.
stats = DataTransformer.rolling_statistics(returns, windows=[20, 60]) # Ajoute: col_mean_20, col_std_20, col_min_20, col_max_20, etc.
quantfinance.utils.helpers pour l'annualisation et la conversion de rendements.Annualise un rendement par multiplication simple : returns × periods_per_year. Pour les rendements quotidiens → annuels, utilise 252.
from quantfinance.utils.helpers import annualize_return, annualize_volatility daily_return = 0.0004 # 0.04%/jour annual = annualize_return(daily_return, periods_per_year=252) print(f"Rendement annualisé: {annual:.2%}") # ≈ 10.08%
Annualise une volatilité par la règle en racine carrée du temps : σ × √periods_per_year.
daily_vol = 0.012 # 1.2%/jour annual_vol = annualize_volatility(daily_vol) print(f"Volatilité annualisée: {annual_vol:.2%}") # ≈ 19.05%
Calcule le rendement composé total : ∏(1 + r_t) − 1.
from quantfinance.utils.helpers import compound_returns total = compound_returns(daily_returns) print(f"Rendement total: {total:.2%}")
Inverses des fonctions d'annualisation. deannualize_return : r / n. deannualize_volatility : σ / √n.
from quantfinance.utils.helpers import deannualize_return, deannualize_volatility daily_r = deannualize_return(0.10) # 10% annuel → quotidien daily_v = deannualize_volatility(0.20) # 20% annuel → quotidien
Crée une plage de dates. Fréquences : 'D' (quotidien), 'B' (jours ouvrés), 'W', 'M', 'Q', 'Y'.
from quantfinance.utils.helpers import date_range trading_days = date_range('2023-01-01', '2023-12-31', freq='B') print(f"{len(trading_days)} jours de trading en 2023")
from quantfinance.utils.plotting import Plotter plotter = Plotter(style='seaborn-v0_8-darkgrid', figsize=(14, 7))
Trace l'évolution des prix. Avec normalize=True, réindexe à 100 en début de période pour comparer des actifs à des échelles différentes.
fig = Plotter.plot_prices(prices, normalize=True, title="Performance relative") fig.savefig('prices.png', dpi=150)
Histogramme de distribution des rendements avec courbe normale de référence pour chaque actif.
fig = Plotter.plot_returns(returns)
Heatmap de corrélation avec triangle inférieur seulement. Méthodes : 'pearson', 'spearman', 'kendall'.
fig = Plotter.plot_correlation_matrix(returns, method='spearman')
2 sous-graphiques : valeur cumulée + série de drawdowns avec annotation automatique du max drawdown.
fig = Plotter.plot_drawdown(returns)
Graphique des poids du portefeuille. kind : 'bar', 'pie', ou 'both' (barre + camembert côte à côte).
result = optimizer.maximize_sharpe() fig = Plotter.plot_portfolio_weights(result['weights'], kind='both')
Frontière efficiente Monte Carlo avec actifs individuels, max Sharpe (★ rouge), min volatilité (★ vert) et Capital Market Line (CML) optionnelle.
fig = Plotter.plot_efficient_frontier(returns, n_portfolios=5000, risk_free_rate=0.03)
Graphique en chandelier. Utilise mplfinance si installé, sinon fallback manuel. Requiert un DataFrame OHLCV avec colonnes Open, High, Low, Close et index DatetimeIndex.
from quantfinance.utils.plotting import FinancialPlotter ohlcv = DataLoader.generate_ohlcv_data(n_days=60) fig = FinancialPlotter.plot_candlestick(ohlcv, volume=True) # pip install mplfinance pour graphique complet
Nuage de points risque (volatilité annualisée) vs rendement annualisé pour chaque actif.
fig = FinancialPlotter.plot_risk_return_scatter(returns)
3 sous-graphiques roulants : rendement annualisé, volatilité annualisée, ratio de Sharpe.
fig = FinancialPlotter.plot_rolling_metrics( returns=portfolio_returns, window=60 )
Bootstrap une courbe de taux zéro-coupon à partir d'une liste de prix d'obligations. Retourne un objet YieldCurve.
from quantfinance.pricing.bonds import bootstrap_zero_curve prices = [99.5, 98.0, 96.5, 94.0] coupons = [0.02, 0.03, 0.04, 0.05] maturities = [1, 2, 3, 5] zero_curve = bootstrap_zero_curve(prices, coupons, maturities) print(f"Taux zéro 3 ans: {zero_curve.interpolate(3):.2%}")
Télécharge des données depuis Yahoo Finance. Nécessite pip install yfinance. Colonnes disponibles : 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume'. Intervalles : '1d', '1wk', '1mo'.
prices = DataLoader.download_yahoo_finance( tickers=['AAPL', 'MSFT', 'GOOGL'], start_date='2020-01-01', end_date='2024-01-01', column='Adj Close' ) print(prices.tail())
Formules mathématiques
Les équations fondamentales implémentées dans QuantFinance.
Black-Scholes
Les Grecques
Value at Risk
EWMA — RiskMetrics
Ratios de performance
Optimisation de Markowitz
Duration & Convexité (Obligations)
Comparaison des méthodes
Pricing d'options
| Méthode | Options américaines | Options exotiques | Vitesse | Précision | Recommandé pour |
|---|---|---|---|---|---|
| BlackScholes | Européennes seul. | Exacte | Options européennes vanille, calcul des grecques | ||
| BinomialTree | Exercice anticipé | Converge | Options américaines, options avec dividendes discrets | ||
| MonteCarlo | Avec LSM | Path-dependent | Stochastique | Options asiatiques, barrières, structures complexes |
Calcul de VaR
| Méthode | Hypothèse | Queues épaisses | Données requises | Vitesse | Recommandé quand |
|---|---|---|---|---|---|
| historical_var | Aucune | Capturées | 500+ jours | Distribution non-normale, données historiques abondantes | |
| parametric_var | Normalité des rendements | Sous-estimées | 30+ jours | Calcul rapide, portefeuilles bien diversifiés | |
| ewma_var | Normalité + vol. variable | Sous-estimées | 100+ jours | Volatilité qui change dans le temps (λ = 0.94) | |
| monte_carlo_var | GBM ou modèle custom | Configurables | Paramètres | Précision maximale, portefeuilles non-linéaires |
Optimisation de portefeuille
| Méthode | Inversion matrice | Robustesse | Grands univers | Complexité | Recommandé quand |
|---|---|---|---|---|---|
| equal_weight | Nulle | Benchmark simple, manque de données | |||
| minimize_volatility | Faible | Minimiser le risque absolu, profil défensif | |||
| maximize_sharpe | Moyenne | Rendement/risque optimal, rendements prévisibles | |||
| risk_parity | Moyenne | Égaliser la contribution au risque de chaque actif | |||
| hierarchical_risk_parity | Élevée | Grands univers, instabilité numérique, fonds alternatifs |
Stratégies de backtesting
| Stratégie | Type | Paramètres | Marchés favorables | Risque sur-optimisation |
|---|---|---|---|---|
| BuyAndHold | Passive | Aucun | Tendances longues | Nul |
| MovingAverageCrossover | Tendance | short_window, long_window | Marchés directionnels | Élevé (2 params) |
Exemples complets
Des workflows de bout en bout combinant plusieurs modules.
from quantfinance.pricing.options import BlackScholes, BinomialTree, MonteCarlo # Paramètres communs params = dict(S=100, K=105, T=1, r=0.05, sigma=0.25, option_type='call') # Comparaison des 3 modèles bs = BlackScholes(**params) bt = BinomialTree(**params, N=500) mc = MonteCarlo(**params, n_simulations=100000) print(" Comparaison des prix ") print(f"Black-Scholes : {bs.price():.4f}") print(f"Binomial Tree : {bt.price():.4f}") print(f"Monte Carlo : {mc.price():.4f}") # Toutes les grecques d'un coup print(" Grecques ") for name, val in {'Delta': bs.delta(), 'Gamma': bs.gamma(), 'Vega': bs.vega(), 'Theta': bs.theta(), 'Rho': bs.rho()}.items(): print(f" {name:6s}: {val:.6f}") # Volatilité implicite depuis un prix de marché impl_vol = bs.implied_volatility(market_price=9.50) print(f" Vol. implicite: {impl_vol:.2%}") # Smile de volatilité print(" Smile de volatilité ") for K in [85, 90, 95, 100, 105, 110, 115]: b = BlackScholes(S=100, K=K, T=1, r=0.05, sigma=0.25) print(f" K={K:3d}: prix={b.price():.2f} delta={b.delta():.3f}")
from quantfinance.utils.data import DataLoader from quantfinance.portfolio.optimization import PortfolioOptimizer, EfficientFrontier from quantfinance.risk.var import VaRCalculator from quantfinance.risk.metrics import RiskMetrics, PerformanceAnalyzer # 1. Données prices = DataLoader.generate_synthetic_prices(n_assets=6, n_days=756) returns = prices.pct_change().dropna() # 2. Comparer les 4 stratégies d'optimisation opt = PortfolioOptimizer(returns, risk_free_rate=0.02) strategies = { 'Équipondéré': opt.equal_weight(), 'Min Volatilité': opt.minimize_volatility(), 'Max Sharpe': opt.maximize_sharpe(), 'Risk Parity': opt.risk_parity(), } print(f"{'Stratégie':20s} {'Rendement':>10s} {'Volatilité':>12s} {'Sharpe':>8s}") for name, res in strategies.items(): print(f"{name:20s} {res['return']:>10.2%} {res['volatility']:>12.2%} {res['sharpe_ratio']:>8.3f}") # 3. Analyse de risque sur le meilleur portefeuille w = strategies['Max Sharpe']['weights'] port_ret = (returns * w).sum(axis=1) print(" Risque (Max Sharpe) ") print(f"VaR 95% Historique : {VaRCalculator.historical_var(port_ret, 0.95):.2%}") print(f"VaR 95% Paramétrique: {VaRCalculator.parametric_var(port_ret, 0.95):.2%}") print(f"CVaR 95% : {VaRCalculator.expected_shortfall(port_ret, 0.95):.2%}") # 4. Rapport complet analyzer = PerformanceAnalyzer(port_ret, risk_free_rate=0.02) print(" Performance ") print(analyzer.summary_statistics())
from quantfinance.utils.data import DataLoader from quantfinance.utils.indicators import TechnicalIndicators from quantfinance.portfolio.backtesting import Backtester, MovingAverageCrossover, BuyAndHoldStrategy # Données OHLCV sur 3 ans data = DataLoader.generate_ohlcv_data(n_days=756) close = data['Close'] # Indicateurs techniques pour analyse préalable rsi = TechnicalIndicators.rsi(close) macd = TechnicalIndicators.macd(close) bands = TechnicalIndicators.bollinger_bands(close) print(f"RSI: {rsi.iloc[-1]:.1f} | MACD: {macd['macd'].iloc[-1]:.4f}") print(f"Bollinger: [{bands['lower'].iloc[-1]:.2f} — {bands['upper'].iloc[-1]:.2f}]") # Comparer plusieurs combinaisons de MA print(f" {'Stratégie':18s} {'Rendement':>10s} {'Sharpe':>8s} {'Max DD':>8s}") for short, long in [(10, 30), (20, 50), (50, 200)]: bt = Backtester(data, MovingAverageCrossover(short, long), initial_capital=10000, commission=0.001) res = bt.run() print(f"MA({short},{long}){' ':10s} {res['Total Return']:>10.2%} {res['Sharpe Ratio']:>8.3f} {res['Max Drawdown']:>8.2%}") # Benchmark Buy & Hold bh = Backtester(data, BuyAndHoldStrategy(), initial_capital=10000) res = bh.run() print(f"Buy & Hold {res['Total Return']:>10.2%} {res['Sharpe Ratio']:>8.3f} {res['Max Drawdown']:>8.2%}")
from quantfinance.utils.data import DataLoader from quantfinance.portfolio.optimization import PortfolioOptimizer from quantfinance.portfolio.rebalancing import Rebalancer from quantfinance.risk.stress import StressTesting # Setup prices = DataLoader.generate_synthetic_prices(n_assets=4, n_days=756) returns = prices.pct_change().dropna() opt = PortfolioOptimizer(returns, risk_free_rate=0.02) weights = opt.maximize_sharpe()['weights'] # Stress testing sur crises historiques print(" Scénarios historiques ") scenarios = StressTesting.historical_scenarios(returns) print(scenarios) # Scénario personnalisé : crash actions + hausse taux shocks = {'equity': -0.35, 'rates': 0.025, 'credit': 0.015} pnl = StressTesting.custom_scenario(returns, shocks) print(f" Scénario crise 2008 amplifié: {pnl:.2%}") # Rééquilibrage périodique vs par seuil rebalancer = Rebalancer(weights, prices, initial_capital=100000, transaction_cost=0.001) res_monthly = rebalancer.periodic_rebalancing(frequency='monthly') res_threshold = rebalancer.threshold_rebalancing(threshold=0.05) print(f" Rééquilibrage mensuel — transactions: {len(res_monthly)}") print(f"Rééquilibrage seuil 5% — transactions: {len(res_threshold)}")
Tests
# Tous les tests pytest # Avec rapport de couverture HTML pytest --cov=quantfinance --cov-report=html # Tests rapides uniquement pytest -m "not slow" # Un module spécifique pytest tests/test_pricing.py -v
Guide de contribution
- Fork le dépôt sur GitHub
- Créez une branche :
git checkout -b feature/ma-fonctionnalite - Implémentez avec des tests unitaires
- Committez :
git commit -m 'feat: description' - Pushez :
git push origin feature/ma-fonctionnalite - Ouvrez une Pull Request sur GitHub