Giter Club home page Giter Club logo

portfolio-returns's Introduction

Beancount Returns Calculator

This will calculator money-weighted and time-weighted returns for a portfolio for beancount, the double-entry plaintext accounting software.

Table of Contents

Quick Usage

python returns.py
    --account Assets:US:Vanguard
    --account Assets:US:Fidelity
    --internal Income:CapitalGainsDistribution
    --internal Income:Dividends
    --year 2018
    portfolio.bean

Dependencies

  • dateutil - used for relative date processing
  • scipy - for Internal Rate of Return (XIRR) calculations
  • beancount - obviously :)

Introduction

This script will determine the rate of return for a portfolio held without beancount. You can specify which accounts & which timeframes you are interested in. We calculate "money-weighted returns", which means taking into account the timing & value of cashflows. In particular, this means you -- the user -- need to tell us which beancount accounts are real cashflows in & out of the account. See below for more on this.

Time-weighted Returns

Warning. Time-weighted returns are not implemented.

The time-weighted rate of return is the geometric mean of a series of equal-length holding periods.

Time-weighted rates of return do not take into account the impact of cash flows into and out of the portfolio.

Time-weighted rates of return attempt to remove the impact of cash flows when calculating the return. This makes it ideal for calculating the performance of broad market indices or the impact of a fund manager on the performance of an investment. Time-weighting is important in this context as fund managers do not control the timing of cash flows into and out of their fund – investors control that – so it is not reasonable to include that effect when evaluating the performance of the fund manager.

To calculate the time-weighted return we calculate the holding period return (HPR) of each day during the full time period and then find the geometric mean across all of the HPRs.

The formula for a single holding period return is: HPR = ((MV1 - MV0 + D1 - CF1)/MV0)

  • HPR: Holding Period Return
  • MV1: The market value at the end of the period
  • MV0: The market value at the start of the period
  • D1: The value of any dividends or interest inflows
  • CF1: Cash flows (i.e. deposits subtracted out or withdrawals added back in)

Money-weighted Returns

The money-weighted rate of return is the Internal Rate of Return (IRR or, in spreadsheets, XIRR).

Money-weighted returns take into account the timing & size of cash flows into and out of the portfolio, in addition to the performance of the underlying portfolio itself. Money-weighted returns can change significantly depending on the timing of large cash flows in & out of the portfolio.

The money-weighted return does not split the time period up into equal-length sub-periods. Instead it searches (via mathematical optimization techniques) for the discount rate that equals the cost of the investment plus all of the cash flows generated.

For the vast majority of investors a money-weighted rate of return is the most appropriate method of measuring the performance of your portfolio as you control inflows and outflows of the investment portfolio.

Illustrated Example of the Difference

If you don't buy any new shares, sell any shares, and all dividends are reinvested, then the money-weighted return and the time-weighted return will be the same over a given time period.

Since most people will be buying or selling shares, in practice they will differ.

Imagine you invest like:

  1. On January 1st you buy 100 shares of FOO at $100.
  2. On January 2nd you buy 100 more shares of FOO, this time at $500 each for $50,000.
  3. On January 3rd you sell 100 shares of FOO, this time at $50 each for $5,000.
  4. On January 4th, you do nothing. The price of FOO returns to $100.

The time-weighted return is 0%, since it ignores the impact of cash flows and just sees that the starting value (100 shares @ $100) is exactly the same as the ending value (100 shares @ $100).

Date Total Amount Shares Share price Holding Period Return
Jan 1 $10,000 100 $100 n/a
Jan 2 $100,000 200 $500 (100,000-10,000-50,000)/10,000 = 400%
Jan 3 $5,000 100 $50 (5,000-100,000+5,000)/100,000 = -90%
Jan 4 $10,000 100 $100 (10,000-5,000)/5,000 = 100%

The geometric mean of the Holding Period Returns is

=((1 + 4.00) * (1 - .90) * (1 + 1.00)) - 1
=0

Since you bought some shares for $50,000 and sold them for $5,000 you don't feel like the return was 0%, though.

The money-weighted return for the same investment is -52%.

External vs. internal cashflows

When calculating money-weighted returns we need to distinguish "real", or external, cashflows from "apparent", or internal, cashflows.

Imagine that your portfolio pays you a dividend but your account is set to automatically reinvest dividends. Even though there is an apparent cashflow between accounts nothing has actually changed; from the rate of return perspective it is as if the money never left your portfolio.

