示例#1
0
def get_assets_holdings(entries, options_map, currency=None):
    """Return holdings for all assets and liabilities.

    Args:
      entries: A list of directives.
      options_map: A dict of parsed options.
      currency: If specified, a string, the target currency to convert all
        holding values to.
    Returns:
      A list of Holding instances and a price-map.
    """
    # Compute a price map, to perform conversions.
    price_map = prices.build_price_map(entries)

    # Get the list of holdings.
    account_types = options.get_account_types(options_map)
    holdings_list = holdings.get_final_holdings(
        entries, (account_types.assets, account_types.liabilities), price_map)

    # Convert holdings to a unified currency.
    if currency:
        holdings_list = holdings.convert_to_currency(price_map, currency,
                                                     holdings_list)

    return holdings_list, price_map
示例#2
0
    def test_get_final_holdings_with_prices(self, entries, _, __):
        """
        2013-01-01 open Assets:Account1
        2013-01-01 open Assets:Account2
        2013-01-01 open Assets:Account3
        2013-01-01 open Assets:Cash
        2013-01-01 open Equity:Unknown

        2013-04-05 *
          Equity:Unknown
          Assets:Cash			50000 USD

        2013-04-01 *
          Assets:Account1             15 HOOL {518.73 USD}
          Assets:Cash

        2013-06-01 price HOOL  578.02 USD

        """
        price_map = prices.build_price_map(entries)
        holdings_list = holdings.get_final_holdings(entries,
                                                    ('Assets', 'Liabilities'),
                                                    price_map)

        holdings_list = sorted(map(tuple, holdings_list))
        expected_values = [
            ('Assets:Account1', D('15'), 'HOOL', D('518.73'), 'USD',
             D('7780.95'), D('8670.30'),
             D('578.02'), datetime.date(2013, 6, 1)),
            ('Assets:Cash', D('42219.05'), 'USD', None, 'USD',
             D('42219.05'), D('42219.05'), None, None),
            # Notice no Equity account.
        ]
        self.assertEqual(expected_values, holdings_list)
示例#3
0
文件: __init__.py 项目: cgrinds/fava
    def holdings(self, aggregation_key=None):
        holdings_list = holdings.get_final_holdings(
            self.entries,
            (self.account_types.assets, self.account_types.liabilities),
            self.price_map)

        if aggregation_key:
            holdings_list = holdings.aggregate_holdings_by(
                holdings_list, operator.attrgetter(aggregation_key))
        return holdings_list
示例#4
0
    def holdings(self, aggregation_key=None):
        holdings_list = holdings.get_final_holdings(
            self.entries,
            (self.account_types.assets, self.account_types.liabilities),
            self.price_map)

        if aggregation_key:
            holdings_list = holdings.aggregate_holdings_by(
                holdings_list, operator.attrgetter(aggregation_key))
        return holdings_list
示例#5
0
    def test_get_final_holdings__check_no_aggregates(self, entries, _, __):
        """
        plugin "beancount.plugins.unrealized" "Unrealized"

        2013-01-01 open Assets:Investment   HOOL
        2013-01-01 open Assets:Cash         USD

        2013-04-01 *
          Assets:Investment             15 HOOL {518.73 USD}
          Assets:Cash

        2013-06-01 price HOOL  600.00 USD
        """
        holdings_list = holdings.get_final_holdings(entries)

        # Ensure that there is no Unrealized balance or sub-account.
        self.assertEqual({'Assets:Cash', 'Assets:Investment'},
                         set(holding.account for holding in holdings_list))
示例#6
0
    def test_get_final_holdings__zero_position(self, entries, _, __):
        """
        1970-01-01 open Assets:Stocks:NYA
        1970-01-01 open Expenses:Financial:Commissions
        1970-01-01 open Assets:Current
        1970-01-01 open Income:Dividends:NYA

        2012-07-02 ! "I received 1 new share in dividend, without paying"
          Assets:Stocks:NYA 1 NYA {0 EUR}
          Income:Dividends:NYA -0 EUR

        2014-11-13 balance Assets:Stocks:NYA 1 NYA
        """
        price_map = prices.build_price_map(entries)
        holdings_list = holdings.get_final_holdings(entries,
                                                    ('Assets', 'Liabilities'),
                                                    price_map)
        self.assertEqual(1, len(holdings_list))
        self.assertEqual('EUR', holdings_list[0].cost_currency)
