B.3 Automation
Build an automation stack that augments your judgment without replacing it — scanners that find setups, alerts that reach you, journals that log themselves — while keeping the entry/exit decisions human.
Bonus Layer — Chapter 3 Goal: Build an automation stack that augments your judgment without replacing it — scanners that find setups, alerts that reach you, journals that log themselves — while keeping the entry/exit decisions human.
The Core Idea
You're an engineer. The temptation is to automate everything end-to-end: scan → signal → execute → manage → exit, no human in the loop. Resist this until you have a profitable manual system with 100+ trades of evidence. Until then, automation should make you faster and more disciplined, not replace your judgment.
The goal of this chapter: build a semi-automated swing trading workflow that runs on free or near-free tools, fits a part-time trader with a day job, and scales from $9K to $100K without rewriting.
The right level of automation for you right now:
| Layer | Automate? | Why |
|---|---|---|
| Universe filtering (which stocks to even watch) | ✅ Yes | Repetitive, rules-based, error-prone manually |
| Setup detection (does this chart match my pattern?) | ✅ Yes | Same |
| Alert delivery (notify you when something fires) | ✅ Yes | Otherwise you miss things at work |
| Position sizing math (shares × ATR × risk %) | ✅ Yes | One math error = real money lost |
| Trade journal (log entry, exit, P&L, screenshots) | ✅ Yes | Manual journaling fails because friction |
| Entry decision (pull the trigger) | ❌ Manual | Requires market context the bot can't see |
| Exit management (when to trim, hold, kill) | ❌ Manual | Requires real-time judgment |
| Stop-loss orders (hard stop sitting at broker) | ✅ Yes | Resting orders are fine — set and forget |
This is the line. Below it: automate aggressively. Above it: human in the loop.
The Tech Stack (Free Tier Sufficient at Your Level)
Data Sources
yfinance (Python library, free, unofficial Yahoo Finance scrape)
- Pros: Free, easy, daily/intraday OHLCV, fundamentals, options chains
- Cons: Unofficial (can break), 15-min delayed intraday, rate-limited, occasional bad data
- Use for: End-of-day scans, backtests, weekend prep
- Don't use for: Real-time entry decisions, live trading triggers
Polygon.io (paid, ~$30/mo "Starter" tier, real-time)
- Pros: Real-time quotes, official data, websockets for streaming
- Cons: Costs money, overkill for swing trading
- Use for: Only if you graduate to intraday timing of swing entries
- At your level: not needed yet
Alpha Vantage (free tier with rate limits)
- Pros: Free, reasonable historical data
- Cons: 5 calls/min, 500/day on free tier — too restrictive for serious use
- Use for: Backup data source, fundamentals fetching
Finnhub (free tier)
- Pros: Earnings calendars, news API, decent free tier
- Cons: Real-time quotes paywalled
- Use for: Earnings calendar automation (when does X report?)
Recommendation for $9K swing trader: yfinance for everything + Finnhub for earnings calendar. Total cost: $0.
# yfinance basics
import yfinance as yf
# Single ticker, daily bars
data = yf.download("AAPL", start="2024-01-01", end="2026-06-01", interval="1d")
# Multiple tickers at once
universe = ["AAPL", "MSFT", "NVDA", "AMD", "META"]
bulk = yf.download(universe, start="2025-01-01", interval="1d", group_by="ticker")
# Get current intraday (15-min delayed)
ticker = yf.Ticker("AAPL")
info = ticker.info
hist = ticker.history(period="1mo", interval="1h")
Brokerage API
Alpaca
- Pros: Free API, paper trading account, REST + websockets, simple auth, Python SDK
- Cons: Limited options support (improving), no futures, no international
- Use for: Paper trading your system, eventually live trading once profitable
- Cost: Free for stocks/ETFs
Interactive Brokers (IBKR)
- Pros: Best execution, options, futures, international, lowest costs at scale
- Cons: API is famously gnarly (TWS or IB Gateway), steeper learning curve
- Use for: When you outgrow Alpaca or need options/futures
Robinhood
- Pros: Where your money already is, mobile UX
- Cons: No official API for retail (the unofficial one is reverse-engineered and against ToS), tax document export decent
- Use for: Manual execution. Don't try to automate Robinhood. Migrate to Alpaca for automation.
Recommendation: Keep Robinhood for now (it's where your account is). When you're ready to automate execution (months 6-12 of this journey), open an Alpaca paper account and rebuild the same setups there. Eventually fund Alpaca live and migrate.
# Alpaca basics (alpaca-py library)
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import MarketOrderRequest, LimitOrderRequest
from alpaca.trading.enums import OrderSide, TimeInForce
client = TradingClient("API_KEY", "SECRET_KEY", paper=True) # paper=True for paper account
# Account info
account = client.get_account()
print(f"Buying power: ${account.buying_power}")
# Place a limit order
order = LimitOrderRequest(
symbol="AAPL",
qty=10,
side=OrderSide.BUY,
time_in_force=TimeInForce.GTC,
limit_price=180.00
)
client.submit_order(order)
# Get positions
positions = client.get_all_positions()
# Get orders
orders = client.get_orders()
Storage
SQLite — single-file database, no server, perfect for solo trader
- Use for: Trade journal, scanner results, watchlist history, backtests
- Tools: Python's built-in
sqlite3module, or SQLAlchemy if you want ORM
Postgres — only if you're sharing data across machines or scaling beyond one user
- Don't bother at your stage
Flat files (CSV/Parquet) — fine for backtests and one-off analysis
- Parquet for large historical datasets, CSV for human-readable journal exports
# SQLite trade journal schema
import sqlite3
conn = sqlite3.connect('trades.db')
conn.execute("""
CREATE TABLE IF NOT EXISTS trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticker TEXT NOT NULL,
setup_name TEXT NOT NULL,
entry_date DATE NOT NULL,
entry_price REAL NOT NULL,
shares INTEGER NOT NULL,
stop_price REAL NOT NULL,
target_price REAL,
exit_date DATE,
exit_price REAL,
pnl REAL,
r_multiple REAL,
notes TEXT,
thesis TEXT,
screenshot_path TEXT,
market_regime TEXT,
grade TEXT
);
""")
conn.commit()
Alert Delivery
Telegram Bot (your preferred channel — good choice)
- Pros: Free, instant push notifications, mobile + desktop, easy bot API, can send images
- Cons: Need a phone with Telegram installed
- Setup: 5 minutes via @BotFather
Discord
- Pros: Free, good for richer formatting, webhooks are trivial
- Cons: Less of a "push" notification feel than Telegram
- Use for: Backup channel or if you're already in trading Discords
- Pros: Universal, free via SMTP or SendGrid/Mailgun free tier
- Cons: Latency (you might not see it for minutes), often filtered
- Use for: Daily digests, end-of-day summaries, not urgent alerts
SMS (via Twilio)
- Pros: Highest reliability, hard to miss
- Cons: Costs money ($0.0075/msg), overkill for swing trading
- Use for: Don't, unless you graduate to time-sensitive intraday signals
Recommendation: Telegram for actionable alerts ("setup fired, review chart") + email for daily/weekly digests.
# Telegram bot setup
# 1. Talk to @BotFather on Telegram, /newbot, get a token
# 2. Send your bot a message
# 3. Get your chat_id from https://api.telegram.org/bot<TOKEN>/getUpdates
import requests
BOT_TOKEN = "your_bot_token_here"
CHAT_ID = "your_chat_id_here"
def send_alert(message: str, image_path: str = None):
if image_path:
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto"
with open(image_path, 'rb') as photo:
requests.post(url, data={"chat_id": CHAT_ID, "caption": message},
files={"photo": photo})
else:
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
requests.post(url, data={"chat_id": CHAT_ID, "text": message, "parse_mode": "Markdown"})
# Usage
send_alert("🚨 *Pullback to 20EMA*: AAPL\nEntry: 182.50\nStop: 178.20\nR/R: 2.5:1")
Scanner Architecture
The scanner is the heart of your automation. Goal: at 4:30pm ET every trading day, the scanner runs through your universe (~500-2000 stocks), identifies setups matching your written system, ranks them, and sends you the top 5 to review during evening prep.
Pipeline
┌──────────────────────────────────────────────────────────────┐
│ 1. UNIVERSE FILTER (run weekly) │
│ S&P 500 + Russell 1000 + your watchlist │
│ Filters: price > $10, avg volume > 1M, market cap > $2B │
│ Output: ~800 tradeable tickers → universe.txt │
└────────────────────┬─────────────────────────────────────────┘
│
┌────────────────────▼─────────────────────────────────────────┐
│ 2. DATA FETCH (daily, after close, ~4:15pm ET) │
│ For each ticker: fetch last 200 daily bars │
│ Store in SQLite or Parquet for reuse │
└────────────────────┬─────────────────────────────────────────┘
│
┌────────────────────▼─────────────────────────────────────────┐
│ 3. SETUP DETECTION (~4:30pm ET) │
│ For each ticker, run setup checks: │
│ - Pullback to 20EMA? │
│ - Breakout from consolidation? │
│ - PEAD candidate? │
│ Each check returns: matched (bool), score (0-100), notes │
└────────────────────┬─────────────────────────────────────────┘
│
┌────────────────────▼─────────────────────────────────────────┐
│ 4. RANKING & FILTERING │
│ Sort by score, take top 10 │
│ Cross-reference: any reporting earnings this week? Skip. │
│ Cross-reference: market regime check (SPY trend ok?) │
└────────────────────┬─────────────────────────────────────────┘
│
┌────────────────────▼─────────────────────────────────────────┐
│ 5. ALERT DELIVERY (~4:35pm ET) │
│ Telegram: top 5 setups with mini-chart screenshots │
│ Email: full digest with all 10 + market regime summary │
└────────────────────┬─────────────────────────────────────────┘
│
┌────────────────────▼─────────────────────────────────────────┐
│ 6. MANUAL REVIEW (you, evening, 15-30 min) │
│ Open each chart in TradingView │
│ Apply your judgment: is the thesis intact? │
│ Calculate position size, set stops │
│ Queue limit orders for next morning │
└──────────────────────────────────────────────────────────────┘
Setup Detection Example: Pullback to 20EMA
import yfinance as yf
import pandas as pd
import numpy as np
def detect_pullback_to_20ema(ticker: str) -> dict:
"""
Returns {matched: bool, score: int, notes: str, data: DataFrame}
"""
# Fetch 6 months of daily data
df = yf.download(ticker, period="6mo", interval="1d", progress=False)
if len(df) < 100:
return {"matched": False, "score": 0, "notes": "insufficient data"}
# Calculate indicators
df["EMA20"] = df["Close"].ewm(span=20, adjust=False).mean()
df["EMA50"] = df["Close"].ewm(span=50, adjust=False).mean()
df["ATR"] = compute_atr(df, period=14)
df["Volume_MA20"] = df["Volume"].rolling(20).mean()
last = df.iloc[-1]
prev = df.iloc[-2]
notes = []
score = 0
# Check 1: Uptrend (20 > 50, both rising)
if last["EMA20"] > last["EMA50"]:
score += 20
notes.append("✓ 20EMA > 50EMA (uptrend)")
else:
return {"matched": False, "score": 0, "notes": "not in uptrend"}
# Check 2: Price near 20EMA (within 1 ATR)
distance_to_ema = abs(last["Close"] - last["EMA20"])
if distance_to_ema < last["ATR"]:
score += 30
notes.append(f"✓ Within 1 ATR of 20EMA")
else:
return {"matched": False, "score": 0, "notes": "not near 20EMA"}
# Check 3: Pullback was on declining volume (healthy)
recent_5_vol = df["Volume"].iloc[-5:].mean()
if recent_5_vol < last["Volume_MA20"]:
score += 20
notes.append("✓ Pullback on lower volume")
# Check 4: Showing reversal candle (today's close > today's open)
if last["Close"] > last["Open"]:
score += 15
notes.append("✓ Green reversal candle")
# Check 5: 50EMA is rising (trend health)
ema50_slope = (df["EMA50"].iloc[-1] - df["EMA50"].iloc[-10]) / 10
if ema50_slope > 0:
score += 15
notes.append("✓ 50EMA rising")
return {
"matched": score >= 70,
"score": score,
"notes": " | ".join(notes),
"entry": float(last["Close"]),
"stop": float(last["EMA20"] - last["ATR"]),
"atr": float(last["ATR"])
}
def compute_atr(df, period=14):
high_low = df["High"] - df["Low"]
high_close = (df["High"] - df["Close"].shift()).abs()
low_close = (df["Low"] - df["Close"].shift()).abs()
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
return tr.rolling(period).mean()
Running the Scanner
# scanner.py - run daily at 4:30pm ET via cron
from datetime import datetime
import json
def run_daily_scan():
universe = load_universe() # list of tickers
earnings_this_week = load_earnings_calendar()
market_regime = check_market_regime() # bull/neutral/bear
if market_regime == "bear":
send_alert("🐻 Market in downtrend — skipping long setups today")
return
results = []
for ticker in universe:
if ticker in earnings_this_week:
continue # skip earnings risk
result = detect_pullback_to_20ema(ticker)
if result["matched"]:
result["ticker"] = ticker
result["setup"] = "pullback_to_20ema"
results.append(result)
# Sort by score, take top 5
results.sort(key=lambda x: x["score"], reverse=True)
top_5 = results[:5]
# Save to journal
log_scan_results(top_5)
# Send alerts
for r in top_5:
msg = format_alert(r)
send_alert(msg)
send_alert(f"📊 Daily scan complete: {len(results)} setups, top {len(top_5)} sent")
if __name__ == "__main__":
run_daily_scan()
Scheduling
Linux/Mac cron (free, simple):
# Edit crontab: crontab -e
# Run scanner Mon-Fri at 4:30pm ET (assuming server in ET; adjust for UTC)
30 16 * * 1-5 cd /home/maverick/trading && /usr/bin/python3 scanner.py >> logs/scanner.log 2>&1
GitHub Actions (free, hosted, no server to maintain):
# .github/workflows/daily_scan.yml
name: Daily Scanner
on:
schedule:
- cron: '30 21 * * 1-5' # 21:30 UTC = 4:30pm ET (winter; adjust for DST)
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- run: python scanner.py
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
Recommendation: Start with cron on a Raspberry Pi or your laptop. Move to GitHub Actions or a cheap VPS ($5/mo DigitalOcean droplet) once reliability matters.
Backtesting Infrastructure
Before going live with any system, backtest it on 2+ years of historical data. This catches obvious bugs and gives you a baseline expectation.
Options
backtrader — mature Python framework, event-driven, realistic
- Pros: Handles slippage, commissions, position sizing properly
- Cons: API is verbose, learning curve
- Use for: Final validation before going live
vectorbt — fast, vectorized, modern
- Pros: 100x faster than backtrader, great for parameter optimization, beautiful charts
- Cons: Can be too forgiving (vectorized backtests often look better than reality)
- Use for: Quick exploration, parameter sweeps, idea generation
Custom Python (pandas) — write your own
- Pros: You understand every line
- Cons: Easy to introduce subtle bugs (look-ahead bias, survivorship bias)
- Use for: Learning, simple ideas
Recommendation: Start with vectorbt for exploration. Validate finalists in backtrader before going live.
What to Measure
Run your backtest, then check:
| Metric | Target | What it tells you |
|---|---|---|
| Win rate | 40-55% (trend) or 55-70% (mean rev) | Hit rate |
| Avg winner / avg loser (R-multiple) | > 1.5 | Asymmetry of outcomes |
| Expectancy per trade | > +0.2R | Edge per trade |
| Max drawdown | < 25% | Pain you'd have experienced |
| Sharpe ratio | > 1.0 | Risk-adjusted return |
| Number of trades | 100+ | Sample size for confidence |
| Worst losing streak | Count it | Mental prep for live |
Critical Bugs to Avoid
- Look-ahead bias — using data that wouldn't have been available at the time (e.g., using today's close to decide today's entry)
- Survivorship bias — backtesting only on current S&P 500 members ignores companies that went bankrupt
- No slippage — assuming you fill at the exact close price
- No commissions — even at $0 commissions, there are SEC/TAF fees on sells
- Overfitting — tuning parameters until backtest looks great = curve-fit garbage
- Insufficient sample — 20 trades isn't a backtest, it's an anecdote
Semi-Automated Workflow (Recommended for You)
Here's the daily/weekly loop:
Weekend (Sunday, 30 min):
- Run weekly universe refresh
- Update earnings calendar for the week
- Check market regime (SPY trend, sector rotation)
- Review last week's trades, update journal
Daily (after close, 4:30-5:30pm ET, 30 min):
- Scanner runs at 4:30pm, sends top setups via Telegram
- You review each chart manually in TradingView
- Apply judgment: is the thesis intact? Is the broader context supportive?
- Calculate position size (automated helper)
- Queue limit orders for next morning in Robinhood/Alpaca
Pre-market (next morning, 9:00-9:30am, 10 min):
- Check overnight news on tickers
- Confirm or cancel queued orders
- If filled, set stop-loss order at the broker
During the day (lunch break or quick checks):
- Don't watch tick-by-tick. Check at lunch and end of day.
- Trailing stop or trim alerts via Telegram if price hits targets
End of day (5 min):
- Log any closed positions to journal (automated via broker API)
- Note any open positions and how they're behaving
This workflow assumes a day job. Total time commitment: ~1 hour/day.
What NOT to Fully Automate (Yet)
These require human judgment until you have substantial evidence your system handles them:
❌ Entry trigger — The bot can identify the setup. You decide whether to take it. "Looks like a pullback to 20EMA" ≠ "you should buy this." Macro context, gut check, news scan — these need a human.
❌ Exit management — Trimming, holding through chop, recognizing when momentum dies — discretionary. Hard stops can be automated (and should be). Discretionary exits should stay manual.
❌ Sizing override — If you're in drawdown, you should size down. If market regime turned bearish, you should pause. These rules can be coded eventually, but learn them manually first.
❌ Anything during earnings season — Algorithmic systems get whipsawed by earnings volatility. Your manual judgment to skip earnings names is part of your edge.
Full Automation: When and How
Eventually, after 6-12 months of profitable semi-automated trading, you might consider full automation. Signs you're ready:
- 100+ live trades with positive expectancy
- Detailed log of all manual interventions and whether they helped or hurt (mostly hurt = automate them; mostly helped = keep them manual)
- Stable system that hasn't been modified in 50+ trades
- Comfort with code reliability (you've handled outages, API failures, edge cases)
Even then, start with kill switches everywhere:
# Pseudo-code for full automation safety layer
def safe_execute_order(order):
# Pre-trade checks
if account.equity_today_pct_change < -0.03:
kill_switch("Daily drawdown limit hit")
return
if account.open_positions_count >= 5:
log("Max positions reached, skipping")
return
if market.spy_atr_ratio > 2.0:
log("Unusual market volatility, skipping")
return
if order.position_size_dollars > account.equity * 0.10:
kill_switch("Position size > 10% account, aborting")
return
# All checks passed, place order
place_order(order)
The kill switch should immediately:
- Stop placing new orders
- Cancel all open orders
- Send urgent Telegram + SMS alert
- Require manual human reset
Cost Summary (At Your Level)
| Component | Cost/mo | Notes |
|---|---|---|
| Data (yfinance) | $0 | Free, good enough for swing |
| Brokerage (Robinhood → Alpaca) | $0 | Free for stocks/ETFs |
| Compute (Pi or laptop) | $0 | Run locally |
| Compute (GitHub Actions) | $0 | Free tier sufficient |
| Compute (VPS, optional) | $5 | DigitalOcean droplet |
| Telegram alerts | $0 | Free |
| Storage (SQLite local) | $0 | Local file |
| Charting (TradingView free) | $0 | Free tier has 3 indicators/chart |
| Charting (TradingView Essential) | $13 | Worth it once active |
| Total realistic | $0-18/mo |
Compare this to what most retail "trading services" charge ($100-500/mo for stock picks). You can build a better system yourself.
Common Mistakes
❌ Automating before you have a profitable manual system. You can't automate a losing strategy into a winning one. Get to break-even manually first.
❌ Trusting backtests with optimized parameters. A backtest that says "65% win rate, 2:1 R/R" with parameters tuned to the historical data will not deliver those numbers live.
❌ No error handling. API goes down at 9:35am, your scanner crashes, you miss a fill. Build retries, logging, monitoring.
❌ Storing credentials in code. Use environment variables or a secrets manager. Never commit API keys to git.
❌ No paper trading phase. Going from backtest → live with real money skips the most important learning step. Paper trade for 50+ trades first.
❌ Over-engineering. You don't need Kafka, Kubernetes, or a microservices architecture to scan 800 stocks daily. A cron job and a SQLite file is fine.
❌ Ignoring data quality. yfinance occasionally returns bad data (zero volume days, mis-adjusted splits). Add sanity checks.
❌ No monitoring of the monitor. If your scanner crashes silently, you don't know it crashed. Send a daily "I ran successfully" message.
A Mental Model
Think of automation as a prosthetic for your weakest faculties, not a replacement for your judgment.
- Your memory is bad → the journal automates remembering
- Your math is error-prone under stress → the sizing calculator automates math
- You can't watch 800 charts → the scanner automates filtering
- You forget to check setups → the alerts automate reminders
- You skip journaling when tired → automated logging removes the friction
You're not building a robot trader. You're building a system that makes the human (you) a better trader by removing the parts you're bad at.
Practical Takeaways
-
Free tier is enough for swing trading at $9K. yfinance + Alpaca paper + Telegram + cron = $0/month.
-
Semi-automated > fully automated for your first year. Scanner finds, you decide.
-
SQLite for journal, Telegram for alerts, GitHub Actions for scheduling. Boring, free, reliable.
-
Paper trade your system for 50+ trades before going live with real money.
-
Backtest with vectorbt for speed, validate with backtrader for realism.
-
Watch for look-ahead bias, survivorship bias, and overfitting. These three kill more strategies than bad ideas do.
-
Kill switches everywhere. Account drawdown, position count, position size — all checked before any order.
-
Don't automate Robinhood. Move to Alpaca for automation. Keep Robinhood for now if that's where your money is.
-
Monitor your monitor. Daily heartbeat message so you know the system is alive.
-
Build the prosthetic, not the replacement. Automate what you're bad at, keep human in the loop for judgment.
Sample Repo Structure
swing_trading/
├── README.md
├── requirements.txt
├── .env.example # template for API keys
├── config/
│ ├── universe.txt # tickers to scan
│ └── system.yaml # parameters for your system
├── scanners/
│ ├── pullback.py
│ ├── breakout.py
│ └── pead.py
├── data/
│ ├── fetch.py # yfinance wrappers
│ └── cache.db # SQLite cache
├── alerts/
│ ├── telegram.py
│ └── email.py
├── execution/
│ ├── alpaca_client.py
│ ├── sizing.py
│ └── order_manager.py
├── journal/
│ ├── logger.py
│ └── trades.db
├── backtest/
│ ├── runner.py
│ └── results/
├── monitoring/
│ ├── heartbeat.py
│ └── healthcheck.py
└── scripts/
├── daily_scan.py # cron entry point
├── weekly_prep.py
└── reconcile.py # nightly journal sync
Quick Self-Check
- I can name the line between "automate" and "keep manual"
- I understand why yfinance + Alpaca + Telegram + cron is sufficient for swing trading at my level
- I know what survivorship bias, look-ahead bias, and overfitting are
- I will paper-trade my automated system for 50+ trades before going live
- I have planned kill switches for account drawdown and position sizing
- I know I should NOT try to automate Robinhood (use Alpaca instead)
- I will start with semi-automated (scanner + alerts, manual execution) before considering full automation
- I have a plan to monitor the monitor (heartbeat messages)
Previous: B.2 Building Your Own System Next: B.4 Tax and Legal