DEV Community

Timevolt
Timevolt

Posted on

šŸ“ˆ The Jedi’s Guide to Building a Python Stock‑Market Trading Bot

(or: How I Turned My Laptop Into a Lightsaber for the Market)

The Quest Begins (The ā€œWhyā€)

Ever stared at a blinking cursor at 2 a.m., wishing you could make your laptop do the heavy lifting while you chased dreams (or just caught up on sleep)? I was there, scrolling through Reddit’s r/investing, watching folks brag about ā€œalgo‑trading gainsā€ while I was still manually refreshing Yahoo Finance like a peasant in a medieval market.

One night, after yet another failed attempt to predict a stock’s move with gut feeling (spoiler: my gut is terrible at math), I remembered a line from The Matrix: ā€œThere is no spoon.ā€ Turns out, there is no magic either—just code, data, and a healthy dose of stubbornness. I decided to slay the dragon of emotion‑driven trading and build a bot that could execute a simple strategy while I binge‑watched Stranger Things.

Spoiler alert: the first version was a hot mess, but the journey taught me more about Python, APIs, and risk management than any textbook ever could. Let’s walk through that adventure together—code, pitfalls, and all the triumphant ā€œI‑did‑it!ā€ moments.

The Revelation (The Insight)

The big ā€œaha!ā€ moment came when I realized a trading bot isn’t some omniscient AI that predicts the future; it’s just a disciplined executor of rules you define. Think of it as Indiana Jones whip‑cracking through a booby‑trapped temple: you set the traps (your strategy), the bot avoids them (risk checks), and grabs the idol (profit) when the conditions are right.

For my first bot I chose a mean‑reversion idea: if a stock’s price deviates too far from its 20‑day moving average, I bet it’ll snap back. It’s not flashy, but it’s easy to understand, back‑test, and implement. The magic happens in three simple steps:

  1. Fetch data – pull recent price bars from a free API (I used Alpha Vantage; you can swap for Polygon, IEX Cloud, etc.).
  2. Calculate the signal – compare the latest close to the moving average and compute a z‑score.
  3. Execute – if the z‑score crosses a threshold, send a market order via a brokerage’s API (I used Alpaca for paper trading).

That’s it. No neural nets, no secret sauce—just a loop that checks the condition every minute and acts.

Wielding the Power (Code & Examples)

The Struggle: Hard‑coded Values & No Error Handling

My first attempt looked like this (don’t use this in production—seriously, it’s a trap!):

# WARNING: This is the "before" code – a classic rookie trap!
import requests, time

API_KEY = "YOUR_ALPHA_VANTAGE_KEY"
SYMBOL = "AAPL"
AVG_WINDOW = 20
THRESHOLD = 1.5   # z‑score trigger

def get_price():
    url = f"https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol={SYMBOL}&interval=1min&apikey={API_KEY}"
    data = requests.get(url).json()
    # Assume the latest minute is the first key in the nested dict
    latest = list(data["Time Series (1min)"].values())[0]
    return float(latest["4. close"])

def simple_moving_average(prices, window):
    return sum(prices[-window:]) / window

def z_score(price, avg, std):
    return (price - avg) / std if std != 0 else 0

prices = []
while True:
    price = get_price()
    prices.append(price)
    if len(prices) >= AVG_WINDOW:
        avg = simple_moving_average(prices, AVG_WINDOW)
        # naive std dev
        variance = sum((p - avg) ** 2 for p in prices[-AVG_WINDOW:]) / AVG_WINDOW
        std = variance ** 0.5
        z = z_score(price, avg, std)
        if z > THRESHOLD:
            print(f"SELL signal! Z={z:.2f}")
        elif z < -THRESHOLD:
            print(f"BUY signal! Z={z:.2f}")
    time.sleep(60)   # wait a minute
Enter fullscreen mode Exit fullscreen mode

What went wrong?

  • The script assumes the JSON structure never changes – a single API format tweak and it crashes.
  • No exception handling: network hiccups send the whole loop into oblivion.
  • Standard deviation calculated on a tiny window is noisy; we got false signals left and right.
  • No order execution – just prints.

I felt like Neo trying to dodge bullets in The Matrix but getting hit by every stray bug.

The Victory: Robust, Modular, and Ready for Paper Trading

After a few hours of debugging (and a healthy dose of caffeine), I refactored the bot into bite‑sized pieces, added proper error handling, and hooked it up to Alpaca’s paper‑trading API. Here’s the ā€œafterā€ version—still simple, but now it won’t explode at the first hiccup.

# -------------------------------------------------
#  Simple Mean‑Reversion Bot – Production‑ish Version
# -------------------------------------------------
import os, time, logging, requests
import pandas as pd
import alpaca_trade_api as tradeapi

# ------------------- CONFIG -------------------
API_KEY_ALPHA = os.getenv("ALPHA_KEY")          # Alpha Vantage
API_KEY_ALPACA = os.getenv("APCA_API_KEY_ID")
API_SECRET_ALPACA = os.getenv("APCA_API_SECRET_KEY")
BASE_URL = "https://paper-api.alpaca.markets"   # switch to live when ready
SYMBOL = "AAPL"
AVG_WINDOW = 20
Z_THRESHOLD = 1.5
QUANTITY = 1                                    # shares per trade
SLEEP_SECONDS = 60
# ------------------------------------------------

logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s %(levelname)s %(message)s")

