Introduction
Options trading can be intimidating due to the complexity of option pricing and the numerous variables that influence it. However, understanding the fundamental principles behind options trading reveals a logical and structured approach to profiting from market inefficiencies.
In this article, we will delve into an advanced options arbitrage strategy that uses delta hedging. This strategy involves identifying mispriced option contracts and executing trades that capitalize on these discrepancies while mitigating directional market risk.
By the end of this practical tutorial, you will have a comprehensive understanding of how to implement this advanced options trading strategy at ease using Alpaca’s Trading API with Python. This includes detailed code examples and explanations to help you replicate and customize the strategy for your trading needs. Note: This strategy needs options level 2 approval.
The Strategy
Our objective is to identify mispriced option contracts and profit from the expected price correction without exposing ourselves to the directional risk of the underlying stock. This strategy comprises two main components:
- Identifying Mispriced Option Contracts: We use the Black-Scholes model to calculate the theoretical price of options and compare these prices with the current market prices. By doing this, we can identify options that are significantly mispriced. The option with the largest discrepancy between the theoretical price and the market price represents an arbitrage opportunity.
- Building a Delta-Neutral Portfolio: Once we identify a mispriced option, we need to construct a portfolio that hedges against the risk associated with the underlying stock's price movement. This is achieved through delta hedging, where we balance our position in the option with an opposing position in the underlying stock. This approach neutralizes the impact of price changes in the underlying asset, allowing us to focus solely on the mispricing of the option.
Black-Scholes Model
The Black-Scholes model is a fundamental tool in options pricing. It provides a theoretical estimate of the price of options based on several key variables: the underlying asset price, the strike price, the time to expiration, the risk-free interest rate, and the volatility of the underlying asset. The formula for calculating the price of a call option is:
$$C = S_0 N(d_1) - Xe^{-rT} N(d_2)$$
where:
$$d_1 = \frac{\ln\left(\frac{S_0}{X}\right) + \left(r + \frac{\sigma^2}{2}\right)T}{\sigma\sqrt{T}}$$
$$d_2 = d_1 - \sigma\sqrt{T}$$
- C is the call option price.
- S0 is the current stock price.
- X is the strike price.
- T is the time to maturity.
- r is the risk-free interest rate.
- σ is the volatility of the underlying asset.
- N(⋅) is the cumulative distribution function of the standard normal distribution.
Using the Black-Scholes model, we scan a predetermined number of option contracts across a universe of stocks (e.g., the S&P 500 stocks). We calculate the theoretical price for each contract and compare it to the current market price. The option with the largest percentage discrepancy between its theoretical and market prices is considered mispriced.
Risks Involved with the Black-Scholes Model
While the Black-Scholes (BS) model is a valuable tool, it does have its limitations. One major concern is that an option's market price might not match its theoretical price due to factors like market inefficiencies, liquidity issues, or unexpected events. The variables we input into the BS model, such as volatility and the risk-free rate, are estimates and can sometimes be off, leading to inaccurate theoretical prices. Although the Black-Sholes model is mainly designed for European options, it can still help with pricing American options. However, it might not be the best fit because it doesn't fully account for the complexities of early exercise features of American options.
Another significant risk is the movement of the underlying stock price. Even if the option price moves toward its theoretical value, large swings in the stock price can still impact profits. For instance, if the stock price drops sharply while you're holding a call option, the resulting decline could cancel out any gains from the option price adjustment. To manage this, our strategy focuses on hedging against stock price movements, allowing us to aim for profits from the option's price correction.
Delta Hedging
Delta is one of the crucial Greeks in options trading, indicating how much an option's price will change with a $1.00 move in the price of the underlying asset. For instance, if an option has a delta of 1.0, its price will change by $1.00 for every $1.00 move in the underlying asset. Similarly, a delta of 0.5 means the option's price will change by $0.50 for every $1.00 movement in the stock. Delta values for call options range from 0 to 1, while for put options, they range from 0 to -1.
Delta hedging is a technique used to manage the risk associated with these price movements. By offsetting the delta of our options with an opposite position in the underlying asset, we create a delta-neutral position. For example, if we buy a call option with a delta of 0.5, we can hedge this by shorting 50 shares of the underlying stock for each option contract. This approach helps ensure that our portfolio remains unaffected by small price changes in the underlying asset, allowing us to focus on profiting from the option's mispricing.
Implementation of the Strategy in Python
1. Creating an Alpaca Trading API Account & Retrieving API Keys
Before diving into the implementation, you need to create a developer account with Alpaca to obtain your API keys. Follow these steps:
- Visit Alpaca and click on "Sign Up" at the top-right corner.
- Select "Trading API" and fill in the required details to create your account.
- Complete your profile on the dashboard. This is mandatory for live trading but optional for paper trading.
- Retrieve your API keys from the dashboard.
2. Setting Up the Python Environment
Before we dive into implementing the delta-hedged options arbitrage strategy, we need to set up our Python environment with the necessary libraries. Here are the steps:
Install the required packages: We’ll be using five different packages in this article that are NumPy for numerical functions, SciPy for complex functions and mathematical computations, datetime for date-related functions, pandas for working with data, and finally, alpaca-py for accessing Alpaca’s API endpoints
Run the following pip command to install all the required libraries:
pip install numpy scipy pandas alpaca-py datetime
Import the necessary libraries:
import numpy as np
from scipy.stats import norm
import datetime
import pandas as pd
from alpaca.trading.client import TradingClient, GetOptionContractsRequest
from alpaca.trading.requests import GetOrdersRequest, LimitOrderRequest
from alpaca.data.requests import StockQuotesRequest
from alpaca.trading.enums import AssetStatus, OrderType, QueryOrderStatus, OrderSide, TimeInForce, ContractType, ExerciseStyle
from alpaca.data.historical import StockHistoricalDataClient
The reason we’re importing specific classes from the alpaca-py library instead of importing it as a whole is for better clarity in the code which ultimately makes it easier to work with.
Initialize the Alpaca API client: After importing the necessary libraries, we need to initialize the Alpaca API client using our API credentials.
# Alpaca API credentials
api_key = ‘YOUR API KEY’
secret_key = ‘YOUR SECRET API KEY’
# Initialize the Alpaca API
trading_client = TradingClient(api_key, secret_key, paper = True)
We start by storing the two API keys into their respective variables. Ensure that you replace both 'YOUR API KEY' and 'YOUR SECRET API KEY' with your actual Alpaca API key and secret key. These credentials are necessary to authenticate your access to Alpaca’s Trading API.
Then, we authenticate our access to the API by creating an instance of the TradingClient class. We set paper=True to indicate that this is Paper trading, not live trading.
3. Extracting the S&P 500 Tickers
To implement our delta-hedged options arbitrage strategy, we need a set of stocks to analyze. We will focus on the S&P 500 stocks because they are among the most actively traded and have sufficient liquidity for both the stocks and their options. By extracting the tickers of these stocks, we can calculate the theoretical prices of their options contracts and identify the ones with the biggest discrepancies between their theoretical and actual prices.
Here is the code to extract the S&P 500 tickers:
def get_sp500_stocks():
url = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"
response = requests.get(url)
sp500_table = pd.read_html(response.text, header=0)[0]
return sp500_table['Symbol'].tolist()
stocks = get_sp500_stocks()
print(f"Total S&P 500 stocks: {len(stocks)}")
In this code, we define a function get_sp500_stocks to fetch the S&P 500 tickers. The url variable stores the URL of the Wikipedia page listing the S&P 500 companies, and we use requests.get to retrieve the HTML content.
The read_html function from the pandas library parses this HTML content, extracting the tables into a list of DataFrames. We then access the 'Symbol' column of the first DataFrame and convert it to a list using the tolist() method. The stocks variable stores this list of tickers, and we print the total number of S&P 500 stocks extracted to verify the data.
4. Extracting Stock and Options Prices
To implement our delta-hedged options arbitrage strategy, we need to fetch the latest prices of the underlying stocks and their respective options contracts. This will allow us to calculate the theoretical prices of the options and identify mispriced contracts. We will first extract the latest quotes for the underlying stocks and then get the available options contracts and their prices.
Extracting the Latest Quote of the Stock:
First, we need to obtain the latest market prices of the underlying stocks. This is crucial for calculating the theoretical option prices and determining the discrepancy between the actual and theoretical prices.
Here is the code to extract the latest prices of the S&P 500 stocks:
# Set up stock historical data client
stock_historical_data_client = StockHistoricalDataClient(api_key, secret_key)
# Function to get the latest quotes for S&P 500 stocks
def get_latest_quotes(stocks):
quotes = {}
for stock in stocks:
req = StockQuotesRequest(symbol_or_symbols=[stock])
res = stock_historical_data_client.get_stock_latest_quote(req)
quotes[stock] = res[stock]
return quotes
# Get the latest quotes for all S&P 500 stocks
latest_quotes = get_latest_quotes(stocks)
print(f"Total S&P 500 stocks quotes retrieved: {len(latest_quotes)}")
The StockHistoricalDataClient class is used to interact with Alpaca’s stock data API, and we initialize this client with our API key and secret key to authenticate our access. We then define a function get_latest_quotes() that takes a list of stock symbols and retrieves the latest quotes for each stock.
Within the function, we iterate over the list of stocks, create a StockQuotesRequest object for each stock symbol, and call the get_stock_latest_quote function to get the latest quote. We store the quotes in a dictionary where the keys are stock symbols and the values are the corresponding quotes.
Finally, we call this function with our list of S&P 500 stocks and print the total number of quotes retrieved.
Extracting the Available Options Contracts and Their Prices:
Now, we are going to fetch the available options contracts and their prices for the S&P 500 stocks which will then be used for calculating the theoretical prices and identifying mispriced options.
Here is the code to extract the options contracts and their prices for the S&P 500 stocks:
# Function to get options contracts for S&P 500 stocks
def get_options_contracts(stocks):
options_data = {}
for stock in stocks:
options_chain = GetOptionContractsRequest(
underlying_symbols=[stock],
status=AssetStatus.ACTIVE,
expiration_date="2024-07-19",
type=ContractType.CALL,
style=ExerciseStyle.AMERICAN,
limit=5
)
options_contracts = trading_client.get_option_contracts(options_chain)
print(len(options_contracts.option_contracts))
options_data[stock] = options_contracts.option_contracts
return options_data
# Get options contracts for all S&P 500 stocks
options_contracts = get_options_contracts(stocks)
print(f"Total options contracts retrieved: {sum(len(v) for v in options_contracts.values())}")
In the get_options_contracts function, we iterate over the list of stocks and create a GetOptionContractsRequest object for each stock symbol, specifying parameters such as the expiration date, option type (call), and style (American). We call the get_option_contracts function to get the options contracts and store the contracts in a dictionary where the keys are stock symbols and the values are lists of options contracts. Finally, we call this function with our list of S&P 500 stocks and print the total number of options contracts retrieved.
By extracting the latest quotes for the underlying stocks and their options contracts, we can proceed to calculate the theoretical prices using the Black-Scholes model and identify mispriced options for our arbitrage strategy.
5. Calculating the Theoretical Price for Each Contract
To identify mispriced options, we need to calculate the theoretical price for each options contract extracted in the previous step. This will allow us to compare the theoretical prices with the actual market prices and find discrepancies. We will use the Black-Scholes model for this purpose.
Creating the Black-Scholes Model:
First, we need to define the Black-Scholes model, which will be used to compute the theoretical price of the options contracts. Here is the code to create the Black-Scholes model:
def black_scholes_price(S, K, T, r, sigma, option_type="call"):
d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
if option_type == "call":
price = S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
elif option_type == "put":
price = K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)
else:
raise ValueError("Invalid option type. Must be 'call' or 'put'.")
return price
We start by defining a function black_scholes_price that calculates the theoretical price of an option using the Black-Scholes model. The function takes several parameters: S (underlying asset price), K (strike price), T (time to maturity in years), r (risk-free interest rate), sigma (volatility of the underlying asset), and option_type (either "call" or "put").
The function uses these parameters to compute d1 and d2 and then applies the Black-Scholes formula to determine the theoretical price.
Calculating Theoretical Prices of All Extracted Options Contracts:
Now, we will use the Black-Scholes model which we just created to compute the theoretical prices for all the options contracts extracted previously. This involves iterating over the options contracts and applying the Black-Scholes model to each one.
Here is the code to calculate the theoretical prices for all the extracted options contracts:
# Function to calculate theoretical prices for all extracted options contracts
def calculate_theoretical_prices(latest_quotes, options_contracts, r, sigma):
theoretical_prices = {}
for stock, contracts in options_contracts.items():
S = latest_quotes[stock].bid_price
for contract in contracts:
K = contract.strike_price
T = (contract.expiration_date - datetime.datetime.now().date()).days / 365.0
option_type = "call" if contract.type == ContractType.CALL else "put"
price = black_scholes_price(S, K, T, r, sigma, option_type)
theoretical_prices[contract.symbol] = price
return theoretical_prices
# Set parameters for the Black-Scholes model
risk_free_rate = 0.01
volatility = 0.2
# Calculate theoretical prices for all options contracts
theoretical_prices = calculate_theoretical_prices(latest_quotes, options_contracts, risk_free_rate, volatility)
print(f"Theoretical prices calculated for {len(theoretical_prices)} options contracts.")
We define a function calculate_theoretical_prices that calculates the theoretical prices for all the extracted options contracts. The function takes the latest quotes for the stocks, the options contracts, the risk-free interest rate (r), and the volatility (sigma) as parameters. We initialize an empty dictionary theoretical_prices to store the results.
Within the function, we iterate over each stock and its associated options contracts. For each contract, we retrieve the strike price (K), calculate the time to maturity (T), and determine the option type (option_type). We then call the black_scholes_price function to compute the theoretical price of the option and store the result in the theoretical_prices dictionary, using the contract symbol as the key.
We set the risk-free interest rate and volatility parameters and then call the calculate_theoretical_prices function with the appropriate arguments. Finally, we print the total number of theoretical prices calculated for the options contracts.
6. Identifying the Most Mispriced Options
To implement our delta-hedged options arbitrage strategy, we need to identify the options contracts with the largest discrepancies between their theoretical prices (calculated using the Black-Scholes model) and their actual market prices. By sorting these contracts based on the price differences, we can select the top contracts that are most likely mispriced.
Here is the code to identify and sort the mispriced options contracts:
# Set parameters for the Black-Scholes model
# Function to identify and sort mispriced options contracts
def find_mispriced_options(options_contracts, theoretical_prices):
mispriced_options = []
for stock, contracts in options_contracts.items():
for contract in contracts:
if contract.symbol in theoretical_prices:
if contract.close_price != None:
actual_price = float(contract.close_price)
theoretical_price = theoretical_prices[contract.symbol]
price_difference = abs(actual_price - theoretical_price)
mispriced_options.append({
'symbol': contract.symbol,
'stock': stock,
'actual_price': actual_price,
'theoretical_price': theoretical_price,
'price_difference': price_difference
})
# Sort the options by the price difference in descending order
mispriced_options.sort(key=lambda x: x['price_difference'], reverse=True)
# Select the top 10 mispriced options
top_mispriced_options = mispriced_options[:10]
return top_mispriced_options
# Identify the mispriced options
top_mispriced_options = find_mispriced_options(options_contracts, theoretical_prices)
print(f"Top 10 mispriced options: {top_mispriced_options}")
In the find_mispriced_options function, we go through each stock and its associated options contracts, checking if the contract symbol is present in the theoretical prices dictionary. If it is, we retrieve the actual price of the contract (close_price) and the theoretical price from the dictionary.
We then calculate the absolute difference between the actual and theoretical prices (price_difference) and store these details in a list mispriced_options. This list is sorted in descending order based on the price difference, and the top 10 mispriced options are selected and returned.
7. Calculating Delta
Delta is a measure of how much the price of an option will change with a $1 change in the price of the underlying asset. For instance, a delta of 0.5 indicates that the option's price will move by $0.50 for every $1.00 movement in the underlying asset's price.
Calculating the delta is crucial for our strategy because it helps determine the number of shares of the underlying stock to short to hedge our position. By knowing the delta, we can establish a delta-neutral position, effectively hedging the directional risk of the underlying stock's price movement.
Here is the code to calculate the delta for each options contract:
# Function to calculate the delta of an option using the Black-Scholes model
def calculate_delta(S, K, T, r, sigma, option_type="call"):
d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
delta = norm.cdf(d1)
return delta
# Function to calculate deltas for all top mispriced options
def calculate_deltas(latest_quotes, top_mispriced_options, r, sigma):
for option in top_mispriced_options:
stock = option['stock']
S = latest_quotes[stock].bid_price
K = option['theoretical_price']
expiration_date = next(contract.expiration_date for contract in options_contracts[stock] if contract.underlying_symbol == option['stock'])
T = (expiration_date - datetime.datetime.now().date()).days / 365.0
option_type = "call"
delta = calculate_delta(S, K, T, r, sigma, option_type)
option['delta'] = delta
return top_mispriced_options
# Calculate deltas for the top mispriced options
top_mispriced_options_with_deltas = calculate_deltas(latest_quotes, top_mispriced_options, risk_free_rate, volatility)
print(f"Top mispriced options with deltas: {top_mispriced_options_with_deltas}")
In the calculate_delta function, we compute the delta of an option using the Black-Scholes model. This function takes parameters such as the underlying asset price (S), strike price (K), time to maturity (T), risk-free interest rate (r), and volatility (sigma). Inside the function, we calculate d1 using the Black-Scholes formula and determine the delta using the cumulative distribution function of the normal distribution (norm.cdf(d1)).
The calculate_deltas function then iterates over the top 10 mispriced options to compute their deltas. For each option, it retrieves the bid price (S) from the latest quotes, the theoretical price (K) as the strike price, and calculates the time to maturity (T). The delta is then computed using the calculate_delta function and stored in the option's dictionary.
8. Submitting Orders for Delta Hedging
After calculating the delta for each mispriced option, we can proceed to the actual execution of the strategy. We place two orders per contract: one to go long on the call option and another to short the underlying stock. Through this approach, we ensure that our portfolio remains delta-neutral while hedging the directional risk of the underlying stock's price movement.
Here is the code to place these orders:
# Function to place orders for delta hedging
def place_delta_hedging_orders(trading_client, top_mispriced_options_with_deltas):
orders = []
for option in top_mispriced_options_with_deltas:
stock = option['stock']
delta = option['delta']
# Place order to buy the call option
call_order_data = LimitOrderRequest(symbol = option['symbol'],
limit_price = option['actual_price'],
qty = 1,
side = OrderSide.BUY,
time_in_force = TimeInForce.DAY)
call_order = trading_client.submit_order(order_data = call_order_data)
orders.append(call_order)
# Place order to short the underlying stock
short_stock_qty = int(delta * 100) # Delta * 100 shares per option contract
stock_order_data = LimitOrderRequest(symbol = stock,
limit_price = latest_quotes[stock].bid_price,
qty = short_stock_qty,
side = OrderSide.SELL,
time_in_force = TimeInForce.DAY)
stock_order = trading_client.submit_order(order_data = stock_order_data)
orders.append(stock_order)
return orders
# Place delta hedging orders
orders = place_delta_hedging_orders(trading_client, top_mispriced_options_with_deltas)
print(f"Total orders placed: {len(orders)}")
We define a function place_delta_hedging_orders that takes trading_client and top_mispriced_options_with_deltas as parameters. This function places the necessary orders to hedge our positions based on the delta of each option. Here’s what’s happening inside the function:
- Initialize Orders List: We initialize an empty list orders to keep track of all placed orders.
- Iterate Through Options: We iterate over each mispriced option with its calculated delta.
- Place Order for Call Option: For each option, we create an instance of the LimitOrderRequest class and store it in the call_order_data variable to buy the call option. We then submit the order using trading_client.submit_order and append the returned call_order to the orders list.
- Calculate Short Stock Quantity: We calculate the quantity of the underlying stock to short based on the delta (delta * 100 shares per option contract).
- Place Order to Short Stock: The process is similar to placing a buy order for call options. We create a LimitOrderRequest object to short the stock, submit the order using trading_client.submit_order, and append the returned stock_order to the orders list.
- Return Orders: Finally, we return the list of all placed orders.
We call the place_delta_hedging_orders function with our trading_client and top_mispriced_options_with_deltas to place the necessary delta hedging orders. The total number of orders placed is printed to verify the execution.
By placing these orders, we ensure that our portfolio is delta-neutral, effectively hedging the directional risk and allowing us to focus on the potential profit from the mispricing of the options.
Conclusion
In this article, we've walked through the process of building a delta-hedged options arbitrage strategy using Alpaca's Trading API. This strategy enables us to profit from mispriced options while reducing the directional risk associated with the underlying stock. By utilizing the Black-Scholes model, we were able to pinpoint mispriced call options and execute delta-neutral trades to take advantage of these pricing discrepancies.
Delta hedging is just one effective method to manage risk, but there are numerous other strategies and Greeks that can be explored to refine your trading approach. Automated systems are particularly valuable for quickly analyzing a broad range of contracts, allowing you to capitalize on fleeting arbitrage opportunities.
It's important to remember that the world of options trading is constantly changing. Staying informed and continually adapting your strategies is essential. I encourage you to experiment with different methods, fine-tune your approach, and fully explore what Alpaca’s Trading API has to offer to stay competitive in the market.
With that said, we've reached the end of this guide. I hope you've found it insightful and useful for implementing your own delta-hedged options arbitrage strategy. Check out part 2 for a more advanced look into the strategy and how to take it a step further. Please share your feedback and experiences on our forum or Slack community as you put these concepts into practice.
Happy trading!
Options trading is not suitable for all investors due to its inherent high risk, which can potentially result in significant losses. Please read Characteristics and Risks of Standardized Options before investing in options.
All investments involve risk and the past performance of a security, or financial product does not guarantee future results or returns. There is no guarantee that any investment strategy will achieve its objectives. Please note that diversification does not assure a profit, or protect against loss. There is always the potential of losing money when you invest in securities, or other financial products. Investors should consider their investment objectives and risks carefully before investing.
The Paper Trading API is offered by AlpacaDB, Inc. and does not require real money or permit a user to conduct real transactions in the market. Providing use of the Paper Trading API is not an offer or solicitation to buy or sell securities, securities derivative or futures products of any kind, or any type of trading or investment advice, recommendation or strategy, given or in any manner endorsed by AlpacaDB, Inc. or any AlpacaDB, Inc. affiliate and the information made available through the Paper Trading API is not an offer or solicitation of any kind in any jurisdiction where AlpacaDB, Inc. or any AlpacaDB, Inc. affiliate (collectively, "Alpaca") is not authorized to do business.
Please note that the content is for informational purposes and is believed to be accurate as of posting date but may be subject to change. All screenshots are for illustrative purposes only.
Securities brokerage services are provided by Alpaca Securities LLC ("Alpaca Securities"), member FINRA/SIPC, a wholly-owned subsidiary of AlpacaDB, Inc. Technology and services are offered by AlpacaDB, Inc.
This is not an offer, solicitation of an offer, or advice to buy or sell securities or open a brokerage account in any jurisdiction where Alpaca Securities are not registered or licensed, as applicable.