Build an AI Options Income Bot

Use Claude AI and Alpaca to analyze and trade cash-secured puts automatically.

Introduction

30 Second Summary

Options trading is one of the ways professional investors generate consistent income from the stock market. What if you could have an AI assistant that analyzes hundreds of potential trades and recommends the best one for you?

In this project, you will build a Python bot that connects to a real brokerage, fetches live options data, and uses Claude AI to pick the smartest income trade to execute automatically.

What You'll Build

A command-line Python bot that fetches real options chain data from Alpaca, sends it to Claude Haiku 4.5 for analysis, and automatically executes the recommended cash-secured put trade on a paper trading account.

By the end of this project, you'll have:

  • A working Python bot that connects to Alpaca's brokerage API and fetches live options data with Greeks like Delta and Theta.
  • An AI analysis engine powered by Claude that evaluates risk and reward, then recommends the best cash-secured put to sell for income.
  • A trade executor that places orders automatically on your paper trading account with $100,000 in virtual cash.
  • Secret Mission: Build a portfolio monitor that uses Claude to decide when to close positions for profit.

Are there any prerequisites?

No prior experience with Python, trading, or APIs is required. This guide walks you through installing every tool from scratch, including Python, your code editor, and your brokerage account.

Install Python and Set Up Your Code Editor

Before you can build the trading bot, you need the right tools on your machine. Python is the programming language your bot runs on. Cursor is a code editor with built-in AI assistance that helps you write and understand code as you go.

In this step, you will install both tools and create an isolated workspace for the project. This workspace keeps your bot's files and dependencies separate from everything else on your Mac.

In this step, get ready to:
  • Install Python 3.13 and verify it works in Terminal.
  • Install the Cursor code editor.
  • Create your project folder and a virtual environment.
Download and install Python 3.13

Python is the programming language your trading bot is written in. You need version 3.13 specifically because the packages this project uses are tested against it.

  • Open your web browser and go to https://www.python.org/downloads/macos/.
  • Find the latest Python 3.13 release and click Download macOS 64-bit universal2 installer.
  • Once the .pkg file finishes downloading, double-click it to open the installer.
  • Click through each step of the installer, accepting the default settings.
  • When the installer finishes, open Finder.
  • Navigate to /Applications/Python 3.13/.
  • Double-click Install Certificates.command.

What does Install Certificates.command do?

This script installs SSL root certificates so Python can make secure HTTPS connections. Without it, your bot would fail when trying to connect to the Alpaca and Claude APIs.

A Terminal window will briefly open, show some output, and close automatically. That means it worked.

Now verify that Python installed correctly.

  • Press Cmd+Space to open Spotlight.
  • Type Terminal and press Enter to open it.
  • Check your Python version by running this command:
python3 --version

✔️ I see Python 3.13

You should see something like Python 3.13.13 (the last number may differ). Python is installed and ready.

ⓧ I see an older version

If you see a version like 3.9 or 3.12, your Mac is resolving to an older Python install.

  • Try running the versioned command to confirm the new install exists:
python3.13 --version

If that prints Python 3.13.x, the install worked but your system PATH resolves the older version first. For this project, use python3.13 wherever you see python3 in later instructions.

If the command is not found, go back and re-run the installer from python.org, making sure to accept all defaults.

ⓧ Command not found

If you see an error saying the command is not recognized, the installer may not have completed successfully.

  • Close Terminal completely and reopen it (the PATH update requires a fresh Terminal session).
  • Run python3 --version again.
  • If it still fails, re-download and re-run the installer from https://www.python.org/downloads/macos/.
Download and install Cursor

Cursor is an AI-powered code editor built on the same foundation as VS Code. It will help you understand the bot's code as you write it.

  • Open your browser and go to https://www.cursor.com/.
  • Click Download for macOS.
  • Once the download finishes, open the .dmg file.
  • Drag the Cursor app into your Applications folder.
  • Press Cmd+Space to open Spotlight.
  • Type Cursor and press Enter to launch it.
  • If macOS asks whether you want to open an app downloaded from the internet, click Open.

What about the sign-up prompts?

When Cursor opens for the first time, it may ask you to sign in or create an account. Follow the on-screen prompts to set up a free account. You can skip any optional configuration for now.

Cursor won't open?

If you see a message saying the app is damaged, open Terminal and run:

xattr -cr /Applications/Cursor.app

Then try launching Cursor again from Spotlight. This removes a macOS quarantine flag that sometimes blocks newly downloaded apps.

Still stuck? Help me fix Cursor not opening on macOS.

Create your project folder and virtual environment

A virtual environment is an isolated space where Python installs packages just for this project. It prevents conflicts with other Python projects on your machine.

  • In your Terminal window, create the project folder and move into it by running these commands:
mkdir ~/options-bot && cd ~/options-bot
  • Create and activate a virtual environment by running:
python3 -m venv .venv && source .venv/bin/activate

