From 3de31a57c394401c2d87fffa67d3da5e0f82ca30 Mon Sep 17 00:00:00 2001 From: Yi-Ting Shih Date: Sat, 5 Sep 2026 08:57:38 +0800 Subject: [PATCH] initial plan --- PLAN.md | 190 ++++++++++++++++++++++++++++++++++++++++++++++++ REQUIREMENTS.md | 23 ++++++ 2 files changed, 213 insertions(+) create mode 100644 PLAN.md create mode 100644 REQUIREMENTS.md diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..b7cd8fe --- /dev/null +++ b/PLAN.md @@ -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 "" --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. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..dcd5f52 --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,23 @@ +Write a service that. +- Fetch data from various data sources. Focusing on free data sources for financial news and market datas. +- Pass to claude and analyze for investment advices. +- Output via discord webhook. + +Also, the service should be +- Highly customizable, via config files +- Modularized, more data sources and more output webhook can be specified +- Run by systemd service, with systemd timer to trigger it on schedule + +## Frequency + +Daily + +## Targets + +- Mainstream Cryptos + - BTC, ETH, SOL, maybe ZEC +- NASDAQ + - AAPL, GOOGL, TSLA, AVGO, NVDA, INTC, AMD, AMZN +- Futures + - XAU, XAG, Crude Oil +