initial plan

This commit is contained in:
ytshih committed 2026-09-05 08:57:38 +08:00
commit 3de31a57c3
2 files changed
+213

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
# 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()`:
```go
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
```yaml
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
### `subscription` mode (recommended default — $0 marginal cost)
- 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.
```ini
# ~/.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
```
```ini
# ~/.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:
```sh
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:
```sh
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.