Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XIRR table #94

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions fava_investor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .modules.cashdrag import libcashdrag
from .modules.summarizer import libsummarizer
from .modules.minimizegains import libminimizegains
from .modules.performance import libperformance
from .common.favainvestorapi import FavaInvestorAPI


Expand Down Expand Up @@ -50,6 +51,10 @@ def build_minimizegains(self):
accapi = FavaInvestorAPI()
return libminimizegains.find_minimized_gains(accapi, self.config.get('minimizegains', {}))

def build_performance(self):
accapi = FavaInvestorAPI()
return libperformance.find_xirrs(accapi, self.config.get('performance', {}))

def recently_sold_at_loss(self):
accapi = FavaInvestorAPI()
return libtlh.recently_sold_at_loss(accapi, self.config.get('tlh', {}))
2 changes: 2 additions & 0 deletions fava_investor/cli/investor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import fava_investor.modules.summarizer.summarizer as summarizer
import fava_investor.modules.tlh.tlh as tlh
import fava_investor.modules.minimizegains.minimizegains as minimizegains
import fava_investor.modules.performance.performance as performance


@click.group()
Expand All @@ -21,6 +22,7 @@ def cli():
cli.add_command(summarizer.summarizer)
cli.add_command(tlh.tlh)
cli.add_command(minimizegains.minimizegains)
cli.add_command(performance.performance)


if __name__ == '__main__':
Expand Down
6 changes: 6 additions & 0 deletions fava_investor/examples/huge-example.beancount
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ option "operating_currency" "USD"
'wash_pattern': 'Assets:US',
},

'performance' : {
'account_field': 'account',
'accounts_pattern': 'Assets:US:Vanguard',
'accuracy': 2,
},

