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!