You should see (.venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

What does (.venv) mean?

The (.venv) prefix tells you that any pip install commands you run will install packages inside the .venv folder, not globally on your system.

Every time you open a new Terminal session for this project, you will need to reactivate it by running source .venv/bin/activate from inside the ~/options-bot folder.

Don't see (.venv) in your prompt?

Make sure you are inside the ~/options-bot folder before activating. Run cd ~/options-bot first, then run source .venv/bin/activate again.

If you get an error that the .venv directory does not exist, the previous python3 -m venv .venv command may have failed. Re-run it and check for error messages.

Need more help? Help me activate my Python virtual environment.

Your development environment is ready. Next up, you will create your Alpaca trading account and get the API keys that let your bot place trades.

Sign Up for Alpaca and Get Your API Keys

Your bot needs a way to talk to a real brokerage. It needs to fetch live options data and place trades on your behalf.

That connection happens through API keys, which work like a username and password for your code. In this step, you'll create a free Alpaca account and generate paper trading API keys. Paper trading simulates real market conditions with $100,000 in virtual cash, so you can test your bot without any financial risk.

In this step, get ready to:
  • Create your free Alpaca account with email verification and MFA.
  • Generate your Paper Trading API key and secret key.
  • Explore the optional Live Trading application.
Create your Alpaca account

Alpaca is a commission-free brokerage built for developers. It provides a Python SDK and a paper trading environment that your bot will use to fetch options data and execute trades.

  • Open your browser and go to https://app.alpaca.markets/signup.
  • Enter your email address and create a password.
  • Click Sign Up.
  • Check your email inbox for a verification message from Alpaca.
  • Click the confirmation link in the email to verify your account.

After confirming your email, Alpaca will prompt you to activate Multi-Factor Authentication (MFA). This is required by Alpaca for security.

  • Follow the on-screen instructions to set up MFA using an authenticator app (like Google Authenticator or your phone's built-in authenticator).
  • Enter the code from your authenticator app to complete the MFA setup.

What is MFA?

Multi-Factor Authentication adds a second layer of security beyond your password. Even if someone guesses your password, they cannot log in without the code from your authenticator app.

Since this account can eventually trade with real money, Alpaca requires MFA on every account.

Once MFA is activated, you will land on your Paper Trading dashboard. You should see a portfolio value of $100,000 in virtual cash.

Stuck on the sign-up flow?

Make sure you check your spam or junk folder for the verification email. If MFA setup fails, ensure your authenticator app's time is synced correctly (go to your phone's date/time settings and enable automatic time).

Ask AI for help: I'm having trouble creating my Alpaca account or setting up MFA. What should I check?

Generate your Paper Trading API keys

API keys are what let your Python bot authenticate with Alpaca's servers. You need two values: an API key (like a username) and a secret key (like a password).

  • On your Paper Trading dashboard, find the API Keys section on the right sidebar.
  • Click Generate New Keys.
  • You will see both your API Key and your Secret Key displayed on screen.
  • Copy both values and paste them into a temporary note or text file on your computer.

Your Secret Key is only shown once

Once you navigate away from the page or refresh, your Secret Key disappears permanently. Make sure you copy it now. If you lose it, you will need to regenerate a new key pair (which invalidates the old one).

Why two keys?

The API key identifies your account. The secret key proves you are the account owner. Together, they let your bot place trades and read market data without needing your email and password.

Your paper trading keys are completely separate from any live trading keys. They only work with the paper (simulated) environment.

(Optional) Apply for live trading

Paper trading is all you need for this project. However, if you want the option to trade with real money later, you can start the Live Trading application now. Approval takes 1-3 business days.

  • In the top-left of your dashboard, look for the option to switch between paper and live accounts.
  • Click Live Account to begin the application.
  • You will need a government-issued ID and a selfie for identity verification.
  • There is no minimum deposit required to open a live account.

Do I need a live account for this project?

No. Everything in this project runs on paper trading. Your bot will work identically on both environments. The only difference when switching to live is changing one line of code and using your live API keys.

Your Alpaca account is ready and your API keys are saved. Next up, you'll grab your Anthropic API key so your bot can talk to Claude AI.

Get Your Anthropic API Key

Your Alpaca account is ready to deliver real market data and execute trades. Now your bot needs something to actually make sense of all that options data.

Claude is Anthropic's AI model that will analyze options chains and recommend the best trades. In this step, you'll create a developer account on Anthropic's Console and generate an API key so your bot can call Claude programmatically.

In this step, get ready to:
  • Create an Anthropic Console account.
  • Generate and save an API key for your bot.
Create your Anthropic Console account

The Anthropic Console is the developer dashboard where you manage API keys and billing. This is separate from the claude.ai chat interface.

  • Go to https://console.anthropic.com/ in your browser.
  • Click Sign Up and create your account using your email address.
  • Complete the email verification when you receive the confirmation message.

Why a separate account from claude.ai?

The claude.ai website is for chatting with Claude in a browser. The Anthropic Console at console.anthropic.com is the developer dashboard for programmatic access.

Your bot needs the Console because it calls Claude through code, not through a chat window.

Generate your API key

An API key is a secret credential that lets your Python bot authenticate with Claude's servers. Without it, Anthropic has no way to know the request is coming from you.

  • Once logged into the Console, click API Keys in the left sidebar.
  • Click Create Key.
  • Enter options-bot as the key name.
  • Copy the key immediately and save it somewhere safe (a password manager or a temporary note).
  • Close the creation dialog after copying.

Copy your key now

Anthropic shows your full API key only once, at the moment of creation. After you close the dialog, the key is masked and cannot be retrieved. If you lose it, you will need to generate a new one.

How much does this cost?

Claude Haiku 4.5 costs $1 per million input tokens and $5 per million output tokens. Your bot sends a small prompt (roughly 500-1000 tokens) and receives a short JSON response each run.

That means each bot run costs approximately $0.01-$0.03. You are charged pay-as-you-go, so there is no monthly subscription or upfront fee.

Don't see the API Keys option?

Make sure you are on console.anthropic.com and not claude.ai. The two sites have different navigation layouts.

If you just signed up, try refreshing the page or logging out and back in. Help me find the API Keys section in the Anthropic Console

You now have both halves of the puzzle: Alpaca for market data and trading, and Claude for AI-powered analysis. Next up, you'll install your Python packages and wire these API keys into your project securely.

Install Python Packages and Configure Environment

You've got all your accounts and API keys ready. Now it's time to bring everything together inside your project folder.

In this step, you'll install the Python packages your bot depends on and create configuration files that keep your credentials secure. Once this is done, your project will be fully wired up and ready for code.

In this step, get ready to:
  • Install the alpaca-py, anthropic, and python-dotenv packages.
  • Create a .env file to securely store your API keys.
  • Create a .gitignore file to protect your secrets from version control.
Install the required Python packages

Your bot needs three packages to function. Each one handles a different responsibility: connecting to Alpaca, talking to Claude, and loading your credentials safely.

  • Install all three packages by running this command in your Terminal (make sure you still see (.venv) at the start of your prompt):
pip install alpaca-py anthropic python-dotenv

What do these packages do?

  • alpaca-py is the official Alpaca SDK. It lets your bot place trades and fetch options data.
  • anthropic is the official Anthropic SDK. It lets your bot send prompts to Claude and receive AI-powered analysis.
  • python-dotenv loads variables from a .env file into your program's environment. This keeps your API keys out of your code.
  • Wait for the installation to finish. You should see a Successfully installed message listing all the packages and their dependencies.

Seeing an error during installation?

  • Make sure your virtual environment is active. You should see (.venv) at the start of your terminal prompt. If not, run source .venv/bin/activate again.
  • Check that you're in the ~/options-bot folder. Run pwd to confirm your location.
  • If you see a permissions error, make sure you're using the pip inside your virtual environment, not the system one.

Why won't my packages install?

Create the environment file for your API keys

Your bot needs your Alpaca and Anthropic API keys to connect to their services. Instead of pasting keys directly into your code (which is dangerous), you'll store them in a separate .env file.

  • Switch to Cursor.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, then into the options-bot folder, and click Open.

You should see the options-bot folder name in the Cursor sidebar, with the .venv folder visible inside it.

  • Create a new file by clicking the new file icon at the top of the Explorer sidebar (or press Cmd+N).
  • Save it immediately with Cmd+S and name it .env (including the dot at the start).
  • Paste the following content into your .env file, replacing each placeholder with your actual keys:
ALPACA_API_KEY=your-alpaca-api-key-here
ALPACA_SECRET_KEY=your-alpaca-secret-key-here
ANTHROPIC_API_KEY=your-anthropic-api-key-here

What is happening in this file?

Each line defines an environment variable as a key-value pair. When your bot runs, python-dotenv reads this file and makes these values available in your code through os.environ.

This approach keeps your secrets separate from your code. You can share your code with others without ever exposing your keys.

  • Replace your-alpaca-api-key-here with the Alpaca API key you saved in Step 2.
  • Replace your-alpaca-secret-key-here with the Alpaca secret key you saved in Step 2.
  • Replace your-anthropic-api-key-here with the Anthropic API key you saved in Step 3.
  • Save the file with Cmd+S.

Not sure where your keys are?

Your Alpaca keys are in the API Keys panel on your Paper Trading dashboard at app.alpaca.markets. Your Anthropic key is at console.anthropic.com under API Keys in the left sidebar.

Help me find my API keys.

✔️ Awesome, I've got everything!

Great. Double check you've saved your .env file with all three keys filled in.

ⓧ I'd like to double check the full code

Here is the complete .env file. Make sure yours matches this structure (with your real keys replacing the placeholders):

ALPACA_API_KEY=your-alpaca-api-key-here
ALPACA_SECRET_KEY=your-alpaca-secret-key-here
ANTHROPIC_API_KEY=your-anthropic-api-key-here
Create a .gitignore to protect your secrets

If you ever push this project to GitHub, you need to make sure your .env file never gets uploaded. A .gitignore file tells Git which files to completely ignore.

  • Create another new file in Cursor (click the new file icon or press Cmd+N).
  • Save it with Cmd+S and name it .gitignore.
  • Paste the following content into your .gitignore file:
.env
.venv/
__pycache__/

What does each line exclude?

  • .env keeps your API keys private.
  • .venv/ excludes the virtual environment folder (it's large and can be recreated with pip install).
  • __pycache__/ excludes Python's auto-generated bytecode files. These are machine-specific and don't belong in version control.
  • Save the file with Cmd+S.
  • Check the Explorer sidebar in Cursor. You should see both .env and .gitignore listed alongside the .venv folder.

Don't see your files in the sidebar?

Make sure you opened the options-bot folder (not a parent folder) in Cursor. Also confirm you saved both files (unsaved files show a dot on their tab). If files starting with a dot are hidden, click the filter icon in the Explorer sidebar and make sure hidden files are visible.

Why aren't my files showing up in Cursor?

✔️ Awesome, I've got everything!

Great. Double check you've saved your .gitignore file.

ⓧ I'd like to double check the full code

Here is the complete .gitignore file:

.env
.venv/
__pycache__/

Your project is fully configured. Next up, you'll write the code that connects to Alpaca and fetches live options data for the stock of your choice.

Build the Options Data Fetcher

Your environment is wired up with all the packages and API credentials your bot needs. Now it is time to write the code that actually talks to Alpaca and pulls real options market data.

Before your bot can recommend trades, it needs to fetch the available put option contracts for a given stock, get live price quotes, and narrow the list to realistic candidates worth analyzing. That is exactly what you will build in this step.

In this step, get ready to:
  • Create trading_bot.py with imports and API client connections.
  • Write functions that fetch and filter put option contracts from Alpaca.
  • Run the bot and confirm it returns live options data.
Create the bot file and connect to Alpaca

Your bot lives in a single Python file that imports three SDKs, loads your API keys from .env, and initializes the client objects that talk to Alpaca and Claude.

  • In Cursor, make sure you have the options-bot folder open (the one you created earlier on your Desktop).
  • Create a new file called trading_bot.py in the root of the options-bot folder.
  • Paste the following imports and client setup code into trading_bot.py:
import os
import json
from datetime import datetime, timedelta
from dotenv import load_dotenv
from anthropic import Anthropic
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import (
    GetOptionContractsRequest,
    LimitOrderRequest,
)
from alpaca.trading.enums import OrderSide, TimeInForce, AssetStatus, ContractType
from alpaca.data.historical.option import OptionHistoricalDataClient
from alpaca.data.requests import OptionLatestQuoteRequest

# Load environment variables from .env file
load_dotenv()

# Initialize clients
trade_client = TradingClient(
    api_key=os.environ["ALPACA_API_KEY"],
    secret_key=os.environ["ALPACA_SECRET_KEY"],
    paper=True,
)

option_data_client = OptionHistoricalDataClient(
    api_key=os.environ["ALPACA_API_KEY"],
    secret_key=os.environ["ALPACA_SECRET_KEY"],
)

anthropic_client = Anthropic()

What does this code do?

The first block imports everything your bot will need across all steps. You will not use every import right away, but having them ready avoids revisiting the top of the file later.

load_dotenv() reads your .env file and makes the keys available through os.environ. Then three clients are created: trade_client for placing orders and fetching contracts, option_data_client for retrieving options pricing data, and anthropic_client for calling Claude AI.

The paper=True flag tells the Alpaca client to use your paper trading account (simulated money) instead of a live brokerage account.

  • Save trading_bot.py.
  • Switch back to your Terminal (with (.venv) active) and run the script to check for import errors:
python3 trading_bot.py
  • The script should exit silently with no output and no errors. This confirms your packages are installed correctly and your API keys are loading from .env.

Seeing a ModuleNotFoundError?

Make sure your terminal prompt shows (.venv) at the beginning. If it does not, activate the virtual environment again by running source .venv/bin/activate from inside the options-bot folder. The packages are installed inside .venv, so Python cannot find them without it.

If you see a KeyError mentioning one of your API key names, double-check that your .env file is saved in the same options-bot folder (not a subfolder) and that it has no extra spaces around the = signs.

Help me fix my import or environment error.

Now add the two helper functions that fetch data from your Alpaca account. Paste this code below the anthropic_client = Anthropic() line, leaving one blank line between them:

def get_account_info():
    """Get current account balance and buying power."""
    account = trade_client.get_account()
    return {
        "cash": float(account.cash),
        "buying_power": float(account.buying_power),
        "portfolio_value": float(account.portfolio_value),
    }


def fetch_put_options(symbol, min_dte=20, max_dte=45):
    """Fetch put option contracts for a given stock symbol."""
    today = datetime.now()
    min_expiry = today + timedelta(days=min_dte)
    max_expiry = today + timedelta(days=max_dte)

    request = GetOptionContractsRequest(
        underlying_symbols=[symbol],
        status=AssetStatus.ACTIVE,
        expiration_date_gte=min_expiry.strftime("%Y-%m-%d"),
        expiration_date_lte=max_expiry.strftime("%Y-%m-%d"),
        type=ContractType.PUT,
        limit=100,
    )

    response = trade_client.get_option_contracts(request)
    return response.option_contracts if response.option_contracts else []

What does this code do?

get_account_info() asks Alpaca for your paper account balance. Your bot will use this later to calculate how much capital is available for a trade.

fetch_put_options() queries Alpaca for all active put contracts on a given stock that expire between 20 and 45 days from today. This range is the sweet spot for selling options for income because time decay (Theta) accelerates the most in the final 20-45 days before expiration.

  • Save trading_bot.py. You will not see a change yet because there is no __main__ block calling these functions. That comes in the next substep.
Add functions to get quotes and filter candidates

Having a list of contracts is not enough. Your bot also needs current bid/ask prices so it knows what premium each contract is worth. Then it filters out contracts with negligible pricing.

  • Below fetch_put_options(), add the following two functions that retrieve quotes and filter the list down to viable candidates:
def get_option_quotes(contracts):
    """Get latest quotes for a list of option contracts."""
    if not contracts:
        return {}

    symbols = [c.symbol for c in contracts[:20]]
    request = OptionLatestQuoteRequest(symbol_or_symbols=symbols)
    quotes = option_data_client.get_option_latest_quote(request)
    return quotes


def filter_candidates(contracts, quotes):
    """Filter options to find good cash-secured put candidates."""
    candidates = []

    for contract in contracts:
        if contract.symbol not in quotes:
            continue

        quote = quotes[contract.symbol]
        bid_price = float(quote.bid_price) if quote.bid_price else 0
        ask_price = float(quote.ask_price) if quote.ask_price else 0
        mid_price = (bid_price + ask_price) / 2

        if mid_price < 0.10:
            continue

        candidates.append({
            "symbol": contract.symbol,
            "strike_price": float(contract.strike_price),
            "expiration_date": str(contract.expiration_date),
            "underlying_symbol": contract.underlying_symbol,
            "bid_price": bid_price,
            "ask_price": ask_price,
            "mid_price": round(mid_price, 2),
        })

    return candidates

What does this code do?

get_option_quotes() fetches the latest bid and ask prices for up to 20 contracts at a time. These prices show what buyers and sellers are currently offering in the market.

filter_candidates() loops through the contracts and keeps only those with a mid-price (average of bid and ask) above $0.10. Contracts priced below that threshold are nearly worthless and not worth selling. The function outputs a clean list of dictionaries containing everything Claude will need to make a recommendation.

  • Save trading_bot.py. The next substep adds the test code that calls these functions.
Test the data fetcher

Time to prove your Alpaca connection works end to end. You will add a short test block at the bottom of the file that fetches options for AAPL and prints the results.

  • At the very bottom of trading_bot.py, add this test block:
if __name__ == "__main__":
    symbol = "AAPL"
    print(f"Fetching put options for {symbol}...")
    contracts = fetch_put_options(symbol)
    print(f"Found {len(contracts)} contracts")
    quotes = get_option_quotes(contracts)
    candidates = filter_candidates(contracts, quotes)
    print(f"Filtered to {len(candidates)} candidates:")
    for c in candidates[:5]:
        print(c)

What does this code do?

The if __name__ == "__main__": block only runs when you execute the file directly. It calls each function in sequence: fetch contracts, get quotes, filter candidates, and print the top 5 results.

You will replace this test block with the full bot logic in the next steps. For now it is a quick way to verify everything connects.

  • Save trading_bot.py.
  • Run the bot in your Terminal:
python3 trading_bot.py
  • You should see output similar to:
Fetching put options for AAPL...
Found 47 contracts
Filtered to 12 candidates:
{'symbol': 'AAPL260620P00180000', 'strike_price': 180.0, 'expiration_date': '2026-06-20', ...}

The exact numbers depend on current market conditions and what contracts are available. As long as you see contracts found and at least a few candidates printed, your data fetcher is working.

What if I see 0 candidates?

Options markets are only open during US regular trading hours: 9:30 AM to 4:00 PM Eastern Time, Monday through Friday. If you run the bot outside these hours, Alpaca may return contracts but the latest quotes may have zero bid/ask prices, causing all candidates to get filtered out.

Try running the bot again during market hours. You can also try a highly liquid symbol like SPY which tends to have more active quoting.

Seeing an authentication error?

If you see a 403 or authentication failure, your Alpaca API key or secret may be incorrect. Return to your Alpaca Paper Trading dashboard, regenerate a new key pair, and update the values in your .env file. Make sure you are using your Paper Trading keys (not live keys).

Help me fix my Alpaca authentication error.

✔️ Awesome, I've got everything!

Great. Double check you have saved trading_bot.py and it runs without errors.

ⓧ I'd like to double check the full code

Here is the complete trading_bot.py as it should look at the end of this step:

import os
import json
from datetime import datetime, timedelta
from dotenv import load_dotenv
from anthropic import Anthropic
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import (
    GetOptionContractsRequest,
    LimitOrderRequest,
)
from alpaca.trading.enums import OrderSide, TimeInForce, AssetStatus, ContractType
from alpaca.data.historical.option import OptionHistoricalDataClient
from alpaca.data.requests import OptionLatestQuoteRequest

# Load environment variables from .env file
load_dotenv()

# Initialize clients
trade_client = TradingClient(
    api_key=os.environ["ALPACA_API_KEY"],
    secret_key=os.environ["ALPACA_SECRET_KEY"],
    paper=True,
)

option_data_client = OptionHistoricalDataClient(
    api_key=os.environ["ALPACA_API_KEY"],
    secret_key=os.environ["ALPACA_SECRET_KEY"],
)

anthropic_client = Anthropic()


def get_account_info():
    """Get current account balance and buying power."""
    account = trade_client.get_account()
    return {
        "cash": float(account.cash),
        "buying_power": float(account.buying_power),
        "portfolio_value": float(account.portfolio_value),
    }


def fetch_put_options(symbol, min_dte=20, max_dte=45):
    """Fetch put option contracts for a given stock symbol."""
    today = datetime.now()
    min_expiry = today + timedelta(days=min_dte)
    max_expiry = today + timedelta(days=max_dte)

    request = GetOptionContractsRequest(
        underlying_symbols=[symbol],
        status=AssetStatus.ACTIVE,
        expiration_date_gte=min_expiry.strftime("%Y-%m-%d"),
        expiration_date_lte=max_expiry.strftime("%Y-%m-%d"),
        type=ContractType.PUT,
        limit=100,
    )

    response = trade_client.get_option_contracts(request)
    return response.option_contracts if response.option_contracts else []


def get_option_quotes(contracts):
    """Get latest quotes for a list of option contracts."""
    if not contracts:
        return {}

    symbols = [c.symbol for c in contracts[:20]]
    request = OptionLatestQuoteRequest(symbol_or_symbols=symbols)
    quotes = option_data_client.get_option_latest_quote(request)
    return quotes


def filter_candidates(contracts, quotes):
    """Filter options to find good cash-secured put candidates."""
    candidates = []

    for contract in contracts:
        if contract.symbol not in quotes:
            continue

        quote = quotes[contract.symbol]
        bid_price = float(quote.bid_price) if quote.bid_price else 0
        ask_price = float(quote.ask_price) if quote.ask_price else 0
        mid_price = (bid_price + ask_price) / 2

        if mid_price < 0.10:
            continue

        candidates.append({
            "symbol": contract.symbol,
            "strike_price": float(contract.strike_price),
            "expiration_date": str(contract.expiration_date),
            "underlying_symbol": contract.underlying_symbol,
            "bid_price": bid_price,
            "ask_price": ask_price,
            "mid_price": round(mid_price, 2),
        })

    return candidates


if __name__ == "__main__":
    symbol = "AAPL"
    print(f"Fetching put options for {symbol}...")
    contracts = fetch_put_options(symbol)
    print(f"Found {len(contracts)} contracts")
    quotes = get_option_quotes(contracts)
    candidates = filter_candidates(contracts, quotes)
    print(f"Filtered to {len(candidates)} candidates:")
    for c in candidates[:5]:
        print(c)

Your data pipeline is complete. The bot can now reach into the real options market and pull back structured data ready for analysis. Next up, you will connect Claude AI to analyze these candidates and recommend the best trade.

Build the AI Analysis Engine

You have raw options data flowing in from Alpaca. Now you need something to make sense of it all and pick the best trade.

In this step, you will connect your bot to Claude Haiku 4.5, which will analyze each options candidate and recommend the single best cash-secured put to sell. You will also build a display function that formats Claude's reasoning so you can see exactly why it picked that trade.

In this step, get ready to:
  • Build the AI analysis function that sends options data to Claude and parses its recommendation.
  • Create a display function that formats Claude's trade recommendation for the terminal.
  • Test the full analysis pipeline end-to-end.
Send options data to Claude for analysis

Your bot needs a brain. The analyze_with_claude() function takes the filtered option candidates, your account info, and the stock symbol, then asks Claude to pick the best trade based on risk management rules.

  • In trading_bot.py, find the filter_candidates function.
  • Add the following analyze_with_claude function directly below it:
def analyze_with_claude(candidates, account_info, underlying_symbol):
    """Send options data to Claude for AI analysis."""
    if not candidates:
        print("No candidates found to analyze.")
        return None

    prompt = f"""You are an expert options trader focused on income generation through selling cash-secured puts.

ACCOUNT INFO:
- Cash available: ${account_info['cash']:,.2f}
- Portfolio value: ${account_info['portfolio_value']:,.2f}

UNDERLYING STOCK: {underlying_symbol}

AVAILABLE PUT OPTIONS TO SELL:
{json.dumps(candidates, indent=2)}

TASK: Analyze these put options and recommend the SINGLE best cash-secured put to SELL for income. 

RULES:
1. Never risk more than 5% of portfolio value on a single trade
2. The strike price times 100 (shares per contract) must be less than available cash (this is the "cash-secured" part)
3. Prefer strikes with Delta between -0.15 and -0.35 (if delta data is available, otherwise use strike distance from current price as a proxy)
4. Higher mid_price means more income collected
5. Consider the risk/reward ratio

RESPOND IN THIS EXACT JSON FORMAT (no other text):
{{
    "recommended_symbol": "the option contract symbol",
    "strike_price": the_strike_price,
    "expiration_date": "YYYY-MM-DD",
    "limit_price": the_price_to_sell_at,
    "max_risk": dollar_amount_at_risk,
    "premium_collected": dollar_income_from_selling,
    "reasoning": "2-3 sentences explaining why this is the best choice"
}}"""

    message = anthropic_client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=500,
        messages=[{"role": "user", "content": prompt}],
    )

    response_text = message.content[0].text

    try:
        recommendation = json.loads(response_text)
        return recommendation
    except json.JSONDecodeError:
        print("Claude's response was not valid JSON. Raw response:")
        print(response_text)
        return None

What does this function do?

This function is the core of your AI trading bot. It does three things:

  • Builds a detailed prompt that gives Claude your account balance, the stock symbol, and all the option candidates with their pricing data.
  • Includes risk management rules (never risk more than 5% of portfolio, strike must be cash-secured) so Claude makes safe recommendations.
  • Asks Claude to respond in strict JSON format, then parses that JSON into a Python dictionary your bot can use.

The double curly braces {{ and }} in the prompt are how you include literal braces inside a Python f-string without Python interpreting them as variables.

Why Claude Haiku 4.5?

Your bot sends a few hundred tokens of options data and receives a short JSON recommendation back. Claude Haiku 4.5 costs $1 per million input tokens and $5 per million output tokens. Each bot run costs roughly $0.01-$0.03.

A larger model like Sonnet would be overkill for this structured analysis task and would cost 3x more per run.

You won't see a change yet. The next function will display Claude's output in a readable format.

Display the trade recommendation

Claude returns a JSON dictionary with fields like the recommended contract, income, and risk. You need a function that prints this information clearly so you can understand the trade before executing it.

  • Still in trading_bot.py, add the following function directly below analyze_with_claude:
def display_recommendation(recommendation):
    """Display Claude's recommendation in a readable format."""
    print("\n" + "=" * 60)
    print("  AI TRADE RECOMMENDATION")
    print("=" * 60)
    print(f"  Contract:    {recommendation['recommended_symbol']}")
    print(f"  Strike:      ${recommendation['strike_price']}")
    print(f"  Expiration:  {recommendation['expiration_date']}")
    print(f"  Sell at:     ${recommendation['limit_price']}")
    print(f"  Income:      ${recommendation['premium_collected']} (per contract)")
    print(f"  Max Risk:    ${recommendation['max_risk']}")
    print(f"\n  Reasoning:   {recommendation['reasoning']}")
    print("=" * 60)

What does this function do?

This prints a formatted summary of Claude's trade recommendation. It shows the contract symbol, strike price, expiration, the price to sell at, the income you would collect, the maximum risk, and Claude's reasoning for why it chose this trade.

Having this clear display is important. You will always see Claude's full reasoning before the bot asks you to confirm a trade.

You won't see a change yet. The next chunk updates your test block so you can run the full analysis.

Test the AI analysis end-to-end

Now wire everything together by updating the test block at the bottom of your file. This will fetch options, send them to Claude, and display the recommendation.

  • In trading_bot.py, scroll to the bottom and find this existing test block:
if __name__ == "__main__":
    symbol = "AAPL"
    print(f"Fetching put options for {symbol}...")
    contracts = fetch_put_options(symbol)
    print(f"Found {len(contracts)} contracts")
    quotes = get_option_quotes(contracts)
    candidates = filter_candidates(contracts, quotes)
    print(f"Filtered to {len(candidates)} candidates:")
    for c in candidates[:5]:
        print(c)
  • Replace the entire if __name__ == "__main__": block with this updated version:
if __name__ == "__main__":
    symbol = "AAPL"
    account_info = get_account_info()
    contracts = fetch_put_options(symbol)
    quotes = get_option_quotes(contracts)
    candidates = filter_candidates(contracts, quotes)
    recommendation = analyze_with_claude(candidates, account_info, symbol)
    if recommendation:
        display_recommendation(recommendation)

What changed?

The updated test block now fetches your account info, runs the full options pipeline, sends the candidates to Claude for analysis, and displays the recommendation. The if recommendation: check ensures you only try to display results if Claude returned a valid response.

  • Save trading_bot.py.
  • Run the bot by entering this command in your Terminal:
python3 trading_bot.py

You should see Claude's formatted trade recommendation appear in your terminal, including the contract symbol, strike price, income, risk, and reasoning.

Run during market hours

Options markets are only open during regular US market hours (9:30 AM to 4:00 PM Eastern Time, Monday through Friday). If you run the bot outside these hours, you may get no candidates because quotes will not be available.

Not seeing a recommendation?

  • If you see "No candidates found to analyze", the market may be closed or the stock has no options in the 20-45 day range. Try running during market hours or try a different symbol like SPY or MSFT.
  • If you see an authentication error, double-check that your ANTHROPIC_API_KEY in .env is correct and that your virtual environment is active (you should see (.venv) in your terminal prompt).
  • If Claude's response is not valid JSON, run the bot again. Occasionally the model may include extra text around the JSON.

Help me debug my options bot

✔️ Awesome, I've got everything!

Great. Double check you have saved trading_bot.py before moving on.

ⓧ I'd like to double check the full code

Here is the full trading_bot.py at this point:

import os
import json
from datetime import datetime, timedelta
from dotenv import load_dotenv
from anthropic import Anthropic
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import (
    GetOptionContractsRequest,
    LimitOrderRequest,
)
from alpaca.trading.enums import OrderSide, TimeInForce, AssetStatus, ContractType
from alpaca.data.historical.option import OptionHistoricalDataClient
from alpaca.data.requests import OptionLatestQuoteRequest

load_dotenv()

trade_client = TradingClient(
    api_key=os.environ["ALPACA_API_KEY"],
    secret_key=os.environ["ALPACA_SECRET_KEY"],
    paper=True,
)

option_data_client = OptionHistoricalDataClient(
    api_key=os.environ["ALPACA_API_KEY"],
    secret_key=os.environ["ALPACA_SECRET_KEY"],
)

anthropic_client = Anthropic()


def get_account_info():
    account = trade_client.get_account()
    return {
        "cash": float(account.cash),
        "buying_power": float(account.buying_power),
        "portfolio_value": float(account.portfolio_value),
    }


def fetch_put_options(symbol, min_dte=20, max_dte=45):
    today = datetime.now()
    min_expiry = today + timedelta(days=min_dte)
    max_expiry = today + timedelta(days=max_dte)
    request = GetOptionContractsRequest(
        underlying_symbols=[symbol],
        status=AssetStatus.ACTIVE,
        expiration_date_gte=min_expiry.strftime("%Y-%m-%d"),
        expiration_date_lte=max_expiry.strftime("%Y-%m-%d"),
        type=ContractType.PUT,
        limit=100,
    )
    response = trade_client.get_option_contracts(request)
    return response.option_contracts if response.option_contracts else []


def get_option_quotes(contracts):
    if not contracts:
        return {}
    symbols = [c.symbol for c in contracts[:20]]
    request = OptionLatestQuoteRequest(symbol_or_symbols=symbols)
    quotes = option_data_client.get_option_latest_quote(request)
    return quotes


def filter_candidates(contracts, quotes):
    candidates = []
    for contract in contracts:
        if contract.symbol not in quotes:
            continue
        quote = quotes[contract.symbol]
        bid_price = float(quote.bid_price) if quote.bid_price else 0
        ask_price = float(quote.ask_price) if quote.ask_price else 0
        mid_price = (bid_price + ask_price) / 2
        if mid_price < 0.10:
            continue
        candidates.append({
            "symbol": contract.symbol,
            "strike_price": float(contract.strike_price),
            "expiration_date": str(contract.expiration_date),
            "underlying_symbol": contract.underlying_symbol,
            "bid_price": bid_price,
            "ask_price": ask_price,
            "mid_price": round(mid_price, 2),
        })
    return candidates


def analyze_with_claude(candidates, account_info, underlying_symbol):
    """Send options data to Claude for AI analysis."""
    if not candidates:
        print("No candidates found to analyze.")
        return None

    prompt = f"""You are an expert options trader focused on income generation through selling cash-secured puts.

ACCOUNT INFO:
- Cash available: ${account_info['cash']:,.2f}
- Portfolio value: ${account_info['portfolio_value']:,.2f}

UNDERLYING STOCK: {underlying_symbol}

AVAILABLE PUT OPTIONS TO SELL:
{json.dumps(candidates, indent=2)}

TASK: Analyze these put options and recommend the SINGLE best cash-secured put to SELL for income. 

RULES:
1. Never risk more than 5% of portfolio value on a single trade
2. The strike price times 100 (shares per contract) must be less than available cash (this is the "cash-secured" part)
3. Prefer strikes with Delta between -0.15 and -0.35 (if delta data is available, otherwise use strike distance from current price as a proxy)
4. Higher mid_price means more income collected
5. Consider the risk/reward ratio

RESPOND IN THIS EXACT JSON FORMAT (no other text):
{{
    "recommended_symbol": "the option contract symbol",
    "strike_price": the_strike_price,
    "expiration_date": "YYYY-MM-DD",
    "limit_price": the_price_to_sell_at,
    "max_risk": dollar_amount_at_risk,
    "premium_collected": dollar_income_from_selling,
    "reasoning": "2-3 sentences explaining why this is the best choice"
}}"""

    message = anthropic_client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=500,
        messages=[{"role": "user", "content": prompt}],
    )

    response_text = message.content[0].text

    try:
        recommendation = json.loads(response_text)
        return recommendation
    except json.JSONDecodeError:
        print("Claude's response was not valid JSON. Raw response:")
        print(response_text)
        return None


