SFSigFinSignal Finance
Specific to Your Path

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 sqlite3 module, 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

Email

  • 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

  1. 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)
  2. Survivorship bias — backtesting only on current S&P 500 members ignores companies that went bankrupt
  3. No slippage — assuming you fill at the exact close price
  4. No commissions — even at $0 commissions, there are SEC/TAF fees on sells
  5. Overfitting — tuning parameters until backtest looks great = curve-fit garbage
  6. Insufficient sample — 20 trades isn't a backtest, it's an anecdote

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:

  1. Stop placing new orders
  2. Cancel all open orders
  3. Send urgent Telegram + SMS alert
  4. 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

  1. Free tier is enough for swing trading at $9K. yfinance + Alpaca paper + Telegram + cron = $0/month.

  2. Semi-automated > fully automated for your first year. Scanner finds, you decide.

  3. SQLite for journal, Telegram for alerts, GitHub Actions for scheduling. Boring, free, reliable.

  4. Paper trade your system for 50+ trades before going live with real money.

  5. Backtest with vectorbt for speed, validate with backtrader for realism.

  6. Watch for look-ahead bias, survivorship bias, and overfitting. These three kill more strategies than bad ideas do.

  7. Kill switches everywhere. Account drawdown, position count, position size — all checked before any order.

  8. Don't automate Robinhood. Move to Alpaca for automation. Keep Robinhood for now if that's where your money is.

  9. Monitor your monitor. Daily heartbeat message so you know the system is alive.

  10. 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