The Quest Begins (The "Why")
Honestly, I was tired of staring at candlestick charts while my coffee went cold. I kept thinking, “What if I could automate the boring parts and let the machine do the heavy lifting while I focus on strategy?” It started as a lazy Sunday experiment—just a script that pulled the latest price for AAPL and printed it to the console. When I saw that number update in real‑time, I felt a tiny rush, like Neo dodging bullets in the Matrix for the first time. That moment sparked the question: Can I turn this into a real trading bot that actually places orders? The dragon I wanted to slay was the endless manual monitoring and the fear of missing a move because I blinked.
The Revelation (The Insight)
The big “aha!” came when I stopped trying to build a monolithic script that did everything at once. Instead, I broke the bot into three clear layers:
- Data fetcher – pulls market data from a reliable API (I used Alpaca’s free tier for paper trading).
- Signal generator – applies a simple strategy (e.g., moving‑average crossover) and returns a signal: buy, sell, or hold.
- Executor – takes the signal and sends the appropriate order to the brokerage.
By separating concerns, debugging became a breeze. If the price feed was wrong, I only touched the fetcher. If the logic felt off, I tweaked the signal generator without worrying about accidentally sending a market order. This modular approach also made it easy to swap out the strategy later—today a moving‑average crossover, tomorrow a machine‑learning model—without rewriting the whole thing.
Wielding the Power (Code & Examples)
The Struggle (Before)
My first attempt was a single, tangled script:
import requests, time
API_KEY = "YOUR_KEY"
API_SECRET = "YOUR_SECRET"
BASE_URL = "https://paper-api.alpaca.markets"
def get_price(symbol):
r = requests.get(f"{BASE_URL}/v2/stocks/{symbol}/trades/latest",
headers={"APCA-API-KEY-ID": API_KEY,
"APCA-API-SECRET-KEY": API_SECRET})
return r.json()["trade"]["price"]
def simple_ma(symbol):
# fetch last 20 closes, compute MA, compare to current price
closes = []
for _ in range(20):
closes.append(get_price(symbol))
time.sleep(1) # naive sleep to avoid rate limits
ma = sum(closes) / len(closes)
price = get_price(symbol)
return "buy" if price > ma * 1.001 else "sell" if price < ma * 0.999 else "hold"
while True:
signal = simple_ma("AAPL")
if signal == "buy":
requests.post(f"{BASE_URL}/v2/orders",
json={"symbol":"AAPL","qty":1,"side":"buy","type":"market","time_in_force":"gtc"},
headers={"APCA-API-KEY-ID": API_KEY,
"APCA-API-SECRET-KEY": API_SECRET})
elif signal == "sell":
requests.post(f"{BASE_URL}/v2/orders",
json={"symbol":"AAPL","qty":1,"side":"sell","type":"market","time_in_force":"gtc"},
headers={"APCA-API-KEY-ID": API_KEY,
"APCA-API-SECRET-KEY": API_SECRET})
time.sleep(60)
What went wrong?
- Tight coupling: The price‑fetching logic lived inside the signal function, making it impossible to test the strategy without hitting the API.
-
Blocking sleeps:
time.sleep(1)inside a loop wasted time and made the bot unresponsive. - No error handling: A single network hiccup crashed the whole loop.
- Hard‑coded qty: Always 1 share—no position sizing.
The Victory (After)
Here’s the refactored version, split into three modules (but shown together for brevity). I kept it short enough to copy‑paste, yet each piece is testable in isolation.
# config.py
API_KEY = "YOUR_KEY"
API_SECRET = "YOUR_SECRET"
BASE_URL = "https://paper-api.alpaca.markets"
HEADERS = {
"APCA-API-KEY-ID": API_KEY,
"APCA-API-SECRET-KEY": API_SECRET,
}
# data_fetcher.py
import requests
from config import BASE_URL, HEADERS
def get_latest_price(symbol: str) -> float:
url = f"{BASE_URL}/v2/stocks/{symbol}/trades/latest"
resp = requests.get(url, headers=HEADERS)
resp.raise_for_status() # <-- trap: forgetting this hides HTTP errors
return resp.json()["trade"]["price"]
def get_recent_closes(symbol: str, limit: int = 20) -> list[float]:
url = f"{BASE_URL}/v2/stocks/{symbol}/bars?timeframe=1Day&limit={limit}"
resp = requests.get(url, headers=HEADERS)
resp.raise_for_status()
return [bar["c"] for bar in resp.json()["bars"]]
# signal.py
def moving_average_crossover(closes: list[float], current_price: float,
short_window: int = 5, long_window: int = 20) -> str:
if len(closes) < long_window:
return "hold"
short_ma = sum(closes[-short_window:]) / short_window
long_ma = sum(closes[-long_window:]) / long_window
# Simple rule: buy when short MA crosses above long MA with a tiny buffer
if short_ma > long_ma * 1.0005:
return "buy"
if short_ma < long_ma * 0.9995:
return "sell"
return "hold"
# executor.py
import requests
from config import BASE_URL, HEADERS
def place_order(symbol: str, qty: int, side: str):
url = f"{BASE_URL}/v2/orders"
payload = {
"symbol": symbol,
"qty": qty,
"side": side,
"type": "market",
"time_in_force": "gtc"
}
resp = requests.post(url, json=payload, headers=HEADERS)
try:
resp.raise_for_status()
except requests.HTTPError as e:
# trap: swallowing the exception silently makes debugging hell
print(f"Order failed: {e.response.text}")
raise
return resp.json()
# main.py – the orchestration loop
import time
from data_fetcher import get_latest_price, get_recent_closes
from signal import moving_average_crossover
from executor import place_order
SYMBOL = "AAPL"
QTY = 10 # now we can size the position sensibly
while True:
try:
price = get_latest_price(SYMBOL)
closes = get_recent_closes(SYMBOL)
signal = moving_average_crossover(closes, price)
print(f"{time.strftime('%X')} | Price: {price:.2f} | Signal: {signal}")
if signal == "buy":
place_order(SYMBOL, QTY, "buy")
elif signal == "sell":
place_order(SYMBOL, QTY, "sell")
except Exception as exc:
# In a real bot you'd push this to a logging service or alert channel
print(f"Error in loop: {exc}")
time.sleep(30) # poll every 30 seconds – respects rate limits and keeps CPU happy
Key improvements:
- Separation of concerns makes unit‑testing the signal generator trivial (just feed it a list of numbers).
-
Explicit error handling (
raise_for_status) means we see HTTP problems instantly instead of a silent failure. - Configurable quantity lets us experiment with position sizing without touching the core logic.
- Reasonable sleep interval avoids hammering the API and gives us breathing room to observe the bot’s behavior.
Common Traps to Watch
- Ignoring API rate limits – Sending a request every second will get you throttled or banned. Always check the provider’s limits and back‑off accordingly.
- Mixing data fetching with decision logic – If your signal function also pulls data, you can’t test the strategy with historical data without hitting the live endpoint. Keep them separate.
Why This New Power Matters
Now you’ve got a living, breathing bot that can watch the markets while you sleep, work, or binge‑watch your favorite show. Because the pieces are decoupled, you can swap in a more sophisticated signal—maybe an RSI‑based model or a lightweight LSTM—without rewriting the whole system. You can also point the executor at a different brokerage (Interactive Brokers, Tradier, etc.) by changing just that file. In short, you’ve moved from manual chart‑watching to algorithmic trading, and that’s a game‑changer for any retail trader who wants to scale their edge without burning out.
Your turn: Take this skeleton, plug in your favorite indicator, run it on paper money first, and see what happens. What’s the first tweak you’ll make? Share your results—or your epic fail—below. Happy coding, and may your spreads be tight!
Top comments (0)