Go CLI that reads the encrypted snapshot written by mt5-pnl-exporter
(snapshot.json.gz.age: JSON → gzip → age scrypt) and prints P&L/account
tables or JSON. Spec: docs/superpowers/specs/2026-06-13-mt5-pnl-cli-v1-design.md.
go test ./... # all tests (CI runs -race; ubuntu leg uploads coverage to codecov)
go test ./internal/render -update # regenerate golden files after render changes
go build -o mt5-pnl-cli .
go run github.com/goreleaser/goreleaser/v2@latest check # validate .goreleaser.yaml
pre-commit install # gitleaks secret-scan hook (one-time)
pre-commit run --all-files # run the gitleaks hook manuallymain.go—run(args, stdout, stderr, getPassphrase)is the testable entry point;main()wiresosstreams +secrets.Get. Oneflag.FlagSetper subcommand;mainis the only caller ofos.Exit.args.go/cmd_common.go— range parsing (--last,--from/--to), snapshot path resolution (flag >MT5_PNL_SNAPSHOT> error), staleness warning, account-label filter resolution.internal/snapshot— schema 1.x structs, streaming age→gzip→JSON read, version gate (CheckSchemaVersion: same major, minor <= supported).internal/aggregate— deals → group rows + summary.--bychooses the grouping: time cuts (day/week/month) emit per-account rows plus a combinedALLrow per period; symbol/magic cuts emit one row per symbol/magic aggregated across accounts (no per-account or combined row, Account nil). Each row and the summary carry net P&L plus its four components (trade_profit / commission / swap / fee, summing to net). The summary also carries expectancy, average and largest win/loss, and max drawdown (a deal-ordered realised-P&L pass, not equity drawdown).AccountsInScopeexposes the contributing logins for the currency guard. Full-precision sums; rounding happens in render only. Breakeven (net == 0) is neither win nor loss.internal/secrets— keychain via zalando/go-keyring, servicemt5-pnl-cli, accountencryption-passphrase.internal/render— fixed-width tables (manual writer; ANSI colour applied after width padding so it never skews alignment), JSON and CSV; all display rounding here.pnl/accountstake--format table|json|csv(defaulttable).internal/snaptest— test-only fixture builder (encrypts JSON the way the exporter does; low scrypt work factor for speed).
- No config file, by design. Snapshot path via flag/env; everything else is flags. Don't add a config file without revisiting the spec.
- The passphrase has no env var or flag, deliberately (see spec
Security section). Don't add one. Tests inject
getPassphrase; cross-process tests can't reach the keychain, so the binary smoke test only covers pre-keychain failure paths. - CI runs on ubuntu/macos/windows — keep paths
filepath-safe and don't add tests that need a real keychain (keyring.MockInit()only). - Schema bumps: when the exporter ships a new minor, update
SupportedMinor, re-vendorschema/snapshot.schema.jsonfrom that release, and add fields to the structs (additive only). - Deal times are Unix seconds bucketed in UTC; weeks start Monday.
--format.pnl/accountstake--format table|json|csv(defaulttable). CSV is rows-only (no summary).- Mixed-currency guard.
pnlnever sums across currencies: when accounts in scope span more than one, combinedALLrows and the summary are suppressed (n/a/null/omitted) with a stderr warning. A--by symbol|magiccut has no per-account row to fall back to, so it refuses under mixed currency (stderr, exit 1) — narrow--accounts. Scope is the accounts that contributed deals (aggregate.AccountsInScope), not the grouped rows (symbol/magic rows carry no account). --quiet/-qsilences stderr warnings (staleness, mixed-currency); errors still print.--color(pnl only): auto/always/never; auto needs a*os.FileTTY and honoursNO_COLOR/TERM=dumb.- Summary block is table/JSON only. The two-group performance/breakdown
summary appears in
--format table(an aligned key/value block) and--format json; CSV is rows-only by design. Max drawdown is realised-P&L drawdown over the ordered in-scope deals, deliberately distinct from broker equity drawdown. group/group_byare uniform across cuts. EverypnlJSON/CSV row carriesgroup(period date, symbol, or magic) andgroup_by(the--byvalue); the Go field isaggregate.Row.Group.accountis the login for per-account time rows andnullfor the combined time row and for every symbol/magic row. The table labels the first columnPERIOD/SYMBOL/MAGICand drops theACCOUNTcolumn for dimension cuts.- Dependencies are Renovate-managed; don't hand-bump pinned actions or module versions.
- British/Commonwealth English in comments and docs.
- TDD; golden files for table output (
-updateto regenerate, then eyeball the diff). - After changing commands, architecture or a gotcha above, update this file and README.md in the same change.