Пример #1
0
    def test_aggregate_holdings_by__commodity(self):
        # Note: Two different prices on HOOL on purpose.
        test_holdings = list(itertools.starmap(holdings.Holding, [
            ('Assets:Cash', D('101.11'), 'USD', None, None,
             None, None, None, None),

            ('Assets:Account1', D('10'), 'HOOL', D('518.73'), 'USD',
             D('5187.30'), D('5780.20'), D('578.02'), datetime.date(2014, 2, 1)),
            ('Assets:Account2', D('20'), 'HOOL', D('519.24'), 'USD',
             D('10384.80'), D('11622.00'), D('581.10'), datetime.date(2014, 2, 15)),

            ('Assets:Account1', D('10'), 'AAPL', D('593.27'), 'USD',
             D('5932.70'), D('6000.10'), D('600.01'), datetime.date(2014, 3, 1)),
        ]))
        expected_holdings = sorted(itertools.starmap(holdings.Holding, [
            ('Assets:Account1', D('10'), 'AAPL', D('593.27'), 'USD',
             D('5932.70'), D('6000.10'), D('600.01'), datetime.date(2014, 3, 1)),

            ('Assets', D('30'), 'HOOL', D('519.07'), 'USD',
             D('15572.10'), D('17402.20'), D('580.0733333333333333333333333'), None),

            ('Assets:Cash', D('101.11'), 'USD', None, None,
             None, None, None, None),
        ]))
        self.assertEqual(expected_holdings,
                         holdings.aggregate_holdings_by(test_holdings,
                                                        lambda holding: holding.currency))
Пример #2
0
    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
Пример #3
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
Пример #4
0
    def test_aggregate_holdings_by__account(self):
        test_holdings = list(itertools.starmap(holdings.Holding, [
            ('Assets:Cash', D('101.11'), 'USD', None, None,
             None, None, None, None),

            ('Assets:Account1', D('10'), 'HOOL', D('518.73'), 'USD',
             D('5187.30'), D('5780.20'), D('578.02'), datetime.date(2014, 2, 1)),
            ('Assets:Account2', D('20'), 'HOOL', D('519.24'), 'USD',
             D('10384.80'), D('11622.00'), D('581.10'), datetime.date(2014, 2, 15)),

            ('Assets:Account1', D('10'), 'AAPL', D('593.27'), 'USD',
             D('5932.70'), D('6000.10'), D('600.01'), datetime.date(2014, 3, 1)),
        ]))
        expected_holdings = sorted(itertools.starmap(holdings.Holding, [
            ('Assets:Account1', D('0'), '*', D('556.00'), 'USD',
             D('11120.00'), D('11780.30'), D('589.015'), None),

            ('Assets:Account2', D('20'), 'HOOL', D('519.24'), 'USD',
             D('10384.80'), D('11622.00'), D('581.10'), datetime.date(2014, 2, 15)),

            ('Assets:Cash', D('101.11'), 'USD', None, None,
             None, None, None, None),
        ]))
        self.assertEqual(expected_holdings,
                         holdings.aggregate_holdings_by(test_holdings,
                                                        lambda holding: holding.account))
Пример #5
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)
Пример #6
0
    def holdings(self, aggregation_key=None):
        holdings_list = holdings.get_final_holdings(
            self.entries,
            (self.account_types.assets, self.account_types.liabilities),
            self.price_map)

        if aggregation_key:
            holdings_list = holdings.aggregate_holdings_by(
                holdings_list, operator.attrgetter(aggregation_key))
        return holdings_list
Пример #7
0
    def holdings(self, aggregation_key=None):
        holdings_list = holdings.get_final_holdings(
            self.entries,
            (self.account_types.assets, self.account_types.liabilities),
            self.price_map)

        if aggregation_key:
            holdings_list = holdings.aggregate_holdings_by(
                holdings_list, operator.attrgetter(aggregation_key))
        return holdings_list
Пример #8
0
 def test_aggregate_holdings__same_price_same_date(self):
     test_holdings = list(itertools.starmap(holdings.Holding, [
         ('Assets:Account1', D('10'), 'HOOL', D('500'), 'USD', D('5000'), D('6000'),
          D('600'), datetime.date(2014, 2, 1)),
         ('Assets:Account1', D('20'), 'HOOL', D('530'), 'USD', D('10600'), D('12000'),
          D('600'), datetime.date(2014, 2, 1)),
     ]))
     expected_holdings = sorted(itertools.starmap(holdings.Holding, [
         ('Assets:Account1', D('30'), 'HOOL', D('520'), 'USD', D('15600'), D('18000'),
          D('600'), datetime.date(2014, 2, 1)),
     ]))
     self.assertEqual(expected_holdings,
                      holdings.aggregate_holdings_by(test_holdings,
                                                     lambda holding: holding.account))
