示例#1
0
def calculate_net_worths(entries, options_map):
    holdings_list, price_map = holdings.get_assets_holdings(
        entries, options_map)
    net_worths = []
    for currency in options_map['operating_currency']:

        # Convert holdings to a unified currency.
        #
        # Note: It's entirely possible that the price map does not have all
        # the necessary rate conversions here. The resulting holdings will
        # simply have no cost when that is the case. We must handle this
        # gracefully below.
        currency_holdings_list = holdings.convert_to_currency(
            price_map, currency, holdings_list)
        if not currency_holdings_list:
            continue

        aggregated_holdings_list = holdings.aggregate_holdings_by(
            currency_holdings_list, lambda holding: holding.cost_currency)

        aggregated_holdings_list = [
            holding for holding in aggregated_holdings_list
            if holding.currency and holding.cost_currency
        ]

        # If after conversion there are no valid holdings, skip the currency
        # altogether.
        if not aggregated_holdings_list:
            continue

        net_worths.append((currency, aggregated_holdings_list[0].market_value))
    return net_worths
示例#2
0
def report_holdings(currency,
                    relative,
                    entries,
                    options_map,
                    aggregation_key=None,
                    sort_key=None):
    """Generate a detailed list of all holdings.

    Args:
      currency: A string, a currency to convert to. If left to None, no
        conversion is carried out.
      relative: A boolean, true if we should reduce this to a relative value.
      entries: A list of directives.
      options_map: A dict of parsed options.
      aggregation_key: A callable use to generate aggregations.
      sort_key: A function to use to sort the holdings, if specified.
    Returns:
      A Table instance.
    """
    holdings_list, _ = holdings.get_assets_holdings(entries, options_map,
                                                    currency)
    if aggregation_key:
        holdings_list = holdings.aggregate_holdings_by(holdings_list,
                                                       aggregation_key)

    if relative:
        holdings_list = holdings.reduce_relative(holdings_list)
        field_spec = RELATIVE_FIELD_SPEC
    else:
        field_spec = FIELD_SPEC

    if sort_key:
        holdings_list.sort(key=sort_key, reverse=True)

    return table.create_table(holdings_list, field_spec)
示例#3
0
    def generate_table(self, entries, errors, options_map):
        holdings_list, price_map = holdings.get_assets_holdings(
            entries, options_map)
        holdings_list_orig = holdings_list

        # Keep only the holdings where currency is the same as the cost-currency.
        holdings_list = [
            holding for holding in holdings_list
            if (holding.currency == holding.cost_currency
                or holding.cost_currency is None)
        ]

        # Keep only those holdings held in one of the operating currencies.
        if self.args.operating_only:
            operating_currencies = set(options_map['operating_currency'])
            holdings_list = [
                holding for holding in holdings_list
                if holding.currency in operating_currencies
            ]

        # Compute the list of ignored holdings and optionally report on them.
        if self.args.ignored:
            ignored_holdings = set(holdings_list_orig) - set(holdings_list)
            holdings_list = ignored_holdings

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

        return table.create_table(holdings_list, FIELD_SPEC)
示例#4
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)
示例#5
0
def get_holdings_entries(entries, options_map):
    """Summarizes the entries to list of entries representing the final holdings..

    This list includes the latest prices entries as well. This can be used to
    load a full snapshot of holdings without including the entire history. This
    is a way of summarizing a balance sheet in a way that filters away history.

    Args:
      entries: A list of directives.
      options_map: A dict of parsed options.
    Returns:
      A string, the entries to print out.
    """

    # The entries will be created at the latest date, against an equity account.
    latest_date = entries[-1].date
    _, equity_account, _ = options.get_previous_accounts(options_map)

    # Get all the assets.
    holdings_list, _ = holdings.get_assets_holdings(entries, options_map)

    # Create synthetic entries for them.
    holdings_entries = []

    for index, holding in enumerate(holdings_list):
        meta = data.new_metadata('report_holdings_print', index)
        entry = data.Transaction(meta, latest_date, flags.FLAG_SUMMARIZE, None,
                                 "", None, None, [])

        # Convert the holding to a position.
        pos = holdings.holding_to_position(holding)
        entry.postings.append(
            data.Posting(holding.account, pos.units, pos.cost, None, None,
                         None))

        cost = -convert.get_cost(pos)
        entry.postings.append(
            data.Posting(equity_account, cost, None, None, None, None))

        holdings_entries.append(entry)

    # Get opening directives for all the accounts.
    used_accounts = {holding.account for holding in holdings_list}
    open_entries = summarize.get_open_entries(entries, latest_date)
    used_open_entries = [
        open_entry for open_entry in open_entries
        if open_entry.account in used_accounts
    ]

    # Add an entry for the equity account we're using.
    meta = data.new_metadata('report_holdings_print', -1)
    used_open_entries.insert(
        0, data.Open(meta, latest_date, equity_account, None, None))

    # Get the latest price entries.
    price_entries = prices.get_last_price_entries(entries, None)

    return used_open_entries + holdings_entries + price_entries
