Skip to main content
⚠️

Charts are visual research tools. Pattern recognition on a chart is subjective and does not constitute financial advice.

TECHNICAL ANALYSIS · PART 5 OF 5

Automated Chart Generation

Every morning, OpenClaw can generate a complete visual chart packet for your entire watchlist — candlesticks, moving averages, RSI, volume — saved to your computer before you've finished your coffee.

Plotly Interactive HTML
matplotlib PNG
Nightly Chart Packet
No Subscription

Why Generate Charts Automatically?

Most charting platforms make you open each stock one at a time. Even fast charting tools take 5–10 seconds per ticker. For a 30-stock watchlist, that's 5 minutes every morning just loading charts — before you've even looked at any of them.

OpenClaw's chart generator produces all your charts overnight. When you wake up, a folder of ready-made charts is waiting for you. Open the index page in your browser and all 30 charts are one click away.

Two Formats, Explained Simply

Plotly HTML Charts
Interactive. You can zoom in, hover to see exact prices, and pan around. Great for your main daily review. Saved as .html files you open in any browser.
matplotlib PNG Charts
Static images. Lightweight, easy to email to yourself or a colleague. Good for batch reports.

Most users use Plotly for their daily watchlist review and matplotlib when they want to email a chart to someone.

What's On Each Chart

Each chart has three stacked panels. Here's how to read them:

┌─────────────────────────────────────────┐ │ PANEL 1: Price │ │ Candlestick bars + SMA 20/50/200 │ │ + Bollinger Bands (upper/lower) │ ├─────────────────────────────────────────┤ │ PANEL 2: Volume │ │ Green bars = up day | Red = down day │ ├─────────────────────────────────────────┤ │ PANEL 3: RSI │ │ Line chart 0–100 | Red line at 70 │ │ Green line at 30 (oversold threshold) │ └─────────────────────────────────────────┘

Explain Each Element

Candlesticks
Each bar shows the open, high, low, and close for one day. Green candle = price closed higher than it opened. Red = closed lower.
SMA Lines (Moving Averages)
The moving averages (yellow=20d, blue=50d, pink=200d) show the underlying trend direction. Use them to spot whether the stock is trending up, down, or sideways.
Bollinger Bands
The teal dotted lines above and below price — they expand in volatility, contract in calm. When price touches the upper band, the stock may be stretched. Lower band means compressed.
Volume
How many shares traded that day. Spikes in volume often confirm a price move. Big moves on low volume are suspect.
RSI Panel
A quick visual read on whether the stock is stretched or compressed. Above 70 = overbought. Below 30 = oversold.

Setting Up Chart Generation in ta-config.yaml

All chart settings live in your ta-config.yaml file. Here's a complete example:

# ta-config.yaml — chart settings

watchlist:
  tickers:
    - AAPL
    - MSFT
    - NVDA
    - GOOGL
    - SPY
    - QQQ

charts:
  format: "plotly"          # "plotly" for interactive HTML, "matplotlib" for PNG
  period: "6mo"             # How much history to show (6mo, 1y, 2y)
  output_dir: "charts"      # Folder where charts are saved

  # Which indicators to overlay on the price panel
  show_sma20:   true
  show_sma50:   true
  show_sma200:  true
  show_bollinger: true

  # Which sub-panels to include
  show_volume: true
  show_rsi:    true

scheduler:
  charts:
    script: ta_charts.py
    schedule: "0 17 * * 1-5"    # 5pm ET, Mon–Fri
    notify: false                # No email needed — just open the folder

Configuration Keys Explained

format
"plotly" for interactive HTML in your browser, or "matplotlib" for static PNG files.
period
How far back to plot history. Options: "6mo", "1y", "2y", or even "5y".
output_dir
Folder path where all charts are saved. Will be created if it doesn't exist.
show_sma20, show_sma50, show_sma200
Set to true to plot each moving average. false to skip.
show_bollinger
Plot Bollinger Bands on the price panel.
show_volume
Include a separate volume panel below price.
show_rsi
Include RSI (Relative Strength Index) as a third panel.
schedule
Cron expression. "0 17 * * 1-5" = 5pm ET, Monday–Friday.

The Chart Script — Interactive Plotly

This Python script reads your ta-config.yaml, downloads price history for each ticker, calculates indicators, and generates an interactive HTML chart for each one.

# ta_charts.py — OpenClaw Chart Generator
# Produces an HTML chart for each ticker in your watchlist

import yaml
import yfinance as yf
import pandas_ta as ta
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import os
from datetime import datetime

# ── Load config ───────────────────────────────────────────────────
def load_config(path: str = "ta-config.yaml") -> dict:
    with open(path) as f:
        return yaml.safe_load(f)

