Esempio n. 1
0
def get_commodities_table(entries: data.Entries, attributes: List[str]) -> Table:
    """Produce a Table of per-commodity attributes."""
    commodities = getters.get_commodity_directives(entries)
    header = ['currency'] + attributes
    getter = lambda entry, key: entry.meta.get(key, None)
    table = get_metamap_table(commodities, header, getter)
    return table
Esempio n. 2
0
def infer_report_groups(entries: data.Entries, investments: InvestmentConfig,
                        out_config: ReportConfig):
    """Logically group accounts for reporting."""

    # Create a group for each commodity.
    groups = collections.defaultdict(list)
    open_close_map = getters.get_account_open_close(entries)
    for investment in investments.investment:
        opn, unused_cls = open_close_map[investment.asset_account]
        assert opn, "Missing open directive for '{}'".format(
            investment.account)
        name = "currency.{}".format(investment.currency)
        groups[name].append(investment.asset_account)

    # Join commodities by metadata gropus and create a report for each.
    for attrname in "assetcls", "strategy":
        comm_map = getters.get_commodity_directives(entries)
        for investment in investments.investment:
            comm = comm_map[investment.currency]
            value = comm.meta.get(attrname)
            if value:
                name = "{}.{}".format(attrname, value)
                groups[name].append(investment.asset_account)

    for name, group_accounts in sorted(groups.items()):
        report = out_config.report.add()
        report.name = name
        report.investment.extend(group_accounts)
Esempio n. 3
0
 def wrapped(self, entries, unused_errors, options_map):
     holdings_list, _ = holdings.get_assets_holdings(
         entries, options_map)
     commodities = getters.get_commodity_directives(entries)
     action_holdings = export_reports.classify_holdings_for_export(
         holdings_list, commodities)
     return fun(self, action_holdings)
Esempio n. 4
0
def process_account_entries(entries: data.Entries, options_map: data.Options,
                            account: Account) -> AccountData:
    """Process a single account."""
    logging.info("Processing account: %s", account)

    # Extract the relevant transactions.
    transactions = transactions_for_account(entries, account)
    if not transactions:
        logging.warning("No transactions for %s; skipping.", account)
        return transactions, None, None

    # Categorize the set of accounts encountered in the filtered transactions.
    seen_accounts = {
        posting.account
        for entry in transactions for posting in entry.postings
    }
    atypes = options.get_account_types(options_map)
    catmap = categorize_accounts(account, seen_accounts, atypes)

    # Process each of the transactions, adding derived values as metadata.
    cash_flows = []
    balance = Inventory()
    decorated_transactions = []
    for entry in transactions:

        # Update the total position in the asset we're interested in.
        positions = []
        for posting in entry.postings:
            category = catmap[posting.account]
            if category is Cat.ASSET:
                balance.add_position(posting)
                positions.append(posting)

        # Compute the signature of the transaction.
        entry = copy_and_normalize(entry)
        signature = compute_transaction_signature(catmap, entry)
        entry.meta["signature"] = signature
        entry.meta["description"] = KNOWN_SIGNATURES[signature]

        # Compute the cash flows associated with the transaction.
        flows = produce_cash_flows(entry)
        entry.meta['cash_flows'] = flows

        cash_flows.extend(
            flow._replace(balance=copy.deepcopy(balance)) for flow in flows)
        decorated_transactions.append(entry)

    currency = accountlib.leaf(account)

    cost_currencies = set(cf.amount.currency for cf in cash_flows)
    assert len(cost_currencies) == 1, str(cost_currencies)
    cost_currency = cost_currencies.pop()

    commodity_map = getters.get_commodity_directives(entries)
    comm = commodity_map[currency]

    return AccountData(account, currency, cost_currency, comm, cash_flows,
                       decorated_transactions, catmap)
