Build a Stock Trading Bot with Python

Use Python OOP and the Alpaca API to build an automated stock trading bot.

Introduction

30 Second Summary

Imagine watching stock prices tick up and down all day, trying to spot the perfect moment to buy or sell. Professional traders automate this process with bots that analyze data and place trades without any manual intervention.

In this project, you will build a Python trading bot that connects to a real stock market API and places simulated trades on a paper trading account. The bot's code is structured with Object-Oriented Programming, keeping each component clean and reusable.

What You'll Build

A Python trading bot that makes REST API calls to Alpaca's paper trading API to fetch real market data as JSON, detect price trends using Simple Moving Averages, and place simulated buy and sell orders.

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

  • An AlpacaClient class that makes authenticated API calls to fetch market data, place orders, and check positions.
  • A Strategy base class and MovingAverageCrossover subclass demonstrating inheritance and polymorphism through trading signal generation.
  • A TradingBot class that uses composition to tie the client and strategy together into an automated trading workflow.
  • Secret Mission: build a multi-stock scanner that analyzes several stocks and ranks them by signal strength using a custom sorting algorithm.

Are there any prerequisites?

You need basic Python knowledge including variables, functions, lists, dictionaries, loops, and conditionals. No trading or OOP experience is required.

Before We Start

Before We Start...

Set Up Your Environment and Alpaca Account

Before your trading bot can analyze stock prices and place trades, it needs a way to communicate with the stock market. That communication happens through Alpaca, a trading platform that provides a free REST API for paper trading.

In this step, you'll install the Python library your bot uses to make HTTP requests. You'll also create a free Alpaca paper trading account and test that your code can connect to Alpaca's servers.

In this step, get ready to:
  • Install the requests library for making HTTP requests.
  • Create a free Alpaca paper trading account with API keys.
  • Test your API connection with a Python script.
Install the requests library

Your trading bot needs to send HTTP requests to Alpaca's servers. The requests library is the standard Python tool for this.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (backtick key, next to the 1 key). You can also click View in the top menu bar, then Terminal.
  • Install the requests library by running this command:
pip install requests
  • Verify the install by running this command:
python -c "import requests; print(requests.__version__)"

You should see a version number printed with no errors.

Seeing an error?

If you see a "command not found" error for pip, try using pip3 install requests instead. Some systems use pip3 for Python 3.

If you see "command not found" for python in the verify step, try python3 instead. If you use python3 here, use it for all commands throughout this project.

Help me troubleshoot my requests install

Create a free Alpaca paper trading account

Paper trading lets you simulate buying and selling stocks with a virtual $100,000 balance. No real money is involved. Alpaca provides a free paper trading account that your bot will connect to through its API.

  • Open https://app.alpaca.markets/signup in your browser.
  • Sign up with your email address and create a password.
  • Confirm your email when you receive the verification email from Alpaca.
  • Alpaca will ask you to activate Multi-Factor Authentication (MFA). Follow the prompts to complete this setup.
  • Once signed in, you should land on the dashboard. Make sure you have the Paper Trading account selected in the upper-left corner of the dashboard.

What is paper trading?

Paper trading is a simulated trading environment. You trade with virtual money using real market data, so you can test your strategies without any financial risk. Alpaca's paper trading account starts with a virtual $100,000 balance.

Now you need to generate API keys so your Python code can authenticate with Alpaca's servers.

  • On the Alpaca dashboard, navigate to the Home section and locate the API Keys panel.
  • Click to generate a new API key pair.
  • You will receive two values: an API Key ID and a Secret Key. Copy both values and save them somewhere safe (like a text file or a notes app).

Save your Secret Key now

The Secret Key is only shown once when first generated. If you lose it, you will need to regenerate a new API key pair, which invalidates the old one. Make sure you save it before closing the dialog.

Your paper trading dashboard should now show your $100,000 starting balance.

Create the project file and test your API connection

With your API keys ready, it is time to write a quick Python script that proves your code can talk to Alpaca's servers. This script will make a GET request to Alpaca's account endpoint and print the JSON response.

  • Switch back to VS Code.
  • In the integrated terminal, move to your Desktop by running this command:
cd ~/Desktop
  • Create a project folder and move into it by running these commands:
mkdir trading-bot
cd trading-bot
  • Confirm the folder was created by running:
ls

The folder should be empty for now. Next, open this folder as your VS Code workspace.

  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the trading-bot folder, and click Open.
  • If VS Code asks you to trust the authors of the files in this folder, click Yes, I trust the authors.
  • Right-click in the Explorer sidebar on the left and select New File.
  • Name the file trading_bot.py and press Enter.

The file opens in the editor. Now write a test script that connects to Alpaca.

  • Copy the following code into trading_bot.py, replacing your-api-key-here and your-api-secret-here with the real API Key ID and Secret Key you saved earlier:
import requests

# Replace these with your real Alpaca API credentials
API_KEY = "your-api-key-here"
API_SECRET = "your-api-secret-here"

# Send a GET request to Alpaca's account endpoint
url = "https://paper-api.alpaca.markets/v2/account"
headers = {
    "APCA-API-KEY-ID": API_KEY,
    "APCA-API-SECRET-KEY": API_SECRET
}
response = requests.get(url, headers=headers)

# Print the account details as JSON
print(response.json())