'asset_alloc_by_account': [{
'title': 'Allocation by Account',
'pattern_type': 'account_name',
Expand Down
21 changes: 21 additions & 0 deletions fava_investor/modules/performance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Performance
_Show XIRR of investments_

## Introduction
This modules shows the XIRR of chosen investments and the summary of all the investments chosen.

## Using this module
- Accounts contains account name
- XIRR contains XIRR of investments

## Limitations
XIRR are calculated using a maximum of 10 newton iterations for speed, so while the output is close to the final value, the accuracy cannot be guaranteed.

## Example configuration:
```
'performance' : {
'account_field': 'account',
'accounts_pattern': 'Assets:Investments',
'accuracy': 2,
},
```
Empty file.
Empty file.
61 changes: 61 additions & 0 deletions fava_investor/modules/performance/example.beancount
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
option "title" "Test"
option "operating_currency" "USD"
option "render_commas" "True"
option "booking_method" "FIFO"

2010-01-01 open Assets:Investments:BNCT
2010-01-01 open Assets:Investments:COFE
2010-01-01 open Assets:Bank
2010-01-01 open Income:Gains

2010-01-01 custom "fava-extension" "fava_investor" "{
'performance' : {
'account_field': 'account',
'accounts_pattern': 'Assets:Investments',
'accuracy': 2,
},
}"


2010-01-01 commodity BNCT
2010-01-01 commodity COFE

2020-01-01 * "Buy stock"
Assets:Investments:BNCT 1000 BNCT {100 USD}
Assets:Investments:COFE 1000 COFE {10 USD}
Assets:Bank

2021-03-12 * "Buy stock"
Assets:Investments:BNCT 1000 BNCT {100 USD}
Assets:Investments:COFE 1000 COFE {10 USD}
Assets:Bank

2022-01-01 * "Sell stock"
Assets:Investments:BNCT -1500 BNCT {} @ 100 USD
Assets:Investments:COFE -1500 COFE {} @ 10 USD
Assets:Bank 165000 USD
Income:Gains

2022-06-01 * "Sell stock"
Assets:Investments:BNCT -500 BNCT {} @ 160 USD
Assets:Bank 88000 USD
Income:Gains

2024-07-14 * "Sell stock"
Assets:Investments:COFE -500 COFE {} @ 25 USD
Assets:Bank 12500 USD
Income:Gains


2020-01-01 price BNCT 100 USD
2021-03-12 price BNCT 100 USD
2022-01-01 price BNCT 100 USD
2022-06-01 price BNCT 160 USD

2020-01-01 price COFE 10 USD
2021-03-12 price COFE 10 USD
2022-01-01 price COFE 10 USD
2022-06-01 price COFE 16 USD
2023-01-01 price COFE 20 USD
2024-01-01 price COFE 25 USD

97 changes: 97 additions & 0 deletions fava_investor/modules/performance/libperformance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/env python3
"""
# Performance
_Calculate XIRR for investments._

See accompanying README.txt
"""

import collections
from datetime import date, timedelta
from fava_investor.common.libinvestor import build_config_table
from beancount.core.number import Decimal
from fava_investor.modules.tlh import libtlh


def calculate_error_grad(investments: list[date, Decimal], guess: Decimal) -> tuple[Decimal, Decimal]:
sum: Decimal = 0
grad: Decimal = 0
init_date: date = investments[0][0]
for item in investments:
investment_date = item[0]
value = item[1]
time_step = Decimal(((investment_date - init_date)/timedelta(days=1))/365)
sum += value / (1 + guess)**time_step
grad += -time_step * value / (1 + guess)**(time_step + 1)

return sum, grad


def calculate_xirr(investments: list[date, Decimal], accuracy) -> Decimal:
guess = Decimal(0.1)

for _ in range(10):
value, grad = calculate_error_grad(investments, guess)
correction: Decimal = value / grad
guess -= correction
if abs(value) < 1 and correction < 10**(-accuracy-2):
break

return round(guess*100, accuracy)


def find_xirrs(accapi, options):
account_field = libtlh.get_account_field(options)
accounts_pattern = options.get('accounts_pattern', '')
accuracy = int(options.get('accuracy', 2))

currency = accapi.get_operating_currencies()[0]

sql = f"""
SELECT
{account_field} as account,
CONVERT(value(position, date), '{currency}') as market_value,
date as date
WHERE account_sortkey(account) ~ "^[01]" AND
account ~ '{accounts_pattern}'
"""
rtypes, rrows = accapi.query_func(sql)
if not rtypes:
return [], {}, [[]]

sql = f"""
SELECT
{account_field} as account,
ONLY('{currency}', NEG(CONVERT(sum(position), '{currency}'))) as market_value,
cost_date as date
WHERE account_sortkey(account) ~ "^[01]" AND
account ~ '{accounts_pattern}'
GROUP BY {account_field}, date, account_sortkey(account)
ORDER BY account_sortkey(account)
"""
remaintypes, remainrows = accapi.query_func(sql)
if not remaintypes:
return [], {}, [[]]

investments = {}
investments["Summary"] = []
for row in rrows:
if row.account not in investments.keys():
investments[row.account] = []
investments[row.account].append([row.date, row.market_value.number])
investments["Summary"].append([row.date, row.market_value.number])
for row in remainrows:
if row.market_value.number == 0:
continue
investments[row.account].append([date.today(), row.market_value.number])
investments["Summary"].append([date.today(), row.market_value.number])

xirr = {key: calculate_xirr(investments[key], accuracy) for key in investments}

retrow_types = [('Account', str), ('XIRR', Decimal)]
RetRow = collections.namedtuple('RetRow', [i[0] for i in retrow_types])
rrows = [RetRow(key, value) for key, value in xirr.items()]

tables = [build_config_table(options)]
tables.append(('XIRR performance', (retrow_types, rrows, None, None)))
return tables
52 changes: 52 additions & 0 deletions fava_investor/modules/performance/performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Beancount Tool to find lots to sell with lowest gains, to minimize the tax burden."""

import fava_investor.modules.performance.libperformance as libpf
import fava_investor.common.beancountinvestorapi as api
from fava_investor.common.clicommon import pretty_print_table, write_table_csv
import click


@click.command()
@click.argument('beancount-file', type=click.Path(exists=True), envvar='BEANCOUNT_FILE')
@click.option('--csv-output', help='In addition to summary, output to performance.csv', is_flag=True)
def performance(beancount_file, csv_output):
"""Generate XIRR for each investment.

The BEANCOUNT_FILE environment variable can optionally be set instead of specifying the file on the
command line.

The configuration for this module is expected to be supplied as a custom directive like so in your
beancount file:

\b
2010-01-01 custom "fava-extension" "fava_investor" "{
'performance' : {
'account_field': 'account',
'accounts_pattern': 'Assets:Investments',
'accuracy': 2,
},
}}"