Esempio n. 5
0
def merge_meta(entries, options_map, config):
    """Load a secondary file and merge its metadata in our given set of entries.

    Args:
      entries: A list of directives. We're interested only in the Transaction instances.
      unused_options_map: A parser options dict.
      config: The plugin configuration string.
    Returns:
      A list of entries, with more metadata attached to them.
    """
    external_filename = config
    new_entries = list(entries)

    ext_entries, ext_errors, ext_options_map = loader.load_file(
        external_filename)

    # Map Open and Close directives.
    oc_map = getters.get_account_open_close(entries)
    ext_oc_map = getters.get_account_open_close(ext_entries)
    for account in set(oc_map.keys()) & set(ext_oc_map.keys()):
        open_entry, close_entry = oc_map[account]
        ext_open_entry, ext_close_entry = ext_oc_map[account]
        if open_entry and ext_open_entry:
            open_entry.meta.update(ext_open_entry.meta)
        if close_entry and ext_close_entry:
            close_entry.meta.update(ext_close_entry.meta)

    # Map Commodity directives.
    commodities = getters.get_commodity_directives(entries)
    ext_commodities = getters.get_commodity_directives(ext_entries)
    for currency in set(commodities) & set(ext_commodities):
        comm_entry = commodities[currency]
        ext_comm_entry = ext_commodities[currency]
        if comm_entry and ext_comm_entry:
            comm_entry.meta.update(ext_comm_entry.meta)

    # Note: We cannot include the external file in the list of inputs so that a
    # change of it triggers a cache rebuild because side-effects on options_map
    # aren't cascaded through. This is something that should be defined better
    # in the plugin interface and perhaps improved upon.

    return new_entries, ext_errors
Esempio n. 6
0
def prune_entries(entries: data.Entries) -> data.Entries:
    """Prune the list of entries to exclude all transactions that include a
    commodity name in at least one of its postings. This speeds up the
    recovery process by removing the majority of non-trading transactions."""
    commodities = getters.get_commodity_directives(entries)
    regexp = re.compile(r"\b({})\b".format("|".join(
        commodities.keys()))).search
    return [
        entry for entry in entries
        if (isinstance(entry, (data.Open, data.Commodity)) or (
            isinstance(entry, data.Transaction) and any(
                regexp(posting.account) for posting in entry.postings)))
    ]
Esempio n. 7
0
def create_row_context(entries, options_map):
    """Create the context container which we will use to evaluate rows."""
    context = RowContext()
    context.balance = inventory.Inventory()

    # Initialize some global properties for use by some of the accessors.
    context.options_map = options_map
    context.account_types = options.get_account_types(options_map)
    context.open_close_map = getters.get_account_open_close(entries)
    context.commodity_map = getters.get_commodity_directives(entries)
    context.price_map = prices.build_price_map(entries)

    return context
Esempio n. 8
0
    def test_get_money_instruments(self, entries, errors, options_map):
        """
        1900-01-01 commodity VMMXX
          export: "MUTF:VMMXX (MONEY:USD)"

        1900-01-01 commodity IGI806
          export: "(MONEY:CAD)"
        """
        commodities = getters.get_commodity_directives(entries)
        self.assertEqual({
            'USD': 'MUTF:VMMXX',
            'CAD': 'IGI806'
        }, export_reports.get_money_instruments(commodities))
Esempio n. 9
0
def find_accounts(entries: data.Entries, options_map: data.Options,
                  start_date: Optional[Date]) -> List[Account]:
    """Return a list of account names from the balance sheet which either aren't
    closed or are closed now but were still open at the given start date.
    """
    commodities = getters.get_commodity_directives(entries)
    open_close_map = getters.get_account_open_close(entries)
    atypes = options.get_account_types(options_map)
    return sorted(
        account for account, (_open, _close) in open_close_map.items()
        if (accountlib.leaf(account) in commodities
            and acctypes.is_balance_sheet_account(account, atypes)
            and not acctypes.is_equity_account(account, atypes) and (
                _close is None or (start_date and _close.date > start_date))))
