Personal Stock Intelligence on a Raspberry Pi Running Home Assistant OS — No Paid APIs, No Scheduled Guesswork

๐Ÿฆž OpenClaw on HAOS · Part 4

Personal Stock Intelligence
on a Raspberry Pi + HAOS

Running on a Raspberry Pi 5 with Home Assistant OS, three Python scripts replace your scheduled cron jobs with on-demand portfolio intelligence — live P&L, Google Sheets sync, and a buy/sell signal engine. No cloud, no paid APIs, no monthly fees.

๐Ÿฅง Raspberry Pi 5 · HAOS ๐Ÿ“Š 25 Holdings ๐Ÿ 3 Python Scripts 0 API calls for data ๐ŸŸข Gemini Free Tier
portfolio.py ▲ running sync_portfolio.py ▲ 25 synced signals.py ▲ 3 buy signals cron 14:30 Sun–Thu ▲ active Gemini Flash ▲ free tier HAOS 14.x ▲ running Yahoo Finance ▲ live portfolio.py ▲ running sync_portfolio.py ▲ 25 synced signals.py ▲ 3 buy signals cron 14:30 Sun–Thu ▲ active Gemini Flash ▲ free tier HAOS 14.x ▲ running Yahoo Finance ▲ live
๐Ÿ portfolio.py · ๐Ÿ”„ sync_portfolio.py · ๐Ÿ“ก signals.py

Cron vs On-Demand Skill

๐Ÿค”
Running OpenClaw on a Raspberry Pi 5 with Home Assistant OS, we started with two cron jobs at 10:15 AM and 2:30 PM. The morning one timed out regularly. Both ran whether the market was interesting or not. The fix wasn't a better cron — it was rethinking when analysis actually adds value.
Cron JobOn-Demand Skill
TriggersFixed time, every dayWhen you ask
ContextNone — runs blindAfter news, market moves
Follow-upNoAsk questions immediately
Rate limit riskSilent failuresYou see errors live
Weekend runsWasted API callsYou wouldn't ask
The Decision

Keep one cron — the 2:30 PM end-of-day summary, Sunday to Thursday only (local market trading days). Drop the morning cron entirely. Replace it with an on-demand skill triggered by pf on Telegram.

New Architecture
Google Sheets
sync_portfolio.py
my-portfolio.json
portfolio.py
Telegram
my-portfolio.json
signals.py
Buy / Sell candidates
๐Ÿ’ก
Key insight

The scripts do all the work. The LLM (Gemini) just receives a trigger phrase, runs one bash command, and forwards the output. This means zero LLM involvement in the actual data fetching, calculation, or formatting.

Telegram Commands
MessageWhat runsLLM calls
pfportfolio.py quick2
full portfolioportfolio.py (full)2
sync portfoliosync_portfolio.py + portfolio.py quick2
top signalssignals.py --top2
buy signalssignals.py --buy2
sell signalssignals.py --sell2
top 5 signalssignals.py --top52

portfolio.py — Live P&L Script

๐Ÿ
The core script. Reads your holdings JSON, fetches live prices from Yahoo Finance via curl, calculates capital P&L + dividend-adjusted total return, and formats a clean Telegram message. No external libraries needed beyond Python stdlib.
Install
1
Copy script to skill directory (HAOS persistent storage)
terminal
root@openclaw:/# mkdir -p /config/clawd/skills/portfolio-tracker
cp /share/portfolio.py /config/clawd/skills/portfolio-tracker/portfolio.py
chmod +x /config/clawd/skills/portfolio-tracker/portfolio.py
2
Test directly from SSH
portfolio.py quick
root@openclaw:/# python3 /config/clawd/skills/portfolio-tracker/portfolio.py quick
๐Ÿ portfolio.py v2.0
⏳ Fetching prices for 21 stocks...

๐Ÿ“Š Portfolio Snapshot — 3:53 PM · Mon 9 Mar 2026

๐Ÿ’ผ Value: $128,450
๐Ÿ“‰ True Total Return: -3.0% (capital + dividends)
๐Ÿ“‰ Today's Move: -$804

๐Ÿ“Š Top Movers Today:
↑ Shell (SHEL) +5.8% → $31.42
↑ Unilever (UL) +4.1% → $48.20
↓ HSBC (HSBC) -3.9% → $42.60

๐Ÿšจ Big Movers (±3.0%): HSBC (-3.9%), BP (-3.4%), Unilever (+4.1%), Shell (+5.8%)
How It Fetches Prices
fetch logic
# One ticker at a time — 2 second delay between each
for i, h in enumerate(holdings):
    if i > 0:
        time.sleep(2)   # avoids Yahoo Finance rate limiting
    current, prev_close = get_price_and_prev_close(h["ticker"])