# ----- Alpha Vantage Helper -----
def fetch_recent_bars(limit=100):
    """Return a DataFrame with the last `limit` minute bars for SYMBOL."""
    url = ("https://www.alphavantage.co/query"
           f"?function=TIME_SERIES_INTRADAY&symbol={SYMBOL}"
           f"&interval=1min&outputsize=compact&apikey={API_KEY_ALPHA}")
    try:
        resp = requests.get(url, timeout=10)
        resp.raise_for_status()
        data = resp.json()
        # The key we need changes based on interval; for 1min it's:
        ts_key = "Time Series (1min)"
        if ts_key not in data:
            logging.error("Unexpected API response: %s", data)
            return pd.DataFrame()
        df = pd.DataFrame.from_dict(data[ts_key], orient="index")
        df = df.rename(columns={
            "1. open": "open",
            "2. high": "high",
            "3. low": "low",
            "4. close": "close",
            "5. volume": "volume"
        })
        df = df.astype(float)
        df.index = pd.to_datetime(df.index)
        return df.sort_index().tail(limit)
    except Exception as e:
        logging.exception("Error fetching data: %s", e)
        return pd.DataFrame()

# ----- Alpaca Wrapper -----
alpaca = tradeapi.REST(API_KEY_ALPACA, API_SECRET_ALPACA, BASE_URL, api_version='v2')

def place_order(side):
    """Submit a market order via Alpaca."""
    try:
        alpaca.submit_order(
            symbol=SYMBOL,
            qty=QUANTITY,
            side=side,
            type='market',
            time_in_force='day'
        )
        logging.info("Placed %s order for %s shares of %s", side.upper(), QUANTITY, SYMBOL)
    except Exception as e:
        logging.exception("Order failed: %s", e)

# ----- Strategy Core -----
def evaluate_signal(df):
    """Return 'buy', 'sell', or None based on z‑score of close vs SMA."""
    if len(df) < AVG_WINDOW:
        return None
    close = df['close']
    sma = close.rolling(window=AVG_WINDOW).mean().iloc[-1]
    std = close.rolling(window=AVG_WINDOW).std().iloc[-1]
    if std == 0 or pd.isna(std):
        return None
    z = (close.iloc[-1] - sma) / std
    logging.debug("Latest close: %.2f, SMA: %.2f, STD: %.2f, Z: %.2f",
                  close.iloc[-1], sma, std, z)
    if z > Z_THRESHOLD:
        return "sell"
    if z < -Z_THRESHOLD:
        return "buy"
    return None

# ----- Main Loop -----
def main():
    logging.info("Starting mean‑reversion bot for %s", SYMBOL)
    while True:
        df = fetch_recent_bars()
        if df.empty:
            logging.warning("No data retrieved; skipping this cycle.")
            time.sleep(SLEEP_SECONDS)
            continue

        signal = evaluate_signal(df)
        if signal == "buy":
            place_order("buy")
        elif signal == "sell":
            place_order("sell")
        else:
            logging.info("No signal this round.")

        time.sleep(SLEEP_SECONDS)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        logging.info("Bot stopped by user.")
    except Exception as e:
        logging.exception("Unexpected error: %s", e)
Enter fullscreen mode Exit fullscreen mode

Key improvements that turned the struggle into a win:

  • Defensive data fetching – we check for the expected JSON key, timeout, and wrap everything in a try/except.
  • Pandas for rolling stats – no more manual variance loops; .rolling() gives us reliable SMA and STD.
  • Logging instead of print – timestamps, levels, and debug info make troubleshooting a breeze.
  • Modular functions – each piece (data, signal, order) can be unit‑tested or swapped out.
  • Environment‑based secrets – no hard‑coded keys; keep them out of git.

Running this on a VPS (or even your laptop) with Alpaca’s paper account lets you watch the bot submit buy/sell orders in real time—no real money at risk, just pure adrenaline when you see that first order fill.

Why This New Power Matters

Now you’ve got a living, breathing algorithm that follows your rules without getting tired, scared, or greedy. Imagine extending it:

  • Add a stop‑loss or take‑profit order via Alpaca’s bracket orders.
  • Swap the mean‑reversion logic for a breakout or momentum signal—just replace evaluate_signal.
  • Pull data from multiple timeframes (e.g., 5‑minute trend filter + 1‑minute entry).
  • Deploy to a cloud scheduler (AWS Lambda, Google Cloud Run, or a simple cron job) and let it run 24/7 while you focus on learning, building side projects, or finally finishing that Netflix series.

The best part? You’ve demystified the black box. You’re not blindly trusting a ā€œguru’sā€ signal; you wrote the logic, you see the data, and you control the risk. That feeling—when the bot executes a trade exactly as you planned—is like pulling the perfect swing in The Matrix and watching the bullets freeze mid‑air.

So go ahead, clone the repo, drop in your API keys, set SYMBOL to your favorite stock, and watch the magic happen.

Your Next Challenge

Pick one improvement from the list above and implement it this week. Maybe it’s adding a simple stop‑loss order, or maybe it’s pulling in a second indicator like RSI. Share your results in the comments—or better yet, write your own Dev.to post about the journey.

Remember: the market is a beast, but with a little code, discipline, and a dash of Jedi patience, you can turn it into your training ground. May the force (and good risk management) be with you! šŸš€


Happy coding, and may your trades be ever in your favor!

Top comments (0)