def get_final_holdings(entries, included_account_types=None, price_map=None, date=None): """Get a list of holdings by account (as Postings).""" simple_entries = [entry for entry in entries if (not isinstance(entry, Transaction) or entry.flag != flags.FLAG_UNREALIZED)] root_account = realization.realize(simple_entries) holdings = [] for real_account in sorted(list(realization.iter_children(root_account)), key=lambda ra: ra.account): account_type = account_types.get_account_type(real_account.account) if (included_account_types and account_type not in included_account_types): continue for pos in real_account.balance: price = None if pos.cost and price_map: base_quote = (pos.units.currency, pos.cost.currency) _, price = prices.get_price(price_map, base_quote, date) holdings.append(Posting(real_account.account, pos.units, pos.cost, price, None, None)) return holdings
def get_postings_table( entries: data.Entries, options_map: Dict, accounts_map: Dict[str, data.Open], threshold: Decimal = D('0.01')) -> Table: """Enumerate all the postings.""" header = [ 'account', 'account_abbrev', 'number', 'currency', 'cost_number', 'cost_currency', 'cost_date' ] balances, _ = summarize.balance_by_account(entries, compress_unbooked=True) acctypes = options.get_account_types(options_map) rows = [] for acc, balance in sorted(balances.items()): # Keep only the balance sheet accounts. acctype = account_types.get_account_type(acc) if not acctype in (acctypes.assets, acctypes.liabilities): continue # Create a posting for each of the positions. for pos in balance: acc_abbrev = abbreviate_account(acc, accounts_map) row = [ acc, acc_abbrev, pos.units.number, pos.units.currency, pos.cost.number if pos.cost else ONE, pos.cost.currency if pos.cost else pos.units.currency, pos.cost.date if pos.cost else None ] rows.append(row) return Table(header, rows)
def get_final_holdings(entries, included_account_types=None, price_map=None, date=None): """Get a list of holdings by account (as Postings).""" simple_entries = [ entry for entry in entries if (not isinstance(entry, Transaction) or entry.flag != flags.FLAG_UNREALIZED) ] root_account = realization.realize(simple_entries) holdings = [] for real_account in sorted(list(realization.iter_children(root_account)), key=lambda ra: ra.account): account_type = account_types.get_account_type(real_account.account) if (included_account_types and account_type not in included_account_types): continue for pos in real_account.balance: price = None if pos.cost and price_map: base_quote = (pos.units.currency, pos.cost.currency) _, price = prices.get_price(price_map, base_quote, date) holdings.append( Posting(real_account.account, pos.units, pos.cost, price, None, None)) return holdings
def get_postings_table(entries: data.Entries, options_map: Dict, accounts_map: Dict[str, data.Open], threshold: Decimal = D('0.01')) -> Table: """Enumerate all the postings.""" header = ['account', 'account_abbrev', 'number', 'currency', 'cost_number', 'cost_currency', 'cost_date'] balances, _ = summarize.balance_by_account(entries) acctypes = options.get_account_types(options_map) rows = [] for acc, balance in sorted(balances.items()): # Keep only the balance sheet accounts. acctype = account_types.get_account_type(acc) if not acctype in (acctypes.assets, acctypes.liabilities): continue # If the account has "NONE" booking method, merge all its postings # together in order to obtain an accurate cost basis and balance of # units. # # (This is a complex issue.) If you accrued positions without having them # booked properly against existing cost bases, you have not properly accounted # for the profit/loss to other postings. This means that the resulting # profit/loss is merged in the cost basis of the positive and negative # postings. dopen = accounts_map.get(acc, None) if dopen is not None and dopen.booking is data.Booking.NONE: average_balance = balance.average() balance = inventory.Inventory(pos for pos in average_balance if pos.units.number >= threshold) # Create a posting for each of the positions. for pos in balance: acc_abbrev = abbreviate_account(acc, accounts_map) row = [acc, acc_abbrev, pos.units.number, pos.units.currency, pos.cost.number if pos.cost else ONE, pos.cost.currency if pos.cost else pos.units.currency, pos.cost.date if pos.cost else None] rows.append(row) return Table(header, rows)
def test_get_account_type(self): self.assertEqual( "Assets", account_types.get_account_type("Assets:US:RBS:Checking")) self.assertEqual( "Assets", account_types.get_account_type("Assets:US:RBS:Savings")) self.assertEqual( "Liabilities", account_types.get_account_type("Liabilities:US:RBS:MortgageLoan")) self.assertEqual("Equity", account_types.get_account_type("Equity:NetIncome")) self.assertEqual( "Equity", account_types.get_account_type("Equity:Opening-Balances")) self.assertEqual( "Income", account_types.get_account_type("Income:US:ETrade:Dividends")) self.assertEqual("Income", account_types.get_account_type("Income:US:Intel")) self.assertEqual( "Expenses", account_types.get_account_type("Expenses:Toys:Computer")) self.assertEqual( "Invalid", account_types.get_account_type("Invalid:Toys:Computer"))
def get_balance_sheet_balances(entries, options_map): """Enumerate all the assets and liabilities. Args: entries: A list of directives, as per the loader. options_map: An options map, as per the parser. Yields: Instances of Posting. """ balances, _ = summarize.balance_by_account(entries) date = entries[-1].date acctypes = options.get_account_types(options_map) for account, balance in sorted(balances.items()): # Keep only the balance sheet accounts. acctype = account_types.get_account_type(account) if not acctype in (acctypes.assets, acctypes.liabilities): continue # Create a posting for each of the positions. for position in balance: yield data.Posting(account, position.units, position.cost, None, None, None)
def is_expense_account(account): return account_types.get_account_type(account) == acctypes.expenses
def get_trades(entries, options_map, symbols, date_end): """Process a series of entries and extract a list of processable trades. The returned rows include computed fees and proceeds, ready to be washed. The list of trades includes buy and sell types. Args: entries: A list of directives to be processed. options_map: An options dict, as per the parser. symbols: A set of currency strings for substantially identical stocks. date_end: The cutoff date after which to stop processing transactions. Returns: XXX """ acc_types = options.get_account_types(options_map) # Inventory of lots to accumulate. balances = inventory.Inventory() # A list of trade information. trades = [] for entry in entries: # Skip other directives. if not isinstance(entry, data.Transaction): continue # Skip entries after the relevant period. if entry.date > date_end: continue # Skip entries not relevant to the currency. if not any(posting.position.lot.currency in symbols for posting in entry.postings): continue # Calculate the fee amount and the total price, in order to split the # fee later on. fee_total = ZERO units_total = ZERO for posting in entry.postings: if account_types.get_account_type( posting.account) == acc_types.expenses: fee_total += posting.position.number if (account_types.get_account_type( posting.account) == acc_types.assets and posting.position.lot.cost is not None): units_total += posting.position.number # Loop through the postings and create trade entries for them, computing # proceeds and fees and all details required to wash sales later on. booked = False for posting in entry.postings: pos = posting.position if pos.lot.currency not in symbols: continue # Check that all sales have the sale price attached to them. if pos.number < ZERO: assert posting.price or re.search('Split', entry.narration, re.I) # Update the shared inventory. booked = True booked_pos = book_position(balances, entry.date, pos) # Determine the dates. if pos.number > ZERO: txn_type = 'BUY' acq_date = entry.date adj_acq_date = None sell_date = None number = pos.number cost = pos.lot.cost price = '' partial_fee = '' proceeds = '' pnl = '' else: # This only holds true because we book lot sales individually. assert len(booked_pos) <= 1, "Internal error." booked_position = booked_pos[0] txn_type = 'SELL' assert pos.number < ZERO acq_date = booked_position.lot.lot_date adj_acq_date = None sell_date = entry.date number = pos.number cost = pos.lot.cost price = posting.price if posting.price else cost partial_fee = fee_total * (number / units_total).quantize( D('0.01')) proceeds = -number * price.number - partial_fee pnl = proceeds - cost_basis cost_basis = -number * cost.number trades.append( (txn_type, acq_date, adj_acq_date, sell_date, number, pos.lot.currency, cost.number, cost_basis, price.number if price else '', proceeds, partial_fee, pnl)) if booked: printer.print_entry(entry) for pos in balances: number, rest = str(pos).split(' ', 1) print(' {:>16} {}'.format(number, rest)) print() print() return trades
def main(): logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') parser = argparse.ArgumentParser(description=__doc__.strip()) parser.add_argument('filename', help='Beancount filename') parser.add_argument('--currency', action='store', default='USD') parser.add_argument('--quantize', action='store', default='0.') parser.add_argument('-o', '--output', action='store', help="Output file") args = parser.parse_args() Q = Decimal(args.quantize) # Read input. entries, errors, options_map = loader.load_file(args.filename) price_map = prices.build_price_map(entries) acctypes = options.get_account_types(options_map) # Compute start of period. today = datetime.date.today() date_min = today - datetime.timedelta(days=2 * 365) date_start = datetime.date(date_min.year, date_min.month, 1) month_start = (date_min.year, date_min.month) # Compute end of period. date_max = datetime.date(today.year, today.month, 1) # Accumulate expenses for the period. balances = collections.defaultdict( lambda: collections.defaultdict(inventory.Inventory)) all_months = set() for entry in data.filter_txns(entries): if entry.date < date_start or entry.date >= date_max: continue if any(tag.startswith(EXCLUDE_TAG_PREFIX) for tag in entry.tags): continue month = (entry.date.year, entry.date.month) all_months.add(month) for posting in entry.postings: if account_types.get_account_type( posting.account) != acctypes.expenses: continue if any(regexp.match(posting.account) for regexp in EXCLUDES): continue if posting.units.currency != args.currency: continue account = posting.account for regexp, target_account in MAPS: if regexp.match(account): account = target_account break balances[account][month].add_position(posting) # Reduce the final balances to numbers. sbalances = collections.defaultdict(dict) for account, months in sorted(balances.items()): for month, balance in sorted(months.items()): year, mth = month date = datetime.date(year, mth, 1) balance = balance.reduce(convert.get_value, price_map, date) balance = balance.reduce(convert.convert_position, args.currency, price_map, date) try: pos = balance.get_only_position() except AssertionError: print(balance) raise total = pos.units.number if pos and pos.units else None sbalances[account][month] = total # Pivot the table. header_months = sorted(all_months) header = ['account'] + ['{}-{:02d}'.format(*m) for m in header_months] rows = [] for account in sorted(sbalances.keys()): row = [account] for month in header_months: total = sbalances[account].get(month, None) row.append(str(total.quantize(Q)) if total else '') rows.append(row) # Write out the table. tbl = table.Table(header, [str] + [Decimal] * (len(header) - 1), rows) if args.output: with open(args.output, 'w') as outfile: table.write_csv(tbl, outfile) print(tbl)
def read_assets(filename, currency, reduce_accounts, quantization): """Read a Beancount file and produce a list of assets. Args: filename: A string, the path to the Beancount file to read. currency: A string, the currency to convert all the contents to. reduce_accounts: A set of account names to be aggregated. quantization: A Decimal instance, to quantize all the resulting amounts. Returns: A list of (account-name, number-balance), numbers being assumed to be in the requested currency. """ # Read the Beancount input file. entries, _, options_map = loader.load_file(filename, log_errors=logging.error) acctypes = options.get_account_types(options_map) price_map = prices.build_price_map(entries) ocmap = getters.get_account_open_close(entries) # Compute aggregations. real_root = realization.realize(entries, compute_balance=True) # Reduce accounts which have been found in details (mutate the tree in-place). for account in reduce_accounts: real_acc = realization.get(real_root, account) real_acc.balance = realization.compute_balance(real_acc) real_acc.clear() # Prune all the closed accounts and their parents. real_root = prune_closed_accounts(real_root, ocmap) # Produce a list of accounts and their balances reduced to a single currency. acceptable_types = (acctypes.assets, acctypes.liabilities) accounts = [] for real_acc in realization.iter_children(real_root): atype = account_types.get_account_type(real_acc.account) if atype not in acceptable_types: continue try: _, close = ocmap[real_acc.account] if close is not None: continue except KeyError: #logging.info("Account not there: {}".format(real_acc.account)) if real_acc.account not in reduce_accounts and real_acc.balance.is_empty( ): continue value_inv = real_acc.balance.reduce( lambda x: convert.get_value(x, price_map)) currency_inv = value_inv.reduce(convert.convert_position, currency, price_map) amount = currency_inv.get_currency_units(currency) accounts.append( (real_acc.account, amount.number.quantize(quantization))) # Reduce this list of (account-name, balance-number) sorted by reverse amount order. accounts.sort(key=lambda x: x[1], reverse=True) return accounts
def _posting_predicate(posting): account_type = account_types.get_account_type(posting.account) if account_type in (types.assets, types.liabilities): return True
def validate_sell_gains(entries, options_map): """Check the sum of asset account totals for lots sold with a price on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. """ errors = [] acc_types = options.get_account_types(options_map) proceed_types = set([ acc_types.assets, acc_types.liabilities, acc_types.equity, acc_types.expenses ]) default_tolerances = options_map['default_tolerance'] for entry in entries: if not isinstance(entry, data.Transaction): continue # Find transactins whose lots at cost all have a price. postings_at_cost = [ posting for posting in entry.postings if posting.position.lot.cost is not None ] if not postings_at_cost or not all(posting.price is not None for posting in postings_at_cost): continue # Accumulate the total expected proceeds and the sum of the asset and # expenses legs. total_price = inventory.Inventory() total_proceeds = inventory.Inventory() for posting in entry.postings: position = posting.position # If the posting is held at cost, add the priced value to the balance. if position.lot.cost is not None: assert posting.price price = posting.price total_price.add_amount( amount.amount_mult(price, -position.number)) else: # Otherwise, use the weight and ignore postings to Income accounts. atype = account_types.get_account_type(posting.account) if atype in proceed_types: total_proceeds.add_amount( interpolate.get_posting_weight(posting)) # Compare inventories, currency by currency. dict_price = {pos.lot.currency: pos.number for pos in total_price} dict_proceeds = { pos.lot.currency: pos.number for pos in total_proceeds } tolerances = interpolate.infer_tolerances(entry.postings, options_map) invalid = False for currency, price_number in dict_price.items(): # Accept a looser than usual tolerance because rounding occurs # differently. Also, it would be difficult for the user to satisfy # two sets of constraints manually. tolerance = inventory.get_tolerance( tolerances, default_tolerances, currency) * EXTRA_TOLERANCE_MULTIPLIER proceeds_number = dict_proceeds.pop(currency, ZERO) diff = abs(price_number - proceeds_number) if diff > tolerance: invalid = True break if invalid or dict_proceeds: errors.append( SellGainsError( entry.meta, "Invalid price vs. proceeds/gains: {} vs. {}".format( total_price, total_proceeds), entry)) return entries, errors
def posting_predicate(posting): account_type = account_types.get_account_type(posting.account) if account_type in (types.assets, types.liabilities): return True
def _calculate_budget_activity(self): # Accumulate expenses for the period balances = collections.defaultdict( lambda: collections.defaultdict(inventory.Inventory)) all_months = set() for entry in data.filter_txns(self.entries): # Check entry in date range if entry.date < self.date_start or entry.date > self.date_end: continue month = (entry.date.year, entry.date.month) # TODO domwe handle no transaction in a month? all_months.add(month) # TODO contains_budget_accounts = False for posting in entry.postings: if any( regexp.match(posting.account) for regexp in self.budget_accounts): contains_budget_accounts = True break if not contains_budget_accounts: continue for posting in entry.postings: account = posting.account for regexp, target_account in self.mappings: if regexp.match(account): account = target_account break account_type = account_types.get_account_type(account) if posting.units.currency != "USD": continue if account_type == self.acctypes.income: account = "Income" elif any( regexp.match(posting.account) for regexp in self.budget_accounts): continue # TODO WARn of any assets / liabilities left # TODO balances[account][month].add_position(posting) # Reduce the final balances to numbers sbalances = collections.defaultdict(dict) for account, months in sorted(balances.items()): for month, balance in sorted(months.items()): year, mth = month date = datetime.date(year, mth, 1) balance = balance.reduce(convert.get_value, self.price_map, date) balance = balance.reduce(convert.convert_position, "USD", self.price_map, date) try: pos = balance.get_only_position() except AssertionError: print(balance) raise total = pos.units.number if pos and pos.units else None sbalances[account][month] = total # Pivot the table header_months = sorted(all_months) header = ['account'] + ['{}-{:02d}'.format(*m) for m in header_months] for account in sorted(sbalances.keys()): for month in header_months: total = sbalances[account].get(month, None) temp = total.quantize(self.Q) if total else 0.00 # swap sign to be more human readable temp *= -1 month_str = f"{str(month[0])}-{str(month[1]).zfill(2)}" if account == "Income": self.income_df.loc["Avail Income", month_str] = Decimal(temp) else: self.envelope_df.loc[account, (month_str, 'budgeted')] = Decimal(0.00) self.envelope_df.loc[account, (month_str, 'activity')] = Decimal(temp) self.envelope_df.loc[account, (month_str, 'available')] = Decimal(0.00)
def get_final_holdings(entries, included_account_types=None, price_map=None, date=None): """Get a dictionary of the latest holdings by account. This basically just flattens the balance sheet's final positions, including that of equity accounts. If a 'price_map' is provided, insert price information in the flattened holdings at the latest date, or at the given date, if one is provided. Only the accounts in 'included_account_types' will be included, and this is always called for Assets and Liabilities only. If left unspecified, holdings from all account types will be included, including Equity, Income and Expenses. Args: entries: A list of directives. included_account_types: A sequence of strings, the account types to include in the output. A reasonable example would be ('Assets', 'Liabilities'). If not specified, include all account types. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance, the date at which to price the holdings. If left unspecified, we use the latest price information. Returns: A list of dicts, with the following fields: """ # Remove the entries inserted by unrealized gains/losses. Those entries do # affect asset accounts, and we don't want them to appear in holdings. # # Note: Perhaps it would make sense to generalize this concept of "inserted # unrealized gains." simple_entries = [ entry for entry in entries if (not isinstance(entry, data.Transaction) or entry.flag != flags.FLAG_UNREALIZED) ] # Realize the accounts into a tree (because we want the positions by-account). root_account = realization.realize(simple_entries) # For each account, look at the list of positions and build a list. holdings = [] for real_account in sorted(list(realization.iter_children(root_account)), key=lambda ra: ra.account): if included_account_types: # Skip accounts of invalid types, we only want to reflect the requested # account types, typically assets and liabilities. account_type = account_types.get_account_type(real_account.account) if account_type not in included_account_types: continue for pos in real_account.balance.get_positions(): if pos.cost is not None: # Get price information if we have a price_map. market_value = None if price_map is not None: base_quote = (pos.units.currency, pos.cost.currency) price_date, price_number = prices.get_price( price_map, base_quote, date) if price_number is not None: market_value = pos.units.number * price_number else: price_date, price_number = None, None holding = Holding(real_account.account, pos.units.number, pos.units.currency, pos.cost.number, pos.cost.currency, pos.units.number * pos.cost.number, market_value, price_number, price_date) else: holding = Holding(real_account.account, pos.units.number, pos.units.currency, None, pos.units.currency, pos.units.number, pos.units.number, None, None) holdings.append(holding) return holdings