Manually checking 50 stock charts every morning takes an hour. OpenClaw's scanner does it in under 2 minutes — checking every ticker on your watchlist against your signal rules and surfacing only the ones that matter.
A stock scanner is a tool that goes through a list of stocks and checks each one against a set of rules. Instead of manually pulling up charts for 50 stocks, you define your criteria once — and the scanner tells you which stocks match.
That's the core idea. The scanner automates the tedious part (checking 50 charts) so you can focus on the interesting part (analyzing the ones that matter).
Here's the step-by-step flow of what the scanner does, in plain English:
The scanner loads your list of tickers from ta-config.yaml — you define it once, the scanner uses it every time.
For each ticker, it fetches the last 1 year of daily price data from the free Yahoo Finance API. No credentials required.
Using the price data, it computes RSI, moving averages, MACD, and Bollinger Bands — all the indicators we covered in Part 1.
For each ticker, it asks: "Does this stock's RSI cross 30? Did the moving averages cross today? Is the Bollinger Band squeezed?" Based on your config.
Tickers that matched your rules are printed to the terminal, sorted by how many signals they triggered.
If configured, the results are also saved to a CSV file you can archive or import into a spreadsheet.
All of this happens in under 2 minutes because the scanner runs in parallel and the Yahoo Finance API is fast.
You never need to edit Python code. Instead, you edit a single YAML file that tells the scanner what to look for. Here's what it looks like:
# ta-config.yaml
# This is the only file you need to edit to customize your scanner
watchlist:
tickers:
- AAPL # Add or remove tickers here
- MSFT
- NVDA
- GOOGL
- AMZN
- SPY # ETFs work too
- QQQ
signals:
# Which signals should the scanner look for?
golden_cross: true # SMA50 just crossed above SMA200 (bullish trend)
death_cross: true # SMA50 just crossed below SMA200 (bearish trend)
rsi_oversold: 30 # Flag stocks with RSI below 30 (potential bounce)
rsi_overbought: 70 # Flag stocks with RSI above 70 (extended rally)
macd_cross: true # MACD momentum shift detected
bb_squeeze: true # Bollinger Band squeeze (low volatility)
scanner:
lookback_period: "1y" # How much price history to use (1y = 1 year)
export_csv: true # Save results to a CSV file? true or false
output_file: "scan_results.csv"
That's it. You never need to open the Python files to change what the scanner looks for — just edit this YAML file. Add tickers, change the RSI threshold, toggle signals on and off.
A list of stock tickers. The scanner checks each one. You can have 5, 50, or 500 tickers here — the scanner will check all of them.
golden_cross / death_cross: Watch for moving average crossovers (bullish/bearish trend shifts).
rsi_oversold / rsi_overbought: Set the RSI thresholds (typically 30 and 70, but you can adjust).
macd_cross: Detect when MACD momentum flips direction.
bb_squeeze: Flag stocks when Bollinger Bands tighten (low volatility before breakout).
lookback_period: How much history to analyze. "1y" = 1 year, "6mo" = 6 months. Longer periods take slightly longer but give better moving average accuracy.
export_csv: Whether to save results to a file. Turn it on for record-keeping.
Three steps to get the scanner running on your machine.
Run this once. It installs Python libraries the scanner depends on:
pip install yfinance pandas-ta pandas pyyaml tabulate
Save the config from the section above as ta-config.yaml in your project folder. Customize the tickers and signal settings to match your trading style.
Execute the scanner script:
python3 ta_scanner.py
You should see output like this:
🦞 OpenClaw Scanner | 7 tickers | Period: 1y
2026-03-28 14:32
──────────────────────────────────────────
[1/7] AAPL ✓ RSI_OVERSOLD
[2/7] MSFT — no signals
[3/7] NVDA ✓ RSI_OVERBOUGHT, BB_SQUEEZE
[4/7] GOOGL ✓ GOLDEN_CROSS
[5/7] AMZN — no signals
[6/7] SPY — no signals
[7/7] QQQ ✓ MACD_BULL_CROSS
══════════════════════════════════════════
🦞 Scan complete: 4 signal(s) found in 7 tickers
══════════════════════════════════════════
AAPL RSI: 25.3 | RSI_OVERSOLD
NVDA RSI: 78.9 | RSI_OVERBOUGHT, BB_SQUEEZE
GOOGL RSI: 52.1 | GOLDEN_CROSS
QQQ RSI: 48.7 | MACD_BULL_CROSS
✓ Saved to scan_results.csv
If no tickers show up in your results, that's actually good — it means nothing unusual is happening in your watchlist today. The scanner is most useful on volatile days or during market corrections.
Now that you understand what the scanner does and how to configure it, here's the full Python implementation. Read it section by section — every part is commented to explain the logic.
#!/usr/bin/env python3
# ta_scanner.py — OpenClaw Technical Analysis Scanner
# Reads ta-config.yaml and scans your watchlist for signal matches
import yaml
import yfinance as yf
import pandas_ta as ta
import pandas as pd
import time
import csv
from datetime import datetime
from tabulate import tabulate
# ── STEP 1: LOAD CONFIGURATION ──────────────────────────────────────────
def load_config(path: str = "ta-config.yaml") -> dict:
"""
Loads the YAML config file and returns it as a Python dictionary.
If the file doesn't exist, exits with an error message.
"""
try:
with open(path) as f:
return yaml.safe_load(f)
except FileNotFoundError:
print(f"❌ ERROR: {path} not found. Create it first.")
exit(1)
cfg = load_config()
TICKERS = cfg["watchlist"]["tickers"]
PERIOD = cfg["scanner"]["lookback_period"]
EXPORT_CSV = cfg["scanner"].get("export_csv", False)
OUTPUT_FILE = cfg["scanner"].get("output_file", "scan_results.csv")
# Print scan header
print(f"\n🦞 OpenClaw Scanner | {len(TICKERS)} tickers | Period: {PERIOD}")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("─" * 50)
# ── STEP 2: SCAN ONE TICKER FOR SIGNALS ─────────────────────────────────
def scan_ticker(ticker: str) -> dict | None:
"""
Downloads price history for a ticker and checks all configured signals.
Returns:
dict: { "ticker": str, "price": float, "rsi": float, "signals": list }
None: if no signals were found or data fetch failed
"""
try:
# Download 1 year (or configured period) of daily price data
# progress=False suppresses Yahoo's download messages
df = yf.download(
ticker,
period=PERIOD,
interval="1d",
progress=False
)
# Check if we got valid data
if df.empty or len(df) < 210:
# Need at least 210 days for 200-day moving average
return None
# Standardize column names to lowercase
df.columns = [c.lower() for c in df.columns]
# Keep only OHLCV columns, drop others (adj close, etc)
df = df[["open", "high", "low", "close", "volume"]].dropna()
# ── Calculate all technical indicators ───────────────────────────
# Simple Moving Averages (trend identification)
df["sma50"] = ta.sma(df["close"], 50)
df["sma200"] = ta.sma(df["close"], 200)
# RSI (momentum/overbought-oversold)
df["rsi"] = ta.rsi(df["close"], 14)
# Bollinger Bands (volatility measurement)
# BBB = Bollinger Band bandwidth (width of bands)
bb = ta.bbands(df["close"], 20, 2)
bb_cols = [c for c in bb.columns if "BBB" in c]
if bb_cols:
df["bb_bandwidth"] = bb[bb_cols[0]]
# MACD (momentum crossover)
macd = ta.macd(df["close"], 12, 26, 9)
macd_cols = [c for c in macd.columns if "MACD_" in c]
signal_cols = [c for c in macd.columns if "MACDs_" in c]
if macd_cols and signal_cols:
df["macd"] = macd[macd_cols[0]]
df["macd_signal"] = macd[signal_cols[0]]
# Remove rows where indicators weren't calculated yet
df = df.dropna()
# Need at least 2 rows to check crossovers
if len(df) < 2:
return None
# ── COMPARE TODAY vs YESTERDAY ──────────────────────────────────
cur = df.iloc[-1] # Today's values (most recent)
prev = df.iloc[-2] # Yesterday's values
signals = []
# ── CHECK SIGNAL CONDITIONS ─────────────────────────────────────
# RSI OVERSOLD: Is RSI below configured threshold?
rsi_oversold_threshold = cfg["signals"].get("rsi_oversold", 30)
if cur["rsi"] < rsi_oversold_threshold:
signals.append("RSI_OVERSOLD")
# RSI OVERBOUGHT: Is RSI above configured threshold?
rsi_overbought_threshold = cfg["signals"].get("rsi_overbought", 70)
if cur["rsi"] > rsi_overbought_threshold:
signals.append("RSI_OVERBOUGHT")
# GOLDEN CROSS: Did SMA50 just cross ABOVE SMA200?
if cfg["signals"].get("golden_cross"):
if cur["sma50"] > cur["sma200"] and prev["sma50"] <= prev["sma200"]:
signals.append("GOLDEN_CROSS")
# DEATH CROSS: Did SMA50 just cross BELOW SMA200?
if cfg["signals"].get("death_cross"):
if cur["sma50"] < cur["sma200"] and prev["sma50"] >= prev["sma200"]:
signals.append("DEATH_CROSS")
# MACD CROSSOVER: Did MACD flip relative to signal line?
if cfg["signals"].get("macd_cross"):
if "macd" in df.columns and "macd_signal" in df.columns:
# Bullish: MACD crossed above signal
if cur["macd"] > cur["macd_signal"] and prev["macd"] <= prev["macd_signal"]:
signals.append("MACD_BULL_CROSS")
# Bearish: MACD crossed below signal
if cur["macd"] < cur["macd_signal"] and prev["macd"] >= prev["macd_signal"]:
signals.append("MACD_BEAR_CROSS")
# BOLLINGER SQUEEZE: Is bandwidth at historic low?
if cfg["signals"].get("bb_squeeze"):
if "bb_bandwidth" in df.columns:
# Squeeze = lowest 20% of bandwidth values
threshold = df["bb_bandwidth"].quantile(0.20)
if cur["bb_bandwidth"] < threshold:
signals.append("BB_SQUEEZE")
# If no signals triggered, skip this ticker
if not signals:
return None
# Return the result
return {
"ticker": ticker,
"price": cur["close"],
"rsi": cur["rsi"],
"signals": signals,
}
except Exception as e:
# Silently skip tickers with errors (delisted, etc)
return None
# ── STEP 3: SCAN ALL TICKERS IN WATCHLIST ───────────────────────────────
results = []
for i, ticker in enumerate(TICKERS, 1):
result = scan_ticker(ticker)
if result:
# Ticker matched signals — add to results
print(f" [{i:>2}/{len(TICKERS)}] {ticker:<8} ✓ {', '.join(result['signals'])}")
results.append(result)
else:
# No signals found for this ticker
print(f" [{i:>2}/{len(TICKERS)}] {ticker:<8} — no signals")
# Rate limit: wait 0.5 seconds between requests to be polite to Yahoo
time.sleep(0.5)
# ── STEP 4: PRINT SUMMARY ───────────────────────────────────────────────
print(f"\n{'═' * 50}")
print(f"🦞 Scan complete: {len(results)} signal(s) in {len(TICKERS)} tickers")
print(f"{'═' * 50}\n")
# Print detailed results
if results:
for r in results:
print(f" {r['ticker']:<8} Price: ${r['price']:>8.2f} "
f"RSI: {r['rsi']:>5.1f} | {', '.join(r['signals'])}")
else:
print(" (no signals found today)")
# ── STEP 5: EXPORT TO CSV ───────────────────────────────────────────────
if EXPORT_CSV and results:
try:
with open(OUTPUT_FILE, "w", newline="") as f:
fieldnames = ["ticker", "price", "rsi", "signals", "timestamp"]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for r in results:
writer.writerow({
"ticker": r["ticker"],
"price": r["price"],
"rsi": r["rsi"],
"signals": " | ".join(r["signals"]),
"timestamp": datetime.now().isoformat(),
})
print(f"\n✓ Results saved to {OUTPUT_FILE}")
except Exception as e:
print(f"\n⚠️ Could not save CSV: {e}")
print() # Final blank line
Every function has a docstring. Every logical section is commented. The code is designed to be readable first, clever second. That's intentional — you should understand what's happening.
Here's what each part of the output means.
AAPL Price: $ 150.23 RSI: 25.3 | RSI_OVERSOLD
NVDA Price: $ 875.41 RSI: 78.9 | RSI_OVERBOUGHT, BB_SQUEEZE
GOOGL Price: $ 142.67 RSI: 52.1 | GOLDEN_CROSS
The stock symbol.
The closing price from today (or the last trading day).
The current RSI value. Below 30 = oversold (potential bounce). Above 70 = overbought (extended rally).
What condition(s) triggered. A stock can have multiple signals (e.g., RSI_OVERBOUGHT AND BB_SQUEEZE at the same time).
If a ticker shows up in your results, that's a starting point for research, not an automatic trade signal.
No signals today? That's actually the normal state. Most days, most stocks aren't doing anything unusual. The scanner's job is to surface the exceptions.
Right now you have to run the scanner manually. Here's how to make it run automatically every day after the market closes.
Open your crontab:
crontab -e
Add this line to run Monday–Friday at 4:15 PM Eastern (15 minutes after market close):
15 16 * * 1-5 cd /path/to/your/openclaw && python3 ta_scanner.py >> scanner.log 2>&1
Replace /path/to/your/openclaw with the actual folder path. The >> scanner.log 2>&1 saves output to a log file.
Create a .bat file:
@echo off
cd C:\path\to\your\openclaw
python3 ta_scanner.py >> scanner.log 2>&1
Then create a scheduled task in Windows Task Scheduler to run this batch file at 4:15 PM daily.
You can extend the scanner to email you results automatically. Add this to your config:
scheduler:
email_on_signals: true
email_address: "you@example.com"
smtp_server: "smtp.gmail.com"
smtp_password: "your_app_password" # Use app-specific password, not Gmail password
Then modify the scanner script to send an email when signals are found. (We'll cover this in the next tutorial.)
So the scanner found a stock that matched your rules. Now what?
The scanner is automated, but your eyes aren't. Pull up the chart and visually confirm what the scanner found. Does the price action match what you'd expect from the signal?
A sudden RSI dip might be due to a disappointing earnings report or broader market weakness. The scanner doesn't know about news — you do.
A golden cross on daily might be a good trend signal, but if you trade 5-minute charts, you need to confirm on intraday timeframes too.
Don't act immediately. Watch the stock for the next few days. Is the move continuing? Or was it a fake-out? Patience is rewarded.
Over time, if you find your scanner rules are too noisy (false signals), adjust them. Tighten RSI thresholds, require multiple signals at once, etc.
The scanner is a filter, not a strategy. It removes the noise (50 tickers with nothing interesting), leaving you with signal (the 3-5 that matter). The strategy — how you trade those signals — is still up to you.