
A bull put spread is an options trading strategy that involves selling a put option at a higher strike price while simultaneously buying another put option at a lower strike price, both with the same expiration date.
In this guide, we will explore the fundamentals of a bull put spread, provide examples, discuss how market factors—including the Greeks—influence its performance, and outline the associated risks. We will also highlight actionable steps you can take for algorithmic integration using Alpaca's Trading API or manual trading using Alpaca’s dashboard.
What is a Bull Put Spread?
A bull put spread, also known as a put credit spread, aims to profit through premium collection while limiting potential losses.
This strategy involves two simultaneous transactions:
- Sell (short) a put option at a higher strike price: This generates potential profits from the premium but obligates the trader to purchase the underlying asset at the strike price if exercised.
- Buy (long) a put option at a lower strike price: This provides protection by limiting potential losses if the underlying asset's price declines significantly.
Both options have the same expiration date and underlying asset. The net result is a credit received upfront, as the premium collected from the short put exceeds the premium paid for the long put. This strategy may be profitable if the underlying asset's price remains above the higher strike price at expiration, causing both puts to expire worthless, allowing the trader to retain the net credit received.
When to Use a Bull Put Spread
The bull put spread is sometimes used in the following market conditions:
- Moderate Bullish Outlook: When the trader anticipates that the underlying asset will increase modestly or remain stable over the option's lifespan.
- Heightened Implied Volatility (IV): The strategy may benefit from heightened IV at entry, as a subsequent decrease in IV can lead to a decline in option premiums, favoring the position.
- Time Decay (Theta): As a credit spread, the position may benefit from time decay; the closer the options get to expiration without being in-the-money (ITM), the more the trader may potentially profit.
Bull Put Spread Example (With Payoff Diagram)
To see how a bull put spread works in different market conditions, let's analyze an example using stock XYZ as the underlying asset. This will illustrate the risk-reward profile, where the maximum possible loss is the spread width ($7.00) minus the initial credit ($4.00 per share). This results in a total loss of $300 if the stock price increases moderately
Example Setup:
- Stock Price at Initiation: $100
- Sell (Short) 100 Strike Put: Premium received = $5.00
- Buy (Long) 95 Strike Put: Premium paid = $2.00
- Net Credit (Total Premium Received): $5.00 - $2.00 = $3.00
Trade Structure:
Potential Outcomes at Expiration:
- Scenario 1: Stock remains at $105 (above both strikes):
- Both put options are out-of-the-money (OTM) and expire worthless. The trader retains the entire net credit received.
- Net Profit: $0 (both options expire) + $300 (initial net credit) = $300 (theoretical maximum profit)
- Scenario 2: Stock falls to $97 (between strikes):
- The short 100 strike put is ITM by $3.00, resulting in a loss, while the long 95 strike put expires worthless.
- Break even: -$300 (loss from short put) + $300 (initial net credit) = $0
- Scenario 3: Stock falls to $90 (below both strikes):
- Both puts are ITM. The loss on the short put is offset by the gain on the long put, but the trader still incurs a loss equal to the difference between the strikes minus the net credit received.
- Net Loss: (-$100 + $95) × 100 (shares) + $300 = -$200 (theoretical maximum loss)
Key Metrics:
- Theoretical Maximum Profit: Net credit received = $3.00 per share ($300 in total).Theoretical Maximum Loss: Difference between strike prices - Net credit = ($100 - $95) - $3.00 = $2.00 per share ($200 in total).
- Breakeven Point: Higher strike price - Net credit received = $100 - $3.00 = $97.00.
Note: For American-style options, there's a possibility of early assignment of the short put, especially around ex-dividend dates. This can affect the strategy's outcome before the expiration date.

