Exemplo n.º 1
0
    def test_truncate__normal2(self):
        truncated_entries = summarize.truncate(self.entries,
                                               datetime.date(2014, 3, 14))
        self.assertEqualEntries(
            """

        2014-01-01 open Assets:US:Bank:Checking
        2014-01-01 open Equity:Opening-Balances

        2014-03-10 * "A"
          Assets:US:Bank:Checking   1 USD
          Equity:Opening-Balances  -1 USD

        2014-03-11 * "B"
          Assets:US:Bank:Checking   1 USD
          Equity:Opening-Balances  -1 USD

        2014-03-12 * "C"
          Assets:US:Bank:Checking   1 USD
          Equity:Opening-Balances  -1 USD

        2014-03-13 * "D1"
          Assets:US:Bank:Checking   1 USD
          Equity:Opening-Balances  -1 USD

        2014-03-13 * "D2"
          Assets:US:Bank:Checking   1 USD
          Equity:Opening-Balances  -1 USD

        """, truncated_entries)
Exemplo n.º 2
0
 def test_truncate__before(self):
     truncated_entries = summarize.truncate(self.entries,
                                            datetime.date(2014, 2, 15))
     self.assertEqualEntries(
         """
     2014-01-01 open Assets:US:Bank:Checking
     2014-01-01 open Equity:Opening-Balances
     """, truncated_entries)
Exemplo n.º 3
0
 def test_truncate__after(self):
     truncated_entries = summarize.truncate(self.entries,
                                            datetime.date(2014, 3, 15))
     self.assertEqual(self.entries, truncated_entries)
Exemplo n.º 4
0
def get_commodities_at_date(entries, options_map, date=None):
    """Return a list of commodities present at a particular date.

    This routine fetches the holdings present at a particular date and returns a
    list of the commodities held in those holdings. This should define the list
    of price date points required to assess the market value of this portfolio.

    Notes:

    * The ticker symbol will be fetched from the corresponding Commodity
      directive. If there is no ticker symbol defined for a directive or no
      corresponding Commodity directive, the currency is still included, but
      'None' is specified for the symbol. The code that uses this routine should
      be free to use the currency name to make an attempt to fetch the currency
      using its name, or to ignore it.

    * The 'cost-currency' is that which is found on the holdings instance and
      can be ignored. The 'quote-currency' is that which is declared on the
      Commodity directive from its 'quote' metadata field.

    This is used in a routine that fetches prices from a data source on the
    internet (either from LedgerHub, but you can reuse this in your own script
    if you build one).

    Args:
      entries: A list of directives.
      date: A datetime.date instance, the date at which to get the list of
        relevant holdings.
    Returns:
      A list of (currency, cost-currency, quote-currency, ticker) tuples, where
        currency: The Beancount base currency to fetch a price for.
        cost-currency: The cost-currency of the holdings found at the given date.
        quote-currency: The currency formally declared as quote currency in the
          metadata of Commodity directives.
        ticker: The ticker symbol to use for fetching the price (extracted from
          the metadata of Commodity directives).
    """
    # Remove all the entries after the given date, if requested.
    if date is not None:
        entries = summarize.truncate(entries, date)

    # Get the list of holdings at the particular date.
    holdings_list = get_final_holdings(entries)

    # Obtain the unique list of currencies we need to fetch.
    commodities_list = {(holding.currency, holding.cost_currency)
                        for holding in holdings_list}

    # Add in the associated ticker symbols.
    commodities_map = getters.get_commodity_map(entries)
    commodities_symbols_list = []
    for currency, cost_currency in sorted(commodities_list):
        try:
            commodity_entry = commodities_map[currency]
            ticker = commodity_entry.meta.get('ticker', None)
            quote_currency = commodity_entry.meta.get('quote', None)
        except KeyError:
            ticker = None
            quote_currency = None

        commodities_symbols_list.append(
            (currency, cost_currency, quote_currency, ticker))

    return commodities_symbols_list
Exemplo n.º 5
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
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