Skip to content
Published on

Options Trading for Engineers: Greeks, Strategies, and Risk Management with Python Implementation

Authors
  • Name
    Twitter
Options Trading for Engineers

Introduction

Options are one of the most powerful financial instruments available, yet many engineers shy away from them due to perceived complexity. In reality, options pricing is fundamentally a mathematical problem -- one that engineers are uniquely qualified to understand and solve.

This post takes an engineer-first approach: we will build the Black-Scholes pricing model from scratch in Python, compute and visualize the Greeks, implement common strategies, and develop a systematic risk management framework. No hand-waving -- just math, code, and data.

Prerequisites: Basic understanding of probability distributions, Python with NumPy/SciPy, and familiarity with stock market fundamentals.

Options Fundamentals

What Is an Option?

An option is a contract that gives the holder the right, but not the obligation, to buy or sell an underlying asset at a specified price (the strike price) on or before a specified date (the expiration date).

  • Call Option: Right to buy at the strike price. You profit when the underlying price rises above the strike.
  • Put Option: Right to sell at the strike price. You profit when the underlying price falls below the strike.

The buyer pays a premium upfront. The seller (writer) collects the premium and takes on the obligation.

Intrinsic Value and Time Value

import numpy as np

def option_intrinsic_value(spot: float, strike: float, option_type: str) -> float:
    """Calculate intrinsic value of an option."""
    if option_type == 'call':
        return max(spot - strike, 0)
    elif option_type == 'put':
        return max(strike - spot, 0)
    else:
        raise ValueError("option_type must be 'call' or 'put'")

def option_time_value(premium: float, spot: float, strike: float, option_type: str) -> float:
    """Time value = Premium - Intrinsic Value."""
    return premium - option_intrinsic_value(spot, strike, option_type)

# Example: AAPL call option
spot_price = 185.0
strike_price = 180.0
call_premium = 8.50

intrinsic = option_intrinsic_value(spot_price, strike_price, 'call')
time_val = option_time_value(call_premium, spot_price, strike_price, 'call')

print(f"Intrinsic Value: ${intrinsic:.2f}")  # $5.00
print(f"Time Value: ${time_val:.2f}")         # $3.50
print(f"Total Premium: ${call_premium:.2f}")  # $8.50

Option Moneyness

| State             | Call (S vs K)   | Put (S vs K)    | Description                     |
|-------------------|-----------------|-----------------|----------------------------------|
| In-the-Money      | S is above K    | S is below K    | Has intrinsic value              |
| At-the-Money      | S equals K      | S equals K      | Strike equals spot price         |
| Out-of-the-Money  | S is below K    | S is above K    | No intrinsic value, only time    |
| Deep ITM          | S far above K   | S far below K   | Behaves like the underlying      |
| Deep OTM          | S far below K   | S far above K   | Very low probability of exercise |

The Black-Scholes Model

The Black-Scholes-Merton (BSM) model, published in 1973 by Fischer Black, Myron Scholes, and Robert Merton, provides a closed-form solution for pricing European-style options. It remains the foundation of modern options pricing.

Assumptions

  1. The underlying follows geometric Brownian motion with constant volatility
  2. No dividends during the option life (can be extended)
  3. Risk-free interest rate is constant
  4. No transaction costs or taxes
  5. European-style exercise (only at expiration)
  6. Markets are frictionless and continuous

The Formula

The Black-Scholes price for a European call option is:

C = S _ N(d1) - K _ e^(-rT) * N(d2)

And for a European put option:

P = K _ e^(-rT) _ N(-d2) - S * N(-d1)

Where:

  • d1 = (ln(S/K) + (r + sigma^2/2) _ T) / (sigma _ sqrt(T))
  • d2 = d1 - sigma * sqrt(T)
  • S = current stock price
  • K = strike price
  • T = time to expiration (in years)
  • r = risk-free interest rate
  • sigma = volatility of the underlying
  • N(x) = standard normal cumulative distribution function

Python Implementation

import numpy as np
from scipy.stats import norm