def display_recommendation(recommendation):
    """Display Claude's recommendation in a readable format."""
    print("\n" + "=" * 60)
    print("  AI TRADE RECOMMENDATION")
    print("=" * 60)
    print(f"  Contract:    {recommendation['recommended_symbol']}")
    print(f"  Strike:      ${recommendation['strike_price']}")
    print(f"  Expiration:  {recommendation['expiration_date']}")
    print(f"  Sell at:     ${recommendation['limit_price']}")
    print(f"  Income:      ${recommendation['premium_collected']} (per contract)")
    print(f"  Max Risk:    ${recommendation['max_risk']}")
    print(f"\n  Reasoning:   {recommendation['reasoning']}")
    print("=" * 60)


if __name__ == "__main__":
    symbol = "AAPL"
    account_info = get_account_info()
    contracts = fetch_put_options(symbol)
    quotes = get_option_quotes(contracts)
    candidates = filter_candidates(contracts, quotes)
    recommendation = analyze_with_claude(candidates, account_info, symbol)
    if recommendation:
        display_recommendation(recommendation)

Your bot can now think. It takes raw market data, feeds it to Claude with clear risk rules, and gets back a structured trade recommendation with full reasoning. Next up, you will build the trade executor that takes Claude's recommendation and actually places the order on Alpaca.