# Curl call — no external libraries
subprocess.run(["curl", "-s", "-H", "User-Agent: Mozilla/5.0", url])
⚠️
Why curl, not yfinance?

The OpenClaw container has Python stdlib but no pip by default. curl is always available and uses the same Yahoo Finance v8 API. No install, no dependency drift, no version conflicts.

Calculations
per stock
cap_pnl       = current_value - cost
total_return  = cap_pnl + dividends
total_ret_pct = total_return / cost × 100    # always dividend-adjusted
day_chg_pct   = (current - prev_close) / prev_close × 100
๐Ÿ’ก
Always show dividend-adjusted return

Capital P&L alone is misleading for dividend stocks. A stock down 15% capital but paying 20% in dividends is a winner. The script always shows total return = capital P&L + dividends received.

sync_portfolio.py — Google Sheets Source of Truth

๐Ÿ”„
Instead of manually editing the portfolio JSON every time you buy, sell, or receive a dividend — keep a Google Sheet as your master record and sync it to the Pi on demand. One command, 25 holdings updated in seconds.
Sheet Structure
SheetKey Columns UsedPurpose
HoldingsCode (col 1), Name (col 3), Qty (col 4), Cost (col 5), Divs (col 11)Current holdings snapshot
TransactionsSymbol (col 0), Qty (col 5), Cost (col 6)Weighted avg cost calculation
DividendsSymbol (col 0), Dividend (col 5)Total dividends per stock
โ„น️
Weighted average cost

The script calculates avg cost from your full transaction history — not just the current cost in MasterData. If you bought 100 shares at $50 and 50 more at $60, your true avg cost is $53.33, not $60.

Setup
1
Create Google Service Account

Go to console.cloud.google.com → New Project → Enable Google Sheets API → IAM & Admin → Service Accounts → Create → Download JSON key.

Copy the JSON key to your Pi:

cp /share/service-account.json /config/clawd/memory/gsheets-credentials.json
chmod 600 /config/clawd/memory/gsheets-credentials.json
2
Share your Google Sheet

Open the downloaded JSON key, copy the client_email field, and share your Google Sheet with that email address (Viewer access is enough).

3
Install dependencies and set Sheet ID
# Install pip first (not present by default in OpenClaw container)
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3 get-pip.py --break-system-packages
python3 -m pip install google-auth google-auth-httplib2 google-api-python-client --break-system-packages

# Set your Sheet ID in the script (line 22)
# Find it in your Sheet URL: .../spreadsheets/d/THIS_PART/edit
nano /config/clawd/skills/portfolio-tracker/sync_portfolio.py
⚠️
HAOS ephemeral container

In Home Assistant OS, the OpenClaw add-on container resets on every restart — pip packages installed to /usr/local/lib are wiped. Re-run the pip install after every restart. Scripts at /config/clawd/skills/ persist because /config is HAOS persistent storage.

4
Dry run, then sync
sync output
root@openclaw:/# python3 sync_portfolio.py --dry-run
๐Ÿ”— Connecting to Google Sheets...
✔ Connected
๐Ÿ“ฅ Reading Holdings... 32 rows found
๐Ÿ“ฅ Reading Transactions... 318 rows found
๐Ÿ“ฅ Reading Dividends... 94 rows found
๐Ÿ“Š Sync Preview (25 holdings)
Apple Inc AAPL 185 shares avg 42.10 div 843.20
Microsoft MSFT 92 shares avg 156.30 div 312.50
... 23 more holdings
Total invested : $135,200
๐Ÿ” Dry run — no changes written

root@openclaw:/# python3 sync_portfolio.py # run for real
✅ Synced 25 holdings → my-portfolio.json

signals.py — Buy & Sell Engine

๐Ÿ“ก
A composite scoring engine that ranks your holdings as buy or sell candidates based on proximity to highs/lows, trend direction, RSI momentum, volume, and dividend yield. Returns ranked candidates — not just a binary signal.
Scoring Components
๐ŸŸข Buy Score
Near 30D / 50D / 6M low 0–40 pts
RSI oversold / neutral 0–20 pts
SMA20 ≥ SMA50 (bull trend) 0–15 pts
Volume spike (1.5× avg) 0–10 pts
Dividend yield ≥ 3% 0–15 pts
๐Ÿ”ด Sell Score
Near 20D / 50D / 6M high 0–40 pts
RSI overbought (≥70) 0–20 pts
SMA20 < SMA50 (bear trend) 0–20 pts
Volume on up day (exhaustion) 0–10 pts
Negative momentum (<-2% today) 0–10 pts
Usage
all modes
python3 signals.py --top    # top 3 buy + top 3 sell (default)
python3 signals.py --top4   # top 4
python3 signals.py --top5   # top 5
python3 signals.py --buy    # buy signals only
python3 signals.py --sell   # sell signals only
python3 signals.py          # full analysis — all holdings
Sample Output
signals.py --top
๐ŸŽฏ Top Signals — 3:53 PM · Mon 9 Mar 2026

