Skip to content

Commit

Permalink
feat: upgraded fava_investor to work with beancount v3 and fava 1.30
Browse files Browse the repository at this point in the history
  • Loading branch information
redstreet committed Jan 19, 2025
1 parent 0b85347 commit 2b60947
Show file tree
Hide file tree
Showing 7 changed files with 33 additions and 62 deletions.
21 changes: 2 additions & 19 deletions fava_investor/common/beancountinvestorapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
37 changes: 8 additions & 29 deletions fava_investor/common/favainvestorapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
14 changes: 14 additions & 0 deletions fava_investor/common/libinvestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion fava_investor/modules/cashdrag/libcashdrag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
4 changes: 1 addition & 3 deletions fava_investor/modules/minimizegains/libminimizegains.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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():
Expand Down
9 changes: 4 additions & 5 deletions fava_investor/modules/summarizer/libsummarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)

Expand Down
8 changes: 3 additions & 5 deletions fava_investor/modules/tlh/libtlh.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit 2b60947

Please sign in to comment.