Пример #9
0
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))
Пример #10
0
 def test_aggregate_holdings__diff_price_diff_date(self):
     test_holdings = list(itertools.starmap(holdings.Holding, [
         ('Assets:Account1', D('10'), 'HOOL', D('500'), 'USD', D('5000'), D('6000'),
          D('610'), datetime.date(2014, 2, 1)),
         ('Assets:Account1', D('20'), 'HOOL', D('530'), 'USD', D('10600'), D('12000'),
          D('600'), datetime.date(2014, 2, 2)),
     ]))
     # Price is recalculated from the market value, date is maintained.
     expected_holdings = sorted(itertools.starmap(holdings.Holding, [
         ('Assets:Account1', D('30'), 'HOOL', D('520'), 'USD', D('15600'), D('18000'),
          D('600'), None),
     ]))
     self.assertEqual(expected_holdings,
                      holdings.aggregate_holdings_by(test_holdings,
                                                     lambda holding: holding.account))
Пример #11
0
    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
Пример #12
0
    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
Пример #13
0
    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)
Пример #14
0
    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
Пример #15
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 promiscuious 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_reports.get_assets_holdings(
        entries, options_map)
    commodities_map = getters.get_commodity_map(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_map)

    # 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_map)

    # 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
Пример #16
0
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()
Пример #17
0
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
Пример #18
0
def add_unrealized_gains(entries, options_map, subaccount=None):
    """Insert entries for unrealized capital gains.

    This function inserts entries that represent unrealized gains, at the end of
    the available history. It returns a new list of entries, with the new gains
    inserted. It replaces the account type with an entry in an income account.
    Optionally, it can book the gain in a subaccount of the original and income
    accounts.

    Args:
      entries: A list of data directives.
      options_map: A dict of options, that confirms to beancount.parser.options.
      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 entries, which includes the new unrealized capital gains entries
      at the end, and a list of errors. The new list of entries is still sorted.
    """
    errors = []
    meta = data.new_metadata('<unrealized_gains>', 0)

    account_types = options.get_account_types(options_map)

    # Assert the subaccount name is in valid format.
    if subaccount:
        validation_account = account.join(account_types.assets, subaccount)
        if not account.is_valid(validation_account):
            errors.append(
                UnrealizedError(meta,
                                "Invalid subaccount name: '{}'".format(subaccount),
                                None))
            return entries, errors

    if not entries:
        return (entries, errors)

    # Group positions by (account, cost, cost_currency).
    price_map = prices.build_price_map(entries)
    holdings_list = holdings.get_final_holdings(entries, price_map=price_map)

    # Group positions by (account, cost, cost_currency).
    holdings_list = holdings.aggregate_holdings_by(
        holdings_list, lambda h: (h.account, h.currency, h.cost_currency))

    # Get the latest prices from the entries.
    price_map = prices.build_price_map(entries)

    # Create transactions to account for each position.
    new_entries = []
    latest_date = entries[-1].date
    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(account_types.income,
                                      account.sans_root(holding.account))
        if subaccount:
            asset_account = account.join(asset_account, subaccount)
            income_account = account.join(income_account, subaccount)

        # Create a new transaction to account for this difference in gain.
        gain_loss_str = "gain" if 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),
                                 latest_date, flags.FLAG_UNREALIZED,
                                 None, narration, EMPTY_SET, EMPTY_SET, [])

        # Book this as income, converting the account name to be the same, but as income.
        # Note: this is a rather convenient but arbitrary 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)
        ])

        new_entries.append(entry)

    # Ensure that the accounts we're going to use to book the postings exist, by
    # creating open entries for those that we generated that weren't already
    # existing accounts.
    new_accounts = {posting.account
                    for entry in new_entries
                    for posting in entry.postings}
    open_entries = getters.get_account_open_close(entries)
    new_open_entries = []
    for account_ in sorted(new_accounts):
        if account_ not in open_entries:
            meta = data.new_metadata(meta["filename"], index)
            open_entry = data.Open(meta, latest_date, account_, None, None)
            new_open_entries.append(open_entry)

    return (entries + new_open_entries + new_entries, 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