Hey Thanks Red! That's a really nice summary of your user journey. I'm glad to see others picking this up and using it.
On Sat, Dec 26, 2020 at 3:00 AM redst...@gmail.com <redstre...@gmail.com> wrote: > This is fantastic, thanks for sharing, Martin. I typed up a review of my > experiences with it. See the doc here > <https://docs.google.com/document/d/1FsbdqbPWaupPg67kZp1CK0BoX2Ri4WhGnLa5vNTBBAs/edit#>. > I tried to paste it below, but the formatting is lost (this used to work > earlier). > > *Overview* > > Extracting cashflows needed to compute IRR is challenging because of the > flexibility that bookkeeping systems like Beancount need to offer in order > to be useful. > > This is a review of beancount/experiments/returns that Martin posted > recently, including what you need to get up and running, and pitfalls to > watch out for. > > First, a review: > > - Fantastic documentation (as usual) and clarity in code (also as > usual), along with clear output that includes the transactions, posting > categorizations, templates that were identified, and extracted cashflows in > the output files. > > > - The simplicity of the generalized cashflow method (investments.py: > produce_cash_flows_general()) is neat and helpful in building one’s mental > model when working with this tool. > - Runtime performance: it takes ~3.5 minutes to run on my transactions > of about 10k investment related postings on older hardware (x86, intel core > duo 1.2GHz) > > > > *Per-commodity accounting: not a hard requirement* > > The documentation may seem to suggest that per-commodity accounts are a > requirement or are assumed by the code, but this is not actually true. The > code works at the account level, and if you don’t have or use per-commodity > accounting, you can still compute returns at an account level. Below are > some pitfalls, solutions, and some drawbacks to not using per-commodity > accounting. > > - The configure.py script may not work for you in this case, but it’s > trivial to write your own configuration. > - One problem is, it’s impossible to know which commodity to ascribe a > dividend to. Even worse, reinvested dividends in the “Dividends” > computation, if the cash flows into the asset account. Eg: > > 2020-01-02 * "Buy" > > Assets:Brokerage 1000 HOOL {1 USD} > > Assets:Bank > > > > 2020-08-01 * "Dividends" > > Assets:Brokerage 500 USD > > Income:Dividends:Brokerage ; ← not a cash account; not counted > > > > 2020-08-01 * "Reinvest dividends" > > Assets:Brokerage 500 HOOL {1 USD} > > Assets:Brokerage -500 USD ; ← not an external account; not counted > > > > However, this is only a problem if you want your total performance broken > down into dividends vs. appreciation. If not, the total performance numbers > you get will still be correct. Simply ignore the ex-dividends and dividends > numbers. > > > > > - Several of my accounts correspond to a “real” brokerage account, > meaning they mix multiple commodities, cash and/or the money market > settlement fund > - returns.py:truncate_cash_flows() fails because my account contains > both cash and a settlement fund, variably used to trade commodities. This > means the cashflow can’t be reduced to a single currency based on cost, but > ends up with two (cash, settlement fund). Note that I use price conversions > for the settlement fund (vs. holding them at cost), to avoid generating a > ton of lots with no real purpose given that the NAV of these don’t stray > much from 1 USD. Fix: > > @@ -141,7 +144,10 @@ def truncate_cash_flows( > > balance = compute_balance_at(account_data.transactions, > date_start) > > if not balance.is_empty(): > > cost_balance = balance.reduce(pricer.get_value, date_start) > > + cost_balance = cost_balance.reduce(convert.convert_position, > "USD", pricer.price_map) > > cost_position = cost_balance.get_only_position() > > @@ -154,7 +160,13 @@ def truncate_cash_flows( > > balance = compute_balance_at(account_data.transactions, date_end) > > if not balance.is_empty(): > > cost_balance = balance.reduce(pricer.get_value, date_end) > > - cost_position = cost_balance.get_only_position() > > + cost_balance = cost_balance.reduce(convert.convert_position, > "USD", pricer.price_map) > > + try: > > + cost_position = cost_balance.get_only_position() > > > > *Verification of results can be challenging:* > > After building a basic config file and perhaps making a few simple changes > to your ledger, compute_returns.py spits out numbers. How can the > correctness of these numbers be verified? > > > > One way to identify problems is to construct groupings that reflect IRRs > provided by brokerages. However, the details and terminology of how IRR > calculations are made can vary widely by brokerage. For example, there > seems to be no agreement on what the start/end dates should be when > computing “3-year performance”. It can thus be a lot of effort to figure > these out even if one has just a handful of brokerages. > > > > Verifying the output of compute_returns.py may involve some effort. The > challenge fundamentally seems to stem from the (fantastic) flexibility > Beancount provides in constructing one's ledger, which means that the > configuration for compute_returns.py needs to be correct. Short of bugs, > all incorrect results are generally because of errors in configuration. > > > > So how can one verify that their configuration is correct? Thankfully, > this is a place where compute_returns.py shines, with its clear, relevant > output. Below, I share a few tips and personal stumbling blocks to help > others figure their own: > > > > > - Examine the helpful “category map” in the output files. It is the > set of all accounts that are involved in transactions with an asset account > in question: > > cat returns.output/investments/*.org | grep "^.'" | grep None > > Also consider whether each of them has been categorized correctly: > > cat returns.output/investments/*.org | grep "^.'" > > - The root finding solver is initialized to 0.2, so if you see a > 20.00% as your CAGR, it’s almost certainly because the solver crashed > - Examining the cashflow graph: > - Do the inflows and outflows make sense? With many accounts, you > have a sense of how frequently you transfer money and how much (eg: > every > paycheck). Is that being violated? > - Do the dividends make sense? > - Transfers should show up as an equal inflow/outflow. If not, > investigate > - Spot check at least some of the transactions in the > investments/*.org files to ensure their categories make sense. See if there > are transactions that may be missing > - Finally, building one’s intuition for what the numbers should be, > how the cashflow should look like, and how compute_returns.py works, is > generally helpful in spotting errors > > > > *Modifications I had to make to my ledger:* > > There were some gotchas that I had to work through that I’ve shared below > to give a sense of what one might need to look out for: > > - Tighten initial pad postings, so they appear on the day of the first > transaction. Else, the period used for computing returns is lengthened, > resulting in incorrectly worse performance > - I use the rename plugin > > <https://github.com/redstreet/beancount_reds_plugins/tree/master/beancount_reds_plugins/rename_accounts> > I wrote to rename Expenses:Taxes:* to Income:Taxes:*, to avoid it from > dominating my expenses for analysis. This meant that the income accounts > were categorized as income (investments.py matches against an “Income:” > regex) and therefore (incorrectly) not counted as casflow, which happens in > transactions that involve tax (eg: sell to cover). I had to add > “Income:Taxes:*” as a cash_account. The problems manifested as accounts > with obviously incorrect IRR values (eg: -144%), though this may not always > happen. > - In transaction entries involving a paycheck (eg: contributions to an > employer sponsored retirement account), many postings were uncategorized, > and thus eventually had incorrect workflows. My solution was to insulate > brokerage transactions from paycheck transactions by having a transfer > account in between. I started using transfer accounts in the last few years > anyway via the zerosum > > <https://github.com/redstreet/beancount_reds_plugins/tree/master/beancount_reds_plugins/zerosum> > plugin, but older transactions didn’t > - Eventually, I plan to evaluate switching to per-commodity accounts > that Martin has long recommended > > > > *Configuration maintenance* > > As one’s ledger evolves, the reports config needs to be kept up to date. > One downside of per-commodity accounts is they are likely to be opened and > closed more frequently than physical accounts. > > > > Omissions of all types in the returns configuration (asset, dividend, and > cash accounts) can be hard to detect. Solutions are likely to be highly > specific to the conventions one uses in their ledger. For my ledger for > example, I plan to ensure all accounts under > “(Assets|Income):Investments:*” appear at least once in the reports config > file. > > *In-kind transfers and returns* > > One case that is not covered seems to be in-kind transfers. For example: > > > > option "operating_currency" "USD" > > plugin "beancount.plugins.implicit_prices" > > > > 2005-01-01 commodity USD > > 2005-01-01 commodity HOOL > > > > 2000-01-01 open Assets:Bank > > 2000-01-01 open Assets:Brokerage:HOOL "STRICT" > > 2000-01-01 open Assets:Zero-Sum-Accounts:Transfers "STRICT" > > > > 2010-01-01 * "Buy" > > Assets:Zero-Sum-Accounts:Transfers 1000 HOOL {0.5 USD} > > Assets:Bank > > > > 2020-01-01 price HOOL 1 USD > > > > 2020-01-02 * "Transfer" > > Assets:Brokerage:HOOL 1000 HOOL {0.5 USD} > > Assets:Zero-Sum-Accounts:Transfers -1000 HOOL {0.5 USD} > > > > 2020-12-23 price HOOL 1.1 USD > > > > Using the market value on the transfer date for the cashflow value should > solve this case. > > > > This appears in several cases in me ledger: moving from one brokerage to > another, and options exercise. It could be argued that the former could be > handled by bundling together the old and new accounts, but that assumes all > accounts in question deal with only a single commodity each, which means > (for me), redoing a whole bunch of historical transactions. > > > > The latter is an interesting case. For context, assume an option to buy > stock ABC at 1 USD was exercised when the fair market value of ABC was 5 > USD. It was then held for a year, and sold for 10 USD. It is helpful to > compute the returns of holding on to the exercised stock (which is an > investing decision) separately from the “compensation” portion, which is > the 5-1 = 4 USD. To do this, I track them in separate accounts like so: > > > > 2015-01-01 * "Exercised" > > Assets:Vested -100 ABC_OPTIONS > > Expenses:Stock-Options 100 ABC_OPTIONS > > Assets:XTrade:Exercised 100 ABC {1 USD} ; FMV was 4 USD > > Assets:Bank 100 USD ; 100 * 1 USD > > > > 2015-01-01 price ABC 4 USD ; FMV > > > > 2015-01-01 * “Transferred to brokerage” > > Assets:XTrade:Exercised -100 ABC {1 USD} > > Assets:XTrade:Invested 100 ABC {1 USD} > > > > If stock transfers in-kind were computed in the way above, one could > simply compute returns separately on Assets:XTrade:Exercised and > Assets:XTrade:Invested, and distinguish between the “compensation” and > the “investment” portions. > > > > *Minor feedback* > > - Legend in graph (in .svg): both amortized value from flows, and > market value show up as a black line. In the graph, one is blue + dots, > other is black > - How rae trailing, rolling, and total returns period define? Since > everyone defines these a bit differently, perhaps balloon help on the UI > would clarify it. This script helps, meanwhile: > > # Run on 2020-12-23 > > >>> from reports import * > > >>> from tabulate import tabulate > > >>> print(tabulate(get_calendar_intervals(TODAY))) > > ---- ---------- ---------- > > 2005 2005-01-01 2006-01-01 > > 2006 2006-01-01 2007-01-01 > > 2007 2007-01-01 2008-01-01 > > 2008 2008-01-01 2009-01-01 > > 2009 2009-01-01 2010-01-01 > > 2010 2010-01-01 2011-01-01 > > 2011 2011-01-01 2012-01-01 > > 2012 2012-01-01 2013-01-01 > > 2013 2013-01-01 2014-01-01 > > 2014 2014-01-01 2015-01-01 > > 2015 2015-01-01 2016-01-01 > > 2016 2016-01-01 2017-01-01 > > 2017 2017-01-01 2018-01-01 > > 2018 2018-01-01 2019-01-01 > > 2019 2019-01-01 2020-01-01 > > 2020 2020-01-01 2020-12-23 > > ---- ---------- ---------- > > >>> print(tabulate(get_cumulative_intervals(TODAY))) > > -------------------- ---------- ---------- > > 15_years_ago 2005-01-01 2020-12-23 > > 10_years_ago 2010-01-01 2020-12-23 > > 5_years_ago 2015-01-01 2020-12-23 > > 4_years_ago 2016-01-01 2020-12-23 > > 3_years_ago 2017-01-01 2020-12-23 > > 2_years_ago 2018-01-01 2020-12-23 > > 1_year_ago 2019-01-01 2020-12-23 > > ytd 2020-01-01 2020-12-23 > > rolling_6_months_ago 2020-06-25 2020-12-23 > > rolling_3_months_ago 2020-09-25 2020-12-23 > > -------------------- ---------- ---------- > > > > -- > You received this message because you are subscribed to the Google Groups > "Beancount" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to beancount+unsubscr...@googlegroups.com. > To view this discussion on the web visit > https://groups.google.com/d/msgid/beancount/ebba4de0-c401-4290-a119-2cab59d5b432n%40googlegroups.com > <https://groups.google.com/d/msgid/beancount/ebba4de0-c401-4290-a119-2cab59d5b432n%40googlegroups.com?utm_medium=email&utm_source=footer> > . > -- You received this message because you are subscribed to the Google Groups "Beancount" group. To unsubscribe from this group and stop receiving emails from it, send an email to beancount+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/beancount/CAK21%2BhMOx5A7AXR%2B_K0YY8_BzHfh6LFUdLU0QQzcb5q4LamV%2Bw%40mail.gmail.com.