Bull Call Spread vs. Bull Put Spread
Both bull call spreads and bull put spreads are similar in that both aim to profit from bullish market, but they differ in structure, cost, and risk profiles.
How the Options Greeks Factor into Bull Put Spreads
To optimize strategy execution when using a bull put spread, it is essential to monitor volatility trends, price movements, and expiration timelines, as various option Greek factors can influence the outcome.
Delta
- The bull put spread has a net positive delta, meaning it gains value as the stock price rises.
- Net delta is calculated by subtracting the long put’s delta from the short put’s delta.
- Unlike a single short put, a bull put spread’s delta remains relatively stable due to its low gamma.
- The impact of gamma depends on the stock price’s position relative to the strike prices:
- Near the short put’s strike price: The net delta is moderately positive.
- Between the strikes: The net delta is positive but diminishes.
- Near the long put’s strike price: The net delta is positive.
Gamma
- Compared to a single short put, the spread’s gamma is lower, reducing the extent of delta fluctuations.
- Net gamma is calculated by subtracting the long put’s gamma from the short put’s gamma.
- The impact of gamma depends on the stock price’s position relative to the strike prices:
- Near the short put’s strike price: The net gamma is negative.
- Between the strikes: The net gamma may transition from negative to positive as the stock price decreases.
- Near the long put’s strike price: The net gamma is positive.
Theta
- As a credit strategy, time decay generally works in favor of the bull put spread position since you receive a net premium to enter the trade.
- The impact of theta depends on the stock price’s position relative to the strike prices:
- Near the short put’s strike price: The net theta is positive.
- Between the strikes: The net theta remains positive but may decrease as the stock price falls.
- Near the long put’s strike price: The net theta can become negative due to the long put’s time decay.
Vega
- The vega of a bull put spread is lower compared to a single short put—due to the offsetting effects of the short and long puts, but vega is not uniformly low in all scenarios.
- The impact of vega depends on the stock price’s position relative to the strike prices:
- Near the short put’s strike price: The net vega is negative.
- Between the strikes: The net vega may remain negative but decreases in magnitude as the stock price falls.
- Near the long put’s strike price: The net vega is positive due to the long put’s significant sensitivity to volatility.
Key Considerations and Risks
While the bull put spread is a defined-risk strategy, traders should be aware of its limitations and risks before entering:
- Limited Profit Potential: The theoretical profit is capped at the net credit received, making this strategy less ideal for highly bullish markets.
- Risk of Assignment: The short put may be exercised early, especially near expiration or if the stock price falls below the strike price, leading to potential obligations to purchase the underlying asset.
- Impact of Volatility: An increase in implied volatility can increase the value of the put options, potentially leading to losses.
- Liquidity Concerns: If the options are not actively traded, buying or selling at a fair price may be difficult, increasing transaction costs.
The Advantages and Disadvantages of Bull Put Spreads
This strategy offers both advantages and disadvantages, as outlined below..
Advantages:
- Profit Generation: By selling the higher strike put option, traders receive a net premium upfront, which can potentially serve as immediate profit.
- Defined Risk: The theoretical potential loss is limited to the difference between the strike prices minus the net premium received, providing a clear understanding of the theoretical maximum risk involved.
- Flexibility: Traders can adjust the strike prices and expiration dates to align with their market outlook and risk tolerance, allowing for customization based on individual strategies.
- Benefit from Time Decay: As options lose value over time, the bull put spread likely benefits from time decay, especially if the underlying asset's price remains above the higher strike price, leading both options to expire worthless.
Disadvantages:
- Limited Profit Potential: The theoretical maximum profit is confined to the net premium received, which may be lower compared to other strategies that offer unlimited upside potential.
- Possibility of Losses: While losses are capped, a significant decline in the underlying asset's price can lead to substantial losses up to the defined theoretical maximum.
- Margin Requirements: Some brokers may impose margin requirements to initiate a bull put spread, which could necessitate maintaining a minimum account balance, potentially limiting accessibility for some traders.
- Assignment Risk: For American-style options, there is a possibility of early assignment on the short put option, especially if the underlying asset's price falls significantly before expiration, obligating the trader to purchase the asset at the strike price.
Building a Bull Put Spread Strategy With Alpaca’s Trading API
This section provides a step-by-step guide to implementing a bull put spread using Python and Alpaca’s Trading API in the Paper Trading environment. The strategy is a two-legged, directional strategy that involves simultaneously buying long put and short put options. This approach helps manage risk and optimize decision-making by leveraging key metrics and filters. If you’re not connected to Alpaca’s API or are unfamiliar with options trading, here are some useful resources to help you follow along:
- Sign up for an Alpaca Trading API account
- How to Connect to Alpaca's Trading API
- How to Start Paper Trading with Alpaca's Trading API
- How to Trade Options with Alpaca’s Trading API
- A Guide to Emotionless Options Trading
Important Notes:
- The code assumes a flat interest rate, excludes dividends, and is not optimized for 0DTE options. It does not consider market hours and serves as a starting point rather than a complete strategy.
- Expiration Day Order Rules:
- Orders must be submitted before 3:15 p.m. ET for stocks/options and 3:30 p.m. ET for broad-based ETFs (e.g., SPY, QQQ).
- Orders placed after these times will be rejected.
- Expiring positions are auto-liquidated at 3:30 p.m. ET (stocks/options) and 3:45 p.m. ET (broad-based ETFs) for risk management.
- Multi-Leg (MLeg) Orders:
- All legs of a strategy must be included in the same order for acceptance, especially when involving uncovered short options (e.g., short put calendar spreads, short call calendar spreads), which require Level 4 approval. See Alpaca's multi-leg order restrictions for details.
- American-Style Options Considerations:
- American-style options may be assigned early, affecting trade outcomes. Profit/loss scenarios assume expiration without early exercise and do not factor in transaction fees (e.g., brokerage fees, commissions). Traders should also monitor ex-dividend dates for short calls to manage assignment risk.
- Example equity:
- "JNJ" is used for demonstration purposes and should not be considered investment advice.
Step 1: Setting Up the Environment and Trade Parameters
The first step involves configuring the environment for trading a bull put spread and specifying your Alpaca Paper Trading API keys. This ensures your API connections and variable setup are secure and streamlined for data retrieval and trading in operations.
Note: While we are using Jupyter Notebook for this tutorial, you can use Google Collab or any other IDE as your code environment. You can also find all of the below code on Alpaca’s Github page.
# Install or upgrade the package `alpaca-py` and import it
# !python3 -m pip install --upgrade alpaca-py
import pandas as pd
import numpy as np
from scipy.stats import norm
import time
from scipy.optimize import brentq
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from dotenv import load_dotenv
import os
from typing import Any, Dict, List, Optional, Tuple
import alpaca
from alpaca.data.timeframe import TimeFrame, TimeFrameUnit
from alpaca.data.historical.option import OptionHistoricalDataClient
from alpaca.data.historical.stock import StockHistoricalDataClient, StockLatestTradeRequest
from alpaca.data.requests import StockBarsRequest, OptionLatestQuoteRequest, OptionSnapshotRequest
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import (
MarketOrderRequest,
GetOptionContractsRequest,
MarketOrderRequest,
OptionLegRequest,
ClosePositionRequest,
)
from alpaca.trading.enums import (
AssetStatus,
OrderSide,
OrderClass,
OrderStatus,
TimeInForce,
ContractType,
)
The script initializes Alpaca clients for trading stocks and options, as well as for handling data requests.
# API credentials for Alpaca
API_KEY = "YOUR_ALPACA_API_KEY_FOR_PAPER_TRADING"
API_SECRET = 'YOUR_ALPACA_API_SECRET_KEY_FOR_PAPER_TRADING'
BASE_URL = None
## We use paper environment for this example (Please do not modify this. This example is for paper trading only)
PAPER = True
# Initialize Alpaca clients
trade_client = TradingClient(api_key=API_KEY, secret_key=API_SECRET, paper=PAPER, url_override=BASE_URL)
option_historical_data_client = OptionHistoricalDataClient(api_key=API_KEY, secret_key=API_SECRET, url_override=BASE_URL)
stock_data_client = StockHistoricalDataClient(api_key=API_KEY, secret_key=API_SECRET)
# Below are the variables for development this documents
# Please do not change these variables
trade_api_url = None
trade_api_wss = None
data_api_url = None
option_stream_data_wss = None
Step 2: Pick Your Strategy Inputs: Risk Management and Position Sizing
For an algorithmic bull put spread, selecting the right strategy inputs is crucial to managing risk and sizing positions effectively. These inputs ensure each trade meets predetermined risk/reward criteria and fits within your overall portfolio limits.
Liquidity and Open Interest Threshold
To avoid thinly traded options and wide bid-ask spreads, a minimum open interest threshold (e.g., 50) is set. This filter ensures only contracts with robust market participation are considered, leading to smoother execution and more reliable pricing.
Option Greeks and IV Ranges
Choosing the right options also involves filtering by key risk measures. For a bull put spread, traders may require both legs to meet specific criteria—such as having an expiration between 20 and 60 days, an IV in the range of 0.15 to 0.50, and delta values that yield a net positive delta. For instance, a short put with a delta between -0.40 and -0.20 paired with a long put with a delta between -0.35 and -0.10 helps ensure the overall position benefits if the underlying remains above the short strike. These ranges fine-tune sensitivity to price movements and time decay.
Target Profit
A clear profit target is essential. By setting a target profit percentage (e.g., 60%), the spread is closed once its value moves in your favor by that amount. This predefined exit may help to secure gains and minimizes the risk of losing accrued profits due to market reversals.
Buying Power Allocation
Limiting capital exposure is another key element. Traders typically allocate around 5% of total buying power to any single trade. This dynamic calculation keeps risk consistent across trades, ensuring that even adverse moves have a limited impact on the overall account.
Evaluating the Cost
Cost evaluation involves setting an acceptable strike range relative to the underlying price. A common method is to define strikes within a fixed percentage (such as ±6%) of the current price. This approach balances potential profit with manageable risk. Incorporating a risk-free rate (typically 1%) into the calculations refines the estimation of the Greeks and IV, resulting in a more accurate assessment of the trade’s risk/reward profile.
Monitoring and Potential Adjustments
Active monitoring is essential once the trade is live. Predefined thresholds for adjustments—such as exit triggers if delta or IV move beyond set limits (e.g., delta above 0.60 or IV rising by more than 20%)—allow traders to roll or exit positions promptly. These dynamic controls help maintain the desired risk profile during volatile periods.
Liquidity Considerations and Stop-Loss Limits
In addition to the open interest filter, traders may require that the distance between the short put’s strike and the underlying price is narrower than that between the underlying and the long put’s strike. This extra criterion limits potential loss on the short leg relative to the protection provided by the long leg. Combined with stop-loss orders, this strategy further safeguards against adverse market movements, especially in fast-moving conditions where slippage is a concern.
# Select the underlying stock
underlying_symbol = 'JNJ'
# Set the timezone
timezone = ZoneInfo('America/New_York')
# Get current date in US/Eastern timezone
today = datetime.now(timezone).date()
# Define a 6% range around the underlying price
STRIKE_RANGE = 0.06
# Buying power percentage to use for the trade
BUY_POWER_LIMIT = 0.05
# Risk free rate for the options greeks and IV calculations
risk_free_rate = 0.01
# Check account buying power
buying_power = float(trade_client.get_account().buying_power)
# Set the open interest volume threshold
OI_THRESHOLD = 50
# Calculate the limit amount of buying power to use for the trade
buying_power_limit = buying_power * BUY_POWER_LIMIT
# Set the expiration date range for the options
min_expiration = today + timedelta(days=21)
max_expiration = today + timedelta(days=60)
# Get the latest price of the underlying stock
def get_underlying_price(symbol):
# Get the latest trade for the underlying stock
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
# Get the latest price of the underlying stock
underlying_price = get_underlying_price(underlying_symbol)
# Set the minimum and maximum strike prices based on the underlying price
min_strike = str(underlying_price * (1 - STRIKE_RANGE))
max_strike = str(underlying_price * (1 + STRIKE_RANGE))
# Define the criteria for selecting the options
# Each key corresponds to a leg and maps to a tuple of: (expiration range, IV range, delta range, theta range)
criteria = {
'short_put': ((20, 60), (0.15, 0.50), (-0.60, -0.20), (-0.10, -0.03)),
'long_put': ((20, 60), (0.15, 0.50), (-0.35, -0.10), (-0.08, -0.01))
}
# Set target profit levels
TARGET_PROFIT_PERCENTAGE = 0.6
DELTA_STOP_LOSS = 0.60
IV_STOP_LOSS = 0.80
# Display the values
print(f"Underlying Symbol: {underlying_symbol}")
print(f"{underlying_symbol} price: {underlying_price}")
print(f"Strike Range: {STRIKE_RANGE}")
print(f"Buying Power Limit: {buying_power_limit}")
print(f"Open Interest Threshold: {OI_THRESHOLD}")
print(f"Minimum Expiration Date: {min_expiration}")
print(f"Maximum Expiration Date: {max_expiration}")
print(f"Minimum Strike Price: {min_strike}")
print(f"Maximum Strike Price: {max_strike}")
Step 3: Market Data Analysis with Underlying Assets
When implementing a bull put spread, it's essential to identify market conditions that support the strategy. Using a combination of price trends, volatility measures, and technical indicators, traders would be able to optimize entry points and improve trade efficiency. The following factors may help the bull put spread setup based on underlying asset price movement and volatility events.
Stock Price Trends (Bar Chart Analysis)
Monitoring historical price movements can help traders evaluate the broader trend of the underlying asset before entering a bull put spread. Reviewing candlestick charts offers valuable insights into recent price action, as well as key support and resistance levels. This analysis helps traders identify whether the asset is trending upward or trading within a range.
As mentioned, bull put spreads often perform well in moderately bullish environments, where the price is expected to rise gradually toward the short put’s strike price. It is less suitable for strongly bearish markets or highly volatile conditions, where sharp reversals could cause the spread to lose value before expiration.
# Get the historical data for the underlying stock by symbol and timeframe
# ref. https://alpaca.markets/sdks/python/api_reference/data/option/historical.html
def get_stock_data(underlying_symbol, days=90):
today = datetime.now(timezone).date()
req = StockBarsRequest(
symbol_or_symbols=[underlying_symbol],
timeframe=TimeFrame(amount=1, unit=TimeFrameUnit.Day), # specify timeframe
start=today - timedelta(days=days), # specify start datetime, default=the beginning of the current day.
)
return stock_data_client.get_stock_bars(req).df
# List of stock agg objects while dropping the symbol column
priceData = get_stock_data(underlying_symbol, days=180).reset_index(level='symbol', drop=True)
import plotly.graph_objects as go
# Bar chart for the stock price
fig = go.Figure(data=[go.Candlestick(x=priceData.index,
open=priceData['open'],
high=priceData['high'],
low=priceData['low'],
close=priceData['close'])])
fig.show()
This code returns the bar chart like in the image below.

