Бэктест опционных стратегий Iron Condor на Python с Implied Volatility

Бэктест опционных стратегий Iron Condor на Python с Implied Volatility ЛИЧНЫЙ БЮДЖЕТ и ЭКОНОМИЯ
Пошаговое руководство по созданию профессионального движка для бэктеста опционной стратегии Iron Condor с использованием исторических данных волатильности (IV) на Python.
Суть: Мы разработаем событийно-ориентированный движок на Python для бэктестинга опционной стратегии Iron Condor. Вместо тяжелых исторических баз опционных досок мы применим аналитическое восстановление страйков по заданной дельте через формулу Блэка-Шоулза и историческую подразумеваемую волатильность (IV).

Тестирование опционных стратегий на исторических данных — нетривиальная задача из-за многомерности данных (Option Chain). Хранить и обрабатывать ежеминутные срезы стаканов по сотням страйков и экспираций крайне накладно. В индустриальной практике количественных финансов (Quantitative Finance) часто применяется подход математического моделирования сетки опционов на основе спотовой цены базового актива и кривой подразумеваемой волатильности (Implied Volatility).

При работе со сложными портфелями деривативов часто приходится снижать размерность признаков волатильности; в таких случаях полезно применять методы вроде PCA для портфеля альткоинов: Уменьшение размерности в Python scikit-learn. Для динамического хеджирования или изменения дельты крыльев можно использовать внешние сигналы сентимента, аналогично тому, как строится Парсинг Twitter API и сентимент-анализ на Python NLTK для предсказания крипто-пампов. А для точного исполнения лимитных ордеров при ролловере позиций незаменимы алгоритмы микроструктуры рынка, такие как Поиск паттернов Order Book Imbalance (OBI) на Python.

В этой статье мы напишем чистый, оптимизированный код на Python для симуляции стратегии Iron Condor (Железный Кондор). Стратегия состоит из одновременной продажи OTM-колла и OTM-пута с покупкой более дальних крыльев (OTM) для ограничения риска.

Исходный код

Ниже представлен готовый класс бэктестера IronCondorBacktester, который автоматически рассчитывает страйки по целевой дельте, оценивает стоимость опционов по Блэку-Шоулзу, отслеживает ежедневный PnL (Mark-to-Market) и фиксирует прибыль при достижении таргета.

import numpy as np
import pandas as pd
from scipy.stats import norm

def bs_price(S, K, T, r, sigma, option_type='call'):
    """
    Расчет стоимости европейского опциона по формуле Блэка-Шоулза.
    """
    if T <= 0:
        if option_type == 'call':
            return np.maximum(0.0, S - K)
        else:
            return np.maximum(0.0, K - S)
    
    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':
        return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    else:
        return K * np.exp(-r * T) * norm.cdf(-d2)

def find_strike_by_delta(S, delta, T, r, sigma, option_type='call'):
    """
    Аналитическое нахождение страйка по заданной дельте опциона.
    """
    if T <= 0:
        return S
    
    if option_type == 'call':
        d1 = norm.ppf(delta)
    else:
        d1 = norm.ppf(delta + 1.0)
        
    K = S * np.exp(-d1 * sigma * np.sqrt(T) + (r + 0.5 * sigma**2) * T)
    return float(K)

