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
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)
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 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)
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
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))) ]
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
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))
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))))
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")])
def get_commodity_directives(self): return getters.get_commodity_directives(self.entries)
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
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 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()))
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)
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)
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)
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