Files
stonks/PLAN.md
T
2026-09-05 08:57:38 +08:00

11 KiB

Stonks — Implementation Plan

Daily financial news + market data analysis service. Fetches free-tier market data and news, sends it to Claude for investment analysis, and posts the result to Discord — on a systemd timer. Written in Go.

Architecture

systemd timer (daily) --> stonks.service (oneshot) --> cmd/stonks/main.go
                                                             |
                                     ---------------------------------------------
                                     |                       |                    |
                                internal/fetch          internal/analyze     internal/output
                                (price + news           (Claude: API-key      (Discord webhook,
                                 fetchers)                or subscription      more later)
                                                          mode)

Directory layout

stonks/
├── go.mod
├── cmd/stonks/main.go          # entrypoint: load config, run pipeline
├── internal/
│   ├── config/
│   │   └── config.go           # YAML config struct + loader
│   ├── fetch/
│   │   ├── fetcher.go          # Fetcher interface + registry (name -> constructor)
│   │   ├── yfinance.go         # prices: crypto/equities/futures via piquette/finance-go (yfinance-equivalent)
│   │   ├── finnhub.go          # quotes + company news via Finnhub free tier
│   │   └── rss.go              # general market news via RSS feeds (Reuters, CNBC, CoinDesk, ...)
│   ├── analyze/
│   │   ├── analyzer.go         # Analyzer interface + registry
│   │   ├── api.go              # Anthropic API-key mode (anthropic-sdk-go)
│   │   └── subscription.go     # Claude Code CLI mode (`claude -p`, subscription-billed)
│   └── output/
│       ├── output.go           # Output interface + registry
│       └── discord.go          # Discord webhook
├── config/
│   ├── config.yaml             # targets, fetchers, analyzer, outputs
│   └── secrets.env             # ANTHROPIC_API_KEY / FINNHUB_API_KEY / DISCORD_WEBHOOK_URL (mode 600, not in git)
└── systemd/
    ├── stonks.service           # user-scope unit (~/.config/systemd/user/)
    └── stonks.timer             # user-scope unit (~/.config/systemd/user/)

Modularity in Go (compile-time registry, not runtime plugins)

Go has no good runtime plugin-loading story (the stdlib plugin package is Linux-only, fragile, and requires matching toolchains — not worth it for this project). Instead:

  • Each fetcher/analyzer/output implements a small interface and self-registers via init():
    func init() { fetch.Register("yfinance", NewYFinanceFetcher) }
    
  • main.go blank-imports every package so init() runs.
  • config.yaml lists which registered names to activate, with params.
  • Adding a new source = write a new file implementing the interface, register it, add a blank import, rebuild the binary. This satisfies "modularized, more sources can be specified" for a compiled language — it trades Python's no-rebuild dynamic loading for compile-time safety and simplicity. Flagging this trade-off explicitly since it's a real difference from a scripting-language design.

Config example

targets:
  crypto: [BTC-USD, ETH-USD, SOL-USD, ZEC-USD]
  equities: [AAPL, GOOGL, TSLA, AVGO, NVDA, INTC, AMD, AMZN]
  futures: {gold: GC=F, silver: SI=F, crude_oil: CL=F}

fetchers:
  - name: yfinance
  - name: finnhub
    params:
      api_key_env_var: FINNHUB_API_KEY
  - name: rss
    params:
      feeds:
        - https://feeds.reuters.com/reuters/businessNews
        - https://www.coindesk.com/arc/outboundfeeds/rss/

analyzer:
  mode: subscription       # "subscription" (Claude Code CLI, uses Pro/Max plan) or "api" (billed per-token)
  model: claude-opus-5     # only used in api mode
  effort: medium           # only used in api mode

outputs:
  - name: discord
    params: {webhook_env_var: DISCORD_WEBHOOK_URL}

Data sources (free tier)

Category Source Notes
Prices — crypto/equities/futures yfinance-equivalent: github.com/piquette/finance-go, a Go port of the same unofficial Yahoo Finance chart API that Python's yfinance scrapes Free, no key. Covers BTC-USD/ETH-USD/SOL-USD/ZEC-USD, all 8 equities, and futures (GC=F/SI=F/CL=F) through one client.
Quotes + company news Finnhub free tier (github.com/Finnhub-Stock-API/finnhub-go, or a thin hand-rolled HTTP client — the API surface is small) Free tier requires an API key (FINNHUB_API_KEY) but is generous (60 calls/min); used here as a second source for equity quotes and per-ticker company news headlines. Good redundancy against the yfinance-equivalent fetcher.
General market news RSS feeds (Reuters Business, CNBC, CoinDesk) Free, no key. Use gofeed (github.com/mmcdole/gofeed) for parsing.

Risk: the yfinance-equivalent path hits an undocumented/unofficial Yahoo endpoint and can change without notice; Finnhub is an official, documented API and is the more stable of the two price sources. Mitigate by wrapping each fetcher call in error handling that logs and skips that source/asset class rather than failing the whole run — a degraded report beats a missed one, and having both yfinance-equivalent and Finnhub means one breaking doesn't blank out prices entirely.