"""
accapi = api.AccAPI(beancount_file, {})
config = accapi.get_custom_config('performance')
tables = libpf.find_xirrs(accapi, config)

# TODO:
# - use same return return API for all of fava_investor
# - ordered dictionary of title: [retrow_types, table]
# - make output printing and csv a common function

if csv_output:
write_table_csv('performance.csv', tables[1])
else:
def _gen_output():
for title, (rtypes, rrows, _, _) in tables:
yield pretty_print_table(title, rtypes, rrows)

click.echo_via_pager(_gen_output())


if __name__ == '__main__':
performance()
72 changes: 72 additions & 0 deletions fava_investor/modules/performance/test_performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python3

import beancountinvestorapi as api
import sys
import os
from beancount.utils import test_utils
sys.path.append(os.path.join(os.path.dirname(__file__), '.'))
import libperformance as libpf


class TestScriptCheck(test_utils.TestCase):
def setUp(self):
self.options = {
'account_field': 'account',
'accounts_pattern': 'Assets:Investments',
'accuracy': 2,
}

@test_utils.docfile
def test_performance_basic(self, f):
"""
2010-01-01 commodity BNCT
2010-01-01 commodity COFE

2020-01-01 * "Buy stock"
Assets:Investments:BNCT 1000 BNCT {100 USD}
Assets:Investments:COFE 1000 COFE {10 USD}
Assets:Bank

2021-03-12 * "Buy stock"
Assets:Investments:BNCT 1000 BNCT {100 USD}
Assets:Investments:COFE 1000 COFE {10 USD}
Assets:Bank

2022-01-01 * "Sell stock"
Assets:Investments:BNCT -1500 BNCT {} @ 100 USD
Assets:Investments:COFE -1500 COFE {} @ 10 USD
Assets:Bank 165000 USD
Income:Gains

2022-06-01 * "Sell stock"
Assets:Investments:BNCT -500 BNCT {} @ 160 USD
Assets:Bank 88000 USD
Income:Gains

2024-07-14 * "Sell stock"
Assets:Investments:COFE -500 COFE {} @ 25 USD
Assets:Bank 12500 USD
Income:Gains

2020-01-01 price BNCT 100 USD
2021-03-12 price BNCT 100 USD
2022-01-01 price BNCT 100 USD
2022-06-01 price BNCT 160 USD

2020-01-01 price COFE 10 USD
2021-03-12 price COFE 10 USD
2022-01-01 price COFE 10 USD
2022-06-01 price COFE 16 USD
2023-01-01 price COFE 20 USD
2024-01-01 price COFE 25 USD

"""
accapi = api.AccAPI(f, {})
ret = libpf.find_xirrs(accapi, self.options)
title, (retrow_types, xirrs, _, _) = ret[1]

self.assertEqual(2, len(xirrs))
self.assertEqual("Assets:Investments:BNCT", xirrs[0].account)
self.assertEqual(9.35, xirrs[0].XIRR)
self.assertEqual("Assets:Investments:COFE", xirrs[1].account)
self.assertEqual(13.70, xirrs[1].XIRR)
7 changes: 6 additions & 1 deletion fava_investor/templates/Investor.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
('cashdrag', _('Cash Drag')),
('tlh', _('Tax Loss Harvestor')),
('summarizer', _('Summarizer')),
('minimizegains', _('Gains Minimizer'))
('minimizegains', _('Gains Minimizer')),
('performance', _('Performance'))
] %}
<h3><b>{% if not (module == key) %}<a href="{{ url_for('extension_report', extension_name='Investor', module=key) }}">{{ label }}</a>{% else %} {{ label }}{% endif %}</b></h3>
{% endfor %}
Expand Down Expand Up @@ -50,6 +51,10 @@ <h2>{{table[0]}}</h2>
{{ table_list_renderer('', extension.build_minimizegains()) }}
{% endif %}

{% if (module == 'performance') %}
{{ table_list_renderer('', extension.build_performance()) }}
{% endif %}

<!-- -------------------------------------------------------------------------------- -->

{% if (module == 'tlh') %}
Expand Down