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 _net_worth_in_intervals(self, interval): interval_tuples = self._interval_tuples(interval, self.entries) interval_totals = [] end_dates = [p[1] for p in interval_tuples] for (begin_date, end_date), holdings_list in \ zip(interval_tuples, holdings_at_dates(self.entries, end_dates, self.price_map, self.options)): totals = {} for currency in self.options['operating_currency']: currency_holdings_list = \ holdings.convert_to_currency(self.price_map, currency, holdings_list) if not currency_holdings_list: continue holdings_ = holdings.aggregate_holdings_by( currency_holdings_list, operator.attrgetter('cost_currency')) holdings_ = [holding for holding in holdings_ if holding.currency and holding.cost_currency] # If after conversion there are no valid holdings, skip the # currency altogether. if holdings_: totals[currency] = holdings_[0].market_value interval_totals.append({ 'date': end_date - datetime.timedelta(1), 'totals': totals }) return interval_totals
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 _net_worth_in_periods(self): month_tuples = self._interval_tuples('month', self.entries) monthly_totals = [] end_dates = [p[1] + timedelta(days=1) for p in month_tuples] for (begin_date, end_date), holdings_list in zip(month_tuples, holdings_at_dates(entries=self.entries, dates=end_dates, options_map=self.options, price_map=self.price_map)): totals = dict() for currency in self.options['operating_currency']: total = ZERO for holding in holdings.convert_to_currency(self.price_map, currency, holdings_list): if holding.cost_currency == currency and holding.market_value: total += holding.market_value if total != ZERO: totals[currency] = total monthly_totals.append({ 'begin_date': begin_date, 'end_date': end_date, 'totals': totals }) return monthly_totals
def _net_worth_in_periods(self): month_tuples = self._interval_tuples('month', self.entries) monthly_totals = [] end_dates = [p[1] + timedelta(days=1) for p in month_tuples] for (begin_date, end_date), holdings_list in zip( month_tuples, holdings_at_dates(entries=self.entries, dates=end_dates, options_map=self.options, price_map=self.price_map)): totals = dict() for currency in self.options['operating_currency']: total = ZERO for holding in holdings.convert_to_currency( self.price_map, currency, holdings_list): if holding.cost_currency == currency and holding.market_value: total += holding.market_value if total != ZERO: totals[currency] = total monthly_totals.append({ 'begin_date': begin_date, 'end_date': end_date, 'totals': totals }) return monthly_totals
def load_csv_and_prices(holdings_filename, prices_filename, currency): """Load the holdings and prices from filenames and convert to a common currency. Args: holdings_filename: A string, the name of a CSV file containing the list of Holdings. prices_filename: A string, the name of a Beancount file containing price directives. currency: A string, the target currency to convert all the holdings to. Returns: Two lists of holdings: a list in the original currencies, and a list all converted to the target currency. """ # Load the price database. # Generate with "bean-query LEDGER holdings" price_entries, errors, options_map = loader.load(prices_filename) price_map = prices.build_price_map(price_entries) # Load the holdings list. # Generate with "bean-query LEDGER print_prices" mixed_holdings_list = list( holdings_reports.load_from_csv(open(holdings_filename))) # Convert all the amounts to a common currency (otherwise summing market # values makes no sense). holdings_list = holdings.convert_to_currency(price_map, currency, mixed_holdings_list) return mixed_holdings_list, holdings_list
def get_assets_holdings(entries, options_map, currency=None): """Return holdings for all assets and liabilities. Args: entries: A list of directives. options_map: A dict of parsed options. currency: If specified, a string, the target currency to convert all holding values to. Returns: A list of Holding instances and a price-map. """ # Compute a price map, to perform conversions. price_map = prices.build_price_map(entries) # Get the list of holdings. account_types = options.get_account_types(options_map) holdings_list = holdings.get_final_holdings( entries, (account_types.assets, account_types.liabilities), price_map) # Convert holdings to a unified currency. if currency: holdings_list = holdings.convert_to_currency(price_map, currency, holdings_list) return holdings_list, price_map
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 _holdings_to_net_worth(self, holdings_list): totals = {} for currency in self.options['operating_currency']: currency_holdings_list = \ holdings.convert_to_currency(self.price_map, currency, holdings_list) holdings_ = holdings.aggregate_holdings_by( currency_holdings_list, operator.attrgetter('cost_currency')) holdings_ = [holding for holding in holdings_ if holding.currency and holding.cost_currency] if holdings_: totals[currency] = holdings_[0].market_value else: totals[currency] = 0 return totals
def _holdings_to_net_worth(self, holdings_list): totals = {} for currency in self.options['operating_currency']: currency_holdings_list = \ holdings.convert_to_currency(self.price_map, currency, holdings_list) if not currency_holdings_list: continue holdings_ = holdings.aggregate_holdings_by( currency_holdings_list, operator.attrgetter('cost_currency')) holdings_ = [holding for holding in holdings_ if holding.currency and holding.cost_currency] # If after conversion there are no valid holdings, skip the # currency altogether. if holdings_: totals[currency] = holdings_[0].market_value return totals
def generate_table(self, entries, errors, options_map): holdings_list, price_map = 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 holdings_list = holdings.aggregate_holdings_by( currency_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.append((currency, holdings_list[0].market_value)) field_spec = [ (0, 'Currency'), (1, 'Net Worth', '{:,.2f}'.format), ] return table.create_table(net_worths, field_spec)
def _net_worth_in_periods(self): month_tuples = self._interval_tuples('month', self.entries) monthly_totals = [] end_dates = [p[1] for p in month_tuples] for (begin_date, end_date), holdings_list in \ zip(month_tuples, holdings_at_dates(self.entries, end_dates, self.price_map, self.options)): totals = {} for currency in self.options['operating_currency']: currency_holdings_list = \ holdings.convert_to_currency(self.price_map, currency, holdings_list) if not currency_holdings_list: continue holdings_list = holdings.aggregate_holdings_by( currency_holdings_list, operator.attrgetter('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 holdings_list: totals[currency] = holdings_list[0].market_value monthly_totals.append({ 'begin_date': begin_date, 'end_date': end_date, 'totals': totals }) return monthly_totals
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('-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() logging.info(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_reports.get_assets_holdings( current_entries, options_map) # Convert the currencies. for currency in 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)) # Extrapolate milestones in various currencies. days_interp = 365 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)) 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) pyplot.show()
def test_convert_to_currency(self, entries, _, __): """ 2013-01-01 price CAD 1.1 USD ; We don't include a price point for NOK. It's unknown. ; 2013-01-01 open Assets:Account2 """ test_holdings = list(itertools.starmap(holdings.Holding, [ # ------------ cost currency == target currency # currency != target currency (None, D('100.00'), 'IVV', D('200'), 'USD', D('10'), D('11'), D('12'), None), # currency == target currency (None, D('100.00'), 'USD', D('200'), 'USD', D('10'), D('11'), D('12'), None), # currency == None (None, D('100.00'), None, D('200'), 'USD', D('10'), D('11'), D('12'), None), # ------------ cost currency == other currency # cost currency available in price map (None, D('100.00'), 'XSP', D('200'), 'CAD', D('10'), D('11'), D('12'), None), # cost currency not available in price map (None, D('100.00'), 'AGF', D('200'), 'NOK', D('10'), D('11'), D('12'), None), # currency == target currency, available in price map (None, D('100.00'), 'USD', D('200'), 'CAD', D('10'), D('11'), D('12'), None), # currency == target currency, not available in price map (None, D('100.00'), 'USD', D('200'), 'NOK', D('10'), D('11'), D('12'), None), # cost currency available in price map, and currency == None (None, D('100.00'), None, D('200'), 'CAD', D('10'), D('11'), D('12'), None), # ------------ cost currency == None # currency available in price map (None, D('100.00'), 'CAD', D('1.2'), None, D('10'), D('11'), D('12'), None), # currency available in price map, with no values (should be filled in) (None, D('100.00'), 'CAD', D('1.2'), None, None, None, None, None), # currency not available in price map (None, D('100.00'), 'EUR', D('1.2'), None, D('10'), D('11'), D('12'), None), # currency = target currency (None, D('100.00'), 'USD', D('1.2'), None, D('10'), D('11'), D('12'), None), ])) price_map = prices.build_price_map(entries) converted_holdings = holdings.convert_to_currency(price_map, 'USD', test_holdings) expected_holdings = list(itertools.starmap(holdings.Holding, [ (None, D('100.00'), 'IVV', D('200'), 'USD', D('10'), D('11'), D('12'), None), (None, D('100.00'), 'USD', D('200'), 'USD', D('10'), D('11'), D('12'), None), (None, D('100.00'), None, D('200'), 'USD', D('10'), D('11'), D('12'), None), (None, D('100.00'), 'XSP', D('220.0'), 'USD', D('11.0'), D('12.1'), D('13.2'), None), (None, D('100.00'), 'AGF', None, None, None, None, None, None), (None, D('100.00'), 'USD', D('220.0'), 'USD', D('11.0'), D('12.1'), D('13.2'), None), (None, D('100.00'), 'USD', None, None, None, None, None, None), (None, D('100.00'), None, D('220.0'), 'USD', D('11.0'), D('12.1'), D('13.2'), None), (None, D('100.00'), 'CAD', D('1.32'), 'USD', D('11.0'), D('12.1'), D('13.2'), None), (None, D('100.00'), 'CAD', D('1.32'), 'USD', D('110.0'), D('110.0'), None, None), (None, D('100.00'), 'EUR', None, None, None, None, None, None), (None, D('100.00'), 'USD', D('1.2'), 'USD', D('10'), D('11'), D('12'), None), ])) self.assertEqual(expected_holdings, converted_holdings) # Fail elegantly if the currency itself is None. none_holding = holdings.Holding(None, D('100.00'), None, D('200'), None, None, None, None, None) with self.assertRaises(ValueError): converted_holdings = holdings.convert_to_currency(price_map, 'USD', [none_holding])