Build the Trade Executor and Run the Bot

Your bot can now fetch options data and ask Claude for a recommendation. The missing piece is execution.

In this step, you will build the function that actually places a trade on Alpaca, wire everything together into a single main() function, and run the complete bot end-to-end.

In this step, get ready to:
  • Add the trade execution function that places a limit order on Alpaca.
  • Wire all functions together into a main orchestrator with user confirmation.
  • Run the complete bot and execute a paper trade.
Add the trade execution function

When Claude recommends a trade, you need a function that translates that recommendation into a real order on Alpaca. Options orders on Alpaca require time_in_force set to day (the only allowed value for options).

First, you need to add import sys to your imports so the bot can read command-line flags later.

  • In trading_bot.py, find the import lines at the top of the file.
  • Find this line:
import json
  • Add import sys on a new line directly after import json, so it looks like this:
import json
import sys

Now add the execute_trade() function.

  • Scroll down to the end of the display_recommendation() function.
  • Add the following function directly after display_recommendation():
def execute_trade(recommendation):
    """Place a limit order to sell a cash-secured put."""
    order_request = LimitOrderRequest(
        symbol=recommendation["recommended_symbol"],
        qty=1,
        side=OrderSide.SELL,
        time_in_force=TimeInForce.DAY,
        limit_price=recommendation["limit_price"],
    )
    order = trade_client.submit_order(order_request)
    return order