def get_networth(entries, options_map, args):
    net_worths = []
    index = 0
    current_entries = []

    dtend = datetime.date.today()
    period = rrule.rrule(rrule.MONTHLY, bymonthday=1, dtstart=args.min_date, until=dtend)

    for dtime in period:
        date = dtime.date()

        # Append new entries until the given date.
        while True:
            entry = entries[index]
            if entry.date >= date:
                break
            current_entries.append(entry)
            index += 1

        # Get the list of holdings.
        raw_holdings_list, price_map = holdings.get_assets_holdings(current_entries,
                                                                            options_map)

        # Remove any accounts we don't in our final total
        filtered_holdings_list = [n for n in raw_holdings_list if n.account not in args.ignore_account]

        # Convert the currencies.
        holdings_list = holdings.convert_to_currency(price_map,
                                                        args.currency,
                                                        filtered_holdings_list)

        holdings_list = holdings.aggregate_holdings_by(
            holdings_list, lambda holding: holding.cost_currency)

        holdings_list = [holding
                            for holding in holdings_list
                            if holding.currency and holding.cost_currency]

        # If after conversion there are no valid holdings, skip the currency
        # altogether.
        if not holdings_list:
            continue

        # TODO: How can something have a book_value but not a market_value?
        value = holdings_list[0].market_value or holdings_list[0].book_value
        net_worths.append((date, float(value)))
        logging.debug("{}: {:,.2f}".format(date, value))

    return pandas.Series(dict(net_worths))