Bollinger Bands for Volatility Assessment
Bollinger Bands provide a dynamic range around the asset's price, helping traders assess current market volatility and potential price extremes. When the underlying stock price approaches the upper Bollinger Band, it may indicate overbought conditions, while a move toward the lower band could suggest oversold levels.
For a bull put spread, Bollinger Bands can help identify whether the asset is in a favorable moderate uptrend, which is ideal for this strategy. A price trending upward toward the middle to upper band — without excessive volatility — may signal conditions where the stock could rise gradually into the profitable zone between the long and short put strikes. In contrast, if the asset is near the lower band or moving erratically, the strategy’s chances of success may decline, as sharp reversals or unpredictable price swings increase the risk of losing the initial premium paid for the spread.
# setup bollinger band calculations
def check_bb(df, period=14, multiplier=2):
bollinger_bands = []
# Calculate the Simple Moving Average (SMA)
df['SMA'] = df['close'].rolling(window=period).mean()
# Calculate the rolling standard deviation
df['StdDev'] = df['close'].rolling(window=period).std()
# Calculate the Upper Bollinger Band (two standard deviation)
df['Upper Band'] = df['SMA'] + (multiplier * df['StdDev'])
# Calculate the Lower Bollinger Band (two standard deviation)
df['Lower Band'] = df['SMA'] - (multiplier * df['StdDev'])
# Get the most recent Upper Band value
upper_bollinger_band = df['Upper Band'].iloc[-1]
lower_bollinger_band = df['Lower Band'].iloc[-1]
bollinger_bands = [upper_bollinger_band, lower_bollinger_band]
return bollinger_bands
bollinger_bands = check_bb(priceData, 14, 2)
# The current market price is not too close to the two-standard deviation level yet but is relatively closer to the higher Bollinger Band.
print(f"Latest Upper Bollinger Band is: {bollinger_bands[0]}. Latest Lower Bollinger Band is {bollinger_bands[1]}; while underlying stock '{underlying_symbol}' price is {underlying_price}.")
Step 4: Set up for a Bull Put Spread (Find two puts)
This code sets up a tool for printing messages from the code. It tells Python to show general messages (INFO level) and more detailed messages (DEBUG level) so we can see what's happening and troubleshoot if needed.
# Configure logging
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
The get_options
function retrieves active option contracts for a specified underlying symbol within a given strike price and expiration date range. Reminder to specify option_type as either ContractType.CALL
or ContractType.PUT
to filter for the desired option type.
# option_type: ContractType.CALL or ContractType.PUT.
def get_options(underlying_symbol, min_strike, max_strike, min_expiration, max_expiration, option_type):
req = GetOptionContractsRequest(
underlying_symbols=[underlying_symbol],
status=AssetStatus.ACTIVE,
type=option_type,
strike_price_gte=min_strike,
strike_price_lte=max_strike,
expiration_date_gte=min_expiration,
expiration_date_lte=max_expiration,
)
return trade_client.get_option_contracts(req).option_contracts
The helper function validate_sufficient_OI
verifies that an option's data includes the necessary fields and that its open interest exceeds a specified threshold. It is used at the start of the workflow to ensure that only eligible options are processed further.
def get_options(underlying_symbol, min_strike, max_strike, min_expiration, max_expiration, option_type):
req = GetOptionContractsRequest(
underlying_symbols=[underlying_symbol],
status=AssetStatus.ACTIVE,
type=option_type,
strike_price_gte=min_strike,
strike_price_lte=max_strike,
expiration_date_gte=min_expiration,
expiration_date_lte=max_expiration,
)
return trade_client.get_option_contracts(req).option_contracts
The calculate_option_metrics
computes essential metrics for an option contract by retrieving current market data and calculating relevant values. Here’s what it does:
- Determine Expiration Details:
- It extracts the option’s expiration date from the provided data.
- It calculates the number of days remaining until expiration by comparing the expiration date with the current date.
- Retrieve Latest Market Data via Alpaca’s API:
- The function constructs an OptionSnapshotRequest—a method provided by Alpaca’s API—using the option’s symbol.
- It then calls
option_historical_data_client.get_option_snapshot()
to fetch a snapshot of the latest market data for that option.
- Calculate the Option Price:
- The mid-price is calculated as the average of the bid and ask prices obtained from the snapshot’s latest quote.
- Extract Implied Volatility and Greeks:
- The implied volatility (IV) is directly extracted from the snapshot.
- Key option Greeks—delta, gamma, theta, and vega—are also extracted from the snapshot for further risk and pricing analysis.
- Return Metrics:
- Finally, the function returns a dictionary containing the calculated option price, expiration date, remaining days, IV, and the Greeks for use in other parts of the trading strategy.
Note: Although the function accepts parameters for underlying_price
and risk_free_rate
, these are not utilized in the current calculations.
def calculate_option_metrics(option_data, underlying_price, risk_free_rate):
"""
Calculate key option metrics including option price, implied volatility (IV), and option Greeks.
"""
# Calculate expiration and remaining days
option_symbol = option_data['symbol']
expiration_date = pd.Timestamp(option_data['expiration_date'])
remaining_days = (expiration_date - pd.Timestamp.now()).days
# Retrieve the latest quote for the option
req = OptionSnapshotRequest(
symbol_or_symbols = option_symbol,
)
snapshot = option_historical_data_client.get_option_snapshot(req)[option_symbol]
option_price = (snapshot.latest_quote.bid_price + snapshot.latest_quote.ask_price) / 2
## implied volatility
iv = snapshot.implied_volatility
## Greeks
delta = snapshot.greeks.delta
gamma = snapshot.greeks.gamma
theta = snapshot.greeks.theta
vega = snapshot.greeks.vega
return {
'option_price': option_price,
'expiration_date': expiration_date,
'remaining_days': remaining_days,
'iv': iv,
'delta': delta,
'gamma': gamma,
'theta': theta,
'vega': vega
}
This function ensures that the provided option_data
is in dictionary format. If option_data
is given in a different format (e.g. OptionContract
object), it converts it to a dictionary.
def ensure_dict(option_data):
"""
Convert option_data to a dict using model_dump() if available (for Pydantic models),
otherwise return the data as-is.
"""
if hasattr(option_data, "model_dump"):
return option_data.model_dump()
return option_data
The build_option_dict
function builds a comprehensive option dictionary by merging the raw option data with the calculated metrics from calculate_option_metrics
. It first converts the input to a dictionary using ensure_dict
, then obtains all necessary metrics (like option price, IV, delta, and vega). Finally, it constructs and returns a candidate dictionary that includes both the original option details (such as id, name, symbol, strike price, etc.) and the calculated metrics. This consolidated dictionary can be used for further processing or trading decisions.
def build_option_dict(option_data, underlying_price, risk_free_rate):
"""
Build an option dictionary by merging raw option data with calculated metrics.
"""
option_data = ensure_dict(option_data) # Convert to dict if necessary
metrics = calculate_option_metrics(option_data, underlying_price, risk_free_rate)
candidate = {
'id': option_data['id'],
'name': option_data['name'],
'symbol': option_data['symbol'],
'strike_price': option_data['strike_price'],
'root_symbol': option_data['root_symbol'],
'underlying_symbol': option_data['underlying_symbol'],
'underlying_asset_id': option_data['underlying_asset_id'],
'close_price': option_data['close_price'],
'close_price_date': option_data['close_price_date'],
'expiration_date': metrics['expiration_date'],
'remaining_days': metrics['remaining_days'],
'open_interest': option_data['open_interest'],
'open_interest_date': option_data['open_interest_date'],
'size': option_data['size'],
'status': option_data['status'],
'style': option_data['style'],
'tradable': option_data['tradable'],
'type': option_data['type'],
'initial_IV': metrics['iv'],
'initial_delta': metrics['delta'],
'initial_gamma': metrics['gamma'],
'initial_theta': metrics['theta'],
'initial_vega': metrics['vega'],
'initial_option_price': metrics['option_price'],
}
return candidate
The function check_candidate_option_conditions
checks if a candidate option meets the filtering criteria provided as a tuple (expiration range, IV range, delta range, theta range). It is used within the main workflow to determine whether a candidate qualifies for a specific leg of the bull put spread.
def check_candidate_option_conditions(candidate: Dict[str, Any], criteria: Tuple, label: str) -> bool:
"""
Check whether a candidate option meets the filtering criteria.
The criteria is a tuple of (expiration_range, iv_range, delta_range, theta_range).
Logs detailed information if a candidate fails a criterion.
"""
expiration_range, iv_range, delta_range, theta_range = criteria
if not (expiration_range[0] <= candidate['remaining_days'] <= expiration_range[1]):
logger.debug(f"{candidate['symbol']} fails expiration condition for {label}: remaining_days {candidate['remaining_days']} not in {expiration_range}.")
return False
if not (iv_range[0] <= candidate['initial_IV'] <= iv_range[1]):
logger.debug(f"{candidate['symbol']} fails IV condition for {label}: initial_IV {candidate['initial_IV']} not in {iv_range}.")
return False
if not (delta_range[0] <= candidate['initial_delta'] <= delta_range[1]):
logger.debug(f"{candidate['symbol']} fails delta condition for {label}: initial_delta {candidate['initial_delta']} not in {delta_range}.")
return False
if not (theta_range[0] <= candidate['initial_theta'] <= theta_range[1]):
logger.debug(f"{candidate['symbol']} fails theta condition for {label}: initial_theta {candidate['initial_theta']} not in {theta_range}.")
return False
return True
The pair_put_candidates
function identifies a valid bull put spread by pairing a long put and short put with the same expiration date. It ensures the long put’s strike is below the underlying price and the short put’s strike is above it, matching the typical setup for this strategy and the width between the short_put
's strike and the underlying price is smaller than that between the underlying price and the long_put
's strike. The first matching pair is returned, or None if no valid pair is found.
def pair_put_candidates(short_puts: List[Dict[str, Any]], long_puts: List[Dict[str, Any]], underlying_price: float) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
"""
For the bull put spread, require:
* long_put strike <= underlying_price < short_put strike
* width between the short_put's strike and the underlying price is smaller than that between the underlying price and the long_put's strike..
Returns the first valid pair found.
"""
for sp in short_puts:
for lp in long_puts:
if sp['expiration_date'] == lp['expiration_date'] and lp['strike_price'] <= underlying_price < sp['strike_price'] and (sp['strike_price'] - underlying_price) < (underlying_price - lp['strike_price']):
logger.info(f"Selected Bull put spread: short_put {sp['symbol']} and long_put {lp['symbol']} with expiration {sp['expiration_date']}.")
return sp, lp
# If no valid pair is found, log the expiration date (if available) from the candidate lists.
expiration_info = None
if short_puts:
expiration_info = short_puts[0]['expiration_date']
elif long_puts:
expiration_info = long_puts[0]['expiration_date']
if expiration_info:
logger.info(f"No valid bull put spread pair found for expiration {expiration_info} with the given candidates and underlying price conditions.")
else:
logger.info("No valid bull put spread pair found: no candidate data available.")
return None, None
This check_buying_power
function calculates the theoretical maximum loss (risk) of a bull put spread and ensures it does not exceed the buying power limit. It compares the difference between the short put’s price and the long put’s price, subtracted by the initial credit and multiplied by the contract size. If the cost is too high, it raises an exception to prevent the trade.
def check_buying_power(short_put: Dict[str, Any], long_put: Dict[str, Any], buying_power_limit: float) -> None:
"""
Calculates the width of the spread minus the credit received for a bull put spread and checks it against the buying power limit.
If the buying power requirement is not met, the exception is thrown and the rest of the code is never executed.
"""
option_size = float(short_put['size'])
premium_received = short_put['initial_option_price'] - long_put['initial_option_price']
spread_width = short_put['strike_price'] - long_put['strike_price']
risk = (spread_width - premium_received) * option_size
logger.info(f"Calculated bull put spread risk: {risk}.")
if risk >= buying_power_limit:
raise Exception('Buying power limit exceeded for a bull put spread risk.')
The find_options_for_bull_put_spread
function coordinates the process of finding and validating a bull put spread. It filters put options based on criteria, groups potential long and short candidates by expiration, and attempts to pair them into valid spreads. If a suitable pair is found, it checks the spread’s buying power requirement before returning the first valid pair. If no valid spread is found, it returns an empty list.
def find_options_for_bull_put_spread(put_options, underlying_price, risk_free_rate, buying_power_limit, criteria, OI_THRESHOLD):
"""
Orchestrates the workflow to build a bull put spread.
Groups options by expiration, filters them with criteria, pairs candidates using helper functions,
and checks buying power.
Returns:
A list of legs [short_put, long_put] if a valid pair is found, or an empty list otherwise.
"""
short_put_candidates_by_exp: Dict[pd.Timestamp, List[Dict[str, Any]]] = {}
long_put_candidates_by_exp: Dict[pd.Timestamp, List[Dict[str, Any]]] = {}
# Process each option candidate
for option_data in put_options:
if not validate_sufficient_OI(option_data, OI_THRESHOLD):
logger.warning(f"Insufficient open interest for option {getattr(option_data, 'symbol', 'unknown')} (threshold: {OI_THRESHOLD}). Skipping candidate.")
continue
candidate = build_option_dict(option_data, underlying_price, risk_free_rate)
expiration_date = candidate['expiration_date']
short_put_candidates_by_exp.setdefault(expiration_date, [])
long_put_candidates_by_exp.setdefault(expiration_date, [])
# Check each candidate for both put criteria
if check_candidate_option_conditions(candidate, criteria['short_put'], 'short_put'):
short_put_candidates_by_exp[expiration_date].append(candidate)
logger.info(f"Added {candidate['symbol']} as a short put candidate for expiration {expiration_date}.")
if check_candidate_option_conditions(candidate, criteria['long_put'], 'long_put'):
long_put_candidates_by_exp[expiration_date].append(candidate)
logger.info(f"Added {candidate['symbol']} as a long put candidate for expiration {expiration_date}.")
# Process only expiration dates common to both candidate groups
common_expirations = set(short_put_candidates_by_exp.keys()) & set(long_put_candidates_by_exp.keys())
for expiration_date in common_expirations:
sp, lp = pair_put_candidates(short_put_candidates_by_exp[expiration_date],
long_put_candidates_by_exp[expiration_date],
underlying_price)
if sp and lp:
try:
check_buying_power(sp, lp, buying_power_limit)
except Exception as e:
logger.error(f"Pair for expiration {expiration_date} failed buying power check: {e}")
continue
logger.info(f"Selected bull put spread for expiration {expiration_date}: short {sp['symbol']}, long {lp['symbol']}.")
return [sp, lp]
logger.info("No valid bull put spread found.")
return [None, None]
We now can run the get_options
function and the find_options_for_bull_put_spread
function to find a reasonable set of options for a bull put spread.
put_options = get_options(underlying_symbol, min_strike, max_strike, min_expiration, max_expiration, ContractType.PUT)
sp, lp = find_options_for_bull_put_spread(put_options, underlying_price, risk_free_rate, buying_power_limit, criteria, OI_THRESHOLD)
We can check the selected options and potential credit by the bull put spread.
# Check spread width
print(f"The width for the bull put spread (initial premium collected): {sp['initial_option_price'] - lp['initial_option_price']}; the initial net delta: {abs(sp['initial_delta']) - lp['initial_delta']}; the initial IV: ")
Step5: Execute a Bull Put Spread Strategy
The script below builds the list of options for the bull put spread and places an order.
def place_bull_put_spread_order(short_put, long_put):
"""
Place a bull put spread order if both short_put and long_put data are provided.
"""
if not (short_put and long_put):
logger.info("No valid bull put spread found.")
return None
try:
# Build order legs: sell the short put and buy the long put.
order_legs = [
OptionLegRequest(
symbol=short_put['symbol'],
side=OrderSide.SELL,
ratio_qty=1
),
OptionLegRequest(
symbol=long_put['symbol'],
side=OrderSide.BUY,
ratio_qty=1
)
]
# Create a market order for a multi-leg (spread) order.
req = MarketOrderRequest(
qty=1,
order_class=OrderClass.MLEG,
time_in_force=TimeInForce.DAY,
legs=order_legs
)
res = trade_client.submit_order(req)
logger.info("A bull put spread order placed successfully.")
return res
except Exception as e:
logger.error(f"Failed to place a bull put spread order: {e}")
return None
We then run the function as shown below.
res = place_bull_put_spread_order(sc, lc)
res
Before the order is filled, traders may choose to cancel it. Please note that individual legs cannot be canceled or replaced. We must cancel the entire order. If you are not familiar with the level 3 option trading with multi-legs, please check out an example code for multi-leg option trading on Alpaca’s Github page.
# Query by the order's id
q1 = trade_client.get_order_by_client_id(res.client_order_id)
# Replace overall order
if q1.status != OrderStatus.FILLED:
# Cancel the whole order
trade_client.cancel_order_by_id(res.id)
print(f"Canceled order: {res}")
else:
print("Order is already filled.")
Step 6: How to Adjust or Exit a Bull Put Spread (Rolling and Rinsing)
The roll_rinse_bull_put_spread
function automates the decision-making process for managing an existing bull put spread. It first gathers current market information—such as the underlying asset’s price and the updated metrics (price, delta, and implied volatility) for both the short and long put options—to assess whether exit conditions have been met.
Here’s how it works:
- Initial Calculations:
- It retrieves the current underlying price and computes the initial net credit of the spread (difference between the initial prices of the short and long puts).
- It calculates the current cost to close the spread and determines a target exit price based on a specified profit percentage.
- Risk Metrics Evaluation:
- The function computes key option Greeks and volatility using the
calculate_option_metrics
function. - It sums the absolute delta values of both options to assess the overall risk exposure.
- It also extracts the short put's implied volatility for further risk evaluation.
- The function computes key option Greeks and volatility using the
- Exit Criteria Determination:The function defines several exit triggers:
- Underlying Price Trigger: If the underlying price falls at or below the long put’s strike price.
- Profit Target: If the cost to close the spread falls below the target price (indicating sufficient profit).
- Delta Threshold: If the combined net delta exceeds a predetermined threshold.
- Implied Volatility Threshold: If the short put’s implied volatility surpasses a set limit.
- Action Execution:
- Exiting (Rinsing): If any exit conditions are met, the function proceeds to close both legs of the spread.
- Rolling: If the rolling flag is enabled, after closing the spread it fetches the latest put options data and searches for new bull put spread candidates. If new candidates are identified, it attempts to open a new bull put spread order.
- Holding: If none of the exit criteria are satisfied, it logs that the position is being held.
- Return Value:
- The function returns a tuple with a status message and, if a new spread is successfully rolled into, the new spread’s data; otherwise, it returns None for the spread data.
def roll_rinse_bull_put_spread(short_put, long_put, rolling, target_profit_percentage, delta_stop_loss_thres, iv_stop_loss_thres, option_type, criteria, risk_free_rate, min_strike, max_strike, min_expiration, max_expiration, buying_power_limit, OI_THRESHOLD):
"""
Checks if a bull put spread meets exit criteria (profit or stop-loss levels)
based on current option price, delta, and IV. If criteria are met, it closes the
spread and, if rolling=True, attempts to open a new one.
Returns:
Tuple: (status message, new spread data if rolled, otherwise None)
"""
underlying_symbol = short_put['underlying_symbol']
underlying_price = get_underlying_price(underlying_symbol)
# Calculate initial premium and current cost to close the spread.
initial_credit = short_put['initial_option_price'] - long_put['initial_option_price']
metrics_sp = calculate_option_metrics(short_put, underlying_price, risk_free_rate)
metrics_lp = calculate_option_metrics(long_put, underlying_price, risk_free_rate)
cost_to_close = metrics_sp['option_price'] - metrics_lp['option_price']
target_price = initial_credit * (1 - target_profit_percentage)
# Compute current risk metrics (current option Greeks and IV).
current_net_delta = abs(metrics_sp['delta']) + metrics_lp['delta']
current_sp_IV = metrics_sp['iv']
# Define exit conditions.
exit_due_to_underlying = underlying_price <= long_put['strike_price']
exit_due_to_profit = cost_to_close <= target_price
exit_due_to_delta = current_net_delta >= delta_stop_loss_thres
exit_due_to_iv = current_sp_IV >= iv_stop_loss_thres
# Check exit criteria: either the short put price is at or below the target,
# the absolute delta exceeds the threshold, or IV is above the threshold.
if exit_due_to_underlying or exit_due_to_profit or exit_due_to_delta or exit_due_to_iv:
logger.info(
f"Exit criteria met for the underlying: {underlying_symbol}: underlying price={underlying_price}, cost to close the position={cost_to_close}, "
f"target price={target_price}, current net delta of the spread={current_net_delta}, current short put's IV={current_sp_IV}"
)
# Execute the roll or rinse (exit) of the spread
try:
# Close the short put
trade_client.close_position(
symbol_or_asset_id=short_put['symbol'],
close_options=ClosePositionRequest(qty='1')
)
logger.info(f"Closed short put: {short_put['symbol']}")
# Close the long put
trade_client.close_position(
symbol_or_asset_id=long_put['symbol'],
close_options=ClosePositionRequest(qty='1')
)
logger.info(f"Closed long put: {long_put['symbol']}")
except Exception as e:
msg = f"Failed to close existing bull put spread on {underlying_symbol}: {e}"
logger.error(msg)
return msg, None
# If rolling, attempt to open a new bull put spread
if rolling:
try:
# Find latest put options
put_options = get_options(underlying_symbol, min_strike, max_strike, min_expiration, max_expiration, option_type)
# Find new bull put spread candidates
sp, lp = find_options_for_bull_put_spread(put_options, underlying_price, risk_free_rate, buying_power_limit, criteria, OI_THRESHOLD)
# Place a new bull put spread order and return the response message
res = place_bull_put_spread_order(sp, lp)
if res:
new_spread = {
'short_put': sp,
'long_put': lp
}
return f"Rolled bull put spread on {underlying_symbol}. {res}", new_spread
else:
msg = f"Failed to open new bull put spread on {underlying_symbol} after closing."
logger.error(msg)
return msg, None
except Exception as e:
msg = f"Failed to roll into a new bull put spread on {underlying_symbol}: {e}"
logger.error(msg)
return msg, None
else:
# If not rolling, simply exit the position
return f"Closed (rinsed) bull put spread on {underlying_symbol}.", None
else:
# Criteria not met; hold the position.
msg = (f"Holding bull put spread on the underlying: {underlying_symbol}: underlying price={underlying_price}, initial credit={initial_credit}, cost to close the position={cost_to_close},"
f"target_price={target_price}, current net delta={current_net_delta}, current net IV={current_sp_IV}.")
logger.info(msg)
return msg, None
The Importance of Backtesting A Bull Put Spread Strategy
Backtesting your options strategy is a critical step in developing a robust bull put spread strategy, especially when implementing it in an algorithmic trading system. Since a bull put spread’s profitability depends on moderate price appreciation and factors like time decay (theta) and implied volatility shifts (vega), backtesting helps traders evaluate how the spread would have performed under historical market conditions.
With Alpaca’s Trading API, we can not only backtest using an underlying asset’s historical data but also extract historical option data. The script below retrieves historical option data. However, accessing the latest OPRA data may require an 'Algo Trader Plus' subscription. Meanwhile, the free 'Basic' account typically provides the necessary historical data.
# Initialize the Option Historical Data Client
option_historical_data_client = OptionHistoricalDataClient(
api_key=API_KEY,
secret_key=API_SECRET,
url_override=BASE_URL
)
# Define the request parameters
req = OptionBarsRequest(
symbol_or_symbols=sp["symbol"], # the short put option
timeframe=TimeFrame.Day, # Choose timeframe (Minute, Hour, Day, etc.)
start="2025-03-01", # Start date
end="2025-04-14" # End date
)
option_historical_data_client.get_option_bars(req)
Trading A Bull Put Spread on Alpaca’s Dashboard
We can also execute a bull put spread using Alpaca’s dashboard manually. Please note we are using JNJ as an example and it should not be considered investment advice.
Step 1. Log in to your account
Sign in to your account and navigate to the “Home” page.
Step 2. Find your desired trade
On your dashboard in the top left, we can select between using your paper trading or live accounts. It’s important that we select the appropriate trading account before implementing an options strategy. If we want to practice without the risk of real capital, we can use your paper trading accounts. If we are looking to implement strategies, we can use your live account.
Once you’ve selected the desired account within your dashboard, we can monitor your portfolio, access market data, execute trades, and review your trading history. In order to create your first options trade, use the search bar to find and select the applicable option ticker symbol.