cfg     = load_config()
TICKERS = cfg["watchlist"]["tickers"]
CHART   = cfg["charts"]
OUT_DIR = CHART.get("output_dir", "charts")
os.makedirs(OUT_DIR, exist_ok=True)

print(f"🦞 OpenClaw Chart Generator | {len(TICKERS)} tickers")
print(f"   Format: {CHART['format']} | Period: {CHART['period']}")
print(f"   Output: {OUT_DIR}/")

# ── Build one chart ───────────────────────────────────────────────
def make_chart(ticker: str) -> str:
    """
    Generates an interactive Plotly chart for one ticker.
    Returns the path of the saved HTML file.
    """
    # Step 1: Download price history
    df = yf.download(ticker, period=CHART["period"], interval="1d", progress=False)
    df.columns = [c.lower() for c in df.columns]
    df = df[["open","high","low","close","volume"]].dropna()

    # Step 2: Calculate indicators (only the ones enabled in config)
    if CHART.get("show_sma20"):  df["sma20"]  = ta.sma(df["close"], 20)
    if CHART.get("show_sma50"):  df["sma50"]  = ta.sma(df["close"], 50)
    if CHART.get("show_sma200"): df["sma200"] = ta.sma(df["close"], 200)
    if CHART.get("show_rsi"):    df["rsi"]    = ta.rsi(df["close"], 14)
    if CHART.get("show_bollinger"):
        bb = ta.bbands(df["close"], 20, 2)
        df["bb_upper"] = bb[[c for c in bb.columns if "BBU" in c][0]]
        df["bb_lower"] = bb[[c for c in bb.columns if "BBL" in c][0]]

    # Step 3: Build the chart layout
    # How many panels do we need?
    panels = 1  # always have price panel
    if CHART.get("show_volume"): panels += 1
    if CHART.get("show_rsi"):    panels += 1

    row_heights = [0.60]
    if CHART.get("show_volume"): row_heights.append(0.15)
    if CHART.get("show_rsi"):    row_heights.append(0.25)

    subplot_titles = [f"{ticker} — Price"]
    if CHART.get("show_volume"): subplot_titles.append("Volume")
    if CHART.get("show_rsi"):    subplot_titles.append("RSI (14)")

    fig = make_subplots(
        rows=panels, cols=1,
        shared_xaxes=True,
        vertical_spacing=0.03,
        row_heights=row_heights,
        subplot_titles=subplot_titles,
    )

    # Panel 1: Candlesticks
    fig.add_trace(go.Candlestick(
        x=df.index, open=df["open"], high=df["high"],
        low=df["low"], close=df["close"],
        name="Price",
        increasing_line_color="#22c55e",
        decreasing_line_color="#ef4444",
    ), row=1, col=1)

    # Moving averages
    for col, color, name in [
        ("sma20", "#fbbf24", "SMA 20"),
        ("sma50", "#60a5fa", "SMA 50"),
        ("sma200","#f472b6", "SMA 200"),
    ]:
        if col in df.columns:
            fig.add_trace(go.Scatter(
                x=df.index, y=df[col],
                name=name, line=dict(color=color, width=1.2), opacity=0.85,
            ), row=1, col=1)

    # Bollinger Bands
    if "bb_upper" in df.columns:
        fig.add_trace(go.Scatter(
            x=df.index, y=df["bb_upper"], name="BB Upper",
            line=dict(color="#0d9488", width=1, dash="dot"), opacity=0.6,
        ), row=1, col=1)
        fig.add_trace(go.Scatter(
            x=df.index, y=df["bb_lower"], name="BB Lower",
            line=dict(color="#0d9488", width=1, dash="dot"),
            fill="tonexty", fillcolor="rgba(13,148,136,0.05)", opacity=0.6,
        ), row=1, col=1)

    current_row = 2
    # Panel 2: Volume
    if CHART.get("show_volume"):
        colors = ["#22c55e" if c >= o else "#ef4444"
                  for c, o in zip(df["close"], df["open"])]
        fig.add_trace(go.Bar(
            x=df.index, y=df["volume"],
            name="Volume", marker_color=colors, opacity=0.7,
        ), row=current_row, col=1)
        current_row += 1

    # Panel 3: RSI
    if CHART.get("show_rsi") and "rsi" in df.columns:
        fig.add_trace(go.Scatter(
            x=df.index, y=df["rsi"],
            name="RSI", line=dict(color="#a78bfa", width=1.5),
        ), row=current_row, col=1)
        fig.add_hline(y=70, line_dash="dot", line_color="#ef4444",
                      opacity=0.5, row=current_row, col=1)
        fig.add_hline(y=30, line_dash="dot", line_color="#22c55e",
                      opacity=0.5, row=current_row, col=1)

    # Step 4: Style the chart
    fig.update_layout(
        template="plotly_dark",
        title=f"{ticker} | {CHART['period']} | Generated {datetime.now().strftime('%Y-%m-%d %H:%M')}",
        height=750, showlegend=True,
        xaxis_rangeslider_visible=False,
        paper_bgcolor="#1c1917", plot_bgcolor="#1c1917",
        font=dict(family="system-ui, sans-serif", color="#e7e5e4"),
    )
    fig.update_xaxes(showgrid=True, gridcolor="#292524")
    fig.update_yaxes(showgrid=True, gridcolor="#292524")

    # Step 5: Save to file
    path = os.path.join(OUT_DIR, f"{ticker}.html")
    fig.write_html(path)
    return path

