Closed redstreet closed 2 years ago
Thanks for the report, I'll look into it over the weekend.
The problem is that the call to beancount.core.convert.convert_position() is (essentially) failing on the 50 IOOLI line. The reason it fails is because there's no IOOLI in the pricemap at that point in time.
This should work, so maybe there's another way I can get the value of a position from beancount. I'll have to research and see.
In the meantime, the workaround is pretty easy: you just need to add a price directive for IOOLI on or before 2018-04-01:
2018-04-01 price IOOLI 2 USD
Hmm, I still get incorrect results. Let me post a few test cases below. In all these test cases, the expected output is 50%.
All these involve a stock conversion, where no cash is transacted. This means that the cost basis of the new stock is based on the cost basis of the existing stock, and is unrelated to the price on that day. Eg:
2018-01-01 * "Buy"
Assets:Investments 100 HOOLI {2 USD}
Assets:Bank
2018-04-01 price IOOLI 3.50 USD
2018-04-01 price IOOLI 7 USD
2018-04-01 * "Conversion"
Assets:Investments -100 HOOLI {}
Assets:Investments 50 IOOLI {4 USD} ; Cost basis of IOOLI is 4 USD, even though current market price is $7 USD, because this is a stock conversion from my existing HOOLI, whose cost basis is 2 USD.
Case 1: This is the case you identified above.
option "operating_currency" "USD"
plugin "beancount.plugins.auto_accounts"
2018-01-01 * "Buy"
Assets:Investments 100 HOOLI {2 USD}
Assets:Bank
2018-04-01 * "Conversion"
Assets:Investments -100 HOOLI {}
Assets:Investments 50 IOOLI {4 USD}
2018-12-31 price HOOLI 3 USD
2018-12-31 price IOOLI 6 US
$ ./irr.py --currency USD --account Assets:Investments test.bc --from 2018-01-01 --to 2018-12-31
Traceback (most recent call last):
File "./irr.py", line 256, in <module>
r = xirr([(d, float(f)) for (d,f) in cashflows])
File "./irr.py", line 66, in xirr
return optimize.newton(lambda r: xnpv(r,cashflows),guess)
File "/usr/lib/python3.6/site-packages/scipy/optimize/zeros.py", line 343, in newton
raise RuntimeError(msg)
RuntimeError: Failed to converge after 50 iterations, value is 9.97188134707855e+22
Case 2: Even though IOOLI's market price price matches the the price at conversion (usually never happens in real life), the results are still incorrect:
option "operating_currency" "USD"
plugin "beancount.plugins.auto_accounts"
2018-01-01 * "Buy"
Assets:Investments 100 HOOLI {2 USD}
Assets:Bank
2018-03-31 price IOOLI 4 USD
2018-04-01 * "Conversion"
Assets:Investments -100 HOOLI {}
Assets:Investments 50 IOOLI {4 USD}
2018-12-31 price HOOLI 3 USD
2018-12-31 price IOOLI 6 USD
$ ./irr.py --currency USD --account Assets:Investments test.bc --from 2018-01-01 --to 2018-12-31
332.10%
Case 3: Typical case. Incorrect, but with a different IRR.
option "operating_currency" "USD"
plugin "beancount.plugins.auto_accounts"
2018-01-01 * "Buy"
Assets:Investments 100 HOOLI {2 USD}
Assets:Bank
2018-03-31 price IOOLI 7 USD
2018-04-01 * "Conversion"
Assets:Investments -100 HOOLI {}
Assets:Investments 50 IOOLI {4 USD}
2018-12-31 price HOOLI 3 USD
2018-12-31 price IOOLI 6 USD
$ ./irr.py --currency USD --account Assets:Investments test.bc --from 2018-01-01 --to 2018-12-31
27.49%
[(datetime.date(2018, 1, 1), Decimal('-100')),
(datetime.date(2018, 1, 1), Decimal('100')),
(datetime.date(2018, 4, 1), Decimal('250')), # this is incorrect
(datetime.date(2018, 12, 31), Decimal('-300'))]
Case 4:
option "operating_currency" "USD"
plugin "beancount.plugins.auto_accounts"
2018-01-01 * "Buy"
Assets:Investments 100 HOOLI {2 USD}
Assets:Bank
2018-03-31 price HOOLI 3.50 USD
2018-03-31 price IOOLI 7 USD
2018-04-01 * "Conversion"
Assets:Investments -100 HOOLI {}
Assets:Investments 50 IOOLI {4 USD}
2018-12-31 price HOOLI 3 USD
2018-12-31 price IOOLI 6 USD
s ./irr.py --currency USD --account Assets:Investments test.bc --from 2018-01-01 --to 2018-12-31 --debug-inflows --debug-outflows --debug-cashflows
Traceback (most recent call last):
File "/home/V/gnu/portfolio-returns/irr.py", line 256, in <module>
r = xirr([(d, float(f)) for (d,f) in cashflows])
File "/home/V/gnu/portfolio-returns/irr.py", line 66, in xirr
return optimize.newton(lambda r: xnpv(r,cashflows),guess)
File "/usr/lib/python3.6/site-packages/scipy/optimize/zeros.py", line 343, in newton
raise RuntimeError(msg)
RuntimeError: Failed to converge after 50 iterations, value is 38034332453.872734
Case 5: correct output, because of "beancount.plugins.implicit_prices"
option "operating_currency" "USD"
plugin "beancount.plugins.auto_accounts"
plugin "beancount.plugins.implicit_prices"
2018-01-01 * "Buy"
Assets:Investments 100 HOOLI {2 USD}
Assets:Bank
2018-04-01 * "Conversion"
Assets:Investments -100 HOOLI {}
Assets:Investments 50 IOOLI {4 USD}
2018-12-31 price HOOLI 3 USD
2018-12-31 price IOOLI 6 USD
irr.py --currency USD --account Assets:Investments test.bc --from 2018-01-01 --to 2018-12-31 --debug-inflows --debug-outflows --debug-cashflows
50.17%
[(datetime.date(2018, 1, 1), Decimal('200')),
(datetime.date(2018, 12, 31), Decimal('-300'))]
>> [inflows]
{'Assets:Bank'}
<< [outflows]
set()
Case 6: incorrect IRR because the conversion transaction cashflow is not zero (it should be zero).
option "operating_currency" "USD"
plugin "beancount.plugins.auto_accounts"
plugin "beancount.plugins.implicit_prices"
2018-01-01 * "Buy"
Assets:Investments 100 HOOLI {2 USD}
Assets:Bank
2018-03-31 price HOOLI 10 USD
2018-03-31 price IOOLI 20 USD
2018-04-01 * "Conversion"
Assets:Investments -100 HOOLI {}
Assets:Investments 50 IOOLI {4 USD}
2018-12-31 price HOOLI 3 USD
2018-12-31 price IOOLI 6 USD
$ ./irr.py --currency USD --account Assets:Investments test.bc --from 2018-01-01 --to 2018-12-31 --debug-inflows --debug-outflows --debug-cashflows
28162.27%
[(datetime.date(2018, 1, 1), Decimal('200')),
(datetime.date(2018, 4, 1), Decimal('-800')), # incorrect
(datetime.date(2018, 12, 31), Decimal('-300'))]
>> [inflows]
{'Assets:Bank'}
<< [outflows]
set()
I posted the test cases in the hope it is helpful, but I believe all these cases fall into two categories:
1) Unable to convert a posting into USD (likely because of what you said: beancount.core.convert.convert_position() fails)
2) The market price of HOOLI/IOOLI on 2018-04-01 should not matter, because it's a stock conversion. The cashflow on 2018-04-01 should be zero. However, it is non-zero likely because the value of one (or both) posting is incorrectly calculated. Note that beancount doesn't complain that the entry does not balance, meaning, it computes the transaction correctly. However, irr.py does not.
Thanks a ton for engaging on this! I'll dig into these bugs it if/when I can as well.
Yeah, all of these are caused by convert_position() failing because the price map doesn't have enough information for it work.
While I investigate a real solution, I added some logging to detect when this happens and at least alert the user that convert_position() failed and the IRR is wrong :/
Case 6: incorrect IRR because the conversion transaction cashflow is not zero (it should be zero).
This case is actually different than the others. The script sees you selling $1,000 of HOOLI and buying $200 of IOOLI, because the implicit_prices plugin is giving the 200 shares of IOOLI a price of $4/share.
If you remove the implicit_prices plugin and add an explicit price for the first HOOLI purchase like this:
2018-01-01 price HOOLI 2 USD
then it works as expected. I'll have to look into exactly what the implicit_prices plugin is doing that confuses things.
I haven't forgotten about this...I was just lazy over the holidays :)
First the easy part:
Having thought about it more, I think Case #6 is simply not a valid case. If you want to migrate a position like that then you need to add a date as well:
Assets:Investments -100 HOOLI {}
Assets:Investments 50 IOOLI {2018-01-01, 4 USD}
This will keep the age of the position correct (for long-term, short-term tax tracking) and also won't confuse the implicit prices plugin.
So I think we can strike Case #6 from the problems. But that still leaves the others.....
Agreed about case 6. Including the original date when specifying the lot is what makes sense.
Beancount does accept case 6 without a date, but whether or not this plugin should at least complain rather than produce incorrect results is more of an academic one, than a practical one :).
Having looked at the beancount code for how it determines if a transaction balances...it puts everything together into an Inventory object which then magically calculates residuals....So what is it doing differently from this script?
It is using beancount.core.convert.get_weight() instead of beancount.core.convert.convert_position().
convert_position() has the bonus of doing currency conversions for us, which would be nice to be able to keep. I think I'll have to post on the mailing list and see if anyone has any better ideas on how to approach this.
Using beancount.core.convert.get_weight()
seems like the right solution to me. We should only require price information to determine the starting and ending balances, not to derive the cashflows in between.
If the weight's currency differs from the user's desired currency, we can still use beancount.core.convert.convert_amount()
to do the currency conversion at the appropriate price.
Using get_weight()
also simplifies the categorization of "internal" cashflows. Dividends, interest, fees, capital gains, and capital gains distributions can all be treated as internal. For example, the following transaction represents an outflow of 2500 USD, which is what we get when treating Assets:Brokerage
and Income:CapitalGains
as internal:
2017-12-01 * "Sell 2,000 shares"
Assets:Brokerage -1,000 ABC {1.00 USD}
Assets:Brokerage -1,000 ABC {2.00 USD}
Assets:Cash 2,500 USD
Income:CapitalGains 500 USD
I implemented the above and added tests derived from the open issues: https://github.com/ankurdave/portfolio-returns. Happy to submit a PR with these changes.
Related: beangrow, which I now use, solves a lot of the issues around returns calculations quite comprehensively IMHO. It takes a two-pass approach where the first pass outputs all the time points at which a price entry is required to make accurate calculations.
I did give beangrow a try after seeing your suggestion and review on the beancount mailing list. I was put off by what seemed like excessive complexity for my use case (calculate the total IRR for a group of investments), which portfolio-returns handles elegantly aside from the price/cost confusion. But I'm starting to see the need for more complexity if I wanted to break down the IRRs for each individual investment and due to the various sources (dividends, capital appreciation, fees). I'll give it another look.
Sure, if you submit a PR I'll apply it.
Thanks!
------- Original Message ------- On Thursday, August 25th, 2022 at 12:03 AM, Ankur Dave @.***> wrote:
Using beancount.core.convert.get_weight() seems like the right solution to me. We should only require price information to determine the starting and ending balances, not to derive the cashflows in between.
If the weight's currency differs from the user's desired currency, we can still use beancount.core.convert.convert_amount() to do the currency conversion at the appropriate price.
Using get_weight() also simplifies the categorization of "internal" cashflows. Dividends, interest, fees, capital gains, and capital gains distributions can all be treated as internal. For example, the following transaction represents an outflow of 2500 USD, which is what we get when treating Assets:Brokerage and Income:CapitalGains as internal:
2017-12-01 * "Sell 2,000 shares" Assets:Brokerage -1,000 ABC {1.00 USD} Assets:Brokerage -1,000 ABC {2.00 USD} Assets:Cash 2,500 USD Income:CapitalGains 500 USD
I implemented the above and added tests derived from the open issues: https://github.com/ankurdave/portfolio-returns. Happy to submit a PR with these changes.
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.Message ID: @.***>
Example below involves a stock conversion (eg: a stock split), which should make no change to inflows, outflows, or cashflows. Expected output: 50% Actual output: 150%