DEV Community

Timevolt
Timevolt

Posted on

How to Build a Stock Market Trading Bot in Python: A Jedi’s Guide to Algorithmic Trading

The Quest Begins (The “Why”)

Honestly, I was just trying to stop myself from staring at candlestick charts at 2 a.m. while my coffee went cold. I kept thinking, “If only I could teach a script to do the boring part—watch the markets, spot a pattern, and fire off a trade—so I could actually get some sleep.” It started as a joke, but the more I poked around, the more I realized the real dragon wasn’t the volatility of the market; it was my own tendency to over‑trade, chase every tick, and end up with a red‑inked P&L statement that looked like a battlefield after a lightsaber duel.

I wanted a bot that would be disciplined, follow a clear rule set, and—most importantly—let me step away from the screen without feeling like I’d abandoned my post. If you’ve ever felt like you’re stuck in a loop of “buy high, sell low” because you’re reacting to noise instead of signal, you know exactly what I mean.

The Revelation (The Insight)

The breakthrough came when I stopped trying to predict the next price tick and started focusing on trend confirmation. A simple moving‑average (SMA) crossover turned out to be the “lightsaber” I needed: when the short‑term SMA crosses above the long‑term SMA, the market is showing bullish momentum; when it crosses below, it’s bearish. It’s not magic, but it’s a reliable signal that filters out a lot of the chatter.

Pair that with a basic risk‑management rule—only risking a fixed % of equity per trade—and you have a strategy that’s easy to understand, easy to code, and, crucially, easy to backtest. The real power wasn’t in the algorithm itself; it was in the discipline it enforced.

Wielding the Power (Code & Examples)

The Struggle (First Attempt)

My first version was… let’s call it a “young Padawan” attempt. I grabbed the latest price, compared it to the price five minutes ago, and if it was up I bought, if it was down I sold. Spoiler: it traded on every tiny wiggle, racked up commissions, and left me with a negative balance faster than a Sith lord draining the Force.

# 🚫 Naïve version – DON’T DO THIS
import yfinance as yf
import pandas as pd

def naive_bot(ticker):
    data = yf.download(ticker, period="5d", interval="5m")
    data['signal'] = 0
    data.loc[data['Close'] > data['Close'].shift(1), 'signal'] = 1   # buy
    data.loc[data['Close'] < data['Close'].shift(1), 'signal'] = -1 # sell
    return data[['Close', 'signal']]

print(naive_bot("AAPL").tail())
Enter fullscreen mode Exit fullscreen mode

The problem? No trend filter, no position sizing, and no regard for how often we were hitting the API. It was like trying to deflect blaster bolts with a toothpick.

The Victory (Refined Bot)

Now let’s upgrade to a proper Jedi‑level bot. We’ll:

  1. Pull daily data (enough to smooth out intraday noise).
  2. Calculate a 20‑day SMA and a 50‑day SMA.
  3. Generate a long signal when SMA20 > SMA50 and we’re not already in a position.
  4. Generate a short (or exit) signal when SMA20 < SMA50.
  5. Size each trade to risk 1 % of equity (assuming a $10 k account).
# ✅ Jedi‑approved SMA crossover bot
import yfinance as yf
import pandas as pd

def fetch_data(ticker, period="600d", interval="1d"):
    """Pull historical data; adjust period as needed."""
    return yf.download(ticker, period=period, interval=interval)

def add_signals(df, short_window=20, long_window=50):
    df = df.copy()
    df['SMA_short'] = df['Close'].rolling(window=short_window).mean()
    df['SMA_long']  = df['Close'].rolling(window=long_window).mean()
    df['signal'] = 0
    # Bullish crossover
    df.loc[df['SMA_short'] > df['SMA_long'], 'signal'] = 1
    # Bearish crossover (or exit)
    df.loc[df['SMA_short'] < df['SMA_long'], 'signal'] = -1
    # Keep only where signal actually changes to avoid repeated orders
    df['position'] = df['signal'].diff()
    return df

def risk_based_size(equity, risk_per_trade=0.01, stop_loss_pct=0.03):
    """
    Return number of shares to buy so that a stop‑loss hit
    would lose roughly `risk_per_trade` of equity.
    """
    dollar_risk = equity * risk_per_trade
    share_risk = equity * stop_loss_pct   # approximate $ loss per share if price drops stop_loss_pct
    return int(dollar_risk / share_risk) if share_risk > 0 else 0

