Python for Finance Part 4: Stock Options
Today we kick it into fifth gear. We are injecting rocket fuel into our trading strategies.
Options are financial contracts that allow the holder to buy or sell an underlying asset, like GameStop (GME) stock, at a predetermined price before a specified expiration date.
For instance, suppose you hold an option to buy 100 shares of GME at $200 per share before a certain date. If the stock price rises to $300 before the option expires, you could choose to exercise the option and purchase the shares at the lower price of $200, making a profit of $10,000 ($100 per share x 100 shares).
Alternatively, if the stock price falls below $200, you could let the option expire and only lose the cost of buying the option.
Options trading can be risky, and investors can lose a lot of money if they are not careful. Here are some reasons why:
In short, options can be a useful tool for managing risk or speculating on price movements, but they can also be very risky if you do not fully understand how they work. It is important to educate yourself and seek professional advice before engaging in options trading.
Today we are going to use the Black-Scholes model to suggest options to buy.
The Black-Scholes model is a mathematical formula used to estimate the value of stock options.
There are many variables to take into consideration, and a lot of data can be pulled from yfinance.
Using this information, the model estimates the fair price of an option at a given point in time.
The model assumes that the price of the underlying stock follows a random walk, meaning that its movement is unpredictable and based on many different factors. It also assumes that the stock price follows a log-normal distribution, meaning that the stock price moves by a certain percentage each day, rather than by a fixed amount.
Lets create a new python file options.py and write some code.
import yfinance as yf
import numpy as np
from scipy.stats import norm
import pandas as pd
import matplotlib.pyplot as plt
# Set stock ticker and expiry date
ticker = input("Enter stock ticker: ")
expiry = input("Enter expiry date: ")
# Get stock data for the past year
start_date = pd.to_datetime('today') - pd.DateOffset(years=1)
end_date = pd.to_datetime('today')
stock_data = yf.download(ticker, start=start_date, end=end_date)
# Calculate historical volatility
returns = stock_data['Adj Close'].pct_change().dropna()
volatility = returns.std() * np.sqrt(252)
# Get option chain for stock ticker and expiry date
option_chain = yf.Ticker(ticker).option_chain(expiry)
# Filter option chain for puts and calls
puts = option_chain.puts.sort_values(by='lastPrice')
calls = option_chain.calls.sort_values(by='lastPrice')
# Filter for conservative trader
conservative_puts = puts[puts["inTheMoney"] == True].head(5)
conservative_calls = calls[calls["inTheMoney"] == True].tail(5)
# Set risk-free interest rate
r = -0.0626
# Calculate time to expiration in years
t = (pd.to_datetime(expiry) - pd.to_datetime('today')).days / 365
The above code imports our favorite yfinance library as well as numpy and pandas for some data handling. We need scipy.stats for some statistics and of course matplotlib for plotting.
We load the options chain from yahoo finance regarding the specific stock. We also calculate the historical volatility which will help us approximate some options better using data from the previous year.
And then we add the Black-Scholes model code. The most important aspect.
# Define Black-Scholes formula
def black_scholes(S, K, t, r, sigma, option='call'):
d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * t) / (sigma * np.sqrt(t))
d2 = d1 - sigma * np.sqrt(t)
if option == 'call':
return S * norm.cdf(d1) - K * np.exp(-r * t) * norm.cdf(d2)
else:
return K * np.exp(-r * t) * norm.cdf(-d2) - S * norm.cdf(-d1)
As you can see, the function makes use of every variable, just the way it's represented in the equation. I even use exponentiation and logarithmic functions.
The equation takes into account various factors, such as the current stock price, the option's strike price, the time until expiration, the risk-free interest rate, and the stock's volatility. By plugging in these variables, the equation calculates the theoretical price of the option, which represents the fair value of the option in a perfectly efficient market.
Now it's time to add the remaining code which filters "conservative options". Also known as "in-the-money" options. I felt this was a decent way to assign some sort of financial safety.
# Estimate fair price for each option using Black-Scholes formula
conservative_puts['fair_price'] = black_scholes(
conservative_puts['lastPrice'], conservative_puts['strike'], t, r, volatility,
option='put')
conservative_calls['fair_price'] = black_scholes(
conservative_calls['lastPrice'], conservative_calls['strike'], t, r, volatility,
option='call')
# Calculate expected return for each option
conservative_puts['expected_return'] = (conservative_puts['fair_price'] - conservative_puts['lastPrice']) / \
conservative_puts['lastPrice']
conservative_calls['expected_return'] = (conservative_calls['fair_price'] - conservative_calls['lastPrice']) / \
conservative_calls['lastPrice']
# Rank options by expected return and suggest top 3 put and call
suggested_puts = conservative_puts.sort_values('expected_return', ascending=False).head(3)
suggested_calls = conservative_calls.sort_values('expected_return', ascending=False).head(3)
# Print suggested options and stock price
market_price = yf.Ticker(ticker).fast_info['lastPrice']
print()
print("Suggested puts:")
print(suggested_puts[['strike', 'lastTradeDate', 'lastPrice', 'ask', 'impliedVolatility', 'fair_price',
'expected_return']].to_string(index=False))
print("\nStock Price:")
print(f"The current price of {ticker} is ${market_price:.2f}")
print("\nSuggested calls:")
print(suggested_calls[['strike', 'lastTradeDate', 'lastPrice', 'ask', 'impliedVolatility', 'fair_price',
'expected_return']].to_string(index=False))
The remaining code ranks the options by their expected return and returns them. I also added the stock price in the center of the output.
Lets test the code at this point. It's time to see some GME options! I found some option contracts that expire on 2023-03-31.
/home/bot/PycharmProjects/finance/venv/bin/python /home/bot/PycharmProjects/finance/options.py
Enter stock ticker: GME
Enter expiry date: 2023-03-31
[*********************100%***********************]? 1 of 1 completed
Suggested puts:
?strike???????????? lastTradeDate? lastPrice? ask? impliedVolatility? fair_price? expected_return
?? 24.0 2023-03-24 19:59:43+00:00?????? 1.49 1.50?????????? 1.106450?? 22.534710??????? 14.123966
?? 25.0 2023-03-24 19:59:56+00:00?????? 2.22 2.22?????????? 1.167973?? 22.805739???????? 9.272856
?? 26.0 2023-03-24 19:02:28+00:00?????? 3.15 3.10?????????? 1.288089?? 22.876769???????? 6.262466
Stock Price:
The current price of GME is $23.98
Suggested calls:
?strike???????????? lastTradeDate? lastPrice?? ask? impliedVolatility? fair_price? expected_return
??? 5.0 2023-03-15 17:42:26+00:00?????? 11.3 19.30?????????? 7.640625??? 6.294852??????? -0.442933
?? 10.0 2023-03-24 14:51:47+00:00?????? 12.3 14.25?????????? 4.257817??? 2.318505??????? -0.811504
?? 13.5 2023-03-22 17:36:05+00:00?????? 11.4 10.75?????????? 2.972659??? 0.062018??????? -0.994560
Process finished with exit code 0
Very cool, as you can see there are numerous call and put options and a calculated fair price and expected return. We can also pull information from yfinance about their implied volatility. I feel like this console output somewhat mimics the option trading page of my brokerage account.
Lets finish up and add code for plotting the options against the stock price over time.
# Plot stock price and options
fig, ax = plt.subplots()
# Plot stock price
stock_data['Adj Close'].plot(ax=ax, label=ticker)
# Plot put options
for i, row in suggested_puts.iterrows():
ax.axhline(y=row['strike'], color='r', linestyle='-')
# Plot call options
for i, row in suggested_calls.iterrows():
ax.axhline(y=row['strike'], color='g', linestyle='-')
plt.legend()
plt.title(f'{ticker} Options for {expiry}')
plt.xlabel('Date')
plt.ylabel('Price')
plt.savefig("plot.png")
Now we can execute the code and see suggested options as well as save a new image. This time I'm looking at Advanced Micro Devices, Inc. (AMD).
/home/bot/PycharmProjects/finance/venv/bin/python /home/bot/PycharmProjects/finance/options.py
Enter stock ticker: AMD
Enter expiry date: 2023-03-31
[*********************100%***********************]? 1 of 1 completed
Suggested puts:
?strike???????????? lastTradeDate? lastPrice? ask? impliedVolatility? fair_price? expected_return
?? 98.0 2023-03-24 19:59:46+00:00?????? 2.75 2.78?????????? 0.500737?? 95.350898??????? 33.673054
? 100.0 2023-03-24 19:58:55+00:00?????? 3.80 3.85?????????? 0.494146?? 96.302957??????? 24.342883
? 105.0 2023-03-24 19:22:38+00:00?????? 7.32 7.70?????????? 0.520024?? 97.788105??????? 12.359031
Stock Price:
The current price of AMD is $97.95
Suggested calls:
?strike???????????? lastTradeDate? lastPrice?? ask? impliedVolatility? fair_price? expected_return
?? 45.0 2023-03-23 19:31:25+00:00????? 54.45 53.60?????????? 2.414066??? 9.410405??????? -0.827173
?? 40.0 2023-03-10 20:33:22+00:00????? 43.08 58.60?????????? 2.757816??? 3.309535??????? -0.923177
?? 50.0 2023-03-24 14:01:54+00:00????? 48.90 48.65?????????? 2.195317??? 0.972903??????? -0.980104
Process finished with exit code 0
If we open the image plot.png in the same directory we have a beautiful image of what an option contact implies.
Notice how we have plotted a nice picture of the suggested calls and puts for technical analysis. AMD has recently had a price increase which effects the placement of puts and calls.
Now think realistically about this program. I chose AMD because it's a stable and profitable technology company. Lets instead look at First Republic (FRC)
/home/bot/PycharmProjects/finance/venv/bin/python /home/bot/PycharmProjects/finance/options.py
Enter stock ticker: FRC
Enter expiry date: 2023-03-31
[*********************100%***********************]? 1 of 1 completed
Suggested puts:
?strike???????????? lastTradeDate? lastPrice? ask? impliedVolatility? fair_price? expected_return
?? 13.0 2023-03-24 19:57:12+00:00?????? 3.30? 3.4?????????? 4.226567??? 9.713384???????? 1.943450
?? 14.0 2023-03-24 19:59:04+00:00?????? 3.90? 4.0?????????? 4.195317?? 10.114414???????? 1.593439
?? 15.0 2023-03-24 19:58:30+00:00?????? 4.44? 4.7?????????? 4.101567?? 10.575444???????? 1.381857
Stock Price:
The current price of FRC is $12.36
Suggested calls:
?strike???????????? lastTradeDate? lastPrice? ask? impliedVolatility?? fair_price? expected_return
??? 5.0 2023-03-24 19:36:51+00:00?????? 7.45 7.90?????????? 5.562503 2.445332e+00??????? -0.671767
?? 10.0 2023-03-24 19:59:33+00:00?????? 4.00 4.20?????????? 4.562504 1.809085e-12??????? -1.000000
?? 12.0 2023-03-24 19:59:42+00:00?????? 2.90 2.95?????????? 4.050786 2.539307e-26??????? -1.000000
Process finished with exit code 0
First Republic Bank is not doing so well, that's because the stock has lost 90% of it's value as of late. Look at the fair price calculated for that $12 call! It's an absolutely minuscule number!
It's not a good idea to try to "catch the falling knife" here with a call option. Or maybe not even a good idea to place puts. The stock could just go to zero right? Or maybe they'll get another $30 billion and rocket up to $40 per share in pre-market trading. A lot of things can happen and no one really knows anything, such is the nature of the free market.
Here is the entire finalized code, please use it carefully.
import yfinance as yf
import numpy as np
from scipy.stats import norm
import pandas as pd
import matplotlib.pyplot as plt
# Set stock ticker and expiry date
ticker = input("Enter stock ticker: ")
expiry = input("Enter expiry date: ")
# Get stock data for the past year
start_date = pd.to_datetime('today') - pd.DateOffset(years=1)
end_date = pd.to_datetime('today')
stock_data = yf.download(ticker, start=start_date, end=end_date)
# Calculate historical volatility
returns = stock_data['Adj Close'].pct_change().dropna()
volatility = returns.std() * np.sqrt(252)
# Get option chain for stock ticker and expiry date
option_chain = yf.Ticker(ticker).option_chain(expiry)
# Filter option chain for puts and calls
puts = option_chain.puts.sort_values(by='lastPrice')
calls = option_chain.calls.sort_values(by='lastPrice')
# Filter for conservative trader
conservative_puts = puts[puts["inTheMoney"] == True].head(5)
conservative_calls = calls[calls["inTheMoney"] == True].tail(5)
# Set risk-free interest rate
r = -0.0626
# Calculate time to expiration in years
# Define Black-Scholes formula
def black_scholes(S, K, t, r, sigma, option='call'):
d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * t) / (sigma * np.sqrt(t))
d2 = d1 - sigma * np.sqrt(t)
if option == 'call':
return S * norm.cdf(d1) - K * np.exp(-r * t) * norm.cdf(d2)
else:
return K * np.exp(-r * t) * norm.cdf(-d2) - S * norm.cdf(-d1)
# Estimate fair price for each option using Black-Scholes formula
conservative_puts['fair_price'] = black_scholes(
conservative_puts['lastPrice'], conservative_puts['strike'],
(pd.to_datetime(expiry) - pd.to_datetime('today')).days / 365, r, volatility,
option='put')
conservative_calls['fair_price'] = black_scholes(
conservative_calls['lastPrice'], conservative_calls['strike'],
(pd.to_datetime(expiry) - pd.to_datetime('today')).days / 365, r, volatility,
option='call')
# Calculate expected return for each option
conservative_puts['expected_return'] = (conservative_puts['fair_price'] - conservative_puts['lastPrice']) / \
conservative_puts['lastPrice']
conservative_calls['expected_return'] = (conservative_calls['fair_price'] - conservative_calls['lastPrice']) / \
conservative_calls['lastPrice']
# Rank options by expected return and suggest top 3 put and call
suggested_puts = conservative_puts.sort_values('expected_return', ascending=False).head(3)
suggested_calls = conservative_calls.sort_values('expected_return', ascending=False).head(3)
# Print suggested options and stock price
market_price = yf.Ticker(ticker).fast_info['lastPrice']
print()
print("Suggested puts:")
print(suggested_puts[['strike', 'lastTradeDate', 'lastPrice', 'ask', 'impliedVolatility', 'fair_price',
'expected_return']].to_string(index=False))
print("\nStock Price:")
print(f"The current price of {ticker} is ${market_price:.2f}")
print("\nSuggested calls:")
print(suggested_calls[['strike', 'lastTradeDate', 'lastPrice', 'ask', 'impliedVolatility', 'fair_price',
'expected_return']].to_string(index=False))
# Plot stock price and options
fig, ax = plt.subplots()
# Plot stock price
stock_data['Adj Close'].plot(ax=ax, label=ticker)
# Plot put options
for i, row in suggested_puts.iterrows():
ax.axhline(y=row['strike'], color='r', linestyle='-')
# Plot call options
for i, row in suggested_calls.iterrows():
ax.axhline(y=row['strike'], color='g', linestyle='-')
plt.legend()
plt.title(f'{ticker} Options for {expiry}')
plt.xlabel('Date')
plt.ylabel('Price')
plt.savefig("plot.png")
Options are very dangerous and I encourage you to make intelligent investing decisions always. - Henry
--
10 个月Excelente!!!!!! tu trabajo. Luego de hacer los ajustes sugeridos por Pete Granneman, el codigo funciona muy bien. FELICITACIONES Y Saludos
Software Engineer
1 年Thank you for this correction!
Outside Sales Representative
1 年Great concept! However, the code implemented uses the spot price of the option in place of the Spot Equity Price (of the Equity) in the BSM equation leading to erroneous 'fair_price' . Code should use 'market_price' in place of conservative_puts['lastPrice'] and conservative_calls['lastPrice'] for the "S" value.