๐ŸŸข Top 3 Buy Opportunities:
BP (BP.L) $4.82 Score: 52 RSI: 31 ๐Ÿ“Š Mild bull
→ Near 6M low (2.1% above)
→ RSI oversold (31)
Lloyds Bank (LLOY.L) $0.58 Score: 38 RSI: 38 ๐Ÿ“‰ Mild bear
→ Near 50D low (1.8% above)
→ Div yield (~5.2%)
Vodafone (VOD.L) $0.72 Score: 35 RSI: 42 ๐Ÿ’ช Strong bull
→ Near 30D low (4.3% above)

๐Ÿ”ด Top 3 Sell Candidates:
HSBC (HSBC) $42.60 Score: 47 RSI: 72 ๐Ÿ“ˆ Mild bull
→ Near 6M high (0.8% below)
→ RSI overbought (72)
Shell (SHEL) $31.42 Score: 33 RSI: 65 ๐Ÿ’ช Strong bull
→ Near 20D high (1.2% below)
Unilever (UL) $48.20 Score: 28 RSI: 61 ๐Ÿ“Š Mild bull
→ Near 50D high (1.9% below)
⚠️
Not financial advice

These signals are mechanical indicators based on price and volume data. They don't account for fundamentals, news, earnings, or macroeconomic conditions. Use as one input among many — not as trading instructions.

LLM Lessons — What We Learned the Hard Way

๐Ÿง 
Building this system involved trying every available LLM backend — Anthropic, Gemini, and four local Ollama models. Here's what actually happened and what it taught us about LLM selection for agentic tasks.
The Provider Journey
Anthropic Claude — Rate Limited

Started here. Works perfectly for portfolio analysis but hit API rate limits on the free tier — the full skill invocation (system prompt + MEMORY.md + SKILL.md + tool use) counts as 2 LLM calls with ~3,500 tokens total. On a basic tier that adds up fast.

Qwen3:8B local — Reached for Web Search

Switched to local Ollama to avoid rate limits. Qwen3 8B ignored the skill instructions entirely and tried to use Brave Search for every portfolio query. The "thinking mode" in Qwen3 made it overthink simple bash commands and reach for tools it didn't have.

Qwen2.5:14B local — Timed Out

Better instruction following than Qwen3 but running on CPU (RTX 4070 couldn't fit it fully), inference was 2–5 tokens/second. OpenClaw's hardcoded ~60s timeout fired before the model could even decide to run the bash command.

GLM-4.7-Flash local — Also Timed Out

A 30B MoE model that only activates ~3B parameters — impressive architecture but still CPU-bound on a 12GB GPU. Same timeout issue as Qwen2.5.

Gemini 3 Flash — The Winner

Free tier, fast, no rate limits for this usage pattern, and correctly follows the SKILL.md instruction to run a bash command and forward output. Two LLM calls per invocation but at milliseconds each, not seconds. Reliable end-to-end.

Key Takeaways
Lesson 01 — Scripts beat prompts for data tasks

Any task involving fetching, calculating, or formatting structured data should be a Python script. The LLM's job is just to run it. This removes model quality as a variable — even a small model can run python3 script.py.

Lesson 02 — Local models need GPU-fit to be useful for agentic tasks

CPU inference is too slow for OpenClaw's timeout. A model needs to fit entirely in VRAM to respond within 60 seconds. On an RTX 4070 (12GB) that means 7B models at Q4 or smaller. The sweet spot for instruction-following at this VRAM level is qwen2.5:7b or mistral-nemo:12b on a 24GB card.

Lesson 03 — Disable web search for local models

Small local models will reach for web_search whenever they're unsure. Since Brave Search requires an API key that's not set, this causes every query to fail. Run openclaw config set tools.web.search.enabled false when using local models.

Lesson 04 — Ollama on Windows needs OLLAMA_HOST set

By default Ollama only listens on localhost. To reach it from the Pi, set the environment variable OLLAMA_HOST=0.0.0.0:11434 on Windows and allow port 11434 through the firewall. Then register the endpoint in OpenClaw via openclaw configure — the llm-switch script alone isn't enough for custom providers.

Lesson 05 — memorySearch must be enabled

Skills and MEMORY.md are only injected into the context if agents.defaults.memorySearch.enabled is true. If a model seems to have no context about your portfolio, check this first: openclaw config set agents.defaults.memorySearch.enabled true.

Comments