示例#7
0
def add_unrealized_gains(entries, options_map, subaccount=None):
    """Insert entries for unrealized capital gains.

    This function inserts entries that represent unrealized gains, at the end of
    the available history. It returns a new list of entries, with the new gains
    inserted. It replaces the account type with an entry in an income account.
    Optionally, it can book the gain in a subaccount of the original and income
    accounts.

    Args:
      entries: A list of data directives.
      options_map: A dict of options, that confirms to beancount.parser.options.
      subaccount: A string, and optional the name of a subaccount to create
        under an account to book the unrealized gain. If this is left to its
        default value, the gain is booked directly in the same account.
    Returns:
      A list of entries, which includes the new unrealized capital gains entries
      at the end, and a list of errors. The new list of entries is still sorted.
    """
    errors = []
    meta = data.new_metadata('<unrealized_gains>', 0)

    account_types = options.get_account_types(options_map)

    # Assert the subaccount name is in valid format.
    if subaccount:
        validation_account = account.join(account_types.assets, subaccount)
        if not account.is_valid(validation_account):
            errors.append(
                UnrealizedError(meta,
                                "Invalid subaccount name: '{}'".format(subaccount),
                                None))
            return entries, errors

    if not entries:
        return (entries, errors)

    # Group positions by (account, cost, cost_currency).
    price_map = prices.build_price_map(entries)
    holdings_list = holdings.get_final_holdings(entries, price_map=price_map)

    # Group positions by (account, cost, cost_currency).
    holdings_list = holdings.aggregate_holdings_by(
        holdings_list, lambda h: (h.account, h.currency, h.cost_currency))

    # Get the latest prices from the entries.
    price_map = prices.build_price_map(entries)

    # Create transactions to account for each position.
    new_entries = []
    latest_date = entries[-1].date
    for index, holding in enumerate(holdings_list):
        if (holding.currency == holding.cost_currency or
            holding.cost_currency is None):
            continue

        # Note: since we're only considering positions held at cost, the
        # transaction that created the position *must* have created at least one
        # price point for that commodity, so we never expect for a price not to
        # be available, which is reasonable.
        if holding.price_number is None:
            # An entry without a price might indicate that this is a holding
            # resulting from leaked cost basis. {0ed05c502e63, b/16}
            if holding.number:
                errors.append(
                    UnrealizedError(meta,
                                    "A valid price for {h.currency}/{h.cost_currency} "
                                    "could not be found".format(h=holding), None))
            continue

        # Compute the PnL; if there is no profit or loss, we create a
        # corresponding entry anyway.
        pnl = holding.market_value - holding.book_value
        if holding.number == ZERO:
            # If the number of units sum to zero, the holdings should have been
            # zero.
            errors.append(
                UnrealizedError(
                    meta,
                    "Number of units of {} in {} in holdings sum to zero "
                    "for account {} and should not".format(
                        holding.currency, holding.cost_currency, holding.account),
                    None))
            continue

        # Compute the name of the accounts and add the requested subaccount name
        # if requested.
        asset_account = holding.account
        income_account = account.join(account_types.income,
                                      account.sans_root(holding.account))
        if subaccount:
            asset_account = account.join(asset_account, subaccount)
            income_account = account.join(income_account, subaccount)

        # Create a new transaction to account for this difference in gain.
        gain_loss_str = "gain" if pnl > ZERO else "loss"
        narration = ("Unrealized {} for {h.number} units of {h.currency} "
                     "(price: {h.price_number:.4f} {h.cost_currency} as of {h.price_date}, "
                     "average cost: {h.cost_number:.4f} {h.cost_currency})").format(
                         gain_loss_str, h=holding)
        entry = data.Transaction(data.new_metadata(meta["filename"], lineno=1000 + index),
                                 latest_date, flags.FLAG_UNREALIZED,
                                 None, narration, EMPTY_SET, EMPTY_SET, [])

        # Book this as income, converting the account name to be the same, but as income.
        # Note: this is a rather convenient but arbitrary choice--maybe it would be best to
        # let the user decide to what account to book it, but I don't a nice way to let the
        # user specify this.
        #
        # Note: we never set a price because we don't want these to end up in Conversions.
        entry.postings.extend([
            data.Posting(
                asset_account,
                amount.Amount(pnl, holding.cost_currency),
                None,
                None,
                None,
                None),
            data.Posting(
                income_account,
                amount.Amount(-pnl, holding.cost_currency),
                None,
                None,
                None,
                None)
        ])

        new_entries.append(entry)

    # Ensure that the accounts we're going to use to book the postings exist, by
    # creating open entries for those that we generated that weren't already
    # existing accounts.
    new_accounts = {posting.account
                    for entry in new_entries
                    for posting in entry.postings}
    open_entries = getters.get_account_open_close(entries)
    new_open_entries = []
    for account_ in sorted(new_accounts):
        if account_ not in open_entries:
            meta = data.new_metadata(meta["filename"], index)
            open_entry = data.Open(meta, latest_date, account_, None, None)
            new_open_entries.append(open_entry)

    return (entries + new_open_entries + new_entries, errors)