Step 3. Check underlying security’s bar chart
If we search the desired underlying asset, a page shows the asset’s bar chart. We can use this bar chart for a simple analysis.

Step 4. Opening an options position
Assume we have already analyzed the market and determined the strike price for the two legs as follows: long put option to be $155.00 and short put to be $165.00 at the expiration date of April 17, 2025. Once determined, select and review your order type (market or limit) and decide on the number of contracts you’d like to trade. We can select up to four legs but in the bull put spread we only need two.
On the asset’s page, toggle from “Stocks” to “Options” and determine your desired contract type between calls and puts. We can also adjust the expiration date from the dropdown, ranging anywhere from 0DTE options to contracts hundreds of days in the future.
A bull put spread, for example, requires longing OTM put options and shorting OTM put options. Therefore, we can establish your position in the dashboard by selecting one long put option with "Buy To Open” and one short put option with “Sell To Open” to purchase the options contracts.
Please note: The order in which we select the options does not matter. Additionally, on Alpaca’s dashboard and Trading API, credits are displayed as negative values, while debits appear as positive values. For example, in the image below, a credit spread (where your initial entry obtains premiums) is shown with a negative limit price (- $3.27 in this case) under 'Limit Price (Credit),' reflecting a total credit of $326.92 (with an extra 8 cents for the OCC Clearing Fee and Options Regulatory Fee).
Once selected, review your order type (such as market, limit, stop, stop limit, or trailing stop) and the number of contracts you’d like to trade. Click the “Confirm Order” button on the bottom right.

