Personal Stock Intelligence on a Raspberry Pi Running Home Assistant OS — No Paid APIs, No Scheduled Guesswork
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.
Cron vs On-Demand Skill
| Cron Job | On-Demand Skill | |
|---|---|---|
| Triggers | Fixed time, every day | When you ask |
| Context | None — runs blind | After news, market moves |
| Follow-up | No | Ask questions immediately |
| Rate limit risk | Silent failures | You see errors live |
| Weekend runs | Wasted API calls | You wouldn't ask |
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.
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.
| Message | What runs | LLM calls |
|---|---|---|
| pf | portfolio.py quick | 2 |
| full portfolio | portfolio.py (full) | 2 |
| sync portfolio | sync_portfolio.py + portfolio.py quick | 2 |
| top signals | signals.py --top | 2 |
| buy signals | signals.py --buy | 2 |
| sell signals | signals.py --sell | 2 |
| top 5 signals | signals.py --top5 | 2 |
portfolio.py — Live P&L Script
curl, calculates capital P&L + dividend-adjusted total return, and formats a clean Telegram message. No external libraries needed beyond Python stdlib.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
๐ 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%)
# 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])
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.
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
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
| Sheet | Key Columns Used | Purpose |
|---|---|---|
| Holdings | Code (col 1), Name (col 3), Qty (col 4), Cost (col 5), Divs (col 11) | Current holdings snapshot |
| Transactions | Symbol (col 0), Qty (col 5), Cost (col 6) | Weighted avg cost calculation |
| Dividends | Symbol (col 0), Dividend (col 5) | Total dividends per stock |
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.
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
Open the downloaded JSON key, copy the client_email field, and share your Google Sheet with that email address (Viewer access is enough).
# 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
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.
๐ 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
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
๐ข 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)
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
Post a Comment