What does this code do?

This function takes Claude's recommendation dictionary and constructs an Alpaca order from it.

  • LimitOrderRequest builds a limit order (you specify the exact price you want to sell at).
  • OrderSide.SELL means you are selling a put contract to collect premium income.
  • TimeInForce.DAY is required for options orders on Alpaca. The order is valid for the current trading day only.
  • qty=1 means one contract, which represents 100 shares of the underlying stock.
  • Save trading_bot.py.

Seeing a red underline under LimitOrderRequest?

Make sure LimitOrderRequest is already imported at the top of your file. Check that your imports include from alpaca.trading.requests import (GetOptionContractsRequest, LimitOrderRequest,).

help me fix my imports for the trade executor function

Wire everything together in the main function

Right now your bot has all the individual pieces: data fetching, AI analysis, and trade execution. The main() function will orchestrate these into a smooth end-to-end flow with user confirmation before executing.

  • In trading_bot.py, find the existing if __name__ == "__main__": block at the bottom of the file.
  • Delete everything from if __name__ == "__main__": to the end of the file.
  • Replace it with the following main() function and updated __main__ block:
def main():
    """Main function to run the trading bot."""
    print("\n🤖 AI Options Income Bot Starting...\n")
    print("📊 Fetching account information...")
    account_info = get_account_info()
    print(f"   Cash: ${account_info['cash']:,.2f}")
    print(f"   Portfolio Value: ${account_info['portfolio_value']:,.2f}")
    symbol = input("\n🔍 Enter a stock symbol to analyze (e.g., AAPL, SPY): ").upper().strip()
    print(f"\n📈 Fetching put options for {symbol}...")
    contracts = fetch_put_options(symbol)
    print(f"   Found {len(contracts)} put contracts (20-45 DTE)")
    if not contracts:
        print("   No contracts found. Try a different symbol (SPY, AAPL, MSFT work well).")
        return
    print("💰 Getting latest quotes...")
    quotes = get_option_quotes(contracts)
    candidates = filter_candidates(contracts, quotes)
    print(f"   {len(candidates)} candidates with valid pricing")
    if not candidates:
        print("   No valid candidates. Market may be closed. Try during trading hours.")
        return
    print("\n🧠 Sending to Claude AI for analysis...")
    recommendation = analyze_with_claude(candidates, account_info, symbol)
    if not recommendation:
        print("   Could not get a recommendation. Try again.")
        return
    display_recommendation(recommendation)
    confirm = input("\n⚡ Execute this trade? (y/n): ").lower().strip()
    if confirm == "y":
        print("\n🚀 Placing order...")
        order = execute_trade(recommendation)
        print(f"   ✅ Order placed! ID: {order.id}")
        print(f"   Status: {order.status}")
    else:
        print("\n   Trade cancelled. No order placed.")
    print("\n🤖 Bot complete. Check your Alpaca dashboard to see the order.\n")