Esempio n. 10
0
    def generate_table(self, entries, errors, options_map):
        commodities = getters.get_commodity_directives(entries)
        ticker_info = getters.get_values_meta(commodities, 'name', 'ticker',
                                              'quote')

        price_rows = [
            (currency, cost_currency, ticker, name)
            for currency, (name, ticker,
                           cost_currency) in sorted(ticker_info.items())
            if ticker
        ]

        return table.create_table(price_rows, [(0, "Currency"),
                                               (1, "Cost-Currency"),
                                               (2, "Symbol"), (3, "Name")])
Esempio n. 11
0
 def get_commodity_directives(self):
     return getters.get_commodity_directives(self.entries)
Esempio n. 12
0
def process_account_entries(entries: data.Entries, config: InvestmentConfig,
                            investment: Investment,
                            check_explicit_flows: bool) -> AccountData:
    """Process a single account."""
    account = investment.asset_account
    logging.info("Processing account: %s", account)

    # Extract the relevant transactions.
    transactions = extract_transactions_for_account(entries, investment)
    if not transactions:
        logging.warning("No transactions for %s; skipping.", account)
        return None

    # Categorize the set of accounts encountered in the filtered transactions.
    seen_accounts = {
        posting.account
        for entry in transactions for posting in entry.postings
    }
    catmap = categorize_accounts(config, investment, seen_accounts)

    # Process each of the transactions, adding derived values as metadata.
    cash_flows = []
    balance = Inventory()
    decorated_transactions = []
    for entry in transactions:

        # Compute the signature of the transaction.
        entry = categorize_entry(catmap, entry)
        signature = compute_transaction_signature(entry)
        entry.meta["signature"] = signature

        # TODO(blais): Cache balance in every transaction to speed up
        # computation? Do this later.
        if False:
            # Update the total position in the asset we're interested in.
            for posting in entry.postings:
                if posting.meta["category"] is Cat.ASSET:
                    balance.add_position(posting)

        # Compute the cash flows associated with the transaction.
        flows_general = produce_cash_flows_general(entry, account)
        if check_explicit_flows:
            # Attempt the explicit method.
            flows_explicit = produce_cash_flows_explicit(entry, account)
            if flows_explicit != flows_general:
                print(
                    "Differences found between general and explicit methods:")
                print("Explicit handlers:")
                for flow in flows_explicit:
                    print("  ", flow)
                print("General handler:")
                for flow in flows_general:
                    print("  ", flow)
                raise ValueError(
                    "Differences found between general and explicit methods:")

        cash_flows.extend(flows_general)
        decorated_transactions.append(entry)

    cost_currencies = set(cf.amount.currency for cf in cash_flows)
    #assert len(cost_currencies) == 1, str(cost_currencies)
    cost_currency = cost_currencies.pop() if cost_currencies else None

    currency = investment.currency
    commodity_map = getters.get_commodity_directives(entries)
    comm = commodity_map[currency] if currency else None

    open_close_map = getters.get_account_open_close(entries)
    opn, cls = open_close_map[account]

    # Compute the final balance.
    balance = compute_balance_at(decorated_transactions)

    return AccountData(account, currency, cost_currency, comm, opn, cls,
                       cash_flows, decorated_transactions, balance, catmap)
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 = getters.get_commodity_directives(entries)
    commodities_symbols_list = []
    for currency, cost_currency in sorted(commodities_list):
        try:
            commodity_entry = commodities[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
Esempio n. 14
0
def get_portfolio_matrix(asof_date=None):
    """
    打印持仓
    Args:
        asof_date: 计算该日为止的持仓, 避免未来预付款项影响。
    """
    if asof_date is None:
        asof_date = datetime.date.today()

    (entries, errors,
     option_map) = beancount.loader.load_file(get_ledger_file())
    entries = [entry for entry in entries if entry.date <= asof_date]

    assets_holdings, price_map = get_assets_holdings(entries, option_map)
    account_map = get_account_map(entries)
    commoditiy_map = getters.get_commodity_directives(entries)

    holding_groups = {}
    for holding in assets_holdings:
        if holding.currency == "DAY":
            continue

        account_obj = account_map[holding.account]
        if account_obj is None:
            raise ValueError(f"account is not defined for {holding}")
        currency_obj = commoditiy_map[holding.currency]
        if currency_obj is None:
            raise ValueError(f"commoditiy is not defined for {holding}")

        if bool(int(account_obj.meta.get("sunk", 0))):
            logger.warning(f"{account_obj.account} is an sunk. Ignored.")
            continue
        if bool(int(account_obj.meta.get("nondisposable", 0))):
            logger.warning(f"{account_obj.account} is nondisposable. Ignored.")
            continue

        for meta_field in ("name", "asset-class", "asset-subclass"):
            if meta_field not in currency_obj.meta:
                raise ValueError("Commodity %s has no '%s' in meta"
                                 "" % (holding.currency, meta_field))

        account_name = account_obj.meta.get("name")
        if not account_name:
            raise ValueError("Account name not set for %s" % holding.account)
        account_nondisposable = bool(
            int(account_obj.meta.get("nondisposable", 0)))
        symbol_name = currency_obj.meta["name"]
        asset_class = currency_obj.meta["asset-class"]
        asset_subclass = currency_obj.meta["asset-subclass"]
        symbol = holding.currency
        currency = holding.cost_currency
        price = holding.price_number
        price_date = holding.price_date
        if price is None:
            if symbol == currency:
                price = 1
                price_date = ''
        qty = holding.number
        if currency == "CNY":
            cny_rate = 1
        else:
            cny_rate = price_map[(currency, "CNY")][-1][1]

        holding_dict = {
            "account": account_name,
            "nondisposable": account_nondisposable,
            "symbol": symbol,
            "symbol_name": symbol_name,
            "asset_class": asset_class,
            "asset_subclass": asset_subclass,
            "quantity": qty,
            "currency": currency,
            "price": price,
            "price_date": price_date,
            "cny_rate": cny_rate,
            # optional:
            # book_value
            # market_value
            #
            # chg_1
            # chg_5
            # chg_30
        }

        hld_prices = price_map.get((holding.currency, holding.cost_currency))
        if hld_prices is not None and holding.cost_number is not None:
            holding_dict["book_value"] = holding.book_value
            holding_dict["market_value"] = holding.market_value

            for dur in (1, 2, 7, 30):
                if len(hld_prices) < dur:
                    continue

                base_date = asof_date - datetime.timedelta(days=dur)
                latest_price = hld_prices[-1][1]
                base_pxs = [(dt, px) for dt, px in hld_prices
                            if dt <= base_date]
                if base_pxs:
                    base_price = base_pxs[-1][-1]  # O(N)!
                    holding_dict[f"chg_{dur}"] = latest_price / base_price - 1
                else:
                    holding_dict[f"chg_{dur}"] = 'n/a'

        group_key = (symbol, account_nondisposable)
        holding_groups.setdefault(group_key, []).append(holding_dict)

    rows = []
    cum_networth = 0
    for (symbol, account_nondisposable), holdings in holding_groups.items():
        qty_by_account = {}
        total_book_value = Decimal(0)
        total_market_value = Decimal(0)
        for holding in holdings:
            if holding["account"] not in qty_by_account:
                qty_by_account[holding["account"]] = 0
            qty_by_account[holding["account"]] += holding["quantity"]
            if "book_value" in holding:
                total_book_value += holding["book_value"]
                total_market_value += holding["market_value"]
        if total_book_value == 0:
            pnlr = "n/a"
        else:
            pnlr = "%.4f%%" % (
                (total_market_value / total_book_value - 1) * 100)

        total_qty = sum(qty_by_account.values())
        hld_px = holding["price"]
        if hld_px is None:
            raise ValueError(f"price not found for {holding}")

        networth = holding["cny_rate"] * hld_px * total_qty
        cum_networth += networth
        row = {
            "一级类别": holding["asset_class"],
            "二级类别": holding["asset_subclass"],
            "标的": holding["symbol_name"],
            "代号": symbol,
            "持仓量": "%.3f" % total_qty,
            "市场价格": "%.4f" % holding["price"],
            "报价日期": holding["price_date"],
            "市场价值": int(round(holding["price"] * total_qty)),
            "货币": holding["currency"],
            "人民币价值": "%.2f" % networth,
            "持仓盈亏%": pnlr,
        }
        for optional_col, col_name in [("chg_1", "1日%"), ("chg_2", "2日%"),
                                       ("chg_7", "7日%"), ("chg_30", "30日%")]:
            if optional_col not in holding or holding[optional_col] == 'n/a':
                row[col_name] = "n/a"
            else:
                row[col_name] = "%.2f%%" % (holding[optional_col] * 100)
        rows.append(row)

    rows.sort(key=sort_key)
    for row in rows:
        pct = Decimal(row["人民币价值"]) / cum_networth
        row.update({"占比": "%.2f%%" % (100 * pct)})
    return rows, cum_networth
Esempio n. 15
0
 def test_get_commodity_directives(self):
     entries, _, options_map = loader.load_string(TEST_INPUT)
     commodities = getters.get_commodity_directives(entries)
     self.assertEqual({'HOOL', 'PIPA'}, commodities.keys())
     self.assertTrue(all(isinstance(value, data.Commodity)
                         for value in commodities.values()))
Esempio n. 16
0
 def test_get_values_meta__single(self):
     entries, _, options_map = loader.load_string(TEST_INPUT)
     commodities = getters.get_commodity_directives(entries)
     values = getters.get_values_meta(commodities, 'name', default='BLA')
     self.assertEqual({'PIPA': 'Pied Piper',
                       'HOOL': 'Hooli Corp.'}, values)
Esempio n. 17
0
 def test_get_values_meta__multi(self):
     entries, _, options_map = loader.load_string(TEST_INPUT)
     commodities = getters.get_commodity_directives(entries)
     values = getters.get_values_meta(commodities, 'name', 'ticker')
     self.assertEqual({'HOOL': ('Hooli Corp.', 'NYSE:HOOLI'),
                       'PIPA': ('Pied Piper', None)}, values)
Esempio n. 18
0
def get_price_jobs_up_to_date(entries,
                              date_last=None,
                              inactive=False,
                              undeclared_source=None,
                              update_rate='weekday',
                              compress_days=1):
    """Get a list of trailing prices to fetch from a stream of entries.

    The list of dates runs from the latest available price up to the latest date.

    Args:
      entries: list of Beancount entries
      date_last: The date up to where to find prices to as an exclusive range end.
      inactive: Include currencies with no balance at the given date. The default
        is to only include those currencies which have a non-zero balance.
      undeclared_source: A string, the name of the default source module to use to
        pull prices for commodities without a price source metadata on their
        Commodity directive declaration.
    Returns:
      A list of DatedPrice instances.
    """
    price_map = prices.build_price_map(entries)

    # Find the list of declared currencies, and from it build a mapping for
    # tickers for each (base, quote) pair. This is the only place tickers
    # appear.
    declared_triples = find_currencies_declared(entries, date_last)
    currency_map = {(base, quote): psources
                    for base, quote, psources in declared_triples}

    # Compute the initial list of currencies to consider.
    if undeclared_source:
        # Use the full set of possible currencies.
        cur_at_cost = find_prices.find_currencies_at_cost(entries)
        cur_converted = find_prices.find_currencies_converted(
            entries, date_last)
        cur_priced = find_prices.find_currencies_priced(entries, date_last)
        currencies = cur_at_cost | cur_converted | cur_priced
        log_currency_list("Currency held at cost", cur_at_cost)
        log_currency_list("Currency converted", cur_converted)
        log_currency_list("Currency priced", cur_priced)
        default_source = import_source(undeclared_source)
    else:
        # Use the currencies from the Commodity directives.
        currencies = set(currency_map.keys())
        default_source = None

    log_currency_list("Currencies in primary list", currencies)

    # By default, restrict to only the currencies with non-zero balances
    # up to the given date.
    # Also, find the earliest start date to fetch prices from.
    # Look at both latest prices and start dates.
    lifetimes_map = lifetimes.get_commodity_lifetimes(entries)
    commodity_map = getters.get_commodity_directives(entries)
    price_start_dates = {}
    stale_currencies = set()

    if inactive:
        for base_quote in currencies:
            if lifetimes_map[base_quote]:
                # Use first date from lifetime
                lifetimes_map[base_quote] = [(lifetimes_map[base_quote][0][0],
                                              None)]
            else:
                # Insert never active commodities into lifetimes
                # Start from date of currency directive
                base, _ = base_quote
                commodity_entry = commodity_map.get(base, None)
                lifetimes_map[base_quote] = [(commodity_entry.date, None)]
    else:
        #Compress any lifetimes based on compress_days
        lifetimes_map = lifetimes.compress_lifetimes_days(
            lifetimes_map, compress_days)

    #Trim lifetimes based on latest price dates.
    for base_quote in lifetimes_map:
        intervals = lifetimes_map[base_quote]
        result = prices.get_latest_price(price_map, base_quote)
        if (result is None or result[0] is None):
            lifetimes_map[base_quote] = \
                lifetimes.trim_intervals(intervals,
                                         None,
                                         date_last)
        else:
            latest_price_date = result[0]
            date_first = latest_price_date + datetime.timedelta(days=1)
            if date_first < date_last:
                lifetimes_map[base_quote] = \
                    lifetimes.trim_intervals(intervals,
                                            date_first,
                                            date_last)
            else:
                # We don't need to update if we're already up to date.
                lifetimes_map[base_quote] = []

    # Remove currency pairs we can't fetch any prices for.
    if not default_source:
        keys = list(lifetimes_map.keys())
        for key in keys:
            if not currency_map.get(key, None):
                del lifetimes_map[key]

    # Create price jobs based on fetch rate
    if update_rate == 'daily':
        required_prices = lifetimes.required_daily_prices(lifetimes_map,
                                                          date_last,
                                                          weekdays_only=False)
    elif update_rate == 'weekday':
        required_prices = lifetimes.required_daily_prices(lifetimes_map,
                                                          date_last,
                                                          weekdays_only=True)
    elif update_rate == 'weekly':
        required_prices = lifetimes.required_weekly_prices(
            lifetimes_map, date_last)
    else:
        raise ValueError('Invalid Update Rate')

    jobs = []
    # Build up the list of jobs to fetch prices for.
    for key in required_prices:
        date, base, quote = key
        psources = currency_map.get((base, quote), None)
        if not psources:
            psources = [PriceSource(default_source, base, False)]

        jobs.append(DatedPrice(base, quote, date, psources))

    return sorted(jobs)
Esempio n. 19
0
def export_holdings(entries,
                    options_map,
                    promiscuous,
                    aggregate_by_commodity=False):
    """Compute a list of holdings to export.

    Holdings that are converted to cash equivalents will receive a currency of
    "CASH:<currency>" where <currency> is the converted cash currency.

    Args:
      entries: A list of directives.
      options_map: A dict of options as provided by the parser.
      promiscuous: A boolean, true if we should output a promiscuous memo.
      aggregate_by_commodity: A boolean, true if we should group the holdings by account.
    Returns:
      A pair of
        exported: A list of ExportEntry tuples, one for each exported position.
        converted: A list of ExportEntry tuples, one for each converted position.
          These will contain multiple holdings.
        holdings_ignored: A list of Holding instances that were ignored, either
          because they were explicitly marked to be ignored, or because we could
          not convert them to a money vehicle matching the holding's cost-currency.
    """
    # Get the desired list of holdings.
    holdings_list, price_map = holdings.get_assets_holdings(
        entries, options_map)
    commodities = getters.get_commodity_directives(entries)
    dcontext = options_map['dcontext']

    # Aggregate the holdings, if requested. Google Finance is notoriously
    # finnicky and if you have many holdings this might help.
    if aggregate_by_commodity:
        holdings_list = holdings.aggregate_holdings_by(
            holdings_list, lambda holding: holding.currency)

    # Classify all the holdings for export.
    action_holdings = classify_holdings_for_export(holdings_list, commodities)

    # The lists of exported and converted export entries, and the list of
    # ignored holdings.
    exported = []
    converted = []
    holdings_ignored = []

    # Export the holdings with tickers individually.
    for symbol, holding in action_holdings:
        if symbol in ("CASH", "IGNORE"):
            continue

        if holding.cost_number is None:
            assert holding.cost_currency in (None, holding.currency)
            cost_number = holding.number
            cost_currency = holding.currency
        else:
            cost_number = holding.cost_number
            cost_currency = holding.cost_currency

        exported.append(
            ExportEntry(symbol, cost_currency, holding.number, cost_number,
                        is_mutual_fund(symbol),
                        holding.account if promiscuous else '', [holding]))

    # Convert all the cash entries to their book and market value by currency.
    cash_holdings_map = collections.defaultdict(list)
    for symbol, holding in action_holdings:
        if symbol != "CASH":
            continue

        if holding.cost_currency:
            # Accumulate market and book values.
            cash_holdings_map[holding.cost_currency].append(holding)
        else:
            # We cannot price this... no cost currency.
            holdings_ignored.append(holding)

    # Get the money instruments.
    money_instruments = get_money_instruments(commodities)

    # Convert all the cash values to money instruments, if possible. If not
    # possible, we'll just have to ignore those values.

    # Go through all the holdings to convert, and for each of those which aren't
    # in terms of one of the money instruments, which we can directly add to the
    # exported portfolio, attempt to convert them into currencies to one of
    # those in the money instruments.
    money_values_book = collections.defaultdict(D)
    money_values_market = collections.defaultdict(D)
    money_values_holdings = collections.defaultdict(list)
    for cost_currency, holdings_list in cash_holdings_map.items():
        book_value = sum(holding.book_value for holding in holdings_list)
        market_value = sum(holding.market_value for holding in holdings_list)

        if cost_currency in money_instruments:
            # The holding is already in terms of one of the money instruments.
            money_values_book[cost_currency] += book_value
            money_values_market[cost_currency] += market_value
            money_values_holdings[cost_currency].extend(holdings_list)
        else:
            # The holding is not in terms of one of the money instruments.
            # Find the first available price to convert it into one
            for money_currency in money_instruments:
                base_quote = (cost_currency, money_currency)
                _, rate = prices.get_latest_price(price_map, base_quote)
                if rate is not None:
                    money_values_book[money_currency] += book_value * rate
                    money_values_market[money_currency] += market_value * rate
                    money_values_holdings[money_currency].extend(holdings_list)
                    break
            else:
                # We could not convert into any of the money commodities. Ignore
                # those holdings.
                holdings_ignored.extend(holdings_list)

    for money_currency in money_values_book.keys():
        book_value = money_values_book[money_currency]
        market_value = money_values_market[money_currency]
        holdings_list = money_values_holdings[money_currency]

        symbol = money_instruments[money_currency]

        assert isinstance(book_value, Decimal)
        assert isinstance(market_value, Decimal)
        converted.append(
            ExportEntry(
                symbol, money_currency,
                dcontext.quantize(market_value, money_currency),
                dcontext.quantize(book_value / market_value, money_currency),
                is_mutual_fund(symbol), '', holdings_list))

    # Add all ignored holdings to a final list.
    for symbol, holding in action_holdings:
        if symbol == "IGNORE":
            holdings_ignored.append(holding)

    return exported, converted, holdings_ignored