def test_truncate__normal2(self): truncated_entries = summarize.truncate(self.entries, datetime.date(2014, 3, 14)) self.assertEqualEntries( """ 2014-01-01 open Assets:US:Bank:Checking 2014-01-01 open Equity:Opening-Balances 2014-03-10 * "A" Assets:US:Bank:Checking 1 USD Equity:Opening-Balances -1 USD 2014-03-11 * "B" Assets:US:Bank:Checking 1 USD Equity:Opening-Balances -1 USD 2014-03-12 * "C" Assets:US:Bank:Checking 1 USD Equity:Opening-Balances -1 USD 2014-03-13 * "D1" Assets:US:Bank:Checking 1 USD Equity:Opening-Balances -1 USD 2014-03-13 * "D2" Assets:US:Bank:Checking 1 USD Equity:Opening-Balances -1 USD """, truncated_entries)
def test_truncate__before(self): truncated_entries = summarize.truncate(self.entries, datetime.date(2014, 2, 15)) self.assertEqualEntries( """ 2014-01-01 open Assets:US:Bank:Checking 2014-01-01 open Equity:Opening-Balances """, truncated_entries)
def test_truncate__after(self): truncated_entries = summarize.truncate(self.entries, datetime.date(2014, 3, 15)) self.assertEqual(self.entries, truncated_entries)
def get_commodities_at_date(entries, options_map, date=None): """Return a list of commodities present at a particular date. This routine fetches the holdings present at a particular date and returns a list of the commodities held in those holdings. This should define the list of price date points required to assess the market value of this portfolio. Notes: * The ticker symbol will be fetched from the corresponding Commodity directive. If there is no ticker symbol defined for a directive or no corresponding Commodity directive, the currency is still included, but 'None' is specified for the symbol. The code that uses this routine should be free to use the currency name to make an attempt to fetch the currency using its name, or to ignore it. * The 'cost-currency' is that which is found on the holdings instance and can be ignored. The 'quote-currency' is that which is declared on the Commodity directive from its 'quote' metadata field. This is used in a routine that fetches prices from a data source on the internet (either from LedgerHub, but you can reuse this in your own script if you build one). Args: entries: A list of directives. date: A datetime.date instance, the date at which to get the list of relevant holdings. Returns: A list of (currency, cost-currency, quote-currency, ticker) tuples, where currency: The Beancount base currency to fetch a price for. cost-currency: The cost-currency of the holdings found at the given date. quote-currency: The currency formally declared as quote currency in the metadata of Commodity directives. ticker: The ticker symbol to use for fetching the price (extracted from the metadata of Commodity directives). """ # Remove all the entries after the given date, if requested. if date is not None: entries = summarize.truncate(entries, date) # Get the list of holdings at the particular date. holdings_list = get_final_holdings(entries) # Obtain the unique list of currencies we need to fetch. commodities_list = {(holding.currency, holding.cost_currency) for holding in holdings_list} # Add in the associated ticker symbols. commodities_map = getters.get_commodity_map(entries) commodities_symbols_list = [] for currency, cost_currency in sorted(commodities_list): try: commodity_entry = commodities_map[currency] ticker = commodity_entry.meta.get('ticker', None) quote_currency = commodity_entry.meta.get('quote', None) except KeyError: ticker = None quote_currency = None commodities_symbols_list.append( (currency, cost_currency, quote_currency, ticker)) return commodities_symbols_list
def add_unrealized_gains_at_date(entries, unrealized_entries, income_account_type, price_map, date, meta, subaccount): """Insert/remove entries for unrealized capital gains This function takes a list of entries and a date and creates a set of unrealized gains transactions, negating previous unrealized gains transactions within the same account. Args: entries: A list of data directives. unrealized_entries: A list of previously generated unrealized transactions. income_account_type: The income account type. price_map: A price map returned by prices.build_price_map. date: The effective date to generate the unrealized transactions for. meta: meta. subaccount: A string, and optional the name of a subaccount to create under an account to book the unrealized gain. If this is left to its default value, the gain is booked directly in the same account. Returns: A list of newly created unrealized transactions and a list of errors. """ errors = [] entries_truncated = summarize.truncate(entries, date + ONEDAY) holdings_list = holdings.get_final_holdings(entries_truncated, price_map=price_map, date=date) # Group positions by (account, cost, cost_currency). holdings_list = holdings.aggregate_holdings_by( holdings_list, lambda h: (h.account, h.currency, h.cost_currency)) holdings_with_currencies = set() # Create transactions to account for each position. new_entries = [] for index, holding in enumerate(holdings_list): if (holding.currency == holding.cost_currency or holding.cost_currency is None): continue # Note: since we're only considering positions held at cost, the # transaction that created the position *must* have created at least one # price point for that commodity, so we never expect for a price not to # be available, which is reasonable. if holding.price_number is None: # An entry without a price might indicate that this is a holding # resulting from leaked cost basis. {0ed05c502e63, b/16} if holding.number: errors.append( UnrealizedError( meta, "A valid price for {h.currency}/{h.cost_currency} " "could not be found".format(h=holding), None)) continue # Compute the PnL; if there is no profit or loss, we create a # corresponding entry anyway. pnl = holding.market_value - holding.book_value if holding.number == ZERO: # If the number of units sum to zero, the holdings should have been # zero. errors.append( UnrealizedError( meta, "Number of units of {} in {} in holdings sum to zero " "for account {} and should not".format( holding.currency, holding.cost_currency, holding.account), None)) continue # Compute the name of the accounts and add the requested subaccount name # if requested. asset_account = holding.account income_account = account.join(income_account_type, account.sans_root(holding.account)) if subaccount: asset_account = account.join(asset_account, subaccount) income_account = account.join(income_account, subaccount) holdings_with_currencies.add( (holding.account, holding.cost_currency, holding.currency)) # Find the previous unrealized gain entry to negate and decide if we # should create a new posting. latest_unrealized_entry = find_previous_unrealized_transaction( unrealized_entries, asset_account, holding.cost_currency, holding.currency) # Don't create a new transaction if our last one hasn't changed. if (latest_unrealized_entry and pnl == latest_unrealized_entry.postings[0].units.number): continue # Don't bother creating a blank unrealized transaction if none existed if pnl == ZERO and not latest_unrealized_entry: continue relative_pnl = pnl if latest_unrealized_entry: relative_pnl = pnl - latest_unrealized_entry.postings[ 0].units.number # Create a new transaction to account for this difference in gain. gain_loss_str = "gain" if relative_pnl > ZERO else "loss" narration = ( "Unrealized {} for {h.number} units of {h.currency} " "(price: {h.price_number:.4f} {h.cost_currency} as of {h.price_date}, " "average cost: {h.cost_number:.4f} {h.cost_currency})").format( gain_loss_str, h=holding) entry = data.Transaction( data.new_metadata(meta["filename"], lineno=1000 + index, kvlist={'prev_currency': holding.currency}), date, flags.FLAG_UNREALIZED, None, narration, set(), set(), []) # Book this as income, converting the account name to be the same, but as income. # Note: this is a rather convenient but arbitraty choice--maybe it would be best to # let the user decide to what account to book it, but I don't a nice way to let the # user specify this. # # Note: we never set a price because we don't want these to end up in Conversions. entry.postings.extend([ data.Posting(asset_account, amount.Amount(pnl, holding.cost_currency), None, None, None, None), data.Posting(income_account, amount.Amount(-pnl, holding.cost_currency), None, None, None, None) ]) if latest_unrealized_entry: for posting in latest_unrealized_entry.postings[:2]: entry.postings.append( data.Posting(posting.account, -posting.units, None, None, None, None)) new_entries.append(entry) return new_entries, holdings_with_currencies, errors
def add_unrealized_gains_at_date(entries, unrealized_entries, income_account_type, price_map, date, meta, subaccount): """Insert/remove entries for unrealized capital gains This function takes a list of entries and a date and creates a set of unrealized gains transactions, negating previous unrealized gains transactions within the same account. Args: entries: A list of data directives. unrealized_entries: A list of previously generated unrealized transactions. income_account_type: The income account type. price_map: A price map returned by prices.build_price_map. date: The effective date to generate the unrealized transactions for. meta: meta. subaccount: A string, and optional the name of a subaccount to create under an account to book the unrealized gain. If this is left to its default value, the gain is booked directly in the same account. Returns: A list of newly created unrealized transactions and a list of errors. """ errors = [] entries_truncated = summarize.truncate(entries, date + ONEDAY) holdings_list = holdings.get_final_holdings(entries_truncated, price_map=price_map, date=date) # Group positions by (account, cost, cost_currency). holdings_list = holdings.aggregate_holdings_by( holdings_list, lambda h: (h.account, h.currency, h.cost_currency)) holdings_with_currencies = set() # Create transactions to account for each position. new_entries = [] for index, holding in enumerate(holdings_list): if (holding.currency == holding.cost_currency or holding.cost_currency is None): continue # Note: since we're only considering positions held at cost, the # transaction that created the position *must* have created at least one # price point for that commodity, so we never expect for a price not to # be available, which is reasonable. if holding.price_number is None: # An entry without a price might indicate that this is a holding # resulting from leaked cost basis. {0ed05c502e63, b/16} if holding.number: errors.append( UnrealizedError(meta, "A valid price for {h.currency}/{h.cost_currency} " "could not be found".format(h=holding), None)) continue # Compute the PnL; if there is no profit or loss, we create a # corresponding entry anyway. pnl = holding.market_value - holding.book_value if holding.number == ZERO: # If the number of units sum to zero, the holdings should have been # zero. errors.append( UnrealizedError( meta, "Number of units of {} in {} in holdings sum to zero " "for account {} and should not".format( holding.currency, holding.cost_currency, holding.account), None)) continue # Compute the name of the accounts and add the requested subaccount name # if requested. asset_account = holding.account income_account = account.join(income_account_type, account.sans_root(holding.account)) if subaccount: asset_account = account.join(asset_account, subaccount) income_account = account.join(income_account, subaccount) holdings_with_currencies.add((holding.account, holding.cost_currency, holding.currency)) # Find the previous unrealized gain entry to negate and decide if we # should create a new posting. latest_unrealized_entry = find_previous_unrealized_transaction(unrealized_entries, asset_account, holding.cost_currency, holding.currency) # Don't create a new transaction if our last one hasn't changed. if (latest_unrealized_entry and pnl == latest_unrealized_entry.postings[0].units.number): continue # Don't bother creating a blank unrealized transaction if none existed if pnl == ZERO and not latest_unrealized_entry: continue relative_pnl = pnl if latest_unrealized_entry: relative_pnl = pnl - latest_unrealized_entry.postings[0].units.number # Create a new transaction to account for this difference in gain. gain_loss_str = "gain" if relative_pnl > ZERO else "loss" narration = ("Unrealized {} for {h.number} units of {h.currency} " "(price: {h.price_number:.4f} {h.cost_currency} as of {h.price_date}, " "average cost: {h.cost_number:.4f} {h.cost_currency})").format( gain_loss_str, h=holding) entry = data.Transaction(data.new_metadata(meta["filename"], lineno=1000 + index, kvlist={'prev_currency': holding.currency}), date, flags.FLAG_UNREALIZED, None, narration, set(), set(), []) # Book this as income, converting the account name to be the same, but as income. # Note: this is a rather convenient but arbitraty choice--maybe it would be best to # let the user decide to what account to book it, but I don't a nice way to let the # user specify this. # # Note: we never set a price because we don't want these to end up in Conversions. entry.postings.extend([ data.Posting( asset_account, amount.Amount(pnl, holding.cost_currency), None, None, None, None), data.Posting( income_account, amount.Amount(-pnl, holding.cost_currency), None, None, None, None) ]) if latest_unrealized_entry: for posting in latest_unrealized_entry.postings[:2]: entry.postings.append( data.Posting( posting.account, -posting.units, None, None, None, None)) new_entries.append(entry) return new_entries, holdings_with_currencies, errors