示例#8
0
def add_unrealized_gains_at_date(entries, unrealized_entries,
                                 income_account_type, price_map, date, meta,
                                 subaccount):
    """Insert/remove entries for unrealized capital gains 

    This function takes a list of entries and a date and creates a set of unrealized gains
    transactions, negating previous unrealized gains transactions within the same account.

    Args:
      entries: A list of data directives.
      unrealized_entries: A list of previously generated unrealized transactions.
      income_account_type: The income account type.
      price_map: A price map returned by prices.build_price_map.
      date: The effective date to generate the unrealized transactions for.
      meta: meta.
      subaccount: A string, and optional the name of a subaccount to create
        under an account to book the unrealized gain. If this is left to its
        default value, the gain is booked directly in the same account.
    Returns:
      A list of newly created unrealized transactions and a list of errors.
    """
    errors = []

    entries_truncated = summarize.truncate(entries, date + ONEDAY)

    holdings_list = holdings.get_final_holdings(entries_truncated,
                                                price_map=price_map,
                                                date=date)

    # Group positions by (account, cost, cost_currency).
    holdings_list = holdings.aggregate_holdings_by(
        holdings_list, lambda h: (h.account, h.currency, h.cost_currency))

    holdings_with_currencies = set()

    # Create transactions to account for each position.
    new_entries = []
    for index, holding in enumerate(holdings_list):
        if (holding.currency == holding.cost_currency
                or holding.cost_currency is None):
            continue

        # Note: since we're only considering positions held at cost, the
        # transaction that created the position *must* have created at least one
        # price point for that commodity, so we never expect for a price not to
        # be available, which is reasonable.
        if holding.price_number is None:
            # An entry without a price might indicate that this is a holding
            # resulting from leaked cost basis. {0ed05c502e63, b/16}
            if holding.number:
                errors.append(
                    UnrealizedError(
                        meta,
                        "A valid price for {h.currency}/{h.cost_currency} "
                        "could not be found".format(h=holding), None))
            continue

        # Compute the PnL; if there is no profit or loss, we create a
        # corresponding entry anyway.
        pnl = holding.market_value - holding.book_value
        if holding.number == ZERO:
            # If the number of units sum to zero, the holdings should have been
            # zero.
            errors.append(
                UnrealizedError(
                    meta,
                    "Number of units of {} in {} in holdings sum to zero "
                    "for account {} and should not".format(
                        holding.currency, holding.cost_currency,
                        holding.account), None))
            continue

        # Compute the name of the accounts and add the requested subaccount name
        # if requested.
        asset_account = holding.account
        income_account = account.join(income_account_type,
                                      account.sans_root(holding.account))
        if subaccount:
            asset_account = account.join(asset_account, subaccount)
            income_account = account.join(income_account, subaccount)

        holdings_with_currencies.add(
            (holding.account, holding.cost_currency, holding.currency))

        # Find the previous unrealized gain entry to negate and decide if we
        # should create a new posting.
        latest_unrealized_entry = find_previous_unrealized_transaction(
            unrealized_entries, asset_account, holding.cost_currency,
            holding.currency)

        # Don't create a new transaction if our last one hasn't changed.
        if (latest_unrealized_entry
                and pnl == latest_unrealized_entry.postings[0].units.number):
            continue

        # Don't bother creating a blank unrealized transaction if none existed
        if pnl == ZERO and not latest_unrealized_entry:
            continue

        relative_pnl = pnl
        if latest_unrealized_entry:
            relative_pnl = pnl - latest_unrealized_entry.postings[
                0].units.number

        # Create a new transaction to account for this difference in gain.
        gain_loss_str = "gain" if relative_pnl > ZERO else "loss"
        narration = (
            "Unrealized {} for {h.number} units of {h.currency} "
            "(price: {h.price_number:.4f} {h.cost_currency} as of {h.price_date}, "
            "average cost: {h.cost_number:.4f} {h.cost_currency})").format(
                gain_loss_str, h=holding)
        entry = data.Transaction(
            data.new_metadata(meta["filename"],
                              lineno=1000 + index,
                              kvlist={'prev_currency': holding.currency}),
            date, flags.FLAG_UNREALIZED, None, narration, set(), set(), [])

        # Book this as income, converting the account name to be the same, but as income.
        # Note: this is a rather convenient but arbitraty choice--maybe it would be best to
        # let the user decide to what account to book it, but I don't a nice way to let the
        # user specify this.
        #
        # Note: we never set a price because we don't want these to end up in Conversions.
        entry.postings.extend([
            data.Posting(asset_account,
                         amount.Amount(pnl, holding.cost_currency), None, None,
                         None, None),
            data.Posting(income_account,
                         amount.Amount(-pnl, holding.cost_currency), None,
                         None, None, None)
        ])
        if latest_unrealized_entry:
            for posting in latest_unrealized_entry.postings[:2]:
                entry.postings.append(
                    data.Posting(posting.account, -posting.units, None, None,
                                 None, None))

        new_entries.append(entry)

    return new_entries, holdings_with_currencies, errors
