100% FreeNo Signup Required

Butterfly Spread Python Tutorial: Neutral Options Strategy

The butterfly spread is a limited-risk, defined-profit strategy perfect for neutral markets.

Q

QuantEngines

March 16, 2026

|6 min read

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)
The resulting position has:
  • Limited max profit (middle strike spread width minus cost)
  • Limited max loss (net debit paid)
  • Highest profit when underlying stays near middle strike
You can lay out these strikes and see the resulting payoff shape visually with our Options Payoff Diagram tool before writing any of the code below.

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 ``

Conclusion

Butterfly spreads are excellent for neutral markets with limited risk. The key to success is proper selection of strikes, monitoring for adjustments, and disciplined position management. Always keep risk to the debit paid and exit when the underlying moves significantly from your center strike.

Related Articles

Get new strategy write-ups by email

New backtesting guides and strategy breakdowns as we publish them. Free, unsubscribe anytime.