def run_bot(ticker, equity=10_000):
    df = fetch_data(ticker)
    df = add_signals(df)

    position = 0   # 0 = flat, 1 = long
    trades = []

    for idx, row in df.iterrows():
        # Enter long on fresh bullish crossover
        if row['position'] == 2 and position == 0:   # signal went from -1 to 1
            size = risk_based_size(equity)
            entry_price = row['Close']
            stop_loss = entry_price * (1 - 0.03)   # 3 % stop
            position = 1
            trades.append({
                'type': 'buy',
                'date': idx,
                'price': entry_price,
                'size': size,
                'stop_loss': stop_loss
            })
            print(f"[{idx}] BUY {size} shares @ {entry_price:.2f}")

        # Exit on bearish crossover or stop‑loss hit
        if position == 1:
            # stop‑loss check
            if row['Low'] <= row['stop_loss']:
                exit_price = row['stop_loss']
                position = 0
                trades.append({
                    'type': 'sell_stop',
                    'date': idx,
                    'price': exit_price,
                    'size': size,
                    'pnl': (exit_price - entry_price) * size
                })
                print(f"[{idx}] STOP‑LOSS @ {exit_price:.2f} – P&L: {(exit_price - entry_price)*size:.2f}")
                continue

            # signal exit
            if row['position'] == -2:   # signal went from 1 to -1
                exit_price = row['Close']
                position = 0
                trades.append({
                    'type': 'sell_signal',
                    'date': idx,
                    'price': exit_price,
                    'size': size,
                    'pnl': (exit_price - entry_price) * size
                })
                print(f"[{idx}] SELL (signal) @ {exit_price:.2f} – P&L: {(exit_price - entry_price)*size:.2f}")

    return pd.DataFrame(trades)

# Example run
results = run_bot("AAPL", equity=10_000)
print("\n=== Trade Summary ===")
print(results[['type','date','price','size','pnl']].to_string(index=False))
Enter fullscreen mode Exit fullscreen mode

What changed?

  • We added a trend filter (SMA crossover) that ignores random noise.
  • Position sizing is now tied to a fixed risk percent, preventing overexposure.
  • We included a simple stop‑loss (3 %) to cap downside.
  • The loop only acts when the signal actually changes, cutting down on needless orders.

Traps to Avoid (The “Bosses” on Your Quest)

  1. Over‑fitting to historical data – It’s tempting to tweak windows until the backtest looks perfect, but that curve‑fit will evaporate in live markets. Keep your parameters simple and validate on out‑of‑sample data.
  2. Ignoring execution costs – Commissions, slippage, and market impact can turn a seemingly profitable strategy into a loss maker. Always subtract a realistic per‑trade cost (e.g., $1) when evaluating performance.

Run the script on a few different tickers, compare the equity curve, and you’ll start to see how a disciplined rule‑based approach smooths out the madness of raw price action.

Why This New Power Matters

Now you’ve got a bot that doesn’t just react to every tick—it waits for the market to show a clear direction, risks a controlled amount per trade, and steps aside when the trend reverses. That means you can step away from the screen, let the code do the heavy lifting, and focus on the parts of trading you actually enjoy: strategy refinement, risk assessment, and maybe even learning a new pattern.

Imagine being able to run this bot across a watchlist of 20 stocks, get a daily email of signals, and only intervene when you want to tweak the risk parameters. It’s like having a trusty droid that handles the repetitive scans while you focus on the big‑picture campaign—your own personal R2‑D2 in the trading trenches.

Your Turn: The Next Challenge

Your mission, should you choose to accept it, is to take this skeleton and make it yours:

  • Add a trailing stop that locks in profits as the trade moves in your favor.
  • Experiment with different SMA windows or even an EMA crossover—see how the character of the signal changes.
  • Most importantly, paper‑trade it for a week. Watch how the bot behaves in real‑time without risking capital, then compare the results to your manual trades.

What tweak gave you the biggest edge? Did you discover a hidden “force” in the data that made the bot sing? Share your findings in the comments—I’d love to hear how your Jedi training went!

May your algorithms be strong and your drawdowns low. Happy coding! 🚀

Top comments (0)