reddit / emacs

(Semi-)Automatically fetching Ledger commodity prices with elisp

rberaldo · 2022-09-14 03:40
tl;dr: skip to the end for functions to fetch and generate ledger commodity price entries! --- I've been learning [`ledger`](https://www.ledger-cli.org/), a plaintext accounting software, for about a week now. It is great and I already feel like I have more agency over my finances. Stocks can be converted by ledger to their values in a given currency, however prices have to be set manually in a file. Here is an example line from my `prices.ledger`: P 2022/09/05 11:11:00 "PETR4.SA" 33.79 BRL Which means that each `PETR4.SA` share was `P`riced at `33.79 BRL` on the date and time indicated. As I didn't find a way to easily fetch prices automatically, I came up with my own set of elisp functions to do that. Now, I only need to set the point anywhere over the last price entry for a some share and `M-x ledger-copy-down-commodity-price` will fetch the price and generate the next line, e.g. ``` P 2022/09/11 11:07:00 "PETR4.SA" 32.23 BRL P 2022/09/14 00:30:42 "PETR4.SA" 30.65 BRL ``` which isn't totally automatic but is way better looking up the prices and writing the whole line! Anyhow, here's the code: ;; ;; ledger helper functions (defun my/ledger-copy-down-commodity-price () "Adds a new, updated ledger commodity price entry based on the current line, which should follow the structure \"P DATE TIME COMMODITY PRICE CURRENCY\". Automatically updates date, time and fetches current prices." (interactive) (let (line commodity price) ;; Read current line and split it. ;; Idea: add error checking, such as an empty line. (setq line (split-string (buffer-substring-no-properties (line-beginning-position) (line-end-position)))) ;; Gets the commodity. Code isn’t going to work if Yahoo does not ;; recognize it. (setq commodity (string-trim (nth 3 line) "\"" "\"")) ;; Fetches price. If the commodity is BTC, convert it to Brazilian ;; Reais. (setq price (format "%0.2f" (if (equal commodity "BTC") (my/fetch-BTC-BRL) ;; If it isn’t BTC, it is probably a commodity. (my/fetch-yahoo-finance-commodity-price commodity)))) ;; Insert the price entry. (move-end-of-line 1) (newline) (my/ledger-insert-commodity-price commodity price))) (defun my/ledger-insert-commodity-price (c p) "Inserts a ledger commodity entry with commodity C and price P." (interactive (list (read-string "Commodity: ") (read-string "Price: "))) (insert (concat "P " (format-time-string "%Y/%m/%d %T ") "\"" c "\"" " " p " BRL"))) (defun my/fetch-yahoo-finance-commodity-price (c) "Fetches commodity C prices from Yahoo Finance." (let (yahoo-base-url price-json) (setq yahoo-base-url "https://query1.finance.yahoo.com/v8/finance/chart/") (setq price-json (with-current-buffer (url-retrieve-synchronously (concat yahoo-base-url c)) (goto-char (point-min)) (goto-char url-http-end-of-headers) (prog1 (json-read) (kill-buffer)))) ;; Extract price from json-derived plist; not sure if this is the ;; correct way. (cdr (assoc 'regularMarketPrice (assoc 'meta (aref (cdr (assoc 'result (assoc 'chart price-json))) 0)))))) (defun my/fetch-BTC-BRL () "Fetches BTC-BRL price from Yahoo Finances." (let (btc-price brl-usd) (setq btc-price (my/fetch-yahoo-finance-commodity-price "BTC-USD")) (setq brl-usd (my/fetch-yahoo-finance-commodity-price "BRL=X")) (* btc-price brl-usd))) And a gist link in case Reddit messes with indentation: https://gist.github.com/rberaldo/82b3f9c463f603665eb16d16dc7b4f32 Mind you that I convert BTC to BRL, so you'll probably have to adapt it, and that I'm not sure about how to parse JSON properly; but I hope it helps someone out there scratch their itches!
char1zard4 · 2022-09-15 00:01
This is awesome! Full disclosure, I built something to do this a while ago, but using elisp is pretty convenient, and this is even interactive! https://git.sr.ht/~adchari/ledgerdb
rberaldo · 2022-09-15 10:59
I wish I'd found your tool before! Thanks for sharing. I'll be sure to check it out, specially the parsing and populating with new entries of the prices file. Maybe I'll be inspired to write another function to further automate this hahaha
github-alphapapa · 2022-09-16 01:03
Cool. A few small tips: 1. Use four-space indentation so the code will display properly on old.reddit.com. 2. `plz` can make the network code easier to write: https://github.com/alphapapa/plz.el 3. `map-nested-elt` would be cleaner than the nested `(cdr (assoc...` tree. `map.el` is built-in to Emacs.
rberaldo · 2022-09-17 00:04
Thanks for the help. I have two short questions: 2. Isn't `requir`ing a whole library like `plz` or `request` just for this a little overkill? Or am I just being too conservative? 3. I figured out how to do it with `map-nested-elt`, `(map-nested-elt (aref (map-nested-elt x '(chart result)) 0) '(meta regularMarketPrice))` but I still need to `aref` my way into the vector in the middle of the alist. Is there a cleaner way to do that?
github-alphapapa · 2022-09-19 23:01
1. It's up to you. If the built-in `url-retrieve` code works for you, great. But it has some issues for some use cases, which is why these alternatives were written. 2. You could use `pcase-let` to destructure the results, but it can be challenging for new users to learn its syntax. See also `let-alist`, which can help, but can't do everything `pcase-let` can.