class BlackScholes:
    """Black-Scholes option pricing model with full Greeks calculation."""

    def __init__(self, S: float, K: float, T: float, r: float, sigma: float):
        """
        Parameters:
        -----------
        S : float - Current stock price
        K : float - Strike price
        T : float - Time to expiration in years
        r : float - Risk-free interest rate (annualized)
        sigma : float - Volatility (annualized)
        """
        self.S = S
        self.K = K
        self.T = T
        self.r = r
        self.sigma = sigma
        self._compute_d1_d2()

    def _compute_d1_d2(self):
        """Compute d1 and d2 parameters."""
        self.d1 = (
            (np.log(self.S / self.K) + (self.r + 0.5 * self.sigma ** 2) * self.T)
            / (self.sigma * np.sqrt(self.T))
        )
        self.d2 = self.d1 - self.sigma * np.sqrt(self.T)

    def call_price(self) -> float:
        """Calculate Black-Scholes call option price."""
        return (
            self.S * norm.cdf(self.d1)
            - self.K * np.exp(-self.r * self.T) * norm.cdf(self.d2)
        )

    def put_price(self) -> float:
        """Calculate Black-Scholes put option price."""
        return (
            self.K * np.exp(-self.r * self.T) * norm.cdf(-self.d2)
            - self.S * norm.cdf(-self.d1)
        )

    # --- Greeks ---

    def delta(self, option_type: str = 'call') -> float:
        """Delta: rate of change of option price w.r.t. underlying price."""
        if option_type == 'call':
            return norm.cdf(self.d1)
        return norm.cdf(self.d1) - 1

    def gamma(self) -> float:
        """Gamma: rate of change of delta w.r.t. underlying price.
        Same for calls and puts."""
        return norm.pdf(self.d1) / (self.S * self.sigma * np.sqrt(self.T))

    def theta(self, option_type: str = 'call') -> float:
        """Theta: rate of change of option price w.r.t. time (per calendar day)."""
        common = -(self.S * norm.pdf(self.d1) * self.sigma) / (2 * np.sqrt(self.T))
        if option_type == 'call':
            theta_annual = common - self.r * self.K * np.exp(-self.r * self.T) * norm.cdf(self.d2)
        else:
            theta_annual = common + self.r * self.K * np.exp(-self.r * self.T) * norm.cdf(-self.d2)
        return theta_annual / 365  # per calendar day

    def vega(self) -> float:
        """Vega: rate of change of option price w.r.t. volatility.
        Same for calls and puts. Per 1% move in vol."""
        return self.S * norm.pdf(self.d1) * np.sqrt(self.T) / 100

    def rho(self, option_type: str = 'call') -> float:
        """Rho: rate of change of option price w.r.t. interest rate.
        Per 1% move in rate."""
        if option_type == 'call':
            return self.K * self.T * np.exp(-self.r * self.T) * norm.cdf(self.d2) / 100
        return -self.K * self.T * np.exp(-self.r * self.T) * norm.cdf(-self.d2) / 100

    def summary(self, option_type: str = 'call') -> dict:
        """Return a complete summary of price and Greeks."""
        price = self.call_price() if option_type == 'call' else self.put_price()
        return {
            'type': option_type,
            'price': round(price, 4),
            'delta': round(self.delta(option_type), 4),
            'gamma': round(self.gamma(), 4),
            'theta': round(self.theta(option_type), 4),
            'vega': round(self.vega(), 4),
            'rho': round(self.rho(option_type), 4),
        }


# --- Example Usage ---
bs = BlackScholes(S=100, K=100, T=30/365, r=0.05, sigma=0.20)

print("=== ATM Call Option (S=100, K=100, 30 DTE, 20% vol) ===")
for key, val in bs.summary('call').items():
    print(f"  {key:>8}: {val}")

print("\n=== ATM Put Option ===")
for key, val in bs.summary('put').items():
    print(f"  {key:>8}: {val}")

# Verify Put-Call Parity: C - P = S - K*e^(-rT)
call_p = bs.call_price()
put_p = bs.put_price()
parity_lhs = call_p - put_p
parity_rhs = bs.S - bs.K * np.exp(-bs.r * bs.T)
print(f"\nPut-Call Parity Check: {parity_lhs:.6f} == {parity_rhs:.6f} -> {np.isclose(parity_lhs, parity_rhs)}")

Expected Output:

