Skip to main content
⚠️ Scanner results are not buy or sell recommendations. They surface stocks matching your rules — judgment and additional research are always required. Not financial advice.
TECHNICAL ANALYSIS · PART 2 OF 5

The OpenClaw Scanner

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.

Scans in ~2 Minutes
Reads ta-config.yaml
CSV Export
S&P 500 Ready

What Is a Scanner?

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.

Your Morning Without a Scanner

  • Open chart for AAPL → check RSI → no signal → close
  • Open chart for MSFT → check RSI → no signal → close
  • Open chart for NVDA → check RSI → overbought → note it down
  • ... repeat 47 more times
  • Total time: ~60 minutes

Your Morning With OpenClaw's Scanner

  • Run the scan → 3 tickers matched your rules → review those 3
  • Total time: ~2 minutes

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

How the OpenClaw Scanner Works

Here's the step-by-step flow of what the scanner does, in plain English:

1

📋 Reads your watchlist

The scanner loads your list of tickers from ta-config.yaml — you define it once, the scanner uses it every time.

2

📥 Downloads price history

For each ticker, it fetches the last 1 year of daily price data from the free Yahoo Finance API. No credentials required.

3

📊 Calculates indicators

Using the price data, it computes RSI, moving averages, MACD, and Bollinger Bands — all the indicators we covered in Part 1.

4

✅ Checks your signal rules

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.

5

📤 Outputs a ranked list

Tickers that matched your rules are printed to the terminal, sorted by how many signals they triggered.

6

💾 Saves to CSV (optional)

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.

Your ta-config.yaml Controls Everything

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.

What Each Setting Means

🎯 Watchlist

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.

📊 Signals

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

⏱️ Scanner Settings

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.

Setting Up OpenClaw TA

Three steps to get the scanner running on your machine.

Step 1: Install Required Packages

Run this once. It installs Python libraries the scanner depends on:

pip install yfinance pandas-ta pandas pyyaml tabulate

Step 2: Create Your Configuration

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.

Step 3: Run the Scanner

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.

The Scanner Script: ta_scanner.py

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.

Complete Script with Line-by-Line Comments

#!/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.

Reading the Scanner Output

Here's what each part of the output means.

The Results Table

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

What Each Column Means

Ticker

The stock symbol.

Price

The closing price from today (or the last trading day).

RSI

The current RSI value. Below 30 = oversold (potential bounce). Above 70 = overbought (extended rally).

Signals

What condition(s) triggered. A stock can have multiple signals (e.g., RSI_OVERBOUGHT AND BB_SQUEEZE at the same time).

Interpreting Results

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.

Automating the Scanner

Right now you have to run the scanner manually. Here's how to make it run automatically every day after the market closes.

Option 1: System Cron (Mac/Linux)

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.

Option 2: Windows Task Scheduler

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.

Option 3: Email Notification (Advanced)

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

What to Do With Scan Results

So the scanner found a stock that matched your rules. Now what?

A Sensible Workflow

1️⃣ LOOK AT THE CHART

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?

2️⃣ CHECK FOR NEWS

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.

3️⃣ CONSIDER YOUR TIMEFRAME

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.

4️⃣ WAIT FOR CONFIRMATION

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.

5️⃣ TEST YOUR RULES

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.

← Part 1 Core Indicators Part 3 → Signal Alerts