示例#9
0
    def test_get_final_holdings(self, entries, _, __):
        """
        2013-01-01 open Assets:Account1
        2013-01-01 open Assets:Account2
        2013-01-01 open Assets:Account3
        2013-01-01 open Assets:Cash
        2013-01-01 open Liabilities:Loan
        2013-01-01 open Equity:Unknown

        2013-04-05 *
          Equity:Unknown
          Assets:Cash			50000 USD

        2013-04-01 *
          Assets:Account1             15 HOOL {518.73 USD}
          Assets:Cash

        2013-04-02 *
          Assets:Account1             10 HOOL {523.46 USD}
          Assets:Cash

        2013-04-03 *
          Assets:Account1             -4 HOOL {518.73 USD}
          Assets:Cash

        2013-04-02 *
          Assets:Account2            20 ITOT {85.195 USD}
          Assets:Cash

        2013-04-03 *
          Assets:Account3             50 HOOL {540.00 USD} @ 560.00 USD
          Assets:Cash

        2013-04-10 *
          Assets:Cash			5111 USD
          Liabilities:Loan
        """
        holdings_list = holdings.get_final_holdings(entries)

        holdings_list = sorted(map(tuple, holdings_list))
        expected_values = [
            ('Assets:Account1', D('10'), 'HOOL', D('523.46'), 'USD',
             D('5234.60'), None, None, None),
            ('Assets:Account1', D('11'), 'HOOL', D('518.73'), 'USD',
             D('5706.03'), None, None, None),
            ('Assets:Account2', D('20'), 'ITOT', D('85.195'), 'USD',
             D('1703.900'), None, None, None),
            ('Assets:Account3', D('50'), 'HOOL', D('540.00'), 'USD',
             D('27000.00'), None, None, None),
            ('Assets:Cash', D('15466.470'), 'USD', None, 'USD',
             D('15466.470'), D('15466.470'), None, None),
            ('Equity:Unknown', D('-50000'), 'USD', None, 'USD',
             D('-50000'), D('-50000'), None, None),
            ('Liabilities:Loan', D('-5111'), 'USD', None, 'USD',
             D('-5111'), D('-5111'), None, None),
        ]
        self.assertEqual(expected_values, holdings_list)

        # Try with some account type restrictions.
        holdings_list = holdings.get_final_holdings(entries, ('Assets', 'Liabilities'))
        holdings_list = sorted(map(tuple, holdings_list))
        expected_values = [holding
                           for holding in expected_values
                           if (holding[0].startswith('Assets') or
                               holding[0].startswith('Liabilities'))]
        self.assertEqual(expected_values, holdings_list)

        # Try with some account type restrictions.
        holdings_list = holdings.get_final_holdings(entries, ('Assets',))
        holdings_list = sorted(map(tuple, holdings_list))
        expected_values = [holding
                           for holding in expected_values
                           if holding[0].startswith('Assets')]
        self.assertEqual(expected_values, holdings_list)