=== ATM Call Option (S=100, K=100, 30 DTE, 20% vol) ===
      type: call
     price: 2.5265
     delta: 0.5282
     gamma: 0.0695
     theta: -0.0343
      vega: 0.1143
       rho: 0.0399

=== ATM Put Option ===
      type: put
     price: 2.1172
     delta: -0.4718
     gamma: 0.0695
     theta: -0.0207
      vega: 0.1143
       rho: -0.0420

Put-Call Parity Check: 0.409336 == 0.409336 -> True

Understanding the Greeks

The Greeks measure the sensitivity of an option's price to various factors. Think of them as partial derivatives of the option pricing function.

Greeks Summary Table

| Greek | Symbol | Measures                          | Call Range     | Put Range       | Key Insight                           |
|-------|--------|-----------------------------------|----------------|-----------------|---------------------------------------|
| Delta | D      | Price sensitivity to underlying   | 0 to +1        | -1 to 0         | Probability proxy for finishing ITM   |
| Gamma | G      | Rate of change of Delta           | Always positive| Always positive | Highest for ATM near expiration       |
| Theta | Th     | Time decay per day                | Usually neg    | Usually neg     | Accelerates near expiration           |
| Vega  | V      | Sensitivity to volatility         | Always positive| Always positive | Highest for ATM, long-dated options   |
| Rho   | Rho    | Sensitivity to interest rates     | Positive       | Negative        | Usually smallest Greek                |

Visualizing Greeks Across Strike Prices

import numpy as np
from scipy.stats import norm

def bs_greeks_surface(S, K_range, T, r, sigma):
    """Compute Greeks across a range of strike prices."""
    results = {'K': [], 'call_delta': [], 'put_delta': [],
               'gamma': [], 'call_theta': [], 'vega': []}

    for K in K_range:
        d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)

        results['K'].append(K)
        results['call_delta'].append(norm.cdf(d1))
        results['put_delta'].append(norm.cdf(d1) - 1)
        results['gamma'].append(norm.pdf(d1) / (S * sigma * np.sqrt(T)))

        theta_common = -(S * norm.pdf(d1) * sigma) / (2 * np.sqrt(T))
        call_theta = theta_common - r * K * np.exp(-r * T) * norm.cdf(d2)
        results['call_theta'].append(call_theta / 365)

        results['vega'].append(S * norm.pdf(d1) * np.sqrt(T) / 100)

    return results

# Parameters
S = 100
K_range = np.arange(70, 131, 1)
T = 30 / 365
r = 0.05
sigma = 0.25

greeks = bs_greeks_surface(S, K_range, T, r, sigma)

# Print a sample of results
print(f"{'Strike':>7} {'CallDelta':>10} {'PutDelta':>10} {'Gamma':>10} {'Theta':>10} {'Vega':>10}")
print("-" * 67)
for i in range(0, len(K_range), 10):
    print(f"{greeks['K'][i]:>7.0f} {greeks['call_delta'][i]:>10.4f} "
          f"{greeks['put_delta'][i]:>10.4f} {greeks['gamma'][i]:>10.4f} "
          f"{greeks['call_theta'][i]:>10.4f} {greeks['vega'][i]:>10.4f}")

Key Intuitions for Each Greek

Delta -- Think of delta as the "equivalent shares" position. A call with delta 0.50 behaves like holding 50 shares (per contract of 100 shares). Delta also approximates the probability that the option finishes in-the-money.

Gamma -- Gamma is the acceleration of delta. High gamma means delta changes rapidly. ATM options near expiration have enormous gamma, which is why they are dangerous to sell and exciting to buy.

Theta -- Time is the option buyer's enemy and the seller's friend. Theta decay is not linear: it accelerates as expiration approaches. An option loses roughly 1/3 of its remaining time value in the final week.

Vega -- Volatility is the most important factor in options pricing after the underlying price itself. Buying options is a bet that realized volatility will exceed implied volatility. Selling options is the opposite bet.

Rho -- Usually the least significant Greek for short-dated options. However, for LEAPS (long-dated options, 1+ years), rho becomes meaningful as interest rate changes compound over time.

Common Options Strategies

Strategy Comparison Table

