Charts are visual research tools. Pattern recognition on a chart is subjective and does not constitute financial advice.
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.
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.
.html files you open in any browser.Most users use Plotly for their daily watchlist review and matplotlib when they want to email a chart to someone.
Each chart has three stacked panels. Here's how to read them:
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
"plotly" for interactive HTML in your browser, or "matplotlib" for static PNG files."6mo", "1y", "2y", or even "5y".true to plot each moving average. false to skip."0 17 * * 1-5" = 5pm ET, Monday–Friday.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")
ta-config.yaml to get your watchlist and settings.yfinance to fetch historical price data for each ticker..html file in your output folder.index.html linking to all charts for easy browsing.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.
AAPL.html to a colleague and they can open it in their browser with all interactivity intact — no server needed.
Run the chart generator on a schedule so charts are ready for you each morning.
# 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
C:\Python\python.exeC:\path\to\ta_charts.pyC:\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.
All driven by a single ta-config.yaml file. Change your watchlist or thresholds once — everything updates.