feat: dedup and format the output
This commit is contained in:
8 files changed
+153
-54
No files matched your search
@@ -9,10 +9,12 @@ A Go service that fetches market data (crypto/equities/futures/forex) and news,
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
go build -o stonks ./cmd/stonks # build
|
||||
go vet ./... # static checks
|
||||
gofmt -l . # list any unformatted files (should be empty); gofmt -w to fix
|
||||
go test ./... # no test files exist yet — this is a no-op until some are added
|
||||
make build # go build -o stonks ./cmd/stonks
|
||||
make install # build, then install to ~/.local/bin/stonks (matches systemd/stonks.service's ExecStart)
|
||||
make vet # go vet ./...
|
||||
make fmt # gofmt -l . — list any unformatted files (should be empty); gofmt -w to fix
|
||||
make test # go test ./... — no test files exist yet, this is a no-op until some are added
|
||||
make clean # remove the built binary
|
||||
```
|
||||
|
||||
Run locally (needs secrets loaded into the environment first — see `config/secrets.env.example`):
|
||||
@@ -22,7 +24,7 @@ set -a; source config/secrets.env; set +a
|
||||
go run ./cmd/stonks --config config/config.yaml --timeout 120s
|
||||
```
|
||||
|
||||
There is no Makefile or CI config — the commands above are the whole toolchain.
|
||||
There is no CI config — the Makefile plus the commands above are the whole toolchain.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -59,7 +61,9 @@ Two interchangeable modes behind one `Analyzer` interface, selected by `analyzer
|
||||
- `api.go`: calls the Messages API directly via `anthropic-sdk-go`, billed per token.
|
||||
- `subscription.go`: shells out to the `claude` CLI (`claude -p`, reading the prompt from stdin), billed against the host's logged-in Claude Pro/Max subscription instead of metered API usage. Requires `claude login` to have been run once on the deploying host/user.
|
||||
|
||||
`prompt.go` builds the user-turn text from `fetch.Result` and holds `SystemPrompt`, which is the single source of truth for the report's structure (Crypto/Equities/Futures/Forex/Watch sections) **and** instructs Claude to append a `===RECOMMENDATIONS_JSON===` marker followed by a JSON buy/sell list as the last thing in its response. `split.go`'s `SplitReport` parses that trailing block into `report.Report{Text, Recommendations}` via `extractJSONObject` (grabs from the first `{` to the last `}` after the marker — tolerant of stray code fences/prose Claude sometimes adds despite being asked not to, observed in practice with multi-agent mode on). If the marker's missing or the JSON still fails to parse, it degrades to a text-only report (logs a warning in `main.go`) rather than failing the run. Keep the marker instruction and the parser in sync if you touch either.
|
||||
`prompt.go` builds the user-turn text from `fetch.Result` and holds `SystemPrompt`, which is the single source of truth for the report's structure (fixed Summary/Crypto/Equities/Futures/Forex/Watch section headers, in that order, every run) **and** instructs Claude to append a `===RECOMMENDATIONS_JSON===` marker followed by a JSON buy/sell list as the last thing in its response. `split.go`'s `SplitReport` parses that trailing block into `report.Report{Text, Recommendations}` via `extractJSONObject` (grabs from the first `{` to the last `}` after the marker — tolerant of stray code fences/prose Claude sometimes adds despite being asked not to, observed in practice with multi-agent mode on). If the marker's missing or the JSON still fails to parse, it degrades to a text-only report (logs a warning in `main.go`) rather than failing the run. Keep the marker instruction and the parser in sync if you touch either.
|
||||
|
||||
Because the timer fires every 5 hours, `main.go` calls `archive.LoadLatest` before building the prompt to read back the previous run's `report.md` and passes it into `BuildUserPrompt` as `previousReport`. `SystemPrompt` instructs Claude to treat that as a "Previous Briefing" section: dedup commentary against it (fresh prices are never skipped, but unchanged analysis should collapse to a one-liner), and only carry forward previously-mentioned info if it's still genuinely important (new regulation, unresolved macro event). This is why archiving is no longer optional — see below.
|
||||
|
||||
**Multi-agent mode** (`advisor.go`, optional via `analyzer.advisors` in config): the same data prompt is also piped to other one-shot CLIs (`gemini`, `codex exec -`, ...) concurrently. This is deliberately **not** a fourth `Analyzer` — advisors don't produce the final report shape, they're extra input to it. `main.go` runs them after building the base prompt, logs failures as warnings (same non-fatal pattern as fetchers — a failing/unauthenticated advisor is just excluded), then `AppendAdvisorSection` appends the successful ones to the prompt before the real `analyzer.Analyze` call. `SystemPrompt` tells Claude to treat that section as opinions to weigh, not ground truth to defer to.
|
||||
|
||||
@@ -69,9 +73,9 @@ Two interchangeable modes behind one `Analyzer` interface, selected by `analyzer
|
||||
|
||||
`discord.go` sends the narrative text as one or more chunked messages (Discord's 2000-char `content` limit), then **always** sends a second message with two separate embeds — green "🟢 Buy" and red "🔴 Sell" — even when a list is empty (states "No buy/sell recommendations today." rather than omitting the embed). If you change the recommendation rendering, keep the two embeds separate; that was an explicit choice, not a simplification opportunity.
|
||||
|
||||
### Archiving (`internal/archive`, optional)
|
||||
### Archiving (`internal/archive`, always on)
|
||||
|
||||
`archive.Save`, called from `main.go` when `config.Archive.Enabled`, writes one timestamped subdirectory per run under `archive.dir` (default `~/.local/state/stonks/runs`) containing `fetched.json`, `prompt.txt` (the *final* prompt, post-advisor-section), `analysis_raw.md` (pre-`SplitReport`), `report.md`, `recommendations.json`. This is a debugging aid, not part of the pipeline — a save failure is logged and non-fatal, same pattern as everything else here.
|
||||
`archive.Save`, called unconditionally from `main.go`, writes one timestamped subdirectory per run under `archive.dir` (default `~/.local/state/stonks/runs`) containing `fetched.json`, `prompt.txt` (the *final* prompt, post-advisor-section), `analysis_raw.md` (pre-`SplitReport`), `report.md`, `recommendations.json`. It's still a debugging aid, but it's also load-bearing now: `archive.LoadLatest` reads the most recent run's `report.md` back at the start of the next run so the prompt can dedup against it (see `prompt.go` above). A save failure is logged and non-fatal — it just means the *next* run won't have anything to dedup against, same non-fatal pattern as everything else here.
|
||||
|
||||
## Config and secrets
|
||||
|
||||
|
||||
Reference in new issue
Block a user