| Strategy         | Max Profit        | Max Loss         | Breakeven              | Best When                    | Complexity |
|------------------|-------------------|------------------|------------------------|------------------------------|------------|
| Long Call        | Unlimited         | Premium paid     | Strike + Premium       | Bullish, expect big move     | Low        |
| Long Put         | Strike - Premium  | Premium paid     | Strike - Premium       | Bearish, expect big drop     | Low        |
| Covered Call     | Premium + (K - S) | S - Premium      | Purchase - Premium     | Neutral to mildly bullish    | Low        |
| Protective Put   | Unlimited upside  | Premium + (S-K)  | S + Premium            | Long stock, hedge downside   | Low        |
| Bull Call Spread  | Width - Debit     | Net debit        | Lower K + Debit        | Moderately bullish           | Medium     |
| Bear Put Spread   | Width - Debit     | Net debit        | Higher K - Debit       | Moderately bearish           | Medium     |
| Long Straddle    | Unlimited         | Total premium    | K +/- Total premium    | Expect big move, either way  | Medium     |
| Iron Condor      | Net credit        | Width - Credit   | Inner strikes +/- credit| Low volatility, range-bound | High       |

Covered Call Implementation

A covered call involves holding 100 shares of stock and selling one call option against the position. It generates income in exchange for capping upside.

import numpy as np

def covered_call_pnl(stock_prices: np.ndarray, purchase_price: float,
                     strike: float, premium: float) -> dict:
    """
    Calculate P&L for a covered call position.

    Parameters:
    -----------
    stock_prices : array of possible expiration prices
    purchase_price : price at which stock was purchased
    strike : call option strike price
    premium : premium received for selling the call
    """
    # Stock P&L
    stock_pnl = stock_prices - purchase_price

    # Short call P&L (negative when stock above strike)
    short_call_pnl = np.where(
        stock_prices > strike,
        premium - (stock_prices - strike),
        premium
    )

    # Combined P&L
    total_pnl = stock_pnl + short_call_pnl

    # Key metrics
    max_profit = (strike - purchase_price) + premium
    breakeven = purchase_price - premium
    max_loss = breakeven  # Stock goes to zero

    return {
        'stock_prices': stock_prices,
        'stock_pnl': stock_pnl,
        'option_pnl': short_call_pnl,
        'total_pnl': total_pnl,
        'max_profit': max_profit,
        'breakeven': breakeven,
        'max_loss': max_loss,
    }

# Example: Buy AAPL at $185, sell $190 call for $3.50
prices = np.arange(160, 211, 1).astype(float)
result = covered_call_pnl(prices, purchase_price=185, strike=190, premium=3.50)

print("=== Covered Call P&L: Long AAPL $185 + Short $190 Call @ $3.50 ===")
print(f"Max Profit: ${result['max_profit']:.2f} (at ${190} or above)")
print(f"Breakeven:  ${result['breakeven']:.2f}")
print(f"Max Loss:   ${result['max_loss']:.2f} (stock goes to $0)")
print(f"\nP&L at select prices:")
print(f"  Stock @ $170: ${result['total_pnl'][10]:.2f}")
print(f"  Stock @ $185: ${result['total_pnl'][25]:.2f}")
print(f"  Stock @ $190: ${result['total_pnl'][30]:.2f}")
print(f"  Stock @ $200: ${result['total_pnl'][40]:.2f}")
print(f"  Stock @ $210: ${result['total_pnl'][50]:.2f}")

Iron Condor Implementation

An iron condor profits when the underlying stays within a range. It consists of a bull put spread (below market) and a bear call spread (above market).

import numpy as np