Claude integration — two modes

  • Requires the claude CLI installed and logged in on the host (claude login, authenticated against your Claude Pro/Max subscription).
  • The analyzer shells out via os/exec: claude -p "<prompt with data appended>" --output-format text.
  • Usage draws from your subscription's rate limits/quotas rather than separate API billing. For one call/day this is a rounding error against those limits.
  • Caveats: ties the automated job to a personal OAuth login session on that machine rather than a stable API key — the login can eventually need manual re-auth, and running unattended automation against a personal-subscription login should be something you're comfortable with operationally (it's officially supported for scripting via -p/headless mode, but it's still your personal account driving it).

api mode (fallback / no subscription available)

  • Uses ANTHROPIC_API_KEY via the official anthropic-sdk-go client (github.com/anthropics/anthropic-sdk-go), billed per token.
  • Model defaults to claude-opus-5; claude-sonnet-5 or claude-haiku-4-5 are configurable if you want to cut cost further (see Cost below — at this volume it barely matters).
  • One request/day: system prompt (analysis format/rules) + user turn with the fetched price/news JSON. effort: medium is sufficient — no agentic tool use needed.

Both modes implement the same Analyzer interface, selected by analyzer.mode in config.

systemd integration — user scope (systemctl --user)

Units live under ~/.config/systemd/user/ and are managed with systemctl --user, not root-owned /etc/systemd/system/. This fits naturally with subscription mode, since claude login's session lives under the same user's $HOME anyway — no User=/permissions mismatch to manage.

# ~/.config/systemd/user/stonks.service
[Unit]
Description=Daily financial news & market analysis
After=network-online.target

[Service]
Type=oneshot
EnvironmentFile=%h/.config/stonks/secrets.env
ExecStart=%h/.local/bin/stonks --config %h/.config/stonks/config.yaml
# ~/.config/systemd/user/stonks.timer
[Unit]
Description=Run stonks daily

[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true

[Install]
WantedBy=timers.target

%h expands to the invoking user's home directory. Enable with:

systemctl --user enable --now stonks.timer

User-scope services normally stop when the user logs out. Since this needs to fire daily unattended (including when nobody's logged in), enable lingering once:

loginctl enable-linger "$USER"

This keeps the user's systemd instance (and anything scheduled under it) running after logout and across reboots, without needing root privileges for the service itself.

Persistent=true catches up a missed run if the machine was off/asleep at trigger time. Secrets (ANTHROPIC_API_KEY if using api mode, FINNHUB_API_KEY, DISCORD_WEBHOOK_URL) live in ~/.config/stonks/secrets.env (mode 600), never in config.yaml or git. subscription mode needs no secret in this file — it uses the same user's existing claude login session under ~/.config.

Feasibility

Straightforward, no blockers:

  • yfinance-equivalent, Finnhub, and RSS are all simple HTTP calls — Finnhub is documented and key-based, the other two need no auth.
  • A single non-agentic Claude call (or CLI invocation) — no tool use, no streaming complexity needed at this size.
  • Discord webhook is a plain POST with a JSON body.
  • systemd user-scope timer + oneshot service + loginctl enable-linger is a standard, documented pattern for unattended per-user automation.

Main risks are the usual "free/unofficial data source" risks (the yfinance-equivalent path can break if Yahoo changes their endpoint; RSS feed URLs occasionally get retired) — both mitigated by graceful per-fetcher degradation rather than hard failure, and by having Finnhub as a documented, more stable second price source.

Estimated build time: a working v1 (one fetcher, one output, hardcoded targets) in a few hours; the full config-driven/registry version with graceful degradation, a bit more.

Cost

subscription mode: $0 marginal cost, covered by your existing Claude Pro/Max subscription, assuming usage stays well within its rate limits — trivially true for one call/day.

api mode, for ~5-15k input / ~1-2k output tokens per day (no caching benefit at one call/day):

Model Input $/MTok Output $/MTok Daily (worst case: 15k in / 2k out) Daily (typical: 8k in / 1.5k out) Monthly (typical)
Claude Opus 5 $5.00 $25.00 $0.125 $0.078 ~$2.30
Claude Sonnet 5 (intro, through 2026-08-31) $2.00 $10.00 $0.050 $0.031 ~$0.94
Claude Sonnet 5 (standard) $3.00 $15.00 $0.075 $0.047 ~$1.40
Claude Haiku 4.5 $1.00 $5.00 $0.025 $0.016 ~$0.47

Even at Opus 5 pricing in api mode this is under $3/month — the choice between modes should be driven by which auth story you'd rather run unattended (personal subscription login vs. a static API key), not by cost.

Open questions / next steps

  • Confirm which analyzer mode to build first (recommend: subscription as default, api as a fallback path — both are cheap to implement since they share the Analyzer interface).
  • Confirm claude login will run interactively once during setup (subscription mode), and get a free Finnhub API key (finnhub.io — instant signup) for FINNHUB_API_KEY.
  • Confirm loginctl enable-linger is acceptable on the target host (needed for the user-scope timer to fire while logged out).
  • Once confirmed, implementation order: internal/fetch (yfinance-equivalent + finnhub + rss) → internal/analyze (both modes) → internal/output (discord) → wire up cmd/stonks/main.go + config loader → user-scope systemd units.