class IronCondorBacktester:
    def __init__(self, df, short_delta=0.15, long_delta=0.05, dte=45, r=0.05, profit_target=0.5):
        self.df = df.copy()
        self.short_delta = short_delta
        self.long_delta = long_delta
        self.dte = dte / 365.0  # Переводим DTE в доли года
        self.r = r
        self.profit_target = profit_target
        self.results = []

    def run(self):
        in_position = False
        entry_idx = 0
        
        # Параметры открытой позиции
        k_long_put, k_short_put = 0.0, 0.0
        k_short_call, k_long_call = 0.0, 0.0
        entry_net_credit = 0.0
        days_held = 0
        
        for i in range(len(self.df)):
            row = self.df.iloc[i]
            current_date = row['date']
            S = row['close']
            sigma = row['iv']
            
            if not in_position:
                # Открываем позицию Iron Condor
                entry_idx = i
                days_held = 0
                
                # Находим страйки по дельте
                k_long_put = find_strike_by_delta(S, -self.long_delta, self.dte, self.r, sigma, 'put')
                k_short_put = find_strike_by_delta(S, -self.short_delta, self.dte, self.r, sigma, 'put')
                k_short_call = find_strike_by_delta(S, self.short_delta, self.dte, self.r, sigma, 'call')
                k_long_call = find_strike_by_delta(S, self.long_delta, self.dte, self.r, sigma, 'call')
                
                # Считаем цены опционов на входе
                p_long_put = bs_price(S, k_long_put, self.dte, self.r, sigma, 'put')
                p_short_put = bs_price(S, k_short_put, self.dte, self.r, sigma, 'put')
                p_short_call = bs_price(S, k_short_call, self.dte, self.r, sigma, 'call')
                p_long_call = bs_price(S, k_long_call, self.dte, self.r, sigma, 'call')
                
                entry_net_credit = (p_short_put + p_short_call) - (p_long_put + p_long_call)
                in_position = True
                continue
            
            # Если мы в позиции, обновляем параметры
            days_held += 1
            t_remaining = self.dte - (days_held / 365.0)
            
            # Текущие цены опционов для оценки MtM
            p_long_put = bs_price(S, k_long_put, t_remaining, self.r, sigma, 'put')
            p_short_put = bs_price(S, k_short_put, t_remaining, self.r, sigma, 'put')
            p_short_call = bs_price(S, k_short_call, t_remaining, self.r, sigma, 'call')
            p_long_call = bs_price(S, k_long_call, t_remaining, self.r, sigma, 'call')
            
            current_value = (p_short_put + p_short_call) - (p_long_put + p_long_call)
            unrealized_pnl = entry_net_credit - current_value
            
            # Проверка условий выхода
            is_expiration = t_remaining <= 0
            is_profit_target = unrealized_pnl >= self.profit_target * entry_net_credit
            
            if is_expiration or is_profit_target:
                self.results.append({
                    'entry_date': self.df.iloc[entry_idx]['date'],
                    'exit_date': current_date,
                    'entry_spot': self.df.iloc[entry_idx]['close'],
                    'exit_spot': S,
                    'net_credit': entry_net_credit,
                    'realized_pnl': unrealized_pnl,
                    'days_held': days_held,
                    'reason': 'Expiration' if is_expiration else 'Profit Target'
                })
                in_position = False
                
        return pd.DataFrame(self.results)

Разбор параметров

  • short_delta: Дельта коротких (проданных) опционов Call и Put. Обычно выбирается в диапазоне от 0.10 до 0.20. Определяет ширину диапазона безубыточности.
  • long_delta: Дельта длинных (купленных) опционов, защищающих крылья стратегии. Разница между коротким и длинным страйком определяет максимальный риск на сделку.
  • dte: Days To Expiration — количество дней до экспирации опционов в момент открытия позиции. Классический выбор для продажи волатильности — 45 дней.
  • profit_target: Процент от полученной премии (Net Credit), при достижении которого позиция досрочно закрывается (например, 0.5 означает фиксацию 50% прибыли).
  • iv: Историческая подразумеваемая волатильность базового актива, используемая для дисконтирования и оценки опционов по модели Блэка-Шоулза.

Как запустить

Для запуска бэктестера вам понадобятся библиотеки pandas, numpy и scipy. Создадим синтетический датасет, имитирующий поведение базового актива в боковике с повышенной волатильностью, и прогоним тест:

# Генерируем синтетические данные
np.random.seed(42)
n_days = 250
spot_prices = 100 + np.cumsum(np.random.normal(0, 0.8, n_days))
iv_values = 0.20 + np.sin(np.linspace(0, 10, n_days)) * 0.05

dates = pd.date_range(start='2023-01-01', periods=n_days, freq='D')
data = pd.DataFrame({
    'date': dates,
    'close': spot_prices,
    'iv': iv_values
})

# Инициализируем и запускаем бэктестер
backtester = IronCondorBacktester(data, short_delta=0.15, long_delta=0.05, dte=45, profit_target=0.5)
results_df = backtester.run()

# Выводим результаты
print(results_df.head())
print(f"Общий финансовый результат: {results_df['realized_pnl'].sum():.2f} USD")
print(f"Винрейт: {(results_df['realized_pnl'] > 0).mean() * 100:.1f}%")

Оцените статью
FinFluct