def add_unrealized_gains_at_date(entries, unrealized_entries, income_account_type,
                                 price_map, date, meta, subaccount):
    """Insert/remove entries for unrealized capital gains 

    This function takes a list of entries and a date and creates a set of unrealized gains
    transactions, negating previous unrealized gains transactions within the same account.

    Args:
      entries: A list of data directives.
      unrealized_entries: A list of previously generated unrealized transactions.
      income_account_type: The income account type.
      price_map: A price map returned by prices.build_price_map.
      date: The effective date to generate the unrealized transactions for.
      meta: meta.
      subaccount: A string, and optional the name of a subaccount to create
        under an account to book the unrealized gain. If this is left to its
        default value, the gain is booked directly in the same account.
    Returns:
      A list of newly created unrealized transactions and a list of errors.
    """
    errors = []

    entries_truncated = summarize.truncate(entries, date + ONEDAY)

    holdings_list = holdings.get_final_holdings(entries_truncated, price_map=price_map, date=date)

    # Group positions by (account, cost, cost_currency).
    holdings_list = holdings.aggregate_holdings_by(
        holdings_list, lambda h: (h.account, h.currency, h.cost_currency))

    holdings_with_currencies = set()

    # Create transactions to account for each position.
    new_entries = []
    for index, holding in enumerate(holdings_list):
        if (holding.currency == holding.cost_currency or
            holding.cost_currency is None):
            continue

        # Note: since we're only considering positions held at cost, the
        # transaction that created the position *must* have created at least one
        # price point for that commodity, so we never expect for a price not to
        # be available, which is reasonable.
        if holding.price_number is None:
            # An entry without a price might indicate that this is a holding
            # resulting from leaked cost basis. {0ed05c502e63, b/16}
            if holding.number:
                errors.append(
                    UnrealizedError(meta,
                                    "A valid price for {h.currency}/{h.cost_currency} "
                                    "could not be found".format(h=holding), None))
            continue

        # Compute the PnL; if there is no profit or loss, we create a
        # corresponding entry anyway.
        pnl = holding.market_value - holding.book_value
        if holding.number == ZERO:
            # If the number of units sum to zero, the holdings should have been
            # zero.
            errors.append(
                UnrealizedError(
                    meta,
                    "Number of units of {} in {} in holdings sum to zero "
                    "for account {} and should not".format(
                        holding.currency, holding.cost_currency, holding.account),
                    None))
            continue

        # Compute the name of the accounts and add the requested subaccount name
        # if requested.
        asset_account = holding.account
        income_account = account.join(income_account_type,
                                      account.sans_root(holding.account))
        if subaccount:
            asset_account = account.join(asset_account, subaccount)
            income_account = account.join(income_account, subaccount)

        holdings_with_currencies.add((holding.account, holding.cost_currency, holding.currency))

        # Find the previous unrealized gain entry to negate and decide if we
        # should create a new posting.
        latest_unrealized_entry = find_previous_unrealized_transaction(unrealized_entries, asset_account, holding.cost_currency, holding.currency)

        # Don't create a new transaction if our last one hasn't changed.
        if (latest_unrealized_entry and
            pnl == latest_unrealized_entry.postings[0].units.number):
            continue

        # Don't bother creating a blank unrealized transaction if none existed
        if pnl == ZERO and not latest_unrealized_entry:
            continue

        relative_pnl = pnl
        if latest_unrealized_entry:
            relative_pnl = pnl - latest_unrealized_entry.postings[0].units.number

        # Create a new transaction to account for this difference in gain.
        gain_loss_str = "gain" if relative_pnl > ZERO else "loss"
        narration = ("Unrealized {} for {h.number} units of {h.currency} "
                     "(price: {h.price_number:.4f} {h.cost_currency} as of {h.price_date}, "
                     "average cost: {h.cost_number:.4f} {h.cost_currency})").format(
                         gain_loss_str, h=holding)
        entry = data.Transaction(data.new_metadata(meta["filename"], lineno=1000 + index,
                                 kvlist={'prev_currency': holding.currency}), date,
                                 flags.FLAG_UNREALIZED, None, narration, set(), set(), [])

        # Book this as income, converting the account name to be the same, but as income.
        # Note: this is a rather convenient but arbitraty choice--maybe it would be best to
        # let the user decide to what account to book it, but I don't a nice way to let the
        # user specify this.
        #
        # Note: we never set a price because we don't want these to end up in Conversions.
        entry.postings.extend([
            data.Posting(
                asset_account,
                amount.Amount(pnl, holding.cost_currency),
                None,
                None,
                None,
                None),
            data.Posting(
                income_account,
                amount.Amount(-pnl, holding.cost_currency),
                None,
                None,
                None,
                None)
        ])
        if latest_unrealized_entry:
            for posting in latest_unrealized_entry.postings[:2]:
                entry.postings.append(
                    data.Posting(
                        posting.account,
                        -posting.units,
                        None,
                        None,
                        None,
                        None))

        new_entries.append(entry)

    return new_entries, holdings_with_currencies, errors