if __name__ == "__main__":
    if "--monitor" in sys.argv:
        monitor_positions()
    else:
        main()

What does this code do?

The main() function runs the full trading pipeline in sequence:

  • Fetches your account balance so Claude knows how much cash you have available.
  • Asks you to type a stock symbol, then fetches put options expiring in 20-45 days.
  • Filters for contracts with valid pricing and sends them to Claude for analysis.
  • Displays Claude's recommendation and asks you to confirm before placing the order.
  • The if __name__ == "__main__": block checks for a --monitor flag (which you will build in the Secret Mission). For now, it just calls main().
  • Save trading_bot.py.

Seeing a NameError about monitor_positions?

The monitor_positions() function does not exist yet. That is expected. The --monitor flag is a placeholder for the Secret Mission. As long as you run the bot without that flag, everything works fine.

help me understand the __main__ block error

✔️ Awesome, I've got everything!

Great. Double check you have saved trading_bot.py with both the execute_trade() function and the main() function.

ⓧ I'd like to double check the full code

Here is the complete trading_bot.py at this point:

import os
import json
import sys
from datetime import datetime, timedelta
from dotenv import load_dotenv
from anthropic import Anthropic
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import (
    GetOptionContractsRequest,
    LimitOrderRequest,
)
from alpaca.trading.enums import OrderSide, TimeInForce, AssetStatus, ContractType
from alpaca.data.historical.option import OptionHistoricalDataClient
from alpaca.data.requests import OptionLatestQuoteRequest