What does this code do?

  • requests.get() sends an HTTP GET request to Alpaca's account endpoint. This is how your code asks the server for information.
  • The headers dictionary contains two authentication keys: APCA-API-KEY-ID and APCA-API-SECRET-KEY. These prove to Alpaca that your request is authorized.
  • response.json() converts the server's response from raw JSON text into a Python dictionary so you can read the data.
  • Save trading_bot.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open the integrated terminal by pressing Ctrl+` if it is not already open.
  • Run the script by entering this command:
python trading_bot.py

You should see a large JSON response printed in the terminal. Look for fields like status (which should say ACTIVE), equity, and buying_power.

Seeing an error instead of account data?

  • If you see a 401 or 403 error, double-check that you replaced your-api-key-here and your-api-secret-here with your actual API Key ID and Secret Key. Make sure you are using the keys from your Paper Trading account, not a live account.
  • If you see "command not found" for python, try running python3 trading_bot.py instead.
  • If you see a ModuleNotFoundError for requests, the library did not install correctly. Go back and re-run the pip install requests step.

Help me debug my Alpaca API connection

✔️ Awesome, I've got everything!

Great. Double check you have saved trading_bot.py and that your API connection returned account data.

ⓧ I'd like to double check the full code

Here is the complete trading_bot.py file at this point. Make sure your file matches this, with your real API keys in place of the placeholder values:

import requests

API_KEY = "your-api-key-here"
API_SECRET = "your-api-secret-here"

url = "https://paper-api.alpaca.markets/v2/account"
headers = {
    "APCA-API-KEY-ID": API_KEY,
    "APCA-API-SECRET-KEY": API_SECRET
}
response = requests.get(url, headers=headers)
print(response.json())

Your environment is ready and your code can talk to Alpaca's trading servers. Next up, you'll build the API client class and data classes that give your bot a clean, structured way to work with orders and positions.

Build the API Client and Data Classes

In Step 1, you confirmed your API connection works by printing raw account data. That test script did the job, but it was a one-off.

In this step, you'll replace that script with proper Python classes. You'll build data classes that turn raw JSON into structured objects, and an API client class that handles all communication with Alpaca.

In this step, get ready to:
  • Create Order and Position data classes that parse JSON into Python objects.
  • Build an AlpacaClient class that handles authenticated API calls.
  • Add order and position methods to the client.
Create the Order and Position classes

Every response from the Alpaca API comes back as a raw JSON dictionary. Data classes give that raw data a clear shape, turning dictionaries into Python objects with named attributes you can work with directly.

  • Switch back to trading_bot.py in VS Code.
  • Select all the existing test code by pressing Cmd+A (macOS) or Ctrl+A (Windows), then delete it.
  • Add the imports and the Order class by copying this code into the empty file:
import requests
from datetime import datetime, timedelta


class Order:
    # Represents a single stock order (buy or sell)
    def __init__(self, id, symbol, qty, side, type, time_in_force, status, filled_avg_price=None):
        self.id = id
        self.symbol = symbol
        self.qty = qty
        self.side = side
        self.type = type
        self.time_in_force = time_in_force
        self.status = status
        self.filled_avg_price = filled_avg_price

    def __str__(self):
        return f"Order({self.side} {self.qty} {self.symbol} | status: {self.status})"

What does this code do?

The datetime and timedelta imports are for calculating date ranges later when fetching price history.

The Order class stores every field the Alpaca API returns when you place an order. filled_avg_price defaults to None because new orders haven't been filled yet. The __str__ method gives you a readable summary when you print an order.

  • Add the Position class below the Order class by copying this code:

class Position:
    # Represents a stock you currently hold
    def __init__(self, symbol, qty, avg_entry_price, current_price, market_value, unrealized_pl):
        self.symbol = symbol
        self.qty = float(qty)
        self.avg_entry_price = float(avg_entry_price)
        self.current_price = float(current_price)
        self.market_value = float(market_value)
        self.unrealized_pl = float(unrealized_pl)

    def __str__(self):
        return f"Position({self.symbol}: {self.qty} shares | P/L: ${self.unrealized_pl:.2f})"

Why convert to float?

The Alpaca API returns numeric values like qty and avg_entry_price as strings. Wrapping them in float() converts them to numbers so you can do math with them later (like calculating profit).

  • Test your data classes by temporarily adding this code at the bottom of trading_bot.py:

# Temporary test code - delete after testing
test_order = Order("123", "AAPL", "1", "buy", "market", "day", "new")
print(test_order)

test_position = Position("AAPL", "1", "150.00", "155.00", "155.00", "5.00")
print(test_position)
  • Save trading_bot.py.
  • Run the script by running this command in your terminal:
python trading_bot.py

You should see two lines of formatted output:

Order(buy 1 AAPL | status: new)
Position(AAPL: 1.0 shares | P/L: $5.00)
  • Delete the temporary test code you just added (everything below the Position class). Keep only the imports and the two class definitions.

Seeing a NameError?

  • Make sure you deleted all of the old test script from Step 1 before pasting the new code. The file should start with import requests.
  • Check that the test code at the bottom is outside both class definitions (not indented under either class).

Ask for help: why is my Order or Position class throwing a NameError?

Build the AlpacaClient class

Your data classes know how to hold order and position data. Now you need a class that actually talks to the Alpaca API to get that data.

  • Add the AlpacaClient class below the Position class. This chunk defines the constructor and a method to check your account:

class AlpacaClient:
    def __init__(self, api_key, api_secret, paper=True):
        self.api_key = api_key
        self.api_secret = api_secret
        # Use the paper trading URL for simulated trades
        if paper:
            self.trading_base_url = "https://paper-api.alpaca.markets"
        else:
            self.trading_base_url = "https://api.alpaca.markets"
        # Market data lives on a separate URL
        self.data_base_url = "https://data.alpaca.markets"
        # Auth headers sent with every request
        self.headers = {
            "APCA-API-KEY-ID": self.api_key,
            "APCA-API-SECRET-KEY": self.api_secret
        }

    def get_account(self):
        # GET /v2/account - returns account info (equity, buying power, etc.)
        url = f"{self.trading_base_url}/v2/account"
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        return response.json()

What does this code do?

The __init__ method stores three key pieces: the trading URL (paper or live), the data URL (for price history), and the authentication headers. The paper flag defaults to True so you always use the simulated trading environment unless you explicitly switch.

The get_account() method makes a GET request to /v2/account. The raise_for_status() call throws an error if the API returns a non-200 status code (like a 401 for bad credentials). This catches authentication problems early.

Why two different base URLs?

Alpaca separates its trading API (paper-api.alpaca.markets) from its market data API (data.alpaca.markets). Orders and account info go through the trading URL. Historical price data goes through the data URL. Your client needs both.

  • Add the get_bars method inside the AlpacaClient class, directly below get_account:
    def get_bars(self, symbol, timeframe, start, end, limit=None):
        # GET /v2/stocks/{symbol}/bars - returns historical price bars
        url = f"{self.data_base_url}/v2/stocks/{symbol}/bars"
        params = {
            "timeframe": timeframe,
            "start": start,
            "end": end,
            "feed": "iex"
        }
        if limit:
            params["limit"] = limit
        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        return response.json()

What does this code do?

This method fetches historical price bars from Alpaca's data API. Each bar contains a timestamp (t), open (o), high (h), low (l), close (c), and volume (v).

The feed parameter is set to iex because free paper trading accounts only have access to the IEX data feed. Without this, the request would fail.

  • Test the client by temporarily adding this code at the bottom of trading_bot.py (replace the placeholder strings with your real API keys):

# Temporary test code - delete after testing
client = AlpacaClient("your-api-key-here", "your-api-secret-here", paper=True)

# Test account endpoint
account = client.get_account()
print(f"Account status: {account['status']}")
print(f"Equity: {account['equity']}")

# Test bars endpoint
bars = client.get_bars("AAPL", "1Day", "2026-05-01", "2026-05-30")
print(f"Bars retrieved: {len(bars.get('bars', []))}")
print(bars)
  • Save trading_bot.py and run the script:
python trading_bot.py

You should see your account status printed as ACTIVE, your equity value, and a list of price bars with fields like t, o, h, l, c, and v.

  • Delete the temporary test code you just added (everything below the AlpacaClient class). Keep only the imports and the three class definitions.
  • Save trading_bot.py.

Seeing a 403 or 401 error?

  • Double-check that you replaced both placeholder strings (your-api-key-here and your-api-secret-here) with your actual Alpaca API credentials.
  • Make sure the header names are exactly APCA-API-KEY-ID and APCA-API-SECRET-KEY. Even a small typo in the header name causes authentication to fail.
  • If you regenerated your keys on the Alpaca dashboard, the old keys no longer work. Use the new pair.

Ask for help: why is my AlpacaClient getting a 401 or 403 error from the API?

Add order and position methods to AlpacaClient

Your client can fetch account info and price bars. Now it needs the ability to place orders and check what positions you hold.

  • Add the place_order method inside the AlpacaClient class, directly below get_bars:
    def place_order(self, symbol, qty, side, order_type="market", time_in_force="day"):
        # POST /v2/orders - places a buy or sell order
        url = f"{self.trading_base_url}/v2/orders"
        order_data = {
            "symbol": symbol,
            "qty": str(qty),  # Alpaca expects qty as a string
            "side": side,
            "type": order_type,
            "time_in_force": time_in_force
        }
        response = requests.post(url, json=order_data, headers=self.headers)
        response.raise_for_status()
        data = response.json()
        # Parse the raw JSON into an Order object
        return Order(
            id=data["id"],
            symbol=data["symbol"],
            qty=data["qty"],
            side=data["side"],
            type=data["type"],
            time_in_force=data["time_in_force"],
            status=data["status"],
            filled_avg_price=data.get("filled_avg_price")
        )

What does this code do?

This method sends a POST request to /v2/orders with a JSON body containing the order details. It defaults to a market order type (fill at the current price) and day time-in-force (cancel if not filled by market close).

Notice that qty is wrapped in str(). The Alpaca API expects quantity as a string, not a number. The response is parsed into an Order object so you get a structured Python object back instead of a raw dictionary.

  • Add the get_positions and get_orders methods below place_order:
    def get_positions(self):
        # GET /v2/positions - returns all current stock holdings
        url = f"{self.trading_base_url}/v2/positions"
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        positions = []
        for p in response.json():
            positions.append(Position(
                symbol=p["symbol"],
                qty=p["qty"],
                avg_entry_price=p["avg_entry_price"],
                current_price=p["current_price"],
                market_value=p["market_value"],
                unrealized_pl=p["unrealized_pl"]
            ))
        return positions

    def get_orders(self, status="all", limit=10):
        # GET /v2/orders - returns order history filtered by status
        url = f"{self.trading_base_url}/v2/orders"
        params = {"status": status, "limit": limit}
        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        orders = []
        for o in response.json():
            orders.append(Order(
                id=o["id"],
                symbol=o["symbol"],
                qty=o["qty"],
                side=o["side"],
                type=o["type"],
                time_in_force=o["time_in_force"],
                status=o["status"],
                filled_avg_price=o.get("filled_avg_price")
            ))
        return orders

What does this code do?

Both methods follow the same pattern: make a GET request, loop through the JSON array, and wrap each item in a Python object (Position or Order). This means the rest of your code never touches raw dictionaries.

The get_orders method accepts status and limit parameters so you can filter to just open orders, filled orders, or see recent history.

  • Save trading_bot.py.
  • You won't test these three methods yet. The trading bot you build in Step 4 will call them automatically.

Seeing an IndentationError?

  • Every method inside AlpacaClient must be indented one level (4 spaces) under the class AlpacaClient: line.
  • Make sure the new methods sit inside the class, not after it. They should be at the same indentation level as get_account and get_bars.

Ask for help: why is my AlpacaClient throwing an IndentationError?

✔️ Awesome, I've got everything!

Great. Double check you've saved trading_bot.py and that the file only contains imports, the Order class, the Position class, and the AlpacaClient class with all six methods. No temporary test code should remain.

ⓧ I'd like to double check the full code

import requests
from datetime import datetime, timedelta


class Order:
    def __init__(self, id, symbol, qty, side, type, time_in_force, status, filled_avg_price=None):
        self.id = id
        self.symbol = symbol
        self.qty = qty
        self.side = side
        self.type = type
        self.time_in_force = time_in_force
        self.status = status
        self.filled_avg_price = filled_avg_price

    def __str__(self):
        return f"Order({self.side} {self.qty} {self.symbol} | status: {self.status})"


class Position:
    def __init__(self, symbol, qty, avg_entry_price, current_price, market_value, unrealized_pl):
        self.symbol = symbol
        self.qty = float(qty)
        self.avg_entry_price = float(avg_entry_price)
        self.current_price = float(current_price)
        self.market_value = float(market_value)
        self.unrealized_pl = float(unrealized_pl)

    def __str__(self):
        return f"Position({self.symbol}: {self.qty} shares | P/L: ${self.unrealized_pl:.2f})"


class AlpacaClient:
    def __init__(self, api_key, api_secret, paper=True):
        self.api_key = api_key
        self.api_secret = api_secret
        if paper:
            self.trading_base_url = "https://paper-api.alpaca.markets"
        else:
            self.trading_base_url = "https://api.alpaca.markets"
        self.data_base_url = "https://data.alpaca.markets"
        self.headers = {
            "APCA-API-KEY-ID": self.api_key,
            "APCA-API-SECRET-KEY": self.api_secret
        }

    def get_account(self):
        url = f"{self.trading_base_url}/v2/account"
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        return response.json()

    def get_bars(self, symbol, timeframe, start, end, limit=None):
        url = f"{self.data_base_url}/v2/stocks/{symbol}/bars"
        params = {
            "timeframe": timeframe,
            "start": start,
            "end": end,
            "feed": "iex"
        }
        if limit:
            params["limit"] = limit
        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        return response.json()

    def place_order(self, symbol, qty, side, order_type="market", time_in_force="day"):
        url = f"{self.trading_base_url}/v2/orders"
        order_data = {
            "symbol": symbol,
            "qty": str(qty),
            "side": side,
            "type": order_type,
            "time_in_force": time_in_force
        }
        response = requests.post(url, json=order_data, headers=self.headers)
        response.raise_for_status()
        data = response.json()
        return Order(
            id=data["id"],
            symbol=data["symbol"],
            qty=data["qty"],
            side=data["side"],
            type=data["type"],
            time_in_force=data["time_in_force"],
            status=data["status"],
            filled_avg_price=data.get("filled_avg_price")
        )

    def get_positions(self):
        url = f"{self.trading_base_url}/v2/positions"
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        positions = []
        for p in response.json():
            positions.append(Position(
                symbol=p["symbol"],
                qty=p["qty"],
                avg_entry_price=p["avg_entry_price"],
                current_price=p["current_price"],
                market_value=p["market_value"],
                unrealized_pl=p["unrealized_pl"]
            ))
        return positions

    def get_orders(self, status="all", limit=10):
        url = f"{self.trading_base_url}/v2/orders"
        params = {"status": status, "limit": limit}
        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        orders = []
        for o in response.json():
            orders.append(Order(
                id=o["id"],
                symbol=o["symbol"],
                qty=o["qty"],
                side=o["side"],
                type=o["type"],
                time_in_force=o["time_in_force"],
                status=o["status"],
                filled_avg_price=o.get("filled_avg_price")
            ))
        return orders

Your API client and data classes are complete. Next up, you'll build the trading strategy that decides when to buy and sell.

Build the Trading Strategy Classes

Your API client can talk to Alpaca and your data classes can hold the responses. But the bot still has no idea when to buy or sell.

In this step, you will build the decision-making brain of your trading bot. You will create a base Strategy class and a MovingAverageCrossover subclass that analyzes price data and returns a clear signal: buy, sell, or hold.

In this step, get ready to:
  • Create a Strategy base class that enforces a common interface for all strategies.
  • Build a MovingAverageCrossover subclass that calculates Simple Moving Averages and detects crossover signals.
  • Test your strategy with sample price data to confirm it generates buy, sell, or hold signals.
Create the Strategy base class

Before building a specific strategy, you need a blueprint that all strategies will follow. A base class defines the interface that every strategy must implement.

  • In trading_bot.py, scroll to the bottom of the file (after the AlpacaClient class) and add the Strategy base class by pasting this code:
class Strategy:
    def __init__(self, name):
        # Store the strategy name for display
        self.name = name

    def generate_signal(self, prices):
        # Force subclasses to implement their own signal logic
        raise NotImplementedError("Subclasses must implement generate_signal()")

    def __str__(self):
        return f"Strategy({self.name})"

What does this code do?

The Strategy class defines a contract that all trading strategies must follow. Every strategy needs a name and a generate_signal() method.

The NotImplementedError in generate_signal() forces any subclass to override this method with its own logic. If someone creates a strategy without implementing it, Python raises an error immediately instead of failing silently later.

What is polymorphism?

Polymorphism means different subclasses can be swapped into the same slot without changing the code that uses them. Any strategy you build later inherits from Strategy and overrides generate_signal().

The bot does not care which strategy it is running. It just calls generate_signal() and gets back "buy", "sell", or "hold".

  • Save trading_bot.py.
  • You won't see a change yet. The MovingAverageCrossover subclass you build next uses this base class.
Create the MovingAverageCrossover subclass

Now you will build a concrete strategy that inherits from Strategy and adds real trading logic based on Simple Moving Averages.

  • Below the Strategy class you just added, paste the MovingAverageCrossover subclass with its constructor and SMA calculator:
class MovingAverageCrossover(Strategy):
    def __init__(self, short_window=5, long_window=20):
        # Call the parent class constructor with a descriptive name
        super().__init__(f"SMA Crossover ({short_window}/{long_window})")
        self.short_window = short_window
        self.long_window = long_window

    def calculate_sma(self, prices, window):
        # Not enough data to calculate the average
        if len(prices) < window:
            return None
        # Average the last 'window' prices
        return sum(prices[-window:]) / window

What is a Simple Moving Average?

A Simple Moving Average (SMA) smooths out price noise by averaging the last N closing prices. A 5-day SMA reacts quickly to price changes. A 20-day SMA moves slowly.

When the fast average crosses above the slow one, it suggests upward momentum (a buy signal). When it crosses below, it suggests downward momentum (a sell signal).

What does this code do?

  • MovingAverageCrossover(Strategy) inherits from Strategy, so it must implement generate_signal(). You will add that in the next substep.
  • super().__init__() calls the parent Strategy.__init__() to set the strategy name. This is how inheritance works in Python.
  • calculate_sma() is a helper that averages the last window prices. If there are not enough prices, it returns None instead of crashing.
  • Save trading_bot.py.
  • You won't see a change yet. The next substep adds the generate_signal() method that completes the strategy.
Implement the crossover signal detection

The SMA calculator is ready, but the strategy still cannot make decisions. You need the crossover detection logic that compares today's averages against yesterday's to spot the moment the lines cross.

  • Inside the MovingAverageCrossover class, add the generate_signal() method directly below calculate_sma() by pasting this code:
    def generate_signal(self, prices):
        # Need at least long_window + 1 bars to compare current vs previous
        if len(prices) < self.long_window + 1:
            return "hold"

        # Calculate current SMAs using all prices
        short_sma = self.calculate_sma(prices, self.short_window)
        long_sma = self.calculate_sma(prices, self.long_window)

        # Calculate previous SMAs using all prices except the latest
        prev_prices = prices[:-1]
        prev_short_sma = self.calculate_sma(prev_prices, self.short_window)
        prev_long_sma = self.calculate_sma(prev_prices, self.long_window)

        if prev_short_sma is None or prev_long_sma is None:
            return "hold"

        # Golden cross: short SMA crosses ABOVE long SMA
        if prev_short_sma <= prev_long_sma and short_sma > long_sma:
            return "buy"
        # Death cross: short SMA crosses BELOW long SMA
        elif prev_short_sma >= prev_long_sma and short_sma < long_sma:
            return "sell"
        else:
            return "hold"

What does this code do?

This method detects the exact moment the two moving averages cross each other. It compares two snapshots in time: the previous bar and the current bar.

  • A golden cross happens when the short SMA was below (or equal to) the long SMA, then crosses above it. This suggests upward momentum, so it returns "buy".
  • A death cross is the opposite. The short SMA was above (or equal to) the long SMA, then drops below it. This suggests downward momentum, so it returns "sell".
  • If no crossover occurred, the method returns "hold". Most days will be hold days.
  • Save trading_bot.py.
  • Test your strategy by running this command in your terminal:
python -c "from trading_bot import MovingAverageCrossover; s = MovingAverageCrossover(); prices = [100,102,104,103,105,107,106,108,110,109,111,113,112,114,116,115,117,119,118,120,115,113,111,109,107]; print(s); print('Signal:', s.generate_signal(prices))"

You should see output showing the strategy name and a signal of sell. The sample prices trend upward and then drop sharply at the end, triggering a death cross.

Seeing an error?

  • If you see IndentationError, make sure generate_signal() is indented inside the MovingAverageCrossover class, at the same level as calculate_sma().
  • If you see ModuleNotFoundError, make sure your terminal is in the same folder as trading_bot.py.
  • If you see NotImplementedError, you may have created a Strategy() instance instead of a MovingAverageCrossover() instance.

Help me debug my MovingAverageCrossover strategy class

✔️ 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 file up to this point. Compare it with yours to make sure everything matches.

import requests
from datetime import datetime, timedelta


class Order:
    def __init__(self, id, symbol, qty, side, type, time_in_force, status, filled_avg_price=None):
        self.id = id
        self.symbol = symbol
        self.qty = qty
        self.side = side
        self.type = type
        self.time_in_force = time_in_force
        self.status = status
        self.filled_avg_price = filled_avg_price

    def __str__(self):
        return f"Order({self.side} {self.qty} {self.symbol} | status: {self.status})"


class Position:
    def __init__(self, symbol, qty, avg_entry_price, current_price, market_value, unrealized_pl):
        self.symbol = symbol
        self.qty = float(qty)
        self.avg_entry_price = float(avg_entry_price)
        self.current_price = float(current_price)
        self.market_value = float(market_value)
        self.unrealized_pl = float(unrealized_pl)

    def __str__(self):
        return f"Position({self.symbol}: {self.qty} shares | P/L: ${self.unrealized_pl:.2f})"


class AlpacaClient:
    def __init__(self, api_key, api_secret, paper=True):
        self.api_key = api_key
        self.api_secret = api_secret
        if paper:
            self.trading_base_url = "https://paper-api.alpaca.markets"
        else:
            self.trading_base_url = "https://api.alpaca.markets"
        self.data_base_url = "https://data.alpaca.markets"
        self.headers = {
            "APCA-API-KEY-ID": self.api_key,
            "APCA-API-SECRET-KEY": self.api_secret
        }

    def get_account(self):
        url = f"{self.trading_base_url}/v2/account"
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        return response.json()

    def get_bars(self, symbol, timeframe, start, end, limit=None):
        url = f"{self.data_base_url}/v2/stocks/{symbol}/bars"
        params = {
            "timeframe": timeframe,
            "start": start,
            "end": end,
            "feed": "iex"
        }
        if limit:
            params["limit"] = limit
        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        return response.json()

    def place_order(self, symbol, qty, side, order_type="market", time_in_force="day"):
        url = f"{self.trading_base_url}/v2/orders"
        order_data = {
            "symbol": symbol,
            "qty": str(qty),
            "side": side,
            "type": order_type,
            "time_in_force": time_in_force
        }
        response = requests.post(url, json=order_data, headers=self.headers)
        response.raise_for_status()
        data = response.json()
        return Order(
            id=data["id"],
            symbol=data["symbol"],
            qty=data["qty"],
            side=data["side"],
            type=data["type"],
            time_in_force=data["time_in_force"],
            status=data["status"],
            filled_avg_price=data.get("filled_avg_price")
        )

    def get_positions(self):
        url = f"{self.trading_base_url}/v2/positions"
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        positions = []
        for p in response.json():
            positions.append(Position(
                symbol=p["symbol"],
                qty=p["qty"],
                avg_entry_price=p["avg_entry_price"],
                current_price=p["current_price"],
                market_value=p["market_value"],
                unrealized_pl=p["unrealized_pl"]
            ))
        return positions

    def get_orders(self, status="all", limit=10):
        url = f"{self.trading_base_url}/v2/orders"
        params = {"status": status, "limit": limit}
        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        orders = []
        for o in response.json():
            orders.append(Order(
                id=o["id"],
                symbol=o["symbol"],
                qty=o["qty"],
                side=o["side"],
                type=o["type"],
                time_in_force=o["time_in_force"],
                status=o["status"],
                filled_avg_price=o.get("filled_avg_price")
            ))
        return orders


class Strategy:
    def __init__(self, name):
        self.name = name

    def generate_signal(self, prices):
        raise NotImplementedError("Subclasses must implement generate_signal()")

    def __str__(self):
        return f"Strategy({self.name})"


class MovingAverageCrossover(Strategy):
    def __init__(self, short_window=5, long_window=20):
        super().__init__(f"SMA Crossover ({short_window}/{long_window})")
        self.short_window = short_window
        self.long_window = long_window

    def calculate_sma(self, prices, window):
        if len(prices) < window:
            return None
        return sum(prices[-window:]) / window

    def generate_signal(self, prices):
        if len(prices) < self.long_window + 1:
            return "hold"

        short_sma = self.calculate_sma(prices, self.short_window)
        long_sma = self.calculate_sma(prices, self.long_window)

        prev_prices = prices[:-1]
        prev_short_sma = self.calculate_sma(prev_prices, self.short_window)
        prev_long_sma = self.calculate_sma(prev_prices, self.long_window)

        if prev_short_sma is None or prev_long_sma is None:
            return "hold"

        if prev_short_sma <= prev_long_sma and short_sma > long_sma:
            return "buy"
        elif prev_short_sma >= prev_long_sma and short_sma < long_sma:
            return "sell"
        else:
            return "hold"

Your trading strategy is locked and loaded. Next up, you will build the TradingBot class that combines your API client and strategy to execute a full automated trading workflow.

Build and Run the Trading Bot

You now have all the building blocks: an API client that talks to Alpaca, data classes that structure JSON responses, and a strategy class that generates trading signals from price data.

Now it is time to tie everything together into an automated trading bot. The TradingBot class will use composition to combine the client and strategy into a single workflow that fetches prices, generates signals, and places orders.

In this step, get ready to:
  • Create the TradingBot class with helper methods for fetching price data and checking positions.
  • Add the execute method that runs the full automated trading workflow.
  • Run the bot and watch it analyze AAPL stock and decide whether to trade.
Create the TradingBot class

The TradingBot does not inherit from AlpacaClient or Strategy. It contains them as components and delegates work to them.

  • In trading_bot.py, add the TradingBot class below the MovingAverageCrossover class by copying the following code:
class TradingBot:
    def __init__(self, client, strategy, symbol, trade_qty=1):
        # Store the API client, strategy, stock symbol, and quantity to trade
        self.client = client
        self.strategy = strategy
        self.symbol = symbol
        self.trade_qty = trade_qty
        self.order_history = []

    def fetch_price_data(self, days=30):
        # Calculate the date range and request daily bars from Alpaca
        end = datetime.now()
        start = end - timedelta(days=days)
        bars_data = self.client.get_bars(
            symbol=self.symbol,
            timeframe="1Day",
            start=start.strftime("%Y-%m-%d"),
            end=end.strftime("%Y-%m-%d")
        )
        # Extract the closing price from each bar into a flat list
        prices = []
        for bar in bars_data.get("bars", []):
            prices.append(bar["c"])
        return prices

    def has_position(self):
        # Check if we already hold shares of this stock
        positions = self.client.get_positions()
        for position in positions:
            if position.symbol == self.symbol:
                return True, position
        return False, None

What is composition?

Composition means a class contains other objects as attributes instead of inheriting from them. The TradingBot stores a client and a strategy as instance variables, then calls their methods when it needs API access or signal generation.

This keeps each class focused on one job. You could swap in a completely different strategy without changing the bot code at all.

What does this code do?

  • __init__ stores the client, strategy, stock symbol, trade quantity, and an empty order_history list to track any orders the bot places.
  • fetch_price_data calculates a date range (last 30 days by default), calls get_bars() on the client, and extracts the closing price ("c") from each bar into a flat list. This list feeds directly into the strategy.
  • has_position checks whether you already own shares of the bot's stock. It returns (True, position) if a match is found, or (False, None) otherwise.
  • Save trading_bot.py.
  • You won't see a change yet. The next substep adds the execute method that ties everything together into a trading workflow.
Add the execute method

The execute method is the brain of the bot. It runs the full trading workflow: check the account, fetch prices, generate a signal, and place a trade if the conditions are right.

  • Inside the TradingBot class, add the execute method below has_position by copying this code:
    def execute(self):
        # Print a header with the strategy and stock symbol
        print(f"\n{'='*50}")
        print(f"Trading Bot: {self.strategy}")
        print(f"Symbol: {self.symbol}")
        print(f"{'='*50}")

        # Fetch and display account details
        account = self.client.get_account()
        print(f"\nAccount equity: ${float(account['equity']):,.2f}")
        print(f"Buying power: ${float(account['buying_power']):,.2f}")

        # Fetch historical closing prices
        print(f"\nFetching price data for {self.symbol}...")
        prices = self.fetch_price_data()
        print(f"Retrieved {len(prices)} daily closing prices")

        if len(prices) < 2:
            print("Not enough price data to generate a signal.")
            return

        print(f"Latest price: ${prices[-1]:.2f}")

        # Generate a buy/sell/hold signal from the strategy
        signal = self.strategy.generate_signal(prices)
        print(f"\nSignal: {signal.upper()}")

        # Check if we already hold this stock
        in_position, current_position = self.has_position()

        # Place a trade based on the signal and current position
        if signal == "buy" and not in_position:
            print(f"\nPlacing BUY order: {self.trade_qty} share(s) of {self.symbol}")
            order = self.client.place_order(self.symbol, self.trade_qty, "buy")
            self.order_history.append(order)
            print(f"Order placed: {order}")
        elif signal == "sell" and in_position:
            print(f"\nPlacing SELL order: {int(current_position.qty)} share(s) of {self.symbol}")
            order = self.client.place_order(self.symbol, int(current_position.qty), "sell")
            self.order_history.append(order)
            print(f"Order placed: {order}")
        else:
            if signal == "hold":
                print("\nHolding. No trade signal.")
            elif signal == "buy" and in_position:
                print(f"\nSignal is BUY but already in position: {current_position}")
            elif signal == "sell" and not in_position:
                print("\nSignal is SELL but no position to sell.")

        # Display all current positions
        print("\n--- Current Positions ---")
        positions = self.client.get_positions()
        if positions:
            for pos in positions:
                print(f"  {pos}")
        else:
            print("  No open positions")

        # Display the 5 most recent orders
        print("\n--- Recent Orders ---")
        recent_orders = self.client.get_orders(limit=5)
        if recent_orders:
            for order in recent_orders:
                print(f"  {order}")
        else:
            print("  No recent orders")

What does this code do?

The execute method runs the entire trading loop in sequence:

  • It prints your account equity and buying power so you can see what you are working with.
  • It fetches 30 days of closing prices and passes them to the strategy's generate_signal method to get a buy, sell, or hold signal.
  • It only places a buy order if the signal is buy AND you do not already hold the stock. It only sells if the signal is sell AND you have shares to sell.
  • Finally, it prints your current positions and the 5 most recent orders for a full status report.
  • Save trading_bot.py.
  • You won't see a change yet. The next substep adds the entry point that creates the bot and runs it.
Run the bot

The bot needs an entry point that creates the client, strategy, and bot objects, then kicks off the trading workflow.

  • At the very bottom of trading_bot.py, below the TradingBot class (not inside it), add the main entry point by copying this code:
if __name__ == "__main__":
    # Replace these placeholder values with your real Alpaca API keys
    API_KEY = "your-api-key-here"
    API_SECRET = "your-api-secret-here"

    # Create the API client for paper trading
    client = AlpacaClient(API_KEY, API_SECRET, paper=True)

    # Create a Moving Average Crossover strategy with 5-day and 20-day windows
    strategy = MovingAverageCrossover(short_window=5, long_window=20)

    # Create the trading bot targeting Apple stock
    bot = TradingBot(
        client=client,
        strategy=strategy,
        symbol="AAPL",
        trade_qty=1
    )

    # Run the automated trading workflow
    bot.execute()

What does this code do?

  • if __name__ == "__main__": ensures this block only runs when you execute the script directly. It does not run if another file imports from this module.
  • It creates an AlpacaClient with your API keys and paper=True so all trades go to the simulated paper trading environment.
  • It creates a MovingAverageCrossover strategy with a 5-day short window and 20-day long window, then wires it into a TradingBot targeting AAPL.
  • Replace your-api-key-here and your-api-secret-here with the real API Key ID and Secret Key from your Alpaca dashboard.
  • Save trading_bot.py.
  • Run the bot by executing this command in your terminal:
python trading_bot.py

You should see output showing your account equity, the number of price bars retrieved, the latest price, the trading signal, and whether the bot placed an order or decided to hold.

What signal will I see?

The signal depends on AAPL's recent price movements at the time you run the script. A crossover between the 5-day and 20-day SMA does not happen every day, so HOLD is the most common signal.

If you see BUY or SELL, the bot places a real order on your paper trading account. All trades use simulated money only.

  • If the bot placed an order, open your Alpaca paper trading dashboard at https://app.alpaca.markets to confirm the order appears.

Running outside market hours?

US stock market Regular Trading Hours are 9:30am to 4:00pm ET, Monday to Friday. Your bot uses market orders with time_in_force set to day.

If you run the bot after 4:00pm ET, any order will be accepted but queued for the next trading day. If it is a Friday evening or holiday, the order will sit until the market opens again.

Seeing an error when running the bot?

  • If you see a 403 or 401 error, double check that you pasted your API Key ID and Secret Key correctly. The secret key is only shown once when you generate it.
  • If you see an IndentationError, make sure the execute method is indented inside the TradingBot class, and the if __name__ block is at the top level (no indentation).
  • If you see no price data, make sure you are requesting bars for a stock that trades on the IEX exchange feed. AAPL should always return bars.

Help me debug my trading bot error

✔️ Awesome, I've got everything!

Great. Double check you have saved trading_bot.py with your latest changes.

ⓧ I'd like to double check the full code

import requests
from datetime import datetime, timedelta


class Order:
    def __init__(self, id, symbol, qty, side, type, time_in_force, status, filled_avg_price=None):
        self.id = id
        self.symbol = symbol
        self.qty = qty
        self.side = side
        self.type = type
        self.time_in_force = time_in_force
        self.status = status
        self.filled_avg_price = filled_avg_price

    def __str__(self):
        return f"Order({self.side} {self.qty} {self.symbol} | status: {self.status})"


class Position:
    def __init__(self, symbol, qty, avg_entry_price, current_price, market_value, unrealized_pl):
        self.symbol = symbol
        self.qty = float(qty)
        self.avg_entry_price = float(avg_entry_price)
        self.current_price = float(current_price)
        self.market_value = float(market_value)
        self.unrealized_pl = float(unrealized_pl)

    def __str__(self):
        return f"Position({self.symbol}: {self.qty} shares | P/L: ${self.unrealized_pl:.2f})"


class AlpacaClient:
    def __init__(self, api_key, api_secret, paper=True):
        self.api_key = api_key
        self.api_secret = api_secret
        if paper:
            self.trading_base_url = "https://paper-api.alpaca.markets"
        else:
            self.trading_base_url = "https://api.alpaca.markets"
        self.data_base_url = "https://data.alpaca.markets"
        self.headers = {
            "APCA-API-KEY-ID": self.api_key,
            "APCA-API-SECRET-KEY": self.api_secret
        }

    def get_account(self):
        url = f"{self.trading_base_url}/v2/account"
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        return response.json()

    def get_bars(self, symbol, timeframe, start, end, limit=None):
        url = f"{self.data_base_url}/v2/stocks/{symbol}/bars"
        params = {
            "timeframe": timeframe,
            "start": start,
            "end": end,
            "feed": "iex"
        }
        if limit:
            params["limit"] = limit
        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        return response.json()

    def place_order(self, symbol, qty, side, order_type="market", time_in_force="day"):
        url = f"{self.trading_base_url}/v2/orders"
        order_data = {
            "symbol": symbol,
            "qty": str(qty),
            "side": side,
            "type": order_type,
            "time_in_force": time_in_force
        }
        response = requests.post(url, json=order_data, headers=self.headers)
        response.raise_for_status()
        data = response.json()
        return Order(
            id=data["id"],
            symbol=data["symbol"],
            qty=data["qty"],
            side=data["side"],
            type=data["type"],
            time_in_force=data["time_in_force"],
            status=data["status"],
            filled_avg_price=data.get("filled_avg_price")
        )

    def get_positions(self):
        url = f"{self.trading_base_url}/v2/positions"
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        positions = []
        for p in response.json():
            positions.append(Position(
                symbol=p["symbol"],
                qty=p["qty"],
                avg_entry_price=p["avg_entry_price"],
                current_price=p["current_price"],
                market_value=p["market_value"],
                unrealized_pl=p["unrealized_pl"]
            ))
        return positions

    def get_orders(self, status="all", limit=10):
        url = f"{self.trading_base_url}/v2/orders"
        params = {"status": status, "limit": limit}
        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()
        orders = []
        for o in response.json():
            orders.append(Order(
                id=o["id"],
                symbol=o["symbol"],
                qty=o["qty"],
                side=o["side"],
                type=o["type"],
                time_in_force=o["time_in_force"],
                status=o["status"],
                filled_avg_price=o.get("filled_avg_price")
            ))
        return orders


class Strategy:
    def __init__(self, name):
        self.name = name

    def generate_signal(self, prices):
        raise NotImplementedError("Subclasses must implement generate_signal()")

    def __str__(self):
        return f"Strategy({self.name})"


class MovingAverageCrossover(Strategy):
    def __init__(self, short_window=5, long_window=20):
        super().__init__(f"SMA Crossover ({short_window}/{long_window})")
        self.short_window = short_window
        self.long_window = long_window

    def calculate_sma(self, prices, window):
        if len(prices) < window:
            return None
        return sum(prices[-window:]) / window

    def generate_signal(self, prices):
        if len(prices) < self.long_window + 1:
            return "hold"

        short_sma = self.calculate_sma(prices, self.short_window)
        long_sma = self.calculate_sma(prices, self.long_window)

        prev_prices = prices[:-1]
        prev_short_sma = self.calculate_sma(prev_prices, self.short_window)
        prev_long_sma = self.calculate_sma(prev_prices, self.long_window)

        if prev_short_sma is None or prev_long_sma is None:
            return "hold"

        if prev_short_sma <= prev_long_sma and short_sma > long_sma:
            return "buy"
        elif prev_short_sma >= prev_long_sma and short_sma < long_sma:
            return "sell"
        else:
            return "hold"


class TradingBot:
    def __init__(self, client, strategy, symbol, trade_qty=1):
        self.client = client
        self.strategy = strategy
        self.symbol = symbol
        self.trade_qty = trade_qty
        self.order_history = []

    def fetch_price_data(self, days=30):
        end = datetime.now()
        start = end - timedelta(days=days)
        bars_data = self.client.get_bars(
            symbol=self.symbol,
            timeframe="1Day",
            start=start.strftime("%Y-%m-%d"),
            end=end.strftime("%Y-%m-%d")
        )
        prices = []
        for bar in bars_data.get("bars", []):
            prices.append(bar["c"])
        return prices

    def has_position(self):
        positions = self.client.get_positions()
        for position in positions:
            if position.symbol == self.symbol:
                return True, position
        return False, None

    def execute(self):
        print(f"\n{'='*50}")
        print(f"Trading Bot: {self.strategy}")
        print(f"Symbol: {self.symbol}")
        print(f"{'='*50}")

        account = self.client.get_account()
        print(f"\nAccount equity: ${float(account['equity']):,.2f}")
        print(f"Buying power: ${float(account['buying_power']):,.2f}")

        print(f"\nFetching price data for {self.symbol}...")
        prices = self.fetch_price_data()
        print(f"Retrieved {len(prices)} daily closing prices")

        if len(prices) < 2:
            print("Not enough price data to generate a signal.")
            return

        print(f"Latest price: ${prices[-1]:.2f}")

        signal = self.strategy.generate_signal(prices)
        print(f"\nSignal: {signal.upper()}")

        in_position, current_position = self.has_position()

        if signal == "buy" and not in_position:
            print(f"\nPlacing BUY order: {self.trade_qty} share(s) of {self.symbol}")
            order = self.client.place_order(self.symbol, self.trade_qty, "buy")
            self.order_history.append(order)
            print(f"Order placed: {order}")
        elif signal == "sell" and in_position:
            print(f"\nPlacing SELL order: {int(current_position.qty)} share(s) of {self.symbol}")
            order = self.client.place_order(self.symbol, int(current_position.qty), "sell")
            self.order_history.append(order)
            print(f"Order placed: {order}")
        else:
            if signal == "hold":
                print("\nHolding. No trade signal.")
            elif signal == "buy" and in_position:
                print(f"\nSignal is BUY but already in position: {current_position}")
            elif signal == "sell" and not in_position:
                print("\nSignal is SELL but no position to sell.")

        print("\n--- Current Positions ---")
        positions = self.client.get_positions()
        if positions:
            for pos in positions:
                print(f"  {pos}")
        else:
            print("  No open positions")

        print("\n--- Recent Orders ---")
        recent_orders = self.client.get_orders(limit=5)
        if recent_orders:
            for order in recent_orders:
                print(f"  {order}")
        else:
            print("  No recent orders")


if __name__ == "__main__":
    API_KEY = "your-api-key-here"
    API_SECRET = "your-api-secret-here"

    client = AlpacaClient(API_KEY, API_SECRET, paper=True)

    strategy = MovingAverageCrossover(short_window=5, long_window=20)

    bot = TradingBot(
        client=client,
        strategy=strategy,
        symbol="AAPL",
        trade_qty=1
    )

    bot.execute()

Your trading bot is live and making decisions on its own. Next up, take on the Secret Mission to build a multi-stock scanner that ranks several stocks by signal strength.

Secret mission

Build a Multi-Stock Scanner with Custom Sorting

Your bot analyzes one stock at a time. What if you could scan an entire watchlist of stocks and rank them all by momentum strength? In this secret mission, you will build a StockScanner class that analyzes multiple stocks in sequence, calculates a quantitative signal strength metric, and sorts the results using a hand-coded insertion sort algorithm.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses Alpaca's free paper trading API, so there are no ongoing costs. Decide whether to keep your resources as-is, pause them to come back later, or delete them entirely.

Resources you used:

  • trading_bot.py file on your local machine.
  • Alpaca paper trading account (free, no real money involved).
  • requests Python library installed via pip.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your trading bot or building new strategies.

  • Your trading_bot.py file stays on your machine and is ready to run anytime.
  • Your Alpaca paper trading account remains active with your simulated balance and order history.
  • The requests library stays installed in your Python environment.
  • Your API keys are still stored as plain text in trading_bot.py. Consider moving them to environment variables if you plan to share or version-control your code.

Pause - I'll come back to this later

Free up your API key exposure while keeping your files and account intact.

  • Open trading_bot.py and replace your real API key and secret values with the placeholder strings your-api-key-here and your-api-secret-here. This prevents accidental use while you are away.
  • Your Alpaca paper trading account stays active. You can regenerate new API keys from the dashboard when you return.
  • Your trading_bot.py file and the requests library remain on your machine.

Delete - I don't want to use this again

Remove all project resources and clean up your accounts.

  • Delete your local project file by running:

macOS

rm trading_bot.py

Windows

del trading_bot.py
  • Uninstall the requests library by running:
pip uninstall requests
  • Delete your Alpaca paper trading account. Go to https://app.alpaca.markets and sign in.
  • Click the paper account number in the upper left corner of the dashboard.
  • Select Account Settings.
  • Click the Delete Account button next to your paper account.

Nice Work!

Nice Work!

Nice work! You just built a fully functional automated trading bot that connects to a real brokerage API, analyzes stock prices with a mathematical strategy, and places simulated trades on a $100,000 paper trading account.

Here's what you accomplished:

  • Built an AlpacaClient class that makes authenticated REST API calls (GET and POST) to Alpaca's paper trading endpoints, and created Order and Position data classes that parse JSON responses into structured Python objects.
  • Built a Strategy base class and a MovingAverageCrossover subclass demonstrating inheritance, polymorphism, and Simple Moving Average calculation for generating buy, sell, and hold signals.
  • Built a TradingBot class using composition to tie the client and strategy together into an automated trading workflow that places simulated trades on a $100,000 paper trading account.
  • Secret Mission: Built a StockScanner that analyzes multiple stocks and ranks them by signal strength using a hand-coded insertion sort.

Ready to quiz yourself?