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
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)
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)
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)
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))
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
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()
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