def iron_condor_pnl(stock_prices: np.ndarray,
                    put_long_K: float, put_short_K: float,
                    call_short_K: float, call_long_K: float,
                    net_credit: float) -> dict:
    """
    Calculate P&L for an iron condor.

    Legs:
    1. Buy put  at put_long_K  (lower protection)
    2. Sell put at put_short_K (collect premium)
    3. Sell call at call_short_K (collect premium)
    4. Buy call at call_long_K  (upper protection)
    """
    # Bull put spread P&L
    short_put_pnl = np.where(stock_prices < put_short_K, -(put_short_K - stock_prices), 0.0)
    long_put_pnl = np.where(stock_prices < put_long_K, put_long_K - stock_prices, 0.0)
    put_spread_pnl = short_put_pnl + long_put_pnl

    # Bear call spread P&L
    short_call_pnl = np.where(stock_prices > call_short_K, -(stock_prices - call_short_K), 0.0)
    long_call_pnl = np.where(stock_prices > call_long_K, stock_prices - call_long_K, 0.0)
    call_spread_pnl = short_call_pnl + long_call_pnl

    # Total P&L
    total_pnl = put_spread_pnl + call_spread_pnl + net_credit

    # Metrics
    put_width = put_short_K - put_long_K
    call_width = call_long_K - call_short_K
    max_width = max(put_width, call_width)

    return {
        'stock_prices': stock_prices,
        'total_pnl': total_pnl,
        'max_profit': net_credit,
        'max_loss': max_width - net_credit,
        'lower_breakeven': put_short_K - net_credit,
        'upper_breakeven': call_short_K + net_credit,
    }

# Example: SPY Iron Condor
# SPY at $500, 30 DTE
# Buy 480 Put, Sell 485 Put, Sell 515 Call, Buy 520 Call
# Net credit: $1.80
prices = np.arange(470, 531, 0.5).astype(float)
ic = iron_condor_pnl(prices,
                     put_long_K=480, put_short_K=485,
                     call_short_K=515, call_long_K=520,
                     net_credit=1.80)

print("=== Iron Condor: SPY 480/485/515/520 @ $1.80 credit ===")
print(f"Max Profit:      ${ic['max_profit']:.2f} (SPY between $485-$515)")
print(f"Max Loss:        ${ic['max_loss']:.2f}")
print(f"Lower Breakeven: ${ic['lower_breakeven']:.2f}")
print(f"Upper Breakeven: ${ic['upper_breakeven']:.2f}")
print(f"Risk/Reward:     1:{ic['max_profit']/ic['max_loss']:.2f}")
print(f"Profit Zone:     ${ic['upper_breakeven'] - ic['lower_breakeven']:.2f} wide "
      f"({(ic['upper_breakeven'] - ic['lower_breakeven'])/500*100:.1f}% of spot)")

P&L Diagrams Explained

Understanding P&L diagrams is critical for visualizing risk before entering a trade. The X-axis represents the underlying price at expiration, and the Y-axis shows the profit or loss.

Long Call P&L: The diagram shows a flat line at the premium paid (max loss) for all prices below the strike, then rises linearly above the breakeven point (strike + premium). The slope is 1:1 with the underlying.

Iron Condor P&L: The diagram is a flat line at the max profit level between the two short strikes, then slopes downward on both sides, reaching max loss at or beyond the long strikes. It forms a shape like a plateau with sloping walls.

Key reading tips for P&L diagrams:

  • Where the line crosses zero is your breakeven
  • The flattest part shows where you want the stock to expire
  • Steeper slopes mean more sensitivity to price movement
  • The distance between max profit and max loss is your risk-reward ratio

Risk Management Framework

Position Sizing with Kelly Criterion

import numpy as np

def kelly_criterion(win_prob: float, win_loss_ratio: float) -> float:
    """
    Calculate optimal bet size using Kelly Criterion.

    f* = p - (1-p)/b

    where:
    p = probability of winning
    b = ratio of win amount to loss amount
    """
    return win_prob - (1 - win_prob) / win_loss_ratio

def options_position_sizing(account_size: float, max_risk_pct: float,
                           max_loss_per_contract: float,
                           kelly_fraction: float = 0.5) -> dict:
    """
    Calculate position size for options trades.

    Parameters:
    -----------
    account_size : total account value
    max_risk_pct : maximum percentage of account to risk per trade
    max_loss_per_contract : maximum loss per contract (including commissions)
    kelly_fraction : fraction of Kelly to use (half-Kelly is common)
    """
    max_risk_dollars = account_size * max_risk_pct
    max_contracts_risk = int(max_risk_dollars / max_loss_per_contract)

    kelly_bet = kelly_fraction * account_size
    max_contracts_kelly = int(kelly_bet / max_loss_per_contract)

    # Use the more conservative of the two
    recommended = min(max_contracts_risk, max_contracts_kelly)

    return {
        'account_size': account_size,
        'max_risk_dollars': max_risk_dollars,
        'max_contracts_by_risk': max_contracts_risk,
        'max_contracts_by_kelly': max_contracts_kelly,
        'recommended_contracts': max(1, recommended),
        'total_risk': max(1, recommended) * max_loss_per_contract,
        'risk_pct_of_account': max(1, recommended) * max_loss_per_contract / account_size * 100,
    }

