Constructing a Delta-Neutral Options Portfolio with Python
Discover advanced techniques for delta-neutral options trading using Python and Alpaca's Trading API. Learn to identify mispriced options and hedge risks.
Introduction
In the first part of this tutorial, we covered the foundational aspects of implementing a delta-hedged options arbitrage strategy using Alpaca’s Trading API. This included setting up the Python environment, creating an Alpaca trading account, and extracting the necessary data for our analysis.
In this second part, we will delve deeper into the implementation, focusing on calculating theoretical prices, identifying mispriced options, calculating delta, and executing the trades.
By the end of this part, you will have a detailed understanding of the advanced steps required to complete the strategy
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. It's important to note that the theoretical price of an option is dependent on the user's own assumptions regarding the volatility input into the Black-Scholes model. I’m providing an example here, but not making any recommendations on what particular volatility to use.
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.
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:
# 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 and 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)
return mispriced_options[:10]
# 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.
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.
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 final part of our tutorial, we've walked through the advanced steps required to complete a delta-hedged options arbitrage strategy using Alpaca’s Trading API. We covered calculating theoretical prices, identifying mispriced options, calculating delta, and executing the necessary trades to create a delta-neutral portfolio.
By understanding and implementing these steps, you can effectively mitigate directional risk while capitalizing on mispriced options in the market. Delta hedging is just one method to manage risk, and there are numerous other strategies and Greeks that can be explored to refine your trading approach.
It's important to note that the delta of an option changes with time and the underlying spot movement. Therefore, to manage delta risk consistently over the life of the option, the delta hedge will need to be rebalanced periodically. Automated systems are particularly valuable for this purpose as they quickly analyze a broad range of contracts, allowing you to capitalize on fleeting arbitrage opportunities.
As the world of options trading is constantly evolving, 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 article. I hope you've found it insightful and useful for implementing your own delta-hedged options arbitrage strategy. 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.