So we need to know which accounts to ignore when looking for cashflows. In practice, this is limited to three kinds of things:

  • Interest that is reinvested
  • Dividends that are reinvested
  • Capital gains distributions that are reinvested

Note on capital gains

There is a difference between a "capital gains distribution" and a "capital gain".

A "capital gains distribution" is when the fund family gives you money. This should be treated as a dividend, as an "internal cashflow". It is generated by the internal operation of the fund.

A "capital gain" is what happens when you sell a fund. This is an external cashflow.

Even though they are identical for tax purposes, they are different for the purposes of rate of return calculations. You need to ensure they going to two separate accounts in beancount.

Multi-currency issues

TBD. I have no idea if this works at all with multiple-currencies....

Parameters in more detail

  • --currency. In order to generate meaningful cashflows we need to convert the securities we hold into a currency. You need to tell the script which currency to use. USD is the default if you don't specify anything.
  • --account. Accounts to calculate the rate of return for. This can be specified multiple times. This takes a regular expression to match account names. So "^Assets:US:.*" would match Assets:US:Schwab and Assets:US:MerrillLynch
  • --internal. Accounts to treat as "internal" when deciding whether to ignore cashflows. This is also takes a regular expression and can be specified multiple times.
  • --to. The start date to use when calculating returns. If not specified, uses the earliest date found in the beancount file.
  • --from. The end date to use when calculating returns. If not specified, uses today.
  • --year. A shortcut to easily calculate returns for a single calendar year.
  • --ytd. A shortcut to calculate returns for the year-to-date.
  • --1year, --2year, --3year, --5year, --10year. A shortcut to calculate returns for various time periods.
  • --debug-inflows. List all of the accounts that generated an outflow. Useful for debugging whether you've specified all of the --internal accounts you need to.
  • --debug-outflows. List all of the accounts that generated an inflow. Useful for debugging whether you've specified all of the --internal accounts you need to.
  • --debug-cashflows. List all of the date & amount of cashflows used for the rate of return calculation.

TODOs & Bugs

  • Generate growth of $10,000 chart.
  • Definitely needs more testing.
  • Add way to specify individual commodities and track just those
  • As I write up the documentation, I become less certain about the need to specify internal accounts, if we just track those cashflows won't it have no effect on the rate of return? I take out $100 and then put $100 back in the same day?
  • double check whether I'm right about capital gains distributions

portfolio-returns's People

Contributors

ankurdave avatar hoostus avatar redstreet avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

portfolio-returns's Issues

Error with simple file (no growth)

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

Expected: 0%
Result: error:

$ /home/user/gnu/portfolio-returns/irr.py --currency USD --account Assets:Investments test.bc --from 2018-01-01 --to 2018-12-31
/usr/lib/python3.6/site-packages/numpy/core/numeric.py:2531: RuntimeWarning: invalid value encountered in multiply
  x = x * ones_like(cond)
Traceback (most recent call last):
  File "/home/user/gnu/portfolio-returns/irr.py", line 248, in <module>
    r = xirr([(d, float(f)) for (d,f) in cashflows])
  File "/home/user/gnu/portfolio-returns/irr.py", line 65, in xirr
    return optimize.newton(lambda r: xnpv(r,cashflows),guess)
  File "/usr/lib/python3.6/site-packages/scipy/optimize/zeros.py", line 338, in newton
    q1 = func(p1, *args)
  File "/home/user/gnu/portfolio-returns/irr.py", line 65, in <lambda>
    return optimize.newton(lambda r: xnpv(r,cashflows),guess)
  File "/home/user/gnu/portfolio-returns/irr.py", line 46, in xnpv
    return sum([cf/(1+rate)**((t-t0).days/365.0) for (t,cf) in chron_order])
  File "/home/user/gnu/portfolio-returns/irr.py", line 46, in <listcomp>
    return sum([cf/(1+rate)**((t-t0).days/365.0) for (t,cf) in chron_order])
OverflowError: complex exponentiation

Issue with multicurrency portfolio.

Hello,

I have a portfolio whose main currency is EURO, but some of the stocks are quoted in CAD, USD or other currencies.

When i use you script, I can see some cashflows entries that should not exist.

Exemple:

python irr.py --currency EUR --internal Revenus --internal Depenses Famille.beancount  --account Actif:Bink:CTO --year 2019 --debug-cashflows --debug-inflows --debug-outflows
25.18%
[(datetime.date(2019, 1, 1), Decimal('14844.08551197826805000953313')),
 (datetime.date(2019, 1, 10), Decimal('150.00')),
 (datetime.date(2019, 1, 14), Decimal('0.093647131589160751001238694')),
 (datetime.date(2019, 1, 24), Decimal('0.93243922021409590941076062')),
 (datetime.date(2019, 2, 4), Decimal('0.022447982097499665471847500')),
 (datetime.date(2019, 2, 11), Decimal('150.00')),
 (datetime.date(2019, 3, 11), Decimal('150.00')),
 (datetime.date(2019, 4, 1), Decimal('0.090920135139649239445324253')),
 (datetime.date(2019, 4, 10), Decimal('150.00')),
 (datetime.date(2019, 4, 11), Decimal('-0.050000000000000000000000001')),
 (datetime.date(2019, 4, 19), Decimal('-2843.796426581529348899641038')),
 (datetime.date(2019, 4, 25), Decimal('0.075807582443060723234804055')),
 (datetime.date(2019, 5, 10), Decimal('150.00')),
 (datetime.date(2019, 6, 10), Decimal('150.00')),
 (datetime.date(2019, 7, 1), Decimal('0.074201957713688278299761169')),
 (datetime.date(2019, 7, 10), Decimal('150.00')),
 (datetime.date(2019, 7, 25), Decimal('0.121507743894273941608576125')),
 (datetime.date(2019, 8, 1), Decimal('-2.901127047095921066015665370')),
 (datetime.date(2019, 8, 5), Decimal('0.032744018669264658502304616')),
 (datetime.date(2019, 8, 5), Decimal('5000.00')),
 (datetime.date(2019, 8, 12), Decimal('150.00')),
 (datetime.date(2019, 9, 10), Decimal('150.00')),
 (datetime.date(2019, 10, 10), Decimal('150.00')),
 (datetime.date(2019, 10, 25), Decimal('0.098772563176895306859205776')),
 (datetime.date(2019, 11, 4), Decimal('0.022749326145552560646900270')),
 (datetime.date(2019, 11, 11), Decimal('150.00')),
 (datetime.date(2019, 11, 27), Decimal('4.82362891304347826086956431')),
 (datetime.date(2019, 12, 11), Decimal('150.00')),
 (datetime.date(2019, 12, 31), Decimal('-22746.48983458110516934046346'))]
>> [inflows]
{'Actif:Boursorama:CCTim'}
<< [outflows]
set()

What i expect:
The cashflows should only include the situation at 01/01/2019, 31/12/2019 and the monthly deposit of 150EUR.
All the other values (with many decimal digits) should not be taken in consideration in the IRR calculation.

In order to understand what happen, here is the transaction of the January 14th that created the following cashflow:
(datetime.date(2019, 1, 14), Decimal('0.093647131589160751001238694')),

2019-01-14 * "Dividende Communications Systems Inc" 
  Revenus:Dividendes                                  -6.70 USD
  Depenses:Impots:PS                                   0.85 EUR
  Actif:Bink:CTO:Cash                                 -0.85 EUR
  Depenses:Impots:RetenueSource                        1.01 EUR
  Actif:Bink:CTO:Cash                                 -1.01 EUR
  Actif:Bink:CTO:Cash                                  5.94 EUR @@ 6.70 USD

As the "Revenus" and "Depenses" should be treated as internal, i do not expect any cashflow for this transaction.

Other situation which is probably not related to the multicurrencies but to a split:
On April 19th, i have th follwing cashflow:
(datetime.date(2019, 4, 19), Decimal('-2843.796426581529348899641038')),

Here is the corresponding transaction (a stock split):

2019-04-19 * "Transfert de titres 800 Appliance Recycling Centers Split- @ 0,99852 $" "Dépôt 160 Appliance Recycling Centers Am -Isin chn @ 4,99262 $"
  Actif:Bink:CTO:US03814F4037                          -800 US03814F4037 {}
  Actif:Bink:CTO:US03814F4037                           160 US03814F4037 {4.99262 USD} @4.99262 USD

Any idea of what may happen ?

Stock conversions cause incorrect output

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%

option "operating_currency" "USD"
plugin "beancount.plugins.auto_accounts"

2018-01-01 * "Buy"
   Assets:Investments 100 HOOLI {1 USD}
   Assets:Bank

2018-04-01 * "Conversion"
   Assets:Investments -100 HOOLI {}
   Assets:Investments 50 IOOLI {2 USD}

2018-12-31 price HOOLI 1.5 USD
2018-12-31 price IOOLI 3 USD
irr.py --account Assets:Investments test.bc --from 2018-01-01 --to 2018-12-31```

License?

I was wondering if you've thought of a license this code is being released under?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.