Your paper trading account will then mimic the execution of the order as though it were a real transaction.
Optional: Cancel your order
Keep in mind that with a multi-leg options order, we cannot cancel or replace an individual child leg. We can only cancel or replace the entire multi-leg options order. To do this, navigate to the 'Order' section, select the order we want to cancel, and click the 'Cancel 1 selected' button.

You’ll then be prompted to confirm.

Step 5. Closing your position
Closing an options position involves selling the options contract that we previously bought (in a long position) or buying back the options contract that we previously sold (in a short position). Either action terminates, or “closes”, your associated position.
If you’d like to close your position, find the desired options contract on your “Home” page or under “Positions”. Click on the appropriate contract symbol and select “Liquidate 2 selected” to close your bull put spread position.

We will, then, click the “Confirm” button to liquidate the position.

Conclusion
The bull put spread is a structured options strategy that allows traders to potentially generate profit while maintaining defined risk in neutral to moderately bullish markets. By simultaneously selling a higher strike put and buying a lower strike put, traders receive a net credit, benefiting from time decay and a stable price outlook. This strategy can be particularly useful when traders expect the underlying asset to stay above the short put’s strike price until expiration.
As part of a balanced trading strategy, the bull put spread offers limited profit potential but also controlled downside risk. Traders should carefully evaluate implied volatility, the impact of time decay (theta), and how their chosen strike prices align with expected market movements. Understanding these factors helps in optimizing trade execution and risk management.
For algorithmic traders, the bull put spread can be efficiently executed using Alpaca’s Trading API, offering a systematic approach to options trading with risk-defined parameters. However, automation should include risk controls such as volatility adjustments, position monitoring, and exit strategies to mitigate potential losses. Early assignment risks and shifting market conditions should also be factored into automated systems.
We hope this guide has provided valuable insights into the bull put spread strategy and its practical applications. As you refine your approach, we encourage you to engage with the trading community, share insights, and continue expanding your knowledge with our options trading resources.
For those looking to integrate options trading with Alpaca’s Trading API, here are some additional resources:
- Sign up for an Alpaca Trading API account
- How to Connect to Alpaca's Trading API
- How to Start Paper Trading with Alpaca's Trading API
- How to Trade Options with Alpaca’s Trading API
- Learn more credit spreads
- Read more Options Trading Tutorials
- Documentation: Options Trading
FAQs
Is a bull put spread risky?
A bull put spread has a defined risk, meaning the maximum potential loss is known upfront. The risk is limited to the difference between the strike prices minus the net credit received. However, like all options strategies, it carries market risk, including price fluctuations, volatility changes, and early assignment risk on the short put. Proper risk management, such as setting exit plans and monitoring market conditions, can help mitigate these risks.
What is the delta of a bull put spread?
The delta of a bull put spread is typically positive, meaning the position benefits from a stable or rising underlying asset price. Since the short put has a higher absolute delta than the long put, the net delta of the spread is positive but lower than holding a single put contract. As expiration approaches, delta will shift depending on whether the trade is in-the-money or out-of-the-money.
Should I let my bull put spread expire?
It depends on the market conditions and risk tolerance. If the underlying asset is trading well above the short puts’ strike price close to expiration, letting the spread expire worthless may maximize potential profit since both options will expire out-of-the-money. However, if the stock is near the short put’s strike price, there is a risk of assignment. Some traders close the spread early to lock in profits or limit potential losses before expiration.
What is the difference between a bull put spread and a bear put spread?
A bull put spread is a credit spread used in neutral to moderately bullish markets. It involves selling a higher strike put and buying a lower strike put, potentially profiting from time decay and a stable or rising price. In contrast, a bear put spread is a debit spread used in bearish markets, where a trader buys a higher strike put and sells a lower strike put, profiting when the underlying asset declines below the breakeven point.
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.
Past hypothetical backtest results do not guarantee future returns, and actual results may vary from the analysis.
The Paper Trading API is offered by AlpacaDB, Inc. and does not require real money or permit a user to transact in real securities 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 this article is for general informational purposes only and is believed to be accurate as of the posting date but may be subject to change. The examples above are for illustrative purposes only.
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 ensure 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.
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.