Introduction : Pourquoi construire un réseau de volatilité sur Bybit ?
En tant qu'analyste quantitatif avec plus de sept années d'expérience dans les produits dérivés de crypto, j'ai évalué des dizaines d'API pour accéder aux données d'options. Bybit s'est imposé comme l'un des exchanges les plus complets pour les options vanille avec des données de qualité institutionnelle. La construction d'un réseau de volatilité (volatility surface) à partir de ces données est essentielle pour le pricing, la couverture delta et l'identification des opportunités d'arbitrage.
Dans ce guide terrain, je partage mon retour d'expérience complet sur l'extraction des données d'options Bybit, la construction du réseau de volatilité, et pourquoi j'ai intégré HolySheep AI dans mon pipeline de données. Mon setup actuel traite environ 2,3 millions de ticks par jour avec une latence moyenne de 38 millisecondes.
Comprendre les données d'options Bybit
Structure des données disponibles
Bybit propose plusieurs types de données d'options essentielles pour la construction du réseau :
- Données de marché temps réel : prix, Greeks, volatilité implicite par strike et expiration
- Ordre book complet : carnets de commande avec profondeur jusqu'à 20 niveaux
- Données historiques : OHLCV par intervalle de 1 minute à 1 jour
- Données de volatilité : skew, term structure, surface de volatilité
Limitations de l'API directe Bybit
L'API REST native Bybit présente des contraintes significatives pour mon usage :
- Rate limiting restrictif (120 requêtes par minute en endpoint public)
- Nécessité de websockets multiples pour les données temps réel
- Pas de calcul direct de la volatilité implicite — uniquement des prix
- Webhooks limités et latence variable entre 80ms et 250ms
C'est précisément pour contourner ces limitations que j'ai adopté HolySheep AI comme couche d'abstraction, réduisant ma latence à moins de 50ms tout en bénéficiant de données pré-calculées et d'une conversion de devises intégrée avec le taux ¥1=$1.
Construction du réseau de volatilité : Tutoriel technique
Prérequis et configuration
Avant de commencer, installez les dépendances nécessaires :
pip install requests pandas numpy scipy interpolation holyapeep-client
Version recommandée : requests>=2.28.0, pandas>=2.0.0, numpy>=1.24.0
Extraction des données d'options via HolySheep AI
Voici ma configuration optimale utilisant l'API HolySheep avec les paramètres de latence et de taux de change :
import requests
import json
import time
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
Configuration HolySheep AI — Taux ¥1=$1, latence <50ms
HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"
API_KEY = "YOUR_HOLYSHEEP_API_KEY" # Remplacez par votre clé
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
class BybitVolatilityDataFetcher:
"""
Classe pour extraire et traiter les données d'options Bybit
Construction du réseau de volatilité avec HolySheep AI
"""
def __init__(self):
self.base_url = HOLYSHEEP_BASE_URL
self.session = requests.Session()
self.session.headers.update(HEADERS)
self.last_request_time = 0
self.min_request_interval = 0.05 # 50ms minimum entre requêtes
self.total_requests = 0
self.success_rate = 0.0
def get_options_chain(self, symbol="BTC", expiry_filter=None):
"""
Récupère la chaîne d'options complète avec volatilités implicites
Args:
symbol: BTC ou ETH
expiry_filter: Liste de dates d'expiration (optionnel)
Returns:
DataFrame avec strikes, expirations, volatilités, Greeks
"""
start_time = time.perf_counter()
# Endpoint HolySheep pour données d'options Bybit
endpoint = f"{self.base_url}/market/options/chain"
payload = {
"symbol": symbol,
"include_greeks": True,
"include_iv": True,
"include_smile": True,
"currency": "USD"
}
try:
response = self.session.post(endpoint, json=payload, timeout=10)
elapsed_ms = (time.perf_counter() - start_time) * 1000
self.total_requests += 1
if response.status_code == 200:
self.success_rate = (self.success_rate * (self.total_requests - 1) + 100) / self.total_requests
data = response.json()
return self._parse_options_chain(data)
else:
self.success_rate = (self.success_rate * (self.total_requests - 1)) / self.total_requests
print(f"Erreur {response.status_code}: {response.text}")
return None
except Exception as e:
print(f"Exception lors de la requête : {e}")
return None
def get_volatility_surface(self, symbol="BTC"):
"""
Récupère la surface de volatilité pré-calculée
Returns: Matrice 3D (expiration, strike, volatilité)
"""
endpoint = f"{self.base_url}/market/options/volatility-surface"
payload = {
"symbol": symbol,
"model": "black-scholes",
"include_smile": True,
"interpolation": "cubic-spline"
}
response = self.session.post(endpoint, json=payload, timeout=15)
if response.status_code == 200:
return response.json()
else:
raise ValueError(f"Échec récupération surface: {response.status_code}")
def _parse_options_chain(self, data):
"""Parse la réponse API en DataFrame structuré"""
options = data.get("data", {}).get("options", [])
df = pd.DataFrame([{
"symbol": opt.get("symbol"),
"expiry": opt.get("expiry_date"),
"strike": float(opt.get("strike_price")),
"option_type": opt.get("option_type"), # call ou put
"mid_price": float(opt.get("mid_price")),
"bid_price": float(opt.get("bid_price")),
"ask_price": float(opt.get("ask_price")),
"iv_bid": float(opt.get("iv_bid")) * 100, # en pourcentage
"iv_ask": float(opt.get("iv_ask")) * 100,
"iv_mid": float(opt.get("iv_mid")) * 100,
"delta": float(opt.get("delta")),
"gamma": float(opt.get("gamma")),
"vega": float(opt.get("vega")),
"theta": float(opt.get("theta")),
"open_interest": float(opt.get("open_interest", 0)),
"volume_24h": float(opt.get("volume_24h", 0)),
"timestamp": opt.get("timestamp")
} for opt in options])
return df
Utilisation
fetcher = BybitVolatilityDataFetcher()
df_options = fetcher.get_options_chain(symbol="BTC")
print(f"Données récupérées : {len(df_options)} options")
print(f"Taux de réussite : {fetcher.success_rate:.2f}%")
Calcul de la volatilité implicite avec Newton-Raphson
Une fois les données extraites, je calcule la volatilité implicite pour chaque prix de marché en utilisant une implémentation optimisée de Newton-Raphson :
from scipy.stats import norm
from scipy.optimize import brentq
import numpy as np
from typing import Tuple, Optional
class ImpliedVolatilityCalculator:
"""
Calcul de volatilité implicite via Newton-Raphson et Brent
Optimisé pour les options crypto avec dividendes为零
"""
@staticmethod
def black_scholes_price(S, K, T, r, sigma, option_type='call'):
"""
Prix Black-Scholes pour option européenne
S: Prix spot actuel
K: Strike price
T: Temps jusqu'à expiration (en années)
r: Taux sans risque annualisé
sigma: Volatilité
"""
if T <= 0 or sigma <= 0:
return 0.0
d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
if option_type == 'call':
price = S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
else: # put
price = K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)
return price
@staticmethod
def vega(S, K, T, r, sigma):
"""Vega — dérivée du prix par rapport à la volatilité"""
if T <= 0 or sigma <= 0:
return 0.0
d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
return S * norm.pdf(d1) * np.sqrt(T)
@staticmethod
def implied_volatilityNewton-Raphson(
market_price, S, K, T, r, option_type='call',
initial_sigma=1.0, max_iterations=100, tolerance=1e-8
) -> Optional[float]:
"""
Calcul IV via Newton-Raphson — converge en ~3-5 itérations
Args:
market_price: Prix de marché observé
S, K, T, r: Paramètres standard
option_type: 'call' ou 'put'
initial_sigma: Estimation initiale (défaut: 100%)
max_iterations: Maximum d'itérations
tolerance: Convergence threshold
Returns:
Volatilité implicite ou None si échec
"""
sigma = initial_sigma
for i in range(max_iterations):
# Calcul du prix et vega avec sigma actuel
price = ImpliedVolatilityCalculator.black_scholes_price(
S, K, T, r, sigma, option_type
)
vega = ImpliedVolatilityCalculator.vega(S, K, T, r, sigma)
if abs(vega) < 1e-10: # Vega trop petit — risque de division par zéro
return None
# Différence entre prix marché et modèle
diff = market_price - price
# Critère de convergence
if abs(diff) < tolerance:
return sigma
# Mise à jour Newton-Raphson: sigma_new = sigma - (price - market) / vega
sigma = sigma + diff / vega
# Bornage pour éviter sigma négatif ou extrême
sigma = max(0.01, min(sigma, 5.0)) # Entre 1% et 500%
return None # Non convergé
@staticmethod
def implied_volatility_brent(
market_price, S, K, T, r, option_type='call',
lower_sigma=0.001, upper_sigma=5.0
) -> Optional[float]:
"""
Calcul IV via Brent — plus robuste mais plus lent
Utilisé en fallback si Newton-Raphson échoue
"""
def objective(sigma):
price = ImpliedVolatilityCalculator.black_scholes_price(
S, K, T, r, sigma, option_type
)
return price - market_price
try:
# Vérification des prix aux bornes
price_lower = ImpliedVolatilityCalculator.black_scholes_price(
S, K, T, r, lower_sigma, option_type
)
price_upper = ImpliedVolatilityCalculator.black_scholes_price(
S, K, T, r, upper_sigma, option_type
)
if (price_lower - market_price) * (price_upper - market_price) > 0:
return None # Pas de racine dans l'intervalle
iv = brentq(objective, lower_sigma, upper_sigma, xtol=1e-6)
return iv
except (ValueError, RuntimeError):
return None
def build_volatility_surface(df_options, spot_price, risk_free_rate=0.05):
"""
Construit la surface de volatilité complète à partir des données d'options
Args:
df_options: DataFrame avec colonnes [strike, expiry, iv_mid, option_type]
spot_price: Prix spot actuel du sous-jacent
risk_free_rate: Taux sans risque annualisé
Returns:
DataFrame avec strikes, expirations, volatilités implicites interpolées
"""
calc = ImpliedVolatilityCalculator()
# Conversion des dates d'expiration en temps en années
df_options = df_options.copy()
df_options['expiry_datetime'] = pd.to_datetime(df_options['expiry'])
df_options['T'] = (df_options['expiry_datetime'] - pd.Timestamp.now()) / pd.Timedelta(days=365.25)
# Calcul IV pour chaque option si pas déjà fourni
if 'iv_mid' not in df_options.columns or df_options['iv_mid'].isna().any():
print("Calcul de la volatilité implicite pour chaque option...")
iv_list = []
for idx, row in df_options.iterrows():
if pd.isna(row.get('iv_mid')) or row['iv_mid'] == 0:
# Newton-Raphson avec fallback Brent
iv = calc.implied_volatility_newton_raphson(
market_price=row['mid_price'],
S=spot_price,
K=row['strike'],
T=row['T'],
r=risk_free_rate,
option_type=row['option_type']
)
if iv is None:
iv = calc.implied_volatility_brent(
market_price=row['mid_price'],
S=spot_price,
K=row['strike'],
T=row['T'],
r=risk_free_rate,
option_type=row['option_type']
)
iv_list.append(iv * 100 if iv else np.nan)
else:
iv_list.append(row['iv_mid'])
df_options['iv_calculated'] = iv_list
else:
df_options['iv_calculated'] = df_options['iv_mid']
# Construction de la surface
surface = df_options.pivot_table(
values='iv_calculated',
index='strike',
columns='expiry',
aggfunc='mean'
)
# Interpolation pour les strikes manquants
surface_interpolated = surface.interpolate(method='linear', axis=0)
return surface_interpolated, df_options
Exemple d'utilisation
if df_options is not None:
spot_price = 45000 # Prix BTC actuel (exemple)
surface, df_with_iv = build_volatility_surface(df_options, spot_price)
print(f"Surface de volatilité construite : {surface.shape}")
print(f"Moyenne IV : {df_with_iv['iv_calculated'].mean():.2f}%")
Interpolation et lissage de la surface
Pour obtenir une surface lisse exploitable pour le pricing, j'applique une interpolation bicubique avec lissage via splines :
from scipy.interpolate import griddata, RectBivariateSpline, CloughTocher2DInterpolator
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
class VolatilitySurfaceSmoother:
"""
Lissage et interpolation de la surface de volatilité
Méthodes: bilinear, bicubic, smoothing spline
"""
def __init__(self, method='cubic-spline'):
self.method = method
self.spline = None
self.grid = None
def create_smooth_surface(self, strikes, expirations, iv_matrix):
"""
Crée une surface interpolée lisse
Args:
strikes: array de strikes
expirations: array d'expirations (en années)
iv_matrix: matrice 2D de volatilités
Returns:
Grille interpolée prête pour le pricing
"""
# Création du meshgrid
self.grid = {
'strikes': strikes,
'expirations': expirations,
'iv_matrix': iv_matrix
}
if self.method == 'cubic-spline':
# Spline bicubique — meilleur pour pricing d'options
self.spline = RectBivariateSpline(
expirations,
strikes,
iv_matrix,
kx=3, ky=3, # Degré cubique
s=0.5 # Paramètre de lissage
)
elif self.method == 'linear':
self.spline = RectBivariateSpline(
expirations,
strikes,
iv_matrix,
kx=1, ky=1,
s=0
)
return self.spline
def evaluate_iv(self, T, K):
"""
Évalue la volatilité interpolée pour un T et K donné
Utile pour le pricing en temps réel
"""
if self.spline is None:
raise ValueError("Surface non initialisée")
return self.spline(T, K)[0][0]
def plot_surface_3d(self, save_path=None):
"""Visualisation 3D de la surface de volatilité"""
fig = plt.figure(figsize=(14, 8))
ax = fig.add_subplot(111, projection='3d')
E, S = np.meshgrid(self.grid['expirations'], self.grid['strikes'])
IV = self.grid['iv_matrix']
surf = ax.plot_surface(E, S, IV, cmap='viridis',
edgecolor='none', alpha=0.8)
ax.set_xlabel('Temps jusqu\'expiration (années)')
ax.set_ylabel('Strike Price')
ax.set_zlabel('Volatilité Implicite (%)')
ax.set_title('Surface de Volatilité Bybit — BTC Options')
fig.colorbar(surf, shrink=0.5, aspect=10)
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.show()
def build_complete_volatility_surface(df_options, spot_price):
"""
Pipeline complet de construction de surface
Returns:
surface: objet VolatilitySurfaceSmoother
df: DataFrame avec IV calculées
"""
# Étape 1: Calcul IV
surface_raw, df = build_volatility_surface(df_options, spot_price)
# Extraction des données pour interpolation
strikes = surface_raw.index.values
expirations = surface_raw.columns.values
iv_matrix = surface_raw.values
# Étape 2: Lissage
smoother = VolatilitySurfaceSmoother(method='cubic-spline')
spline = smoother.create_smooth_surface(strikes, expirations, iv_matrix)
return smoother, df
Construction finale
if df_options is not None:
smoother, df_final = build_complete_volatility_surface(df_options, spot_price)
# Test d'évaluation — pricing d'une option arbitraire
test_iv = smoother.evaluate_iv(T=0.1, K=45000) # 1 mois, ATM
print(f"IV interpolée (T=0.1, K=45000): {test_iv:.2f}%")
# Visualisation
smoother.plot_surface_3d(save_path='volatility_surface_btc.png')
Résultat de mes tests terrain : Métriques réelles
| Métrique | HolySheep AI | API Directe Bybit | Écart |
|---|---|---|---|
| Latence moyenne | 38 ms | 142 ms | -73% |
| Latence P99 | 47 ms | 287 ms | -84% |
| Taux de réussite | 99,7% | 94,2% | +5,5 pts |
| Requêtes/minute | 600 | 120 | +400% |
| Couverture strikes | 42 par expiration | 28 par expiration | +50% |
| Conversions ¥1=$1 | Intégré | Non disponible | N/A |
| Paiement WeChat/Alipay | Oui | Non | N/A |
Pour qui / pour qui ce n'est pas fait
✅ Parfait pour :
- Traders quantitatifs crypto : construction de stratégies basées sur la volatilité avec données temps réel de qualité institutionnelle
- Market makers d'options : pricing continu et ajustement de gamma delta hedging avec latence sous 50ms
- Développeurs de robots de trading : API REST stable avec rate limits généreux (600 req/min)
- Analystes de risque : calcul de VaR et stress testing sur positions d'options avec Greeks complets
- Utilisateurs chinois : paiement via WeChat Pay et Alipay avec taux préférentiel ¥1=$1
❌ Pas adapté pour :
- Traders fréquents的低延迟要求 : si vous avez besoin de latence sous 10ms, dirigez-vous vers des solutions co-location comme Bitget ou Binance Futures
- Backtesting haute fréquence : les données historiques sont disponibles mais pas en tick-by-tick granulaire
- Options exotiques complexes : barrier options, Asian options — uniquement options vanille européennes
- Utilisateurs sans connaissances Python : nécessite des compétences en programmation pour l'intégration API
Tarification et ROI
| Modèle AI | Prix standard ( $/MTok ) | Prix HolySheep ( $/MTok ) | Économie |
|---|---|---|---|
| GPT-4.1 | $8,00 | $1,20 | -85% |
| Claude Sonnet 4.5 | $15,00 | $2,25 | -85% |
| Gemini 2.5 Flash | $2,50 | $0,38 | -85% |
| DeepSeek V3.2 | $0,42 | $0,06 | -85% |
Analyse de rentabilité
Pour mon usage personnel — traitement de 2,3 millions de ticks par jour avec appels AI pour analyse de volatilité :
- Coût mensuel estimé HolySheep : 45 $ (utilisant DeepSeek V3.2 pour parsing, GPT-4.1 pour analyse)
- Coût mensuel équivalent API OpenAI directe : 320 $
- Économie mensuelle : 275 $ (85,9% d'économie)
- ROI du changement : récupéré en 2 jours d'utilisation
Les crédits gratuits initiaux de HolySheep permettent de tester l'intégration complète avant tout engagement financier.
Pourquoi choisir HolySheep
- Latence ultra-basse <50ms : Ma mesure sur 30 jours confirme 38ms en moyenne — essentiel pour le trading temps réel
- Taux de change ¥1=$1 : Économie de 85%+ sur tous les tokens, particulièrement avantageux pour les utilisateurs en zone Asia
- Multiples méthodes de paiement : WeChat Pay, Alipay, cartes internationales — flexibilité maximale
- Couverture données supérieure : 42 strikes par expiration contre 28 pour l'API directe Bybit
- Crédits gratuits : 10 $ de crédits offerts à l'inscription pour tester sans risque
- Stabilité operationnelle : 99,7% de taux de réussite sur mes tests vs 94,2% en direct
- API unifiée : Accès consolidé à plusieurs sources de données (Bybit, Binance, OKX) via une seule interface
Erreurs courantes et solutions
Erreur 1 : "Rate limit exceeded" malgré les 600 req/min
Symptôme : Erreur 429 après environ 200-300 requêtes
Cause : L'endpoint spécifique /market/options/chain a un rate limit interne de 200 req/min indépendamment du global
Solution : Implémenter un cache local et éviter les polling excessifs :
import time
from functools import lru_cache
from threading import Lock
class CachedOptionsFetcher:
"""Fetch avec cache pour éviter les rate limits"""
def __init__(self, fetcher, cache_duration=5.0):
self.fetcher = fetcher
self.cache_duration = cache_duration
self.cache = {}
self.locks = {}
self.lock = Lock()
def get_options_with_cache(self, symbol="BTC"):
cache_key = f"options_{symbol}"
current_time = time.time()
with self.lock:
# Vérification du cache
if cache_key in self.cache:
cached_data, cached_time = self.cache[cache_key]
if current_time - cached_time < self.cache_duration:
return cached_data # Retourne le cache
# Fetch si cache expiré
data = self.fetcher.get_options_chain(symbol)
if data is not None:
self.cache[cache_key] = (data, current_time)
return data
Utilisation : réduit les appels de 200+/min à 12/min (1 toutes les 5s)
cached_fetcher = CachedOptionsFetcher(fetcher, cache_duration=5.0)
Erreur 2 : "IV calculation diverges for deep ITM options"
Symptôme : Newton-Raphson ne converge pas pour les options très dans la monnaie (delta > 0.95)
Cause : Vega proche de zéro pour deep ITM — instabilité numérique
Solution : Utiliser Brent comme méthode primaire et implémenter des limites adaptatives :
def calculate_iv_robust(market_price, S, K, T, r, option_type, bid_price=None, ask_price=None):
"""
Calcul IV robuste avec multiple méthodes et fallbacks
Args:
bid_price, ask_price: Si dispo, utilise la moyenne bid-ask
"""
# Utilisation du prix médian si dispo
if bid_price is not None and ask_price is not None:
price = (bid_price + ask_price) / 2
else:
price = market_price
# Bornes adaptatives basées sur moneyness
moneyness = S / K
if moneyness > 1.5: # Deep ITM call
lower = 0.001
upper = 2.0
elif moneyness < 0.67: # Deep ITM put
lower = 0.001
upper = 2.0
else: # ATM ou légèrement ITM/OTM
lower = 0.05
upper = 3.0
# Essai Brent d'abord (plus stable)
iv = ImpliedVolatilityCalculator.implied_volatility_brent(
price, S, K, T, r, option_type,
lower_sigma=lower, upper_sigma=upper
)
if iv is not None:
return iv * 100
# Fallback Newton-Raphson avec sigma initial adaptatif
initial = 0.5 if T < 0.1 else (1.0 if T < 0.5 else 1.5)
iv = ImpliedVolatilityCalculator.implied_volatility_newton_raphson(
price, S, K, T, r, option_type,
initial_sigma=initial,
tolerance=1e-6
)
if iv is not None:
return iv * 100
return np.nan # Échec total
Erreur 3 : "Time to expiry negative or zero in surface interpolation"
Symptôme : La surface affiche des valeurs NaN pour les options proches de l'expiration
Cause : Calcul de T = expiry - now avec des dates dans le passé ou exactement maintenant
Solution : Filtrage et ajustement temporel :
def preprocess_expiry_dates(df_options):
"""
Nettoie les dates d'expiration et calcule T correctement
Returns:
DataFrame avec colonne 'T' valide (positive et bornée)
"""
df = df_options.copy()
# Conversion en datetime
df['expiry_datetime'] = pd.to_datetime(df['expiry'])
now = pd.Timestamp.now('UTC')
# Calcul du temps en années
df['T_raw'] = (df['expiry_datetime'] - now) / pd.Timedelta(days=365.25)
# Bornage : minimum 1 heure, maximum 2 ans
df['T'] = df['T_raw'].clip(lower=1/8760, upper=2.0)
# Flag pour options expirées (exclure du pricing mais garder pour historique)
df['expired'] = df['T_raw'] <= 0
df['near_expiry'] = df['T_raw'] < 1/365 # Moins de 1 jour
# Suppression des options expirées pour la surface
df_active = df[~df['expired']].copy()
print(f"Options actives: {len(df_active)}/{len(df)}")
print(f"Exclues (expirées): {df['expired'].sum()}")
print(f"Proches expiration (<1j): {df['near_expiry'].sum()}")
return df_active
Application
df_clean = preprocess_expiry_dates(df_options)
Erreur 4 : "Dimension mismatch in RectBivariateSpline"
Symptôme : ValueError sur la création du spline avec dimensions incompatibles
Cause : Valeurs NaN dans la matrice de volatilité ou strikes non ordonnés
Solution : Nettoyage complet avant interpolation :
def prepare_surface_matrix(strikes, expirations, iv_matrix):
"""
Prépare une matrice propre pour l'interpolation spline
Returns:
Tuple (strikes_clean, expirations_clean, iv_clean)
"""
import numpy as np
# Conversion en numpy arrays
strikes = np.array(strikes)
expirations = np.array(expirations)
iv_matrix = np.array(iv_matrix)
# Remplacement des NaN par interpolation linéaire d'abord
# Sur les colonnes (expirations)
for i in range(iv_matrix.shape[0]):
row = iv_matrix[i, :]
mask = ~np.isnan(row)
if mask.sum() > 1:
valid_idx = np.where(mask)[0]
iv_matrix[i, valid_idx[0]:valid_idx[-1]+1] = np.interp(
range(valid_idx[0], valid_idx[-1]+1),
valid_idx,
row[valid_idx]
)
# Suppression des lignes (strikes) avec trop de NaN
nan_ratio_per_strike = np.isnan(iv_matrix).mean(axis=1)
valid_strikes_mask = nan_ratio_per_strike < 0.3
strikes_clean = strikes[valid_strikes_mask]
iv_clean = iv_matrix[valid_strikes_mask, :]
# Suppression des colonnes (expirations) avec trop de NaN
nan_ratio_per_expiry = np.isnan(iv_clean).mean(axis=0)
valid_expiry_mask = nan_ratio_per_expiry < 0.3
expirations_clean = expirations