We will step through the case where buy the 1 year ATM call for 25% vol, and simulate a stock path that realizes an expected vol of 25%.
Sampling
Because our daily moves are sampled from a distribution with a 25% realized vol, the actual realized vol for any single run will differ from exactly 25%. The average realized vol over all 500 runs of 250 days in the batch is of course 25% even though any particular 1-year sim differs slightly.
Exposition
- We will step through several charts with explanations and interpretations for a single run randomly chosen from the batch of 500 runs seeded with a 25% realized vol.
- That template will help you step through other examples in the chartbooks below on your own to get a feel of how p/l paths look for various realized vols. Remember, in every case in this entire document we are assuming the implied vol of the option is 25% at all times.
A Tour Through A Single Run
The Code
import numpy as np import pandas as pd def option_price_and_greeks_simulation(realized_vol, implied_vol, rfr, days_til_expiry, strike_price, spot_price, num_simulations, tenor = 365, option_type='call'): """ Master function to simulate stock prices, calculate option prices, Greeks, delta-hedged P&L, and track the P&L components and hedge shares for each simulation. Parameters: - realized_vol: The realized volatility of the stock (σ_realized) - implied_vol: The implied volatility used for pricing the option (σ_implied) - rfr: Risk-free interest rate (r) as a decimal (e.g., 0.05 for 5%) - days_til_expiry: Number of days until option expiry (T) - strike_price: Strike price of the option (K) - spot_price: Current spot price of the stock (S) - num_simulations: Number of Monte Carlo simulations (n) - tenor: Number of days in a year (e.g., 365 for daily) - option_type: 'call' or 'put' Returns: - DataFrame containing option prices, Greeks (Delta, Gamma, Vega, Theta), delta-hedged P&L, hedge shares, and P&L components for each simulation. """ # Simulate stock price paths using the realized volatility price_paths = monte_carlo_simulation(num_simulations, days_til_expiry, rfr, realized_vol, spot_price) # Initialize DataFrame to store results and statistics results = pd.DataFrame(index=range(days_til_expiry)) stats_df = pd.DataFrame(columns=['realized_vol', 'implied_vol', 'cumulative_portfolio_pnl', 'pnl_percentage_of_option_price', 'expiry_price']) # Calculate option prices, Greeks, and delta-hedged P&L for each path for path in range(num_simulations): prices = price_paths[f'path_{path}'] # Calculate log returns log_returns = np.log(prices / prices.shift(1)).fillna(0) option_prices = [] deltas = [] gammas = [] vegas = [] thetas = [] stock_pnl = [] option_pnl = [] interest_pnl = [] portfolio_pnl = [] cumulative_portfolio_pnl = [] hedge_shares = [] # Initial values initial_delta = black_scholes_euro_delta(prices[0], strike_price, rfr, implied_vol, days_til_expiry, tenor, option_type) hedge_position = -initial_delta # long delta of the option, hence short delta in the stock initial_option_price = black_scholes_euro_price(prices[0], strike_price, rfr, implied_vol, days_til_expiry, tenor, option_type) # Day 0: No P&L, just initial setup option_prices.append(initial_option_price) deltas.append(initial_delta) gammas.append(black_scholes_euro_gamma(prices[0], strike_price, rfr, implied_vol, days_til_expiry, tenor)) vegas.append(black_scholes_euro_vega(prices[0], strike_price, rfr, implied_vol, days_til_expiry, tenor)) thetas.append(black_scholes_euro_theta(prices[0], strike_price, rfr, implied_vol, days_til_expiry,tenor, option_type)) stock_pnl.append(0) option_pnl.append(0) interest_pnl.append(0) portfolio_pnl.append(0) cumulative_portfolio_pnl.append(0) hedge_shares.append(hedge_position) for day in range(1, days_til_expiry): # Calculate days to maturity maturity_days = days_til_expiry - day # Calculate option price and Greeks using the implied volatility option_price = black_scholes_euro_price(prices[day], strike_price, rfr, implied_vol, maturity_days, tenor, option_type) delta = black_scholes_euro_delta(prices[day], strike_price, rfr, implied_vol, maturity_days, tenor, option_type) gamma = black_scholes_euro_gamma(prices[day], strike_price, rfr, implied_vol, maturity_days, tenor) vega = black_scholes_euro_vega(prices[day], strike_price, rfr, implied_vol, maturity_days, tenor) theta = black_scholes_euro_theta(prices[day], strike_price, rfr, implied_vol, maturity_days, tenor, option_type) # Calculate stock P&L as the change in price times prior day's hedge position stock_daily_pnl = hedge_position * (prices[day] - prices[day - 1]) stock_pnl.append(stock_daily_pnl) # Option P&L from previous day to current day option_daily_pnl = option_price - option_prices[-1] option_pnl.append(option_daily_pnl) # Interest P&L from previous day to current day interest_daily_pnl = rfr/tenor * (-hedge_position * prices[day - 1]) interest_pnl.append(interest_daily_pnl) # Portfolio P&L daily_pnl = stock_daily_pnl + option_daily_pnl + interest_daily_pnl portfolio_pnl.append(daily_pnl) # Cumulative Portfolio P&L cumulative_pnl = cumulative_portfolio_pnl[-1] + daily_pnl cumulative_portfolio_pnl.append(cumulative_pnl) # Update hedge position for the next day hedge_position = -delta # Store the results hedge_shares.append(hedge_position) option_prices.append(option_price) deltas.append(delta) gammas.append(gamma) vegas.append(vega) thetas.append(theta) # Add the results to the DataFrame results[f'price_path_{path}'] = prices results[f'log_return_path_{path}'] = log_returns results[f'option_price_path_{path}'] = option_prices results[f'delta_path_{path}'] = deltas results[f'gamma_path_{path}'] = gammas results[f'vega_path_{path}'] = vegas results[f'theta_path_{path}'] = thetas results[f'hedge_shares_path_{path}'] = hedge_shares results[f'stock_pnl_path_{path}'] = stock_pnl results[f'option_pnl_path_{path}'] = option_pnl results[f'interest_pnl_path_{path}'] = interest_pnl results[f'portfolio_pnl_path_{path}'] = portfolio_pnl results[f'cumulative_portfolio_pnl_path_{path}'] = cumulative_portfolio_pnl # Calculate and store summary statistics for this simulation realized_vol = np.std(log_returns[1:]) * np.sqrt(251) cumulative_pnl = cumulative_portfolio_pnl[-1] pnl_percentage_of_option_price = cumulative_pnl / initial_option_price expiry_price = prices.iloc[-1] new_row = pd.DataFrame({ 'realized_vol': [realized_vol], 'implied_vol': [implied_vol], 'cumulative_portfolio_pnl': [cumulative_pnl], 'pnl_percentage_of_option_price': [pnl_percentage_of_option_price], 'expiry_price': [expiry_price] }) stats_df = pd.concat([stats_df, new_row], ignore_index=True) return results, stats_df #test the function #describe the parameters in one line of comment #realized_vol = 0.25, implied_vol = 0.25, rfr = 0.00, days_til_expiry = 251, strike_price = 110, spot_price = 100, num_simulations = 500, tenor = 251, option_type = 'call' #output to a variable results, stats = option_price_and_greeks_simulation(0.25, 0.25, 0.00, 21, 100, 100, 500, tenor = 251, option_type='call') #export each dataframe to an excel file with a descriptive name results.to_excel('option_simulations_and_profits25v_1_month.xlsx') stats.to_excel('option_simulation_stats25v_1_month.xlsx')
The Feeder Tables
The code output looks like this (an example of 1 of the 500 paths):
I then staged the data in Excel for analysis and charting. That table:
You can refer back to this table later. The columns will make sense as we examine the charts they spawn.
Charts & Interpretations
Histogram of daily returns
This particular run drawn from a normal distribution of logreturns with an annual of standard deviation of 25% yielded a sample that had a 24.7% realized vol. The histogram of logreturns for this run:
Stock Path vs Cumulative P/L
Cumulative P/L = Cumulative daily portfolio P/L
Daily Portfolio P/L = Option P/L + Share P/L
- Share P/L is coming from the hedge position * stock price change
- Option P/L is coming almost entirely from the gamma p/l minus the daily decay (ie theta)
- There is no vega P/L because IV is constant at 25%
In this example, despite buying vol for 25%, the stock realized 24.7% and the cumulative p/l was $.058. The initial option premium was $9.95 for a return of .60%.
A few real-life considerations
- It is possible to make money on a long option even if the realized vol is less than the implied you paid. You can also lose if the realized is higher than what you paid. These caveats are mirror images for the option seller.
- After daily hedging costs this would be a loser.
- We assumed a RFR of 0% meaning you’d earn 0% on your short stock proceeds. In reality, there’s not a single interest rate — there’s a funding spread between what you pay on long stock and what you receive on short stock. If the interest rate is 0% and the funding spread is 200 bps wide, then you will have paid annualized 1% interest on the average notional value of your position (ie you would have paid to be short).
What to notice in the chart
- Sharp moves in the stock, regardless of direction, drive the portfolio p/l. You will see the cumulative p/l line uptick on those moves.
- When the stock makes relatively small moves, theta decay overwhelms the gamma p/l and the long option holder experiences losses.
To highlight the daily interaction between the move size and p/l we can see this chart:
What to notice in the chart
Do you see how absolute moves greater than ~1.50% are profitable for the long option holder?
That makes sense. Our option is priced with 25% vol. 25% vol implies daily moves of:
25% / √251 = 1.58%
The delta-neutral long option holder has a daily breakeven on their cost of gamma of 1.58% if the option implies 25% annual vol.
An eagle-eyed observer will notice that:
- There are some small moves [red box] that correspond to zero p/l instead of losses
- There are large moves [green box] that also don’t contribute to profits
The key to those unusual data points lies in the next section of charts…
Gamma vs Moneyness
Near-the-money options have more gamma than options with strikes far from the spot price.
- A deep ITM option has little gamma and theta. At the extreme, where the option trades for intrinsic there is 0 gamma or theta
- A far OTM option also has little to gamma or theta, and zero if it’s worthless. This is actually a redundant since any worthless OTM call or put has a corresponding put or call on the dame strike that is a deep ITM represented in statement (1)
For an option to spit off vol p/l due to gamma & theta, its strike must be “close enough” to the underlying spot price.
To appreciate “close enough” we can use charts. As a refresher, this was the stock vs cumulative p/l chart:
Here’s the gamma of the $100 strike call plotted against the stock price as we approach expiration:
Observations:
- Gamma increases as the stock gets closer to the strike, and declines as it moves away from the strike.
- The cumulative p/l and gamma flatline together. Makes sense. No gamma, no option p/l. Remember the delta is hedged. By the time the stock is around $120 with a few weeks until expiry the delta of the call is near 1.00 and the option has no gamma. No more “volatility p/l”.
Another way to represent this idea is by plotting the gamma of the position against how far the stock is from the strike. Using the implied volatility we compute how the stock is from $100 in units of standard deviation. Intuitively at a 25% vol with 1 year until expiry, a $95 stock is “pretty close” to the 100-strike. With one day to go, it’s 3 standard deviations from the strike.
The key takeway is stock moves have little impact on option p/l unless you have gamma. Likewise, if you are short options, they don’t decay much if they don’t have gamma.
If you sell low priced strangles that have little gamma and the stock doesn’t move for a few days you are getting the outcome you wanted, but you’re not capturing any real decay either.
Your vol p/l is a function of how much gamma your position times the square of how large the stock moves are. And theta cost (or payment if you’re short options) will be roughly proportional to the gamma for that day.
Theta is not an edge. It’s rent for a service known as gamma. You can pay to much for it. Or you can sell it for too little.
Chartbooks for other realized vol paths
The same setup every time
- Buy a 1-year ATM call for 25% implied vol
- This implies our daily breakeven move is 1.5%
- Hedge the delta daily