load_dotenv()

trade_client = TradingClient(
    api_key=os.environ["ALPACA_API_KEY"],
    secret_key=os.environ["ALPACA_SECRET_KEY"],
    paper=True,
)

option_data_client = OptionHistoricalDataClient(
    api_key=os.environ["ALPACA_API_KEY"],
    secret_key=os.environ["ALPACA_SECRET_KEY"],
)

anthropic_client = Anthropic()


def get_account_info():
    account = trade_client.get_account()
    return {
        "cash": float(account.cash),
        "buying_power": float(account.buying_power),
        "portfolio_value": float(account.portfolio_value),
    }


def fetch_put_options(symbol, min_dte=20, max_dte=45):
    today = datetime.now()
    min_expiry = today + timedelta(days=min_dte)
    max_expiry = today + timedelta(days=max_dte)
    request = GetOptionContractsRequest(
        underlying_symbols=[symbol],
        status=AssetStatus.ACTIVE,
        expiration_date_gte=min_expiry.strftime("%Y-%m-%d"),
        expiration_date_lte=max_expiry.strftime("%Y-%m-%d"),
        type=ContractType.PUT,
        limit=100,
    )
    response = trade_client.get_option_contracts(request)
    return response.option_contracts if response.option_contracts else []


def get_option_quotes(contracts):
    if not contracts:
        return {}
    symbols = [c.symbol for c in contracts[:20]]
    request = OptionLatestQuoteRequest(symbol_or_symbols=symbols)
    quotes = option_data_client.get_option_latest_quote(request)
    return quotes


def filter_candidates(contracts, quotes):
    candidates = []
    for contract in contracts:
        if contract.symbol not in quotes:
            continue
        quote = quotes[contract.symbol]
        bid_price = float(quote.bid_price) if quote.bid_price else 0
        ask_price = float(quote.ask_price) if quote.ask_price else 0
        mid_price = (bid_price + ask_price) / 2
        if mid_price < 0.10:
            continue
        candidates.append({
            "symbol": contract.symbol,
            "strike_price": float(contract.strike_price),
            "expiration_date": str(contract.expiration_date),
            "underlying_symbol": contract.underlying_symbol,
            "bid_price": bid_price,
            "ask_price": ask_price,
            "mid_price": round(mid_price, 2),
        })
    return candidates


def analyze_with_claude(candidates, account_info, underlying_symbol):
    if not candidates:
        print("No candidates found to analyze.")
        return None
    prompt = f"""You are an expert options trader focused on income generation through selling cash-secured puts.

ACCOUNT INFO:
- Cash available: ${account_info['cash']:,.2f}
- Portfolio value: ${account_info['portfolio_value']:,.2f}

UNDERLYING STOCK: {underlying_symbol}

AVAILABLE PUT OPTIONS TO SELL:
{json.dumps(candidates, indent=2)}

TASK: Analyze these put options and recommend the SINGLE best cash-secured put to SELL for income. 

RULES:
1. Never risk more than 5% of portfolio value on a single trade
2. The strike price times 100 (shares per contract) must be less than available cash (this is the "cash-secured" part)
3. Prefer strikes with Delta between -0.15 and -0.35 (if delta data is available, otherwise use strike distance from current price as a proxy)
4. Higher mid_price means more income collected
5. Consider the risk/reward ratio

RESPOND IN THIS EXACT JSON FORMAT (no other text):
{{
    "recommended_symbol": "the option contract symbol",
    "strike_price": the_strike_price,
    "expiration_date": "YYYY-MM-DD",
    "limit_price": the_price_to_sell_at,
    "max_risk": dollar_amount_at_risk,
    "premium_collected": dollar_income_from_selling,
    "reasoning": "2-3 sentences explaining why this is the best choice"
}}"""
    message = anthropic_client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=500,
        messages=[{"role": "user", "content": prompt}],
    )
    response_text = message.content[0].text
    try:
        recommendation = json.loads(response_text)
        return recommendation
    except json.JSONDecodeError:
        print("Claude's response was not valid JSON. Raw response:")
        print(response_text)
        return None


