Example #1
0
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
Example #2
0
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)
Example #3
0
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
Example #4
0
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"))
Example #6
0
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)
Example #7
0
 def is_expense_account(account):
     return account_types.get_account_type(account) == acctypes.expenses
Example #8
0
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
Example #9
0
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)
Example #10
0
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
Example #11
0
 def _posting_predicate(posting):
     account_type = account_types.get_account_type(posting.account)
     if account_type in (types.assets, types.liabilities):
         return True
Example #12
0
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
Example #13
0
 def posting_predicate(posting):
     account_type = account_types.get_account_type(posting.account)
     if account_type in (types.assets, types.liabilities):
         return True
Example #14
0
    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)
Example #15
0
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