# Example: Iron Condor position sizing
account = 100_000
win_prob = 0.70  # IC wins about 70% of the time
avg_win = 1.80 * 100  # $180 per contract
avg_loss = 3.20 * 100  # $320 per contract

kelly = kelly_criterion(win_prob, avg_win / avg_loss)
print(f"Full Kelly: {kelly:.2%}")
print(f"Half Kelly: {kelly/2:.2%}")

sizing = options_position_sizing(
    account_size=account,
    max_risk_pct=0.02,  # 2% max risk per trade
    max_loss_per_contract=320,
    kelly_fraction=kelly / 2  # half-Kelly
)

print(f"\n=== Position Sizing for Iron Condor ===")
for key, val in sizing.items():
    if isinstance(val, float):
        print(f"  {key}: ${val:,.2f}" if 'pct' not in key else f"  {key}: {val:.2f}%")
    else:
        print(f"  {key}: {val}")

Risk Management Rules

Here is a systematic checklist for managing options risk:

  1. Never risk more than 2% of account on a single trade -- this ensures survival through losing streaks.
  2. Total portfolio theta should not exceed 0.5% of account per day -- excessive theta collection implies excessive risk.
  3. Diversify across expirations -- do not have all positions expiring in the same week.
  4. Set mechanical stop-losses -- close at 2x premium received for credit spreads.
  5. Track portfolio Greeks -- monitor net delta, gamma, theta, and vega exposure.

Common Mistakes and Loss Scenarios

Mistake 1: Selling Naked Options Without Understanding Tail Risk

Selling naked (uncovered) calls has unlimited theoretical loss. Even selling naked puts can result in catastrophic losses. In February 2018, XIV (inverse VIX) collapsed 96% in a single day, wiping out sellers of volatility.

Rule: Always use defined-risk strategies (spreads, condors) unless you have significant experience and margin.

Mistake 2: Ignoring Implied Volatility When Buying Options

# Demonstration: Buying options before earnings (high IV)
# vs. after earnings (IV crush)

pre_earnings_iv = 0.60   # 60% IV before earnings
post_earnings_iv = 0.25  # 25% IV after (IV crush)

bs_pre = BlackScholes(S=100, K=100, T=14/365, r=0.05, sigma=pre_earnings_iv)
bs_post = BlackScholes(S=102, K=100, T=13/365, r=0.05, sigma=post_earnings_iv)

print("=== IV Crush Example ===")
print(f"Pre-earnings call price (IV=60%):  ${bs_pre.call_price():.2f}")
print(f"Post-earnings call price (IV=25%, stock UP $2): ${bs_post.call_price():.2f}")
print(f"P&L despite stock moving UP: ${bs_post.call_price() - bs_pre.call_price():.2f}")
print(f"\nThe stock went up $2 but you LOST money due to IV crush!")

This scenario is extremely common. Engineers who buy calls before earnings often lose money even when they correctly predict the direction, because the volatility contraction overwhelms the directional gain.

Mistake 3: Poor Position Sizing

Allocating too much capital to a single options trade is the most common way to blow up an account. Options can go to zero. Unlike stocks, where a 50% drawdown requires a 100% gain to recover, options that expire worthless result in a 100% loss of the invested amount.

Mistake 4: Not Understanding Assignment Risk

American-style options can be exercised at any time before expiration. Short in-the-money options, especially puts near expiration or calls approaching an ex-dividend date, carry significant early assignment risk. Always close or roll positions before expiration if you do not want assignment.

Mistake 5: Overcomplicating Strategies

Multi-leg strategies with 4+ legs often have high commission costs and wide bid-ask spreads that eat into theoretical edge. Start with simple strategies (covered calls, cash-secured puts) and add complexity only when you have a demonstrated edge.

Practical Workflow: From Analysis to Execution

import numpy as np
from scipy.stats import norm