def execute_trade(recommendation):
    """Place a limit order to sell a cash-secured put."""
    order_request = LimitOrderRequest(
        symbol=recommendation["recommended_symbol"],
        qty=1,
        side=OrderSide.SELL,
        time_in_force=TimeInForce.DAY,
        limit_price=recommendation["limit_price"],
    )
    order = trade_client.submit_order(order_request)
    return order


def display_recommendation(recommendation):
    print("\n" + "=" * 60)
    print("  AI TRADE RECOMMENDATION")
    print("=" * 60)
    print(f"  Contract:    {recommendation['recommended_symbol']}")
    print(f"  Strike:      ${recommendation['strike_price']}")
    print(f"  Expiration:  {recommendation['expiration_date']}")
    print(f"  Sell at:     ${recommendation['limit_price']}")
    print(f"  Income:      ${recommendation['premium_collected']} (per contract)")
    print(f"  Max Risk:    ${recommendation['max_risk']}")
    print(f"\n  Reasoning:   {recommendation['reasoning']}")
    print("=" * 60)


def main():
    """Main function to run the trading bot."""
    print("\n🤖 AI Options Income Bot Starting...\n")
    print("📊 Fetching account information...")
    account_info = get_account_info()
    print(f"   Cash: ${account_info['cash']:,.2f}")
    print(f"   Portfolio Value: ${account_info['portfolio_value']:,.2f}")
    symbol = input("\n🔍 Enter a stock symbol to analyze (e.g., AAPL, SPY): ").upper().strip()
    print(f"\n📈 Fetching put options for {symbol}...")
    contracts = fetch_put_options(symbol)
    print(f"   Found {len(contracts)} put contracts (20-45 DTE)")
    if not contracts:
        print("   No contracts found. Try a different symbol (SPY, AAPL, MSFT work well).")
        return
    print("💰 Getting latest quotes...")
    quotes = get_option_quotes(contracts)
    candidates = filter_candidates(contracts, quotes)
    print(f"   {len(candidates)} candidates with valid pricing")
    if not candidates:
        print("   No valid candidates. Market may be closed. Try during trading hours.")
        return
    print("\n🧠 Sending to Claude AI for analysis...")
    recommendation = analyze_with_claude(candidates, account_info, symbol)
    if not recommendation:
        print("   Could not get a recommendation. Try again.")
        return
    display_recommendation(recommendation)
    confirm = input("\n⚡ Execute this trade? (y/n): ").lower().strip()
    if confirm == "y":
        print("\n🚀 Placing order...")
        order = execute_trade(recommendation)
        print(f"   ✅ Order placed! ID: {order.id}")
        print(f"   Status: {order.status}")
    else:
        print("\n   Trade cancelled. No order placed.")
    print("\n🤖 Bot complete. Check your Alpaca dashboard to see the order.\n")


if __name__ == "__main__":
    if "--monitor" in sys.argv:
        monitor_positions()
    else:
        main()
Run the complete bot

Everything is in place. Time to run your AI trading bot for real.

Options markets are only open during US trading hours

Options markets are open 9:30 AM to 4:00 PM Eastern Time, Monday through Friday. If you run the bot outside these hours, you will see "No valid candidates" because quote data is unavailable.

If you are outside trading hours, you can still run the bot to see it connect and attempt to fetch data. The order placement will only work during market hours.

  • Make sure your virtual environment is active (you should see (.venv) in your terminal prompt).
  • Run the bot by entering this command in your terminal:
python3 trading_bot.py
  • When prompted, type a stock symbol like AAPL or SPY and press **Enter**.
  • Review Claude's recommendation. It will show you the contract, strike price, expiration, income, max risk, and reasoning.
  • When asked "Execute this trade?", type y and press **Enter** to place the paper trade.

What should I see?

After typing y, you should see output like:

  • "Order placed! ID:" followed by a unique order identifier.
  • "Status:" showing accepted or new (meaning Alpaca received the order).

This is paper trading with $100,000 of virtual cash. No real money is involved.

  • Open your browser and go to https://app.alpaca.markets.
  • Log in and check the **Orders** section in your Paper Trading dashboard to see your submitted order.

Seeing "No contracts found" or "No valid candidates"?

This usually means the options market is closed. Options quotes are only available during US market hours (9:30 AM to 4:00 PM Eastern Time, Monday through Friday).

Try a high-volume symbol like SPY or AAPL during trading hours. These have the most liquid options chains.

help me troubleshoot why my options bot is not finding contracts

Your AI options income bot is fully operational. You have a working pipeline from live market data through AI analysis to automated trade execution. In the Secret Mission, you will extend this bot to monitor your open positions and use Claude to decide when to close them for profit.

Secret mission

Portfolio Monitor with AI Exit Strategy

Your bot can open trades, but a real trading system also knows when to close them. Build a portfolio monitor that checks your open positions and uses Claude to decide whether to take profits early or hold until expiration.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally and only costs money when you actively run the bot (Claude API calls cost roughly $0.01-0.03 per run).

Resources you used:

  • Alpaca paper trading account with API keys.
  • Anthropic Console account with API key.
  • Local project folder ~/options-bot with virtual environment and bot code.

Keep everything running

No action needed. Choose this if you are still actively building or plan to run the bot regularly.

  • Your Alpaca paper trading account is free. No charges accrue regardless of how many paper trades you have open.
  • Your Anthropic Console account is pay-per-use. You are only charged when the bot calls the Claude API.
  • Your local options-bot folder stays on your Desktop. The virtual environment uses roughly 50-100 MB of disk space.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and accounts stay intact for when you return.

  • Stop the bot if it is currently running by pressing Ctrl+C in your terminal.
  • Deactivate your virtual environment by running:
deactivate
  • Your Alpaca and Anthropic accounts remain active. Neither charges you while the bot is idle.
  • Your project files remain on your Desktop ready for next time.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to fully clean up.

  • Delete your Alpaca paper trading API keys. Go to https://app.alpaca.markets, find the API Keys section on the right sidebar, and regenerate or remove your keys.
  • Delete your Alpaca paper trading account. Click the paper account number in the upper left corner of the dashboard, then go to Account Settings. Locate the paper account and click Delete Account.
  • Delete your Anthropic API key. Go to https://console.anthropic.com/, click API Keys in the left sidebar, find the key you created, and delete it.
  • Remove your local project folder by running this command in Terminal:
rm -rf ~/options-bot
  • Delete the .env file is already handled by removing the folder above. Your API keys are no longer stored locally.

Nice Work!

Nice Work!

You just built an AI-powered options trading bot from scratch. Your bot connects to real market data, thinks through trades with Claude, and executes them automatically on your paper trading account.

You learned how to:

  • Connect to Alpaca's brokerage API and fetch real options market data including quotes, strike prices, and expiration dates.
  • Integrate Claude AI to analyze trades and make intelligent cash-secured put recommendations based on risk management rules.
  • Execute automated paper trades based on AI analysis, with a one-line switch (paper=True to paper=False) to transition to live trading when ready.
  • Secret Mission: Built a portfolio monitor that uses Claude to evaluate open positions and recommend when to close them for profit.

Ready to quiz yourself?