diff --git a/CHANGELOG.md b/CHANGELOG.md index 365d3bd..ad434ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 1.0.0 (2025-01-19) +Investor is now compatible with Beancount v3 and Fava 1.30 + +### Improvements +- scaled_navs now only considers last 10 most recent ratios + + ### Fixes +- scaled-navs was ignoring date if specified 2024-04-24 10:15 +0200 c202048 Adriano Di Luzio o fix: assetalloc_class fail due to favapricemap vs beancount pricemap (#92) +- assetalloc_class fail due to favapricemap vs beancount pricemap (#92) + ## 0.7.0 (2024-02-02) This release brings fava_investor up to date with upstream changes in Fava. Primarily, diff --git a/fava_investor/common/beancountinvestorapi.py b/fava_investor/common/beancountinvestorapi.py index 6711e80..cfb09dd 100644 --- a/fava_investor/common/beancountinvestorapi.py +++ b/fava_investor/common/beancountinvestorapi.py @@ -46,8 +46,9 @@ def root_tree(self): # return realization.realize(self.entries) def query_func(self, sql): - # Convert this into Beancount v2 format rtypes, rrows = query.run_query(self.entries, self.options_map, sql) + + # Convert this into Beancount v2 format, so the rows are namedtuples field_names = [t.name for t in rtypes] rtypes = [(t.name, t.datatype) for t in rtypes] Row = namedtuple("Row", field_names) @@ -90,21 +91,3 @@ def get_custom_config(self, module_name): if module_config: return module_config[0] return {} - - def get_only_position(self, inventory): - """This function exists because Fava uses SimpleCounterInventory while beancount uses - beancount.core.inventory.Inventory""" - # TODO: assert there is only one item - return inventory.get_only_position() - - def val(self, inv): - if inv is None or inv.is_empty(): - return 0 - pos = self.get_only_position(inv) - if pos is not None: - return pos.units.number - return None - -def split_currency(self, value): - units = value.get_only_position().units - return units.number, units.currency diff --git a/fava_investor/common/favainvestorapi.py b/fava_investor/common/favainvestorapi.py index dd16fb2..7e72036 100644 --- a/fava_investor/common/favainvestorapi.py +++ b/fava_investor/common/favainvestorapi.py @@ -7,7 +7,8 @@ from fava.core.conversion import convert_position from beancount.core import realization from beancount.core import prices -from beancount.core.inventory import Inventory, Amount, Position +from beancount.core.inventory import Inventory, Amount +from beanquery import query class FavaInvestorAPI: @@ -40,13 +41,14 @@ def query_func(self, sql): # Based on the fava version, determine if we need to add a new # positional argument to fava's execute_query() if version.parse(fava_version) >= version.parse("1.30"): - result = g.ledger.query_shell.execute_query_serialised(g.filtered.entries, sql) - # Convert this into Beancount v2 format (TODO: affects performance?) - field_names = [t.name for t in result.types] + rtypes, rrows = query.run_query(g.ledger.all_entries, g.ledger.options, sql) + + # Convert this into Beancount v2 format, so the rows are namedtuples + field_names = [t.name for t in rtypes] Row = namedtuple("Row", field_names) + rtypes = [(t.name, t.datatype) for t in rtypes] + rrows = [Row(*row) for row in rrows] - rtypes = [(t.name, t.dtype) for t in result.types] - rrows = [Row(*row) for row in result.rows] elif version.parse(fava_version) >= version.parse("1.22"): _, rtypes, rrows = g.ledger.query_shell.execute_query(g.filtered.entries, sql) else: @@ -72,26 +74,3 @@ def cost_or_value(self, node, date, include_children): if include_children: nodes = node.balance_children return cost_or_value_without_context(nodes, g.conversion, g.ledger.prices, date) - - def get_only_position(self, fava_inventory): - """This function exists because Fava uses SimpleCounterInventory while beancount uses - beancount.core.inventory.Inventory""" - # TODO: assert there is only one item - beancount_inventory = Inventory() - - for currency, units in fava_inventory.items(): - # Create a Position with Amount and no cost - position = Position(Amount(units, currency), None) - # Add the Position to the Inventory - beancount_inventory.add_position(position) - - return beancount_inventory.get_only_position() - - def val(self, inv): - if inv.values(): - return list(inv.values())[0] - return None - - def split_currency(self, value): - currency, number = next(iter(value.items())) - return number, currency diff --git a/fava_investor/common/libinvestor.py b/fava_investor/common/libinvestor.py index 63d1c17..c3e4982 100644 --- a/fava_investor/common/libinvestor.py +++ b/fava_investor/common/libinvestor.py @@ -34,6 +34,16 @@ def pre_order(self, level=0): yield from c.pre_order(level + 1) +def val(inv): + if inv is not None: + pos = inv.get_only_position() + if pos is not None: + return pos.units.number + if inv.is_empty(): + return 0 + return None + + def remove_column(col_name, rows, types): """Remove a column by name from a beancount query return pair of rows and types""" try: @@ -100,3 +110,7 @@ def build_config_table(options): rrows = [RetRow(k, str(v)) for k, v in options.items()] return 'Config Summary', (retrow_types, rrows, None, None) + +def split_currency(value): + units = value.get_only_position().units + return units.number, units.currency diff --git a/fava_investor/modules/cashdrag/libcashdrag.py b/fava_investor/modules/cashdrag/libcashdrag.py index b25ca0b..f0f618f 100644 --- a/fava_investor/modules/cashdrag/libcashdrag.py +++ b/fava_investor/modules/cashdrag/libcashdrag.py @@ -44,7 +44,7 @@ def find_loose_cash(accapi, options): rrows = [r for r in rrows if r.position != Inventory()] threshold = options.get('min_threshold', 0) if threshold: - rrows = [r for r in rrows if accapi.get_only_position(r.position).units.number >= threshold] + rrows = [r for r in rrows if r.position.get_only_position().units.number >= threshold] footer = libinvestor.build_table_footer(rtypes, rrows, accapi) return [('Cash Drag Analysis', (rtypes, rrows, None, footer))] diff --git a/fava_investor/modules/minimizegains/libminimizegains.py b/fava_investor/modules/minimizegains/libminimizegains.py index 3277fb9..d2f60b9 100644 --- a/fava_investor/modules/minimizegains/libminimizegains.py +++ b/fava_investor/modules/minimizegains/libminimizegains.py @@ -8,7 +8,7 @@ import collections from datetime import datetime -from fava_investor.common.libinvestor import build_config_table, insert_column +from fava_investor.common.libinvestor import val, build_config_table, insert_column, split_currency from beancount.core.number import Decimal, D from fava_investor.modules.tlh import libtlh @@ -84,8 +84,6 @@ def find_minimized_gains(accapi, options): RetRow = collections.namedtuple('RetRow', [i[0] for i in retrow_types]) - val = accapi.val - split_currency = accapi.split_currency to_sell = [] for row in rrows: if row.market_value.get_only_position(): diff --git a/fava_investor/modules/summarizer/libsummarizer.py b/fava_investor/modules/summarizer/libsummarizer.py index 636048b..555bb42 100644 --- a/fava_investor/modules/summarizer/libsummarizer.py +++ b/fava_investor/modules/summarizer/libsummarizer.py @@ -27,8 +27,7 @@ def get_active_commodities(accapi): ORDER BY currency, cost_currency """ rtypes, rrows = accapi.query_func(sql) - - retval = {accapi.get_only_position(r).units.currency: r.market_value for r in rrows if not r.units.is_empty()} + retval = {r.units.get_only_position().units.currency: r.market_value for r in rrows if not r.units.is_empty()} return retval @@ -159,17 +158,17 @@ def active_accounts_metadata(accapi, options): if add_account: row['account'] = acc if add_balance: - row['balance'] = get_balance(accapi, realacc, acc, pm, currency) + row['balance'] = get_balance(realacc, acc, pm, currency) retval.append(row) return retval -def get_balance(accapi, realacc, account, pm, currency): +def get_balance(realacc, account, pm, currency): subtree = realization.get(realacc, account) balance = realization.compute_balance(subtree) vbalance = balance.reduce(convert.get_units) market_value = vbalance.reduce(convert.convert_position, currency, pm) - val = accapi.val(market_value) + val = libinvestor.val(market_value) return val # return int(val) diff --git a/fava_investor/modules/tlh/libtlh.py b/fava_investor/modules/tlh/libtlh.py index 8d05835..45b2fbc 100644 --- a/fava_investor/modules/tlh/libtlh.py +++ b/fava_investor/modules/tlh/libtlh.py @@ -7,7 +7,7 @@ import itertools from datetime import datetime from dateutil import relativedelta -from fava_investor.common.libinvestor import build_table_footer, insert_column +from fava_investor.common.libinvestor import val, build_table_footer, insert_column, split_currency from beancount.core.number import Decimal, D from beancount.core.inventory import Inventory @@ -115,10 +115,8 @@ def find_harvestable_lots(accapi, options): commodities = accapi.get_commodity_directives() wash_buy_counter = itertools.count() - val = accapi.val - split_currency = accapi.split_currency for row in rrows: - if val(row.market_value) and \ + if row.market_value.get_only_position() and \ (val(row.market_value) - val(row.basis) < -loss_threshold): loss = D(val(row.basis) - val(row.market_value)) @@ -253,7 +251,7 @@ def recently_sold_at_loss(accapi, options): for row in rrows: loss = Inventory(row.proceeds) loss.add_inventory(-(row.basis)) - if loss != Inventory() and accapi.val(loss) < 0: + if loss != Inventory() and val(loss) < 0: identicals = get_metavalue(row.currency, commodities, 'a__substidenticals').replace(',', ', ') return_rows.append(RetRow(row.sale_date, row.until, row.currency, identicals, row.basis, row.proceeds, loss))