def screen_iron_condor(spot: float, sigma: float, T: float, r: float,
                       target_delta: float = 0.16,
                       spread_width: float = 5.0) -> dict:
    """
    Screen for an iron condor with target short strike deltas.

    A delta of ~0.16 corresponds to roughly 1 standard deviation OTM,
    giving approximately 68% probability of profit.
    """
    # Find strikes at target delta using BS inverse
    # For put: find K where |delta| = target_delta
    # For call: find K where delta = target_delta

    # Binary search for strikes
    def find_strike(target_d, option_type, lo, hi, tol=0.01):
        for _ in range(100):
            mid = (lo + hi) / 2
            d1 = (np.log(spot / mid) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
            if option_type == 'call':
                current_d = norm.cdf(d1)
            else:
                current_d = abs(norm.cdf(d1) - 1)

            if abs(current_d - target_d) < tol:
                return round(mid)
            if option_type == 'call':
                if current_d > target_d:
                    lo = mid
                else:
                    hi = mid
            else:
                if current_d > target_d:
                    hi = mid
                else:
                    lo = mid
        return round(mid)

    call_short_K = find_strike(target_delta, 'call', spot, spot * 1.5)
    put_short_K = find_strike(target_delta, 'put', spot * 0.5, spot)
    call_long_K = call_short_K + spread_width
    put_long_K = put_short_K - spread_width

    # Price each leg
    def bs_price(K, opt_type):
        d1 = (np.log(spot / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)
        if opt_type == 'call':
            return spot * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
        return K * np.exp(-r * T) * norm.cdf(-d2) - spot * norm.cdf(-d1)

    credit_call_spread = bs_price(call_short_K, 'call') - bs_price(call_long_K, 'call')
    credit_put_spread = bs_price(put_short_K, 'put') - bs_price(put_long_K, 'put')
    total_credit = credit_call_spread + credit_put_spread
    max_loss = spread_width - total_credit

    return {
        'put_long': put_long_K,
        'put_short': put_short_K,
        'call_short': call_short_K,
        'call_long': call_long_K,
        'credit_received': round(total_credit, 2),
        'max_loss': round(max_loss, 2),
        'risk_reward': f"1:{total_credit/max_loss:.2f}",
        'credit_pct_of_width': f"{total_credit/spread_width*100:.1f}%",
    }

# Screen for SPY iron condor
result = screen_iron_condor(spot=500, sigma=0.18, T=30/365, r=0.05)
print("=== Iron Condor Screening Result (SPY, 30 DTE, 18% IV) ===")
for key, val in result.items():
    print(f"  {key}: {val}")

Summary

Options are a powerful tool in every engineer's financial toolkit. The key takeaways are:

  1. Master the Black-Scholes model -- not just the formula, but the assumptions and their limitations.
  2. Understand the Greeks deeply -- they tell you exactly how your position will behave under different scenarios.
  3. Start with defined-risk strategies -- covered calls, protective puts, and vertical spreads before attempting complex multi-leg trades.
  4. Position sizing is everything -- no strategy survives poor risk management.
  5. Implied volatility matters more than direction -- especially for option buyers.
  6. Paper trade first -- use tools like QuantLib or options paper trading platforms before risking real capital.

The code examples in this post provide a foundation for building your own options analysis toolkit. Extend them with real market data, backtesting capabilities, and automated screening to develop a systematic approach to options trading.

References

  • Hull, John C. Options, Futures, and Other Derivatives, 11th Edition. Pearson, 2022. The definitive textbook on derivatives pricing and risk management.
  • Black, Fischer; Scholes, Myron. "The Pricing of Options and Corporate Liabilities." Journal of Political Economy, Vol. 81, No. 3, 1973, pp. 637-654. The original paper introducing the Black-Scholes model.
  • CBOE Options Education -- Chicago Board Options Exchange educational resources on options strategies and mechanics.
  • QuantLib Documentation -- Open-source library for quantitative finance with comprehensive options pricing implementations.
  • Investopedia Options Guide -- Accessible introduction to options concepts, strategies, and terminology.
  • Natenberg, Sheldon. Option Volatility and Pricing, 2nd Edition. McGraw-Hill, 2014. Essential reading on volatility trading and options pricing.
  • Macroption Black-Scholes Formula Reference -- Detailed breakdown of Black-Scholes formulas including all Greeks.