示例#7
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
def compute_networth_series(since_date, end_date=None):
    if end_date is None:
        end_date = datetime.date.today()
    (entries, errors,
     options_map) = beancount.loader.load_file(get_ledger_file())
    account_map, commodity_map = get_maps(entries)

    target_currency = 'CNY'
    curr_date = since_date

    result = []
    prev_networth = None
    prev_disposable_networth = None
    cum_invest_nav = decimal.Decimal("1.0")
    cum_invest_nav_ytd = decimal.Decimal("1.0")
    cum_invest_pnl = decimal.Decimal(0)
    cum_invest_pnl_ytd = decimal.Decimal(0)

    while curr_date <= end_date:
        entries_to_date = [
            entry for entry in entries if entry.date <= curr_date
        ]
        holdings_list, price_map_to_date = get_assets_holdings(
            entries_to_date, options_map, target_currency)
        raw_networth_in_cny = decimal.Decimal(0)  # 包含sunk资产的理论净资产
        networth_in_cny = decimal.Decimal(0)  # 不包含sunk资产的净资产
        disposable_networth_in_cny = decimal.Decimal(0)
        dnw_by_asset_class = {}

        for hld in holdings_list:
            if hld.currency == "DAY":
                continue

            acc = account_map[hld.account]
            cmdt = commodity_map[hld.currency]
            if hld.market_value is None:
                raise ValueError(hld)

            raw_networth_in_cny += hld.market_value
            # 预付但大部分情况下不能兑现的沉没资产,比如预付的未来房租
            is_sunk = bool(int(acc.meta.get("sunk", 0)))
            if is_sunk:
                continue

            # 不可支配,比如房租押金
            nondisposable = bool(int(acc.meta.get("nondisposable", 0)))
            if not nondisposable:
                disposable_networth_in_cny += hld.market_value
                asset_class = cmdt.meta["asset-class"]
                if asset_class not in dnw_by_asset_class:
                    dnw_by_asset_class[asset_class] = decimal.Decimal(0)
                dnw_by_asset_class[asset_class] += hld.market_value

            networth_in_cny += hld.market_value

        txs_of_date = [
            entry for entry in entries if entry.date == curr_date
            and isinstance(entry, beancount.core.data.Transaction)
        ]

        non_trade_expenses = decimal.Decimal(0)
        non_trade_incomes = decimal.Decimal(0)
        for tx in txs_of_date:
            is_time_tx = any(
                (posting.units.currency == "DAY" for posting in tx.postings))
            if is_time_tx:
                continue

            for posting in tx.postings:
                acc = posting.account
                is_non_trade_exp = (
                    acc.startswith(EXPENSES_PREFIX)
                    and not acc.startswith(EXPENSES_TRADE_PREFIX)) or (
                        acc.startswith(EXPENSES_PREPAYMENTS_PREFIX))

                is_non_trade_inc = acc.startswith(
                    "Income:") and not acc.startswith("Income:Trade:")
                if is_non_trade_exp or is_non_trade_inc:
                    if posting.units.currency != target_currency:
                        base_quote = (posting.units.currency, target_currency)
                        _, rate = prices.get_latest_price(
                            price_map_to_date, base_quote)
                    else:
                        rate = decimal.Decimal(1)

                    if is_non_trade_exp:
                        non_trade_expenses += (posting.units.number * rate)
                    else:
                        non_trade_incomes -= (posting.units.number * rate)

        if prev_networth:
            pnl = networth_in_cny - non_trade_incomes + non_trade_expenses - prev_networth
            pnl_str = ("%.2f" % pnl) if not isinstance(pnl, str) else pnl
            pnl_rate_str = "%.4f%%" % (100 * pnl / prev_disposable_networth)
            cum_invest_nav *= (1 + pnl / prev_disposable_networth)
            cum_invest_nav_ytd *= (1 + pnl / prev_disposable_networth)
            cum_invest_pnl += pnl
            cum_invest_pnl_ytd += pnl
        else:
            pnl = None
            pnl_str = 'n/a'
            pnl_rate_str = 'n/a'

        daily_status = {
            "日期": curr_date,
            # 理论净资产=总资产 - 负债
            "理论净资产": "%.2f" % raw_networth_in_cny,
            # 净资产=总资产' - 负债(信用卡)- 沉没资产
            "净资产": "%.2f" % networth_in_cny,
            "沉没资产": "%.2f" % (raw_networth_in_cny - networth_in_cny),
            # 可投资金额=净资产 - 不可支配资产(公积金、预付房租、宽带)
            "可投资净资产": "%.2f" % disposable_networth_in_cny,
            # Income:Trade(已了结盈亏、分红) 以外的 Income (包含公积金收入、储蓄利息)
            "非投资收入": "%.2f" % non_trade_incomes,
            # Expenses:Trade 以外的 Expenses (包含社保等支出)
            "非投资支出": "%.2f" % non_trade_expenses,
            "投资盈亏": pnl_str,
            # 投资盈亏% = 当日投资盈亏/昨日可投资金额
            "投资盈亏%": pnl_rate_str,
            "累计净值": "%.4f" % cum_invest_nav,
            "累计盈亏": "%.2f" % cum_invest_pnl,
            "当年净值": "%.4f" % cum_invest_nav_ytd,
            "当年盈亏": "%.2f" % cum_invest_pnl_ytd,
        }

        if curr_date.weekday() >= 5:
            if pnl is not None:
                assert pnl <= 0.01, daily_status  # 预期周末不应该有投资盈亏

        assert abs(
            sum(dnw_by_asset_class.values()) - disposable_networth_in_cny) < 1

        assert set(dnw_by_asset_class.keys()
                   ) <= KNOWN_ASSET_CLASSES, dnw_by_asset_class
        for asset_class in KNOWN_ASSET_CLASSES:
            propotion = 100 * dnw_by_asset_class.get(
                asset_class, 0) / disposable_networth_in_cny
            daily_status[f"{asset_class}%"] = f"{propotion:.2f}%"

        result.append(daily_status)

        next_date = curr_date + datetime.timedelta(days=1)
        if curr_date.year != next_date.year:
            cum_invest_nav_ytd = decimal.Decimal("1.0")
            cum_invest_pnl_ytd = decimal.Decimal(0)
        curr_date = next_date
        prev_networth = networth_in_cny
        prev_disposable_networth = disposable_networth_in_cny
    return result
示例#9
0
 def test_get_assets_holdings(self):
     holdings_list, price_map = holdings.get_assets_holdings(self.entries,
                                                             self.options_map)
     self.assertTrue(isinstance(holdings_list, list))
     self.assertTrue(isinstance(price_map, dict))