# ── Generate all charts and an index page ────────────────────────
generated = []
for ticker in TICKERS:
    try:
        path = make_chart(ticker)
        generated.append(ticker)
        print(f"  ✓ {ticker}")
    except Exception as e:
        print(f"  ✗ {ticker}: {e}")

# Write an index.html that links to all charts
index_html = f"""
<html lang="en">
<head><meta charset="UTF-8">
<title>🦞 OpenClaw Charts — {datetime.now().strftime('%Y-%m-%d')}</title>
<style>
  body {{ font-family: system-ui; background: #1c1917; color: #e7e5e4; padding: 40px; }}
  h1 {{ color: #0d9488; }} a {{ color: #0d9488; text-decoration: none; }}
  ul {{ list-style: none; padding: 0; }}
  li {{ margin: 10px 0; font-size: 1.1rem; }}
  li a:hover {{ text-decoration: underline; }}
</style>
</head>
<body>
<h1>🦞 OpenClaw Chart Packet</h1>
<p>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')} | {len(generated)} charts</p>
<ul>
{"".join(f'<li><a href="{t}.html">📈 {t}</a></li>' for t in generated)}
</ul>
</body></html>"""

with open(os.path.join(OUT_DIR, "index.html"), "w") as f:
    f.write(index_html)

print(f"\n✅ Done — {len(generated)}/{len(TICKERS)} charts generated")
print(f"   Open: {OUT_DIR}/index.html")

How the Script Works

  1. Load config: Reads ta-config.yaml to get your watchlist and settings.
  2. Download data: Uses yfinance to fetch historical price data for each ticker.
  3. Calculate indicators: Runs SMA, RSI, and Bollinger Bands calculations based on your config.
  4. Build subplots: Creates a Plotly figure with 1–3 stacked panels depending on your settings.
  5. Style: Applies the dark theme (teal accents, readable fonts) to match OpenClaw branding.
  6. Save HTML: Writes each chart as a standalone .html file in your output folder.
  7. Create index: Generates an index.html linking to all charts for easy browsing.

Opening Your Chart Packet

After the script runs, open the charts/ folder on your computer. Double-click index.html — it opens in your browser showing a list of all your charts. Click any ticker to open its interactive chart.

What You Can Do On Each Chart

Pro tip: Plotly charts are fully self-contained. You can email AAPL.html to a colleague and they can open it in their browser with all interactivity intact — no server needed.

Automating the Nightly Chart Run

Run the chart generator on a schedule so charts are ready for you each morning.

macOS / Linux Cron

# Run every weekday at 5pm — charts are ready when you wake up
# Add to crontab with: crontab -e
0 17 * * 1-5 cd /path/to/openclaw && python3 ta_charts.py

Windows Task Scheduler

  1. Open Task Scheduler
  2. Click Create Basic Task
  3. Set trigger: Daily at 5 PM
  4. Set action: Start a program
  5. Program: C:\Python\python.exe
  6. Arguments: C:\path\to\ta_charts.py
  7. Start in: C:\path\to\openclaw\

Timing note: The script takes about 30–60 seconds for a 10-ticker watchlist. For 50 tickers it takes 3–5 minutes. Run it at 5pm and your charts are ready before dinner.

Series Complete — What You've Built

The Complete OpenClaw Technical Analysis Stack

  • Part 1 — Core Indicators: OpenClaw calculates RSI, MACD, moving averages, and Bollinger Bands on any ticker
  • Part 2 — Scanner: Scans your entire watchlist for signal conditions in minutes, exports to CSV
  • Part 3 — Alerts: Emails you when a signal fires, with cooldown logic and daily digest option
  • Part 4 — Backtesting: Tests your rules against 5 years of history before you trust them
  • Part 5 — Charts: Generates a complete chart packet for your watchlist every night automatically

All driven by a single ta-config.yaml file. Change your watchlist or thresholds once — everything updates.

← Return to Terminal