Тестирование опционных стратегий на исторических данных — нетривиальная задача из-за многомерности данных (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}%") 



