Introduction
Trading options can seem daunting due to the intricate nature of option pricing and the myriad of variables at play. However, by grasping the core principles, traders can adopt structured approaches to harness market inefficiencies. One such sophisticated strategy is gamma scalping, which involves dynamically adjusting delta-neutral positions to capture the gamma of an option.
In this guide, we'll walk through the process of building, executing, and refining a gamma scalping strategy using Python, leveraging real-time data and dynamic rebalancing to maintain a delta-neutral position.
Understanding Gamma Scalping
To effectively implement gamma scalping, it is crucial to understand the fundamental concepts behind it and why they matter in options trading.
What is Gamma Scalping?
Gamma scalping is an advanced options trading strategy that involves continuously adjusting a delta-neutral position to profit from the gamma of an option. Gamma, a second-order Greek, measures the rate of delta change in response to price movements in the underlying asset.
This strategy leverages the convexity of an option's price curve, aiming to capture profits as the price of the underlying asset fluctuates.
Why is Gamma Important in Options Trading?
Gamma plays a pivotal role in options trading as it directly impacts how an option's delta changes with price movements. A higher gamma indicates that the delta will change more rapidly, which can be beneficial during times of high volatility.
By understanding gamma, traders can better anticipate changes in their positions' sensitivity to the underlying asset's price movements, allowing them to make more informed decisions and optimize their strategies.
Building A Gamma Scalping Trading Strategy: Example Model
Now let’s build the gamma scalping model step by step.
We'll start by collecting historical and real-time data using Alpaca's API, calculate the necessary Greeks, establish and maintain a delta-neutral position, and implement the gamma scalping strategy.
Note: Make sure you have already created a Trading API account and are connected with your Python environment of choice.
Note: The code outlined below assumes a flat interest rate, does not consider dividends, is not optimized for 0DTE options, and does not consider whether the market is open or closed. The below code should only be considered a starting point to creating a gamma scalping strategy, not a finished strategy.
Step 1: Set up your imports
# Import necessary packets
from datetime import datetime, timedelta
import time
import asyncio
import pandas as pd
import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq
import nest_asyncio
from alpaca.data.historical.option import OptionHistoricalDataClient, OptionLatestQuoteRequest
from alpaca.data.historical.stock import StockHistoricalDataClient, StockLatestTradeRequest
from alpaca.trading.models import TradeUpdate
from alpaca.trading.client import TradingClient
from alpaca.trading.stream import TradingStream
from alpaca.trading.requests import GetOptionContractsRequest, MarketOrderRequest
from alpaca.trading.enums import AssetStatus, ContractType, AssetClass
# Apply nest_asyncio to allow running the event loop
nest_asyncio.apply()
First, it’s important to import all the necessary libraries for data analysis, numerical calculations, and asynchronous operations. This code specifically leverages Alpaca's API for historical and real-time stock and options data, as well as trade execution. The `nest_asyncio.apply()
` line ensures compatibility with asynchronous programming within environments like Jupyter Notebooks.
Essentially, this block sets up the environment for a Python-based options trading strategy by importing required modules, establishing a connection to the Alpaca trading platform, and enabling asynchronous functionality.
Step 2: Set up your API Keys and initialize Alpaca client
# API credentials
TRADE_API_KEY = 'XXX'
TRADE_API_SECRET = 'XXX'
paper = True
Next, you’ll want to define the API credentials required to connect to the Alpaca trading platform. It includes placeholders for the `TRADE_API_KEY
` and `TRADE_API_SECRET
`, which should be replaced with the user's actual credentials. The `paper = True
` variable indicates that the code will interact with Alpaca's paper trading environment, allowing for risk-free testing of the trading strategy.
# Initialize Alpaca clients
trading_client = TradingClient(api_key=TRADE_API_KEY, secret_key=TRADE_API_SECRET, paper=paper)
trade_update_stream = TradingStream(api_key=TRADE_API_KEY, secret_key=TRADE_API_SECRET, paper=paper)
stock_data_client = StockHistoricalDataClient(api_key=TRADE_API_KEY, secret_key=TRADE_API_SECRET)
option_data_client = OptionHistoricalDataClient(api_key=TRADE_API_KEY, secret_key=TRADE_API_SECRET)
It’s also important to initialize the Alpaca API clients required for trading and data retrieval. This code creates instances of `TradingClient
`, `TradingStream`, `StockHistoricalDataClient
`, and `OptionHistoricalDataClient
` using the provided API credentials. These clients enable the code to interact with Alpaca's trading platform for order execution, real-time data streaming, and historical stock and options data.
Step 3: Set up key variables
# Configuration
underlying_symbol = "JPM"
max_abs_notional_delta = 500
risk_free_rate = 0.045
positions = {}
Set your key variables like the underlying symbol (`JPM
`) and risk_free_rate
.* This code also includes logic to liquidate any pre-existing positions related to the underlying symbol, ensuring a clean slate for the gamma scalping strategy. Please note that we are using JPM as an example, and it should not be considered investment advice.
Step 4: Liquidate any existing positions
#liquidate existing positions
print(f"Liquidating pre-existing positions related to underlying {underlying_symbol}")
all_positions = trading_client.get_all_positions()
for p in all_positions:
if p.asset_class == AssetClass.US_OPTION:
option_contract = trading_client.get_option_contract(p.symbol)
if option_contract.underlying_symbol == underlying_symbol:
print(f"Liquidating {p.qty} of {p.symbol}")
trading_client.close_position(p.symbol)
elif p.asset_class == AssetClass.US_EQUITY:
if p.symbol == underlying_symbol:
print(f"Liquidating {p.qty} of {p.symbol}")
trading_client.close_position(p.symbol)
This block is responsible for liquidating any pre-existing positions related to the underlying symbol. It first retrieves all current positions using `trading_client.get_all_positions()
`. Then, it iterates through each position. If the position is a US option, it checks if the underlying symbol matches the one being used in the strategy. If there's a match, it closes the position. Similarly, if the position is a US equity and the symbol matches the underlying symbol, it closes that position as well. This ensures that the portfolio starts with a clean slate before initiating the gamma scalping strategy.
# Add underlying symbol to positions list
print(f"Adding {underlying_symbol} to position list")
positions[underlying_symbol] = {'asset_class': 'us_equity', 'position': 0, 'initial_position': 0}
This adds the underlying stock symbol to the `positions
` dictionary. It initializes the stock's position and initial position to zero, indicating that no shares of the underlying stock are currently held.
Step 5: Create date ranges for your options
# Set expiration range for options
today = datetime.now().date()
min_expiration = today + timedelta(days=14)
max_expiration = today + timedelta(days=60)
It’s important to set the expiration date range for the options contracts to be considered in the strategy. It uses the `datetime
` library to get the current date (`today
`). Then, it calculates the minimum expiration date as 14 days from today and the maximum expiration date as 60 days from today. These values will be used later to filter the option contracts fetched from the Alpaca API.
Step 6: Get the price of the underlying asset
# Get the latest price of the underlying stock
def get_underlying_price(symbol):
underlying_trade_request = StockLatestTradeRequest(symbol_or_symbols=symbol)
underlying_trade_response = stock_data_client.get_stock_latest_trade(underlying_trade_request)
return underlying_trade_response[symbol].price
underlying_price = get_underlying_price(underlying_symbol)
min_strike = round(underlying_price * 1.01, 2)
print(f"{underlying_symbol} price: {underlying_price}")
print(f"Min Expiration: {min_expiration}, Max Expiration: {max_expiration}, Min Strike: {min_strike}")
This code focuses on fetching the latest price of the underlying stock and establishing a minimum strike price for options contracts. It defines a function `get_underlying_price(symbol)
` to retrieve the current stock price using Alpaca's API. It then calls this function to get the price of the underlying symbol and sets the minimum strike price for options as 1% above this value. Finally, it prints key information like the underlying stock's price, minimum and maximum expiration dates, and the calculated minimum strike price.
Step 7: Filter for option contracts that fit the criteria
# Search for option contracts to add to the portfolio
req = GetOptionContractsRequest(
underlying_symbols=[underlying_symbol],
status=AssetStatus.ACTIVE,
expiration_date_gte=min_expiration,
expiration_date_lte=max_expiration,
root_symbol=underlying_symbol,
type=ContractType.CALL,
strike_price_gte=str(min_strike),
limit=5,
)
option_chain_list = trading_client.get_option_contracts(req).option_contracts
This section constructs a request to fetch option contracts for the gamma scalping strategy. It uses the `GetOptionContractsRequest
` to specify the criteria for the options to be retrieved. The request filters for active call options on the underlying symbol (`JPM
`) with expiration dates between the previously calculated `min_expiration
` and `max_expiration
` dates. It also sets a minimum strike price (`min_strike
`) and limits the number of contracts returned to 5. The retrieved option contracts are then stored in the `option_chain_list
` variable.
# Add the first 3 options to the position list
for option in option_chain_list[:3]:
symbol = option.symbol
print(f"Adding {symbol} to position list")
positions[symbol] = {
'asset_class': 'us_option',
'underlying_symbol': option.underlying_symbol,
'expiration_date': pd.Timestamp(option.expiration_date),
'strike_price': float(option.strike_price),
'type': option.type,
'size': float(option.size),
'position': 1.0,
'initial_position': 1.0
}
This section iterates through the first 3 option contracts in the `option_chain_list
`. For each option, it extracts relevant details like the symbol, underlying symbol, expiration date, strike price, type, and size. It then adds this information to the `positions
` dictionary with an initial position of 1.0, indicating the intention to purchase one contract of each of these options initially. Essentially, this block prepares the initial set of options contracts to be included in the gamma scalping strategy.
Step 8: Calculate the Greeks
# Add the first 3 options to the position list
for option in option_chain_list[:3]:
symbol = option.symbol
print(f"Adding {symbol} to position list")
positions[symbol] = {
'asset_class': 'us_option',
'underlying_symbol': option.underlying_symbol,
'expiration_date': pd.Timestamp(option.expiration_date),
'strike_price': float(option.strike_price),
'type': option.type,
'size': float(option.size),
'position': 1.0,
'initial_position': 1.0
}
This section defines the function `calculate_implied_volatility
` to estimate the implied volatility of an option. It uses the Black-Scholes option pricing model and Brent's method (an optimization technique) to find the volatility value that makes the model price match the given market price of the option. The function takes inputs like option price, underlying asset price (S), strike price (K), time to expiration (T), risk-free rate (r), and option type (call or put). It is a crucial component for calculating the option Greeks (delta and gamma) needed for the gamma scalping strategy.
Once you have the implied volatility, you can calculate for delta and gamma.
# Calculate option Greeks (Delta and Gamma)
def calculate_greeks(option_price, strike_price, expiry, underlying_price, risk_free_rate, option_type):
T = (expiry - pd.Timestamp.now()).days / 365
implied_volatility = calculate_implied_volatility(option_price, underlying_price, strike_price, T, risk_free_rate, option_type)
d1 = (np.log(underlying_price / strike_price) + (risk_free_rate + 0.5 * implied_volatility ** 2) * T) / (implied_volatility * np.sqrt(T))
d2 = d1 - implied_volatility * np.sqrt(T)
delta = norm.cdf(d1) if option_type == 'call' else -norm.cdf(-d1)
gamma = norm.pdf(d1) / (underlying_price * implied_volatility * np.sqrt(T))
return delta, gamma
Next, define the function `calculate_greeks
` to compute the delta and gamma of an option. It calculates the time to expiration (T), calls the `calculate_implied_volatility
` function to get the implied volatility, and then uses the Black-Scholes model to calculate the delta and gamma. The function takes inputs such as option price, strike price, expiration date, underlying price, risk-free rate, and option type. These calculated Greeks are essential for maintaining a delta-neutral position and executing the gamma scalping strategy.
Step 9: Set up real-time trading updates
# Handle trade updates
async def on_trade_updates(data: TradeUpdate):
symbol = data.order.symbol
if symbol in positions:
if data.event in {'fill', 'partial_fill'}:
side = data.order.side
qty = data.order.qty
filled_avg_price = data.order.filled_avg_price
position_qty = data.position_qty
print(f"{data.event} event: {side} {qty} {symbol} @ {filled_avg_price}")
print(f"updating position from {positions[symbol]['position']} to {position_qty}")
positions[symbol]['position'] = float(position_qty)
trade_update_stream.subscribe_trade_updates(on_trade_updates)
This code sets up a mechanism to handle real-time trade updates. It defines an asynchronous function `on_trade_updates(data: TradeUpdate)
` that is triggered whenever a trade update is received. The function checks if the symbol involved in the trade is part of the strategy's positions. If so, and the event is a "fill
" or "partial_fill
" (indicating a trade execution), it extracts details like the side (buy/sell), quantity, filled average price, and the new position quantity. It then prints this information and updates the corresponding position in the `positions
` dictionary. Finally, it subscribes to the `trade_update_stream
` using this `on_trade_updates
` function to receive and process real-time trade updates as they occur.
Step 10: Create your trade execution
# Execute initial trades
async def initial_trades():
await asyncio.sleep(5)
print("executing initial option trades")
for symbol, pos in positions.items():
if pos['asset_class'] == 'us_option' and pos['initial_position'] != 0:
side = 'buy' if pos['initial_position'] > 0 else 'sell'
order_request = MarketOrderRequest(
symbol=symbol,
qty=abs(pos['initial_position']),
side=side,
type='market',
time_in_force='day'
)
print(f"Submitting order to {side} {abs(pos['initial_position'])} contracts of {symbol} at market")
trading_client.submit_order(order_request)
Next, define an asynchronous function `initial_trades
` to execute the initial purchase of options contracts. It waits for 5 seconds using `asyncio.sleep(5)
` and then iterates through the `positions
` dictionary. For each position that is a US option and has a non-zero initial position, it determines the appropriate side ('buy' or 'sell') based on the sign of the initial position. It then constructs a `MarketOrderRequest
` to buy or sell the specified quantity of the option contract at market price with a time in force of 'day
'. Finally, it submits the order using `trading_client.submit_order(order_request)
`.
Step 11: Ensure delta-neutrality is maintained
# Maintain delta-neutral strategy
def maintain_delta_neutral():
current_delta = 0.0
underlying_price = get_underlying_price(underlying_symbol)
print(f"Current price of {underlying_symbol} is {underlying_price}")
for symbol, pos in positions.items():
if pos['asset_class'] == 'us_equity' and symbol == underlying_symbol:
current_delta += pos['position']
elif pos['asset_class'] == 'us_option' and pos['underlying_symbol'] == underlying_symbol:
option_quote_request = OptionLatestQuoteRequest(symbol_or_symbols=symbol)
option_quote = option_data_client.get_option_latest_quote(option_quote_request)[symbol]
option_quote_mid = (option_quote.bid_price + option_quote.ask_price) / 2
delta, gamma = calculate_greeks(
option_price=option_quote_mid,
strike_price=pos['strike_price'],
expiry=pos['expiration_date'],
underlying_price=underlying_price,
risk_free_rate=risk_free_rate,
option_type=pos['type']
)
current_delta += delta * pos['position'] * pos['size']
adjust_delta(current_delta, underlying_price)
def adjust_delta(current_delta, underlying_price):
if current_delta * underlying_price > max_abs_notional_delta:
side = 'sell'
elif current_delta * underlying_price < -max_abs_notional_delta:
side = 'buy'
else:
return
qty = abs(round(current_delta,0))
order_request = MarketOrderRequest(symbol=underlying_symbol, qty=qty, side=side, type='market', time_in_force='day')
print(f"Submitting {side} order for {qty} shares of {underlying_symbol} at market")
trading_client.submit_order(order_request)
This section defines two functions, `maintain_delta_neutral
` and `adjust_delta
`, that work together to maintain a delta-neutral position in the options trading strategy.
The `maintain_delta_neutral
` function calculates the current delta of the portfolio by iterating through the positions, fetching option quotes, and using the `calculate_greeks
` function to determine the delta of each option. It then calls the `adjust_delta
` function to rebalance the portfolio if necessary.
The `adjust_delta
` function checks if the current delta exceeds a predefined threshold. If so, it determines the appropriate side ('buy' or 'sell') and quantity of the underlying stock to trade in order to bring the delta back within the desired range. It then submits a market order to execute this trade.
These functions are crucial for dynamically managing the portfolio's risk and ensuring that the gamma scalping strategy remains delta-neutral.
Step 12: Execute gamma scalping
# Gamma scalping strategy
async def gamma_scalp(initial_interval=30, interval=120):
await asyncio.sleep(initial_interval)
maintain_delta_neutral()
while True:
await asyncio.sleep(interval)
maintain_delta_neutral()
This code defines the core gamma scalping strategy function `gamma_scalp
`. It's an asynchronous function that initially waits for a specified `initial_interval
` (defaulting to 30 seconds). Then, it calls the `maintain_delta_neutral
` function to establish a delta-neutral position. Subsequently, it enters an infinite loop (`while True
`) where it periodically (every `interval
`, defaulting to 120 seconds) calls `maintain_delta_neutral
` again. This ensures that the portfolio's delta neutrality is continuously monitored and adjusted, which is the essence of the gamma scalping strategy.
Step 13: Run the code as a loop
# Main event loop
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
trade_update_stream._run_forever(),
#initial_trades(),
gamma_scalp()
))
loop.close()
This code represents the main event loop that drives the execution of the gamma scalping strategy. It utilizes asyncio's event loop to manage concurrent tasks. The `trade_update_stream._run_forever()
` task runs indefinitely, continuously listening for and processing real-time trade updates. The `initial_trades()
` line executes the initial trades to establish the starting positions for the strategy.
The `gamma_scalp()
` task is the core of the strategy, continuously monitoring and adjusting the portfolio to maintain delta neutrality. The `asyncio.gather
` function runs these tasks concurrently, allowing the strategy to react to real-time market events while periodically rebalancing the portfolio. Finally, `loop.close()
` is called to clean up and terminate the event loop.
Conclusion
With this guide, you’ve gained hands-on experience in building an algorithmic gamma scalping strategy. By understanding how to dynamically adjust positions based on real-time data and managing its risk, you’re developing a robust trading approach. A well-implemented gamma scalping strategy coupled with ongoing refinement can help mitigate risks and maximize returns – especially in volatile markets.
*Please note that the risk-free rate shown is for illustrative purposes and may not be the actual risk-free rate depending on the time this is read.
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 of this article is for informational purposes and is believed to be accurate as of posting date but may be subject to change. All examples and 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.