def main():
    import argparse, logging
    logging.basicConfig(level=logging.INFO,
                        format='%(levelname)-8s: %(message)s')
    parser = argparse.ArgumentParser(description=__doc__.strip())

    parser.add_argument('--min-date',
                        action='store',
                        type=lambda string: parse(string).date(),
                        help="Minimum date")

    parser.add_argument('--days-interp',
                        action='store',
                        type=int,
                        default=365,
                        help="Number of days to interpolate the future")

    parser.add_argument('-o',
                        '--output',
                        action='store',
                        help="Save the figure to the given file")

    parser.add_argument('--hide',
                        action='store_true',
                        help="Mask out the vertical axis")

    parser.add_argument('--period',
                        choices=['weekly', 'monthly', 'daily'],
                        default='weekly',
                        help="Period of aggregation")

    parser.add_argument('filename', help='Beancount input filename')
    args = parser.parse_args()

    entries, errors, options_map = loader.load_file(args.filename)

    if args.min_date:
        dtstart = args.min_date
    else:
        for entry in entries:
            if isinstance(entry, data.Transaction):
                dtstart = entry.date
                break

    net_worths_dict = collections.defaultdict(list)
    index = 0
    current_entries = []

    dtend = datetime.date.today()
    if args.period == 'weekly':
        period = rrule.rrule(rrule.WEEKLY,
                             byweekday=rrule.FR,
                             dtstart=dtstart,
                             until=dtend)
    elif args.period == 'monthly':
        period = rrule.rrule(rrule.MONTHLY,
                             bymonthday=1,
                             dtstart=dtstart,
                             until=dtend)
    elif args.period == 'daily':
        period = rrule.rrule(rrule.DAILY, dtstart=dtstart, until=dtend)

    for dtime in period:
        date = dtime.date()

        # Append new entries until the given date.
        while True:
            entry = entries[index]
            if entry.date >= date:
                break
            current_entries.append(entry)
            index += 1

        # Get the list of holdings.
        raw_holdings_list, price_map = holdings.get_assets_holdings(
            current_entries, options_map)

        # Convert the currencies.
        for i, currency in enumerate(options_map['operating_currency']):
            holdings_list = holdings.convert_to_currency(
                price_map, currency, raw_holdings_list)

            holdings_list = holdings.aggregate_holdings_by(
                holdings_list, lambda holding: holding.cost_currency)

            holdings_list = [
                holding for holding in holdings_list
                if holding.currency and holding.cost_currency
            ]

            # If after conversion there are no valid holdings, skip the currency
            # altogether.
            if not holdings_list:
                continue

            net_worths_dict[currency].append(
                (date, holdings_list[0].market_value))
            if i == 0:
                logging.info("{}: {:,.2f}".format(
                    date, holdings_list[0].market_value))

    # Extrapolate milestones in various currencies.
    days_interp = args.days_interp
    if args.period == 'weekly':
        num_points = int(days_interp / 7)
    elif args.period == 'monthly':
        num_points = int(days_interp / 30)
    elif args.period == 'daily':
        num_points = 365

    lines = []
    today = datetime.date.today()
    for currency, currency_data in net_worths_dict.items():
        recent_data = currency_data[-num_points:]
        dates = [time.mktime(date.timetuple()) for date, _ in recent_data]
        values = [float(value) for _, value in recent_data]
        poly = numpy.poly1d(numpy.polyfit(dates, values, 1))

        logging.info("Extrapolations based on the last %s data points for %s:",
                     num_points, currency)
        for amount in EXTRAPOLATE_WORTHS:
            try:
                date_reach = date.fromtimestamp(
                    (amount - poly.c[1]) / poly.c[0])
                if date_reach < today:
                    continue
                time_until = (date_reach - today).days / 365.
                logging.info("%10d %s: %s (%.1f years)", amount, currency,
                             date_reach, time_until)
            except OverflowError:
                pass
        logging.info("Time to save 1M %s: %.1f years", currency,
                     (1000000 / poly.c[0]) / (365 * 24 * 60 * 60))

        last_date = dates[-1]
        one_month_ago = last_date - (30 * 24 * 60 * 60)
        one_week_ago = last_date - (7 * 24 * 60 * 60)
        logging.info(
            "Increase in net-worth per month: %.2f %s  ; per week: %.2f %s",
            poly(last_date) - poly(one_month_ago), currency,
            poly(last_date) - poly(one_week_ago), currency)

        dates = [today - datetime.timedelta(days=days_interp), today]
        amounts = [
            time.mktime(date.timetuple()) * poly.c[0] + poly.c[1]
            for date in dates
        ]
        lines.append((dates, amounts))

    # Plot each operating currency as a separate curve.
    for currency, currency_data in net_worths_dict.items():
        dates = [date for date, _ in currency_data]
        values = [float(value) for _, value in currency_data]
        pyplot.plot(dates, values, '-', label=currency)
    pyplot.tight_layout()
    pyplot.title("Net Worth")
    pyplot.legend(loc=2)
    if args.hide:
        pyplot.yticks([])

    for dates, amounts in lines:
        pyplot.plot(dates, amounts, 'k--')

    # Output the plot.
    if args.output:
        pyplot.savefig(args.output, figsize=(11, 8), dpi=600)
    logging.info("Showing graph")
    pyplot.show()
示例#11
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