Butterfly Spread Python Tutorial: Neutral Options Strategy
The butterfly spread is a limited-risk, defined-profit strategy perfect for neutral markets. This tutorial covers implementation and optimization of butterfly spreads.Butterfly Spread Mechanics
A butterfly spread consists of:- 1 long call/put at lower strike (ATM - width)
- 2 short calls/puts at middle strike (ATM)
- 1 long call/put at higher strike (ATM + width)
- Limited max profit (middle strike spread width minus cost)
- Limited max loss (net debit paid)
- Highest profit when underlying stays near middle strike
Building Butterfly Spread Bots
``python
import numpy as np
from scipy.special import norm
from typing import Dict, List, Tuple
import logging
from datetime import datetime, timedelta
import pandas as pd
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ButterflySpreadBuilder:
"""Build optimized butterfly spreads"""
def __init__(self, S: float, r: float = 0.05):
self.S = S
self.r = r
def black_scholes_call(self, S: float, K: float, T: float, sigma: float) -> float:
"""Calculate call price using Black-Scholes"""
d1 = (np.log(S / K) + (self.r + 0.5 sigma 2) T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
return S norm.cdf(d1) - K np.exp(-self.r T) norm.cdf(d2)
def design_butterfly_spread(self, K: float, T: float, sigma: float,
spread_width: float = 5) -> Dict:
"""Design optimal butterfly spread"""
# Strikes
K_low = K - spread_width
K_mid = K
K_high = K + spread_width
# Prices
C_low = self.black_scholes_call(self.S, K_low, T, sigma)
C_mid = self.black_scholes_call(self.S, K_mid, T, sigma)
C_high = self.black_scholes_call(self.S, K_high, T, sigma)
# Net cost
net_debit = C_low - 2 * C_mid + C_high
# Max profit
max_profit = spread_width - net_debit
# Max loss
max_loss = net_debit
# Breakevens
be_low = K_low + net_debit
be_high = K_high - net_debit
return {
'strikes': {
'long': K_low,
'short': K_mid,
'short_qty': 2,
'long_high': K_high
},
'prices': {
'long': C_low,
'short': C_mid,
'long_high': C_high
},
'net_debit': net_debit,
'max_profit': max_profit,
'max_loss': max_loss,
'breakevens': (be_low, be_high),
'profit_range': (be_low, be_high),
'roi': (max_profit / max_loss) if max_loss > 0 else float('inf')
}
def calculate_butterfly_pnl(self, current_S: float, K: float,
T: float, sigma: float,
spread_width: float) -> float:
"""Calculate current P&L for butterfly"""
K_low = K - spread_width
K_mid = K
K_high = K + spread_width
C_low = self.black_scholes_call(current_S, K_low, T, sigma)
C_mid = self.black_scholes_call(current_S, K_mid, T, sigma)
C_high = self.black_scholes_call(current_S, K_high, T, sigma)
pnl = C_low - 2 * C_mid + C_high
return pnl
def identify_optimal_strikes(self, T: float, sigma: float) -> Dict:
"""Identify optimal middle strike for butterfly"""
# Test range of strikes
K_range = np.linspace(self.S 0.95, self.S 1.05, 11)
best_butterfly = None
best_roi = 0
for K in K_range:
butterfly = self.design_butterfly_spread(K, T, sigma)
if butterfly['roi'] > best_roi:
best_roi = butterfly['roi']
best_butterfly = butterfly
best_butterfly['middle_strike'] = K
return best_butterfly
def generate_butterfly_pnl_chart(self, butterfly: Dict,
spot_range: Tuple[float, float] = None) -> pd.DataFrame:
"""Generate P&L chart for butterfly"""
if spot_range is None:
spot_range = (self.S 0.9, self.S 1.1)
K = butterfly['strikes']['short']
K_low = butterfly['strikes']['long']
K_high = butterfly['strikes']['long_high']
spots = np.linspace(spot_range[0], spot_range[1], 100)
pnls = []
for S in spots:
# Simplified P&L calculation at expiration
value_low = max(S - K_low, 0)
value_mid = max(S - K, 0) * 2
value_high = max(S - K_high, 0)
pnl = value_low - value_mid + value_high - butterfly['net_debit']
pnls.append({
'spot': S,
'pnl': pnl,
'profit': max(pnl, 0),
'loss': min(pnl, 0)
})
return pd.DataFrame(pnls)
`
Dynamic Butterfly Management
`python
class ButterflySpreadManager:
"""Manage butterfly spread positions"""
def __init__(self):
self.positions = []
self.adjustments = []
def add_butterfly_position(self, butterfly: Dict, quantity: int = 1):
"""Add butterfly position to portfolio"""
self.positions.append({
'timestamp': datetime.now(),
'butterfly': butterfly,
'quantity': quantity,
'status': 'open',
'adjustments_count': 0
})
def monitor_butterfly_position(self, position: Dict,
current_S: float, current_T: float,
current_sigma: float) -> Dict:
"""Monitor butterfly position for adjustments"""
butterfly = position['butterfly']
K_low = butterfly['strikes']['long']
K_mid = butterfly['strikes']['short']
K_high = butterfly['strikes']['long_high']
# Calculate distance from optimal
distance_from_mid = abs(current_S - K_mid)
spread_width = K_mid - K_low
# Check if underlying is moving away from profitable zone
if distance_from_mid > spread_width * 1.5:
return {
'action': 'consider_adjustment',
'reason': 'underlying moving away from profitable zone',
'current_S': current_S,
'mid_strike': K_mid,
'recommendation': 'consider closing or adjusting to iron butterfly'
}
# Check time decay
time_decay_multiplier = current_T / butterfly.get('T', 0.25)
if current_T < 0.5 / 365: # Less than 12 hours to expiration
return {
'action': 'prepare_to_close',
'reason': 'near expiration',
'current_T': current_T,
'recommendation': 'close position to capture remaining theta'
}
return {
'action': 'hold',
'reason': 'position within parameters'
}
def adjust_butterfly_to_iron_condor(self, butterfly: Dict,
adjustment_width: float = 5) -> Dict:
"""Convert butterfly to iron condor (wider spreads)"""
K = butterfly['strikes']['short']
spread_width = butterfly['strikes']['long_high'] - butterfly['strikes']['long']
# Add iron condor wings
put_spread = (K - adjustment_width, K - adjustment_width - spread_width)
call_spread = (K + adjustment_width, K + adjustment_width + spread_width)
return {
'original_butterfly': butterfly,
'added_puts': put_spread,
'added_calls': call_spread,
'new_strategy': 'iron_butterfly',
'benefit': 'additional income from wider spreads'
}
def calculate_adjustment_cost(self, position: Dict,
current_S: float) -> float:
"""Calculate cost to adjust butterfly"""
# Cost to close butterfly and open new one
# Simplified calculation
return position['butterfly']['net_debit'] * 0.1 # Estimate 10% of initial debit
def execute_butterfly_adjustments(self, position: Dict,
current_market_data: Dict) -> List[Dict]:
"""Execute recommended adjustments"""
adjustments = []
S = current_market_data['spot']
T = current_market_data['time_to_expiry']
K = position['butterfly']['strikes']['short']
# Adjustment 1: Roll if underlying moved significantly
distance = abs(S - K)
spread_width = position['butterfly']['strikes']['long_high'] - position['butterfly']['strikes']['long']
if distance > spread_width:
# Roll to new butterfly centered at current spot
roll_adjustment = {
'type': 'roll',
'from_strike': K,
'to_strike': S,
'rationale': 'center butterfly on current spot'
}
adjustments.append(roll_adjustment)
# Adjustment 2: Close if time decay is excessive
if T < 0.02: # Less than 5 days
close_adjustment = {
'type': 'close',
'reason': 'high time decay near expiration'
}
adjustments.append(close_adjustment)
return adjustments
`
Butterfly Portfolio Optimization
`python
class ButterflyPortfolioOptimizer:
"""Optimize portfolio of butterfly spreads"""
def __init__(self):
self.butterflies = []
self.optimization_history = []
def create_butterfly_ladder(self, S: float, strikes: List[float],
T: float, sigma: float,
spread_width: float = 5) -> List[Dict]:
"""Create ladder of butterflies at different strikes"""
builder = ButterflySpreadBuilder(S)
butterflies = []
for K in strikes:
butterfly = builder.design_butterfly_spread(K, T, sigma, spread_width)
butterfly['middle_strike'] = K
butterflies.append(butterfly)
return butterflies
def calculate_portfolio_metrics(self, butterflies: List[Dict]) -> Dict:
"""Calculate metrics for butterfly portfolio"""
total_debit = sum(b['net_debit'] for b in butterflies)
total_max_profit = sum(b['max_profit'] for b in butterflies)
avg_roi = np.mean([b['roi'] for b in butterflies if not np.isinf(b['roi'])])
# Calculate coverage
strikes = [b['middle_strike'] for b in butterflies]
coverage = max(strikes) - min(strikes)
return {
'num_butterflies': len(butterflies),
'total_debit': total_debit,
'total_max_profit': total_max_profit,
'average_roi': avg_roi,
'strike_coverage': coverage,
'margin_efficiency': total_max_profit / total_debit if total_debit > 0 else 0
}
def optimize_butterfly_spacing(self, S: float, T: float, sigma: float,
num_butterflies: int = 3) -> List[float]:
"""Find optimal spacing for butterfly strikes"""
# Use optimization to find best spacing
# Simplified: evenly spaced approach
width = S * 0.1 # 10% range
step = width / (num_butterflies + 1)
strikes = [S - width/2 + step * (i + 1) for i in range(num_butterflies)]
return strikes
def hedge_butterfly_portfolio(self, butterflies: List[Dict],
portfolio_delta: float) -> Dict:
"""Recommend hedges for butterfly portfolio"""
# Butterflies are delta neutral, but slight adjustments may be needed
if abs(portfolio_delta) > 10:
hedge_size = portfolio_delta
return {
'hedge_type': 'index_futures' if portfolio_delta > 0 else 'short_futures',
'size': abs(hedge_size),
'rationale': f"Delta is {portfolio_delta}"
}
return {'hedge_needed': False}
`
Backtesting Butterfly Strategies
`python
class ButterflyBacktester:
"""Backtest butterfly spread strategies"""
def __init__(self):
self.results = []
def backtest_butterfly_strategy(self, price_data: pd.DataFrame,
expiration_days: int = 30,
rebalance_frequency: int = 7) -> Dict:
"""Backtest butterfly spread strategy"""
results = {
'trades': [],
'total_pnl': 0,
'num_trades': 0,
'win_rate': 0
}
# Trade setup every rebalance_frequency days
for i in range(0, len(price_data) - expiration_days, rebalance_frequency):
entry_price = price_data.iloc[i]['close']
entry_idx = i
exit_idx = min(i + expiration_days, len(price_data) - 1)
exit_price = price_data.iloc[exit_idx]['close']
# Simplified P&L: profit if within profitable range
if abs(exit_price - entry_price) < entry_price * 0.05: # Within 5%
trade_pnl = 100 # Fixed max profit example
winner = True
else:
trade_pnl = -50 # Fixed max loss example
winner = False
results['trades'].append({
'entry_price': entry_price,
'exit_price': exit_price,
'pnl': trade_pnl,
'winner': winner
})
results['total_pnl'] += trade_pnl
results['num_trades'] += 1
if results['num_trades'] > 0:
results['win_rate'] = sum(1 for t in results['trades'] if t['winner']) / results['num_trades']
return results
``