Ejemplo n.º 1
0
 def render_htmldiv(self, entries, errors, options_map, file):
     table_ = self.generate_table(entries, errors, options_map)
     table.render_table(table_,
                        file,
                        'htmldiv',
                        css_id=self.css_id,
                        css_class=self.css_class)
Ejemplo n.º 2
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('filename', help='Beancount input filename to process')

    parser.add_argument('symbols',
                        nargs='+',
                        help='Substantially identical stock symbols.')

    parser.add_argument('-y',
                        '--year',
                        action='store',
                        type=int,
                        default=datetime.date.today().year - 1,
                        help="Calendar year to analyze")

    args = parser.parse_args()

    entries, errors, options_map = loader.load_file(args.filename)

    trades = get_trades(entries, options_map, set(args.symbols),
                        datetime.date(args.year + 1, 1, 1))

    field_spec = list(
        enumerate(
            'type acq_date adj_acq_date sell_date number currency cost cost_basis price proceeds fee pnl'
            .split()))
    table_ = table.create_table(trades, field_spec)
    table.render_table(table_, sys.stdout, 'text')

    table.render_table(table_, open('/tmp/washsales.csv', 'w'), 'csv')
Ejemplo n.º 3
0
    def render_real_text(self, real_root, price_map, price_date, options_map,
                         file):
        rows = []
        account_types = options.get_account_types(options_map)
        for root in (account_types.assets, account_types.liabilities):
            for unused_first_line, unused_cont_line, real_account in realization.dump(
                    realization.get(real_root, root)):

                last_posting = realization.find_last_active_posting(
                    real_account.txn_postings)

                # Don't render updates to accounts that have been closed.
                # Note: this is O(N), maybe we can store this at realization.
                if last_posting is None or isinstance(last_posting,
                                                      data.Close):
                    continue

                last_date = data.get_entry(last_posting).date
                # Skip this posting if a cutoff date has been specified and the
                # account has been updated to at least the cutoff date.
                if self.args.cutoff and self.args.cutoff <= last_date:
                    continue

                rows.append((real_account.account, last_date))

        table_ = table.create_table(rows, [(0, 'Account'),
                                           (1, 'Last Date', '{}'.format)])
        table.render_table(table_, file, 'text')
Ejemplo n.º 4
0
def main():
    """Extract trades from metadata-annotated postings and report on them.
    """
    logging.basicConfig(level=logging.INFO,
                        format='%(levelname)-8s: %(message)s')
    parser = version.ArgumentParser(description=__doc__.strip())
    parser.add_argument('filename', help='Beancount input filename')

    oparser = parser.add_argument_group('Outputs')
    oparser.add_argument(
        '-o',
        '--output',
        action='store',
        help="Filename to output results to (default goes to stdout)")
    oparser.add_argument('-f',
                         '--format',
                         default='text',
                         choices=['text', 'csv'],
                         help="Output format to render to (text, csv)")

    args = parser.parse_args()

    # Load the input file.
    entries, errors, options_map = loader.load_file(args.filename)

    # Get the list of trades.
    trades = extract_trades(entries)

    # Produce a table of all the trades.
    columns = ('units currency cost_currency '
               'buy_date buy_price sell_date sell_price pnl').split()
    header = [
        'Units', 'Currency', 'Cost Currency', 'Buy Date', 'Buy Price',
        'Sell Date', 'Sell Price', 'P/L'
    ]
    body = []
    for aug, red in trades:
        units = -red.posting.units.number
        buy_price = aug.posting.price.number
        sell_price = red.posting.price.number
        pnl = (units * (sell_price - buy_price)).quantize(buy_price)
        body.append([
            -red.posting.units.number, red.posting.units.currency,
            red.posting.price.currency,
            aug.txn.date.isoformat(), buy_price,
            red.txn.date.isoformat(), sell_price, pnl
        ])
    trades_table = table.Table(columns, header, body)

    # Render the table as text or CSV.
    outfile = open(args.output, 'w') if args.output else sys.stdout
    table.render_table(trades_table, outfile, args.format)
Ejemplo n.º 5
0
def main():
    logging.basicConfig(level=logging.INFO,
                        format='%(levelname)-8s: %(message)s')
    parser = argparse.ArgumentParser(description=__doc__.strip())

    parser.add_argument('filename', help='Beancount input file')
    parser.add_argument('accounts', nargs='+', help='Account names')

    parser.add_argument('--date',
                        type=date_utils.parse_date_liberally,
                        help="Date")

    parser.add_argument('-o',
                        '--output',
                        action='store',
                        help="Output directory for the CSV files")

    args = parser.parse_args()

    entries, errors, options_map = loader.load_file(args.filename)

    # Filter out anything after the given date.
    if args.date is None:
        args.date = entries[-1].date
    entries = [entry for entry in entries if entry.date < args.date]

    # Compute the balance in each account and process it.
    real_root = realization.realize(entries)
    rows = []
    fieldspec = list(enumerate(['Vest Date', 'Units', 'Instrument', 'Cost']))
    for account in args.accounts:
        real_acc = realization.get(real_root, account)
        if real_acc is None:
            logging.error("Account '%s' does not exist", account)
            continue
        for position in real_acc.balance:
            rows.append((position.cost.date, position.units.number,
                         position.units.currency, position.cost.number,
                         position.cost.currency))
    rows = sorted(rows)
    tbl = table.create_table(rows, fieldspec)

    table.render_table(tbl, sys.stdout, 'text')
    if args.output:
        with open(path.join(args.output), 'w') as file:
            table.render_table(tbl, file, 'csv')
Ejemplo n.º 6
0
 def test_generate_table(self):
     table_object = self.test_create_table()
     oss = io.StringIO()
     table.render_table(table_object, oss, 'csv')
     self.assertTrue(oss.getvalue())
     oss = io.StringIO()
     table.render_table(table_object, oss, 'txt')
     self.assertTrue(oss.getvalue())
     oss = io.StringIO()
     table.render_table(table_object, oss, 'html')
     self.assertTrue(oss.getvalue())
Ejemplo n.º 7
0
def main():
    logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s')
    parser = argparse.ArgumentParser(description=__doc__.strip())
    parser.add_argument('report', choices=['detail', 'aggregate', 'summary'],
                        help='Type of report')
    parser.add_argument('filename',
                        help='Beancount input file')
    parser.add_argument('account',
                        help='Account name')

    parser.add_argument('--start', type=date_utils.parse_date_liberally,
                        help="Start date")
    parser.add_argument('--end', type=date_utils.parse_date_liberally,
                        help="End date; if not set, at the end of star'ts year")

    parser.add_argument('-o', '--output', action='store',
                        help="Output filename for the CSV file")

    args = parser.parse_args()

    calculate_commission = False

    # Setup date interval.
    if args.start is None:
        args.start = datetime.date(datetime.date.today().year, 1, 1)
    if args.end is None:
        args.end = datetime.date(args.start.year + 1, 1, 1)

    entries, errors, options_map = loader.load_file(args.filename)

    # Expand each of the sales legs.
    balances = collections.defaultdict(inventory.Inventory)
    sales = []
    for txn in data.filter_txns(entries):
        # If we got to the end of the period, bail out.
        if txn.date >= args.end:
            break

        # Accumulate the balances before the start date.
        if txn.date < args.start:
            for posting in txn.postings:
                if re.match(args.account, posting.account):
                    balance = balances[posting.account]
                    balance.add_position(posting)
            continue

        # Fallthrough: we're not in the period. Process the matching postings.

        # Find reducing postings (i.e., for each lot).
        txn_sales = []
        for posting in txn.postings:
            if re.match(args.account, posting.account):
                balance = balances[posting.account]
                reduced_position, booking = balance.add_position(posting)
                # Set the cost on the posting from the reduced position.
                # FIXME: Eventually that'll happen automatically during the full
                # booking stage.
                if booking == inventory.Booking.REDUCED:
                    posting = posting._replace(cost=reduced_position.cost)

                # If the postings don't have a reference number, ignore them.
                if 'ref' not in txn.meta:
                    continue

                if (posting.cost and
                    posting.units.number < ZERO):
                    if not posting.price:
                        logging.error("Missing price on %s", posting)
                    txn_sales.append(data.TxnPosting(txn, posting))

        if txn_sales and calculate_commission:
            # Find total commission.
            for posting in txn.postings:
                if re.search('Commission', posting.account):
                    commission = posting.units.number
                    break
            else:
                commission = ZERO

            # Compute total number of units.
            tot_units = sum(sale.posting.units.number
                            for sale, _ in txn_sales)

            # Assign a proportion of the commission to each of the sales by
            # inserting it into its posting metadata. This will be processed below.
            for sale, _ in txn_sales:
                fraction = sale.posting.units.number / tot_units
                sale.posting.meta['commission'] = fraction * commission

        sales.extend(txn_sales)

    # Convert into a table of data, full detail of very single log.
    Q = D('0.01')
    lots = []
    total_loss = collections.defaultdict(D)
    total_gain = collections.defaultdict(D)
    total_adj = collections.defaultdict(D)

    # If no mssb number has been assigned explicitly, assign a random one. I
    # need to figure out how to find those numbers again.
    auto_mssb_number = itertools.count(start=1000000 + 1)

    for sale in sales:
        try:
            sale_no = sale.txn.meta['mssb']
        except KeyError:
            sale_no = next(auto_mssb_number)
        ref = sale.txn.meta['ref']

        units = sale.posting.units
        totcost = (-units.number * sale.posting.cost.number).quantize(Q)
        totprice = (-units.number * sale.posting.price.number).quantize(Q)

        commission_meta = sale.posting.meta.get('commission', None)
        if commission_meta is None:
            commission = ZERO
        else:
            if calculate_commission:
                commission = commission_meta
            else:
                # Fetch the commission that was inserted by the commissions plugin.
                commission = commission_meta[0].units.number
        commission = commission.quantize(Q)

        pnl = (totprice - totcost - commission).quantize(Q)
        is_wash = sale.posting.meta.get('wash', False)
        if totprice > totcost:
            total_gain[units.currency] += pnl
        else:
            total_loss[units.currency] += pnl
        if is_wash:
            total_adj[units.currency] += pnl
            code = 'W'
            adj = -pnl
        else:
            code = ''
            adj = ''

        days_held = (sale.txn.date - sale.posting.cost.date).days
        term = 'LONG' if days_held >= 365 else 'SHORT'
        lot = LotSale(sale_no,
                      ref,
                      sale.posting.cost.date,
                      sale.txn.date,
                      days_held,
                      term,
                      units.currency,
                      -units.number.quantize(Q),
                      sale.posting.cost.number.quantize(Q),
                      sale.posting.price.number.quantize(Q),
                      totcost,
                      totprice,
                      commission,
                      totprice - commission,
                      pnl,
                      code,
                      adj)
        lots.append(lot)
    tab_detail = table.create_table(lots, fieldspec)

    # Aggregate by transaction in order to be able to cross-check against the
    # 1099 forms.
    agglots = [aggregate_sales(lots)
               for _, lots in misc_utils.groupby(
                       lambda lot: (lot.no, lot.ref), lots).items()]
    tab_agg = table.create_table(sorted(agglots, key=lambda lot: (lot.ref, lot.no)),
                                 fieldspec)

    # Write out a summary of P/L.
    summary_fields = list(enumerate(['Currency', 'Gain', 'Loss', 'Net', 'Adj/Wash']))
    summary = []
    gain = ZERO
    loss = ZERO
    adj = ZERO
    for currency in sorted(total_adj.keys()):
        gain += total_gain[currency]
        loss += total_loss[currency]
        adj += total_adj[currency]
        summary.append((currency,
                        total_gain[currency],
                        total_loss[currency],
                        total_gain[currency] + total_loss[currency],
                        total_adj[currency]))
    summary.append(('*', gain, loss, gain + loss, adj))
    tab_summary = table.create_table(summary, summary_fields)

    if args.report == 'detail':
        # Render to the console.
        print('Detail of all lots')
        print('=' * 48)
        table.render_table(tab_detail, sys.stdout, 'txt')
        print()
        if args.output:
            with open(args.output, 'w') as file:
                table.render_table(tab_detail, file, 'csv')

    elif args.report == 'aggregate':
        print('Aggregated by trade & Reference Number (to Match 1099/Form8459)')
        print('=' * 48)
        table.render_table(tab_agg, sys.stdout, 'txt')
        print()
        if args.output:
            with open(args.output, 'w') as file:
                table.render_table(tab_agg, file, 'csv')

    elif args.report == 'summary':
        print('Summary')
        print('=' * 48)
        table.render_table(tab_summary, sys.stdout, 'txt')
        print()
        if args.output:
            with open(args.output, 'w') as file:
                table.render_table(tab_summary, file, 'csv')
Ejemplo n.º 8
0
 def render_csv(self, entries, errors, options_map, file):
     table_ = self.generate_table(entries, errors, options_map)
     table.render_table(table_, file, 'csv')
Ejemplo n.º 9
0
def main():
    logging.basicConfig(level=logging.INFO,
                        format='%(levelname)-8s: %(message)s')
    parser = argparse.ArgumentParser(description=__doc__.strip())
    parser.add_argument('report',
                        choices=['detail', 'aggregate', 'summary'],
                        help='Type of report')
    parser.add_argument('filename', help='Beancount input file')
    parser.add_argument('account', help='Account name')

    parser.add_argument('--start',
                        type=date_utils.parse_date_liberally,
                        help="Start date")
    parser.add_argument(
        '--end',
        type=date_utils.parse_date_liberally,
        help="End date; if not set, at the end of star'ts year")

    parser.add_argument(
        '-o',
        '--output',
        action='store',
        help="Output directory of all the reports, in txt and csv formats")

    args = parser.parse_args()

    calculate_commission = False

    # Setup date interval.
    if args.start is None:
        args.start = datetime.date(datetime.date.today().year, 1, 1)
    if args.end is None:
        args.end = datetime.date(args.start.year + 1, 1, 1)

    entries, errors, options_map = loader.load_file(args.filename)

    # Create the list of sales.
    sales = expand_sales_legs(entries, args.account, args.start, args.end,
                              calculate_commission)

    # Produce a detailed table.
    lots, tab_detail, totals = create_detailed_table(sales,
                                                     calculate_commission)

    # Aggregate by transaction in order to be able to cross-check against the
    # 1099 forms.
    agglots = aggregate_sales(lots)
    tab_agg = table.create_table(
        sorted(agglots, key=lambda lot: (lot.ref, lot.no)), fieldspec)

    # Create a summary table of P/L.
    tab_summary = create_summary_table(totals)

    # Render all the reports to an output directory.
    if args.output:
        os.makedirs(args.output, exist_ok=True)
        for name, tab in [('detail', tab_detail), ('aggregate', tab_agg),
                          ('summary', tab_summary)]:
            for fmt in 'txt', 'csv':
                with open(path.join(args.output, '{}.{}'.format(name, fmt)),
                          'w') as outfile:
                    table.render_table(tab, outfile, fmt)

    # Rendering individual reports to the console.
    if args.report == 'detail':
        print('Detail of all lots')
        print('=' * 48)
        table.render_table(tab_detail, sys.stdout, 'txt')
        print()
    elif args.report == 'aggregate':
        print(
            'Aggregated by trade & Reference Number (to Match 1099/Form8459)')
        print('=' * 48)
        table.render_table(tab_agg, sys.stdout, 'txt')
        print()
    elif args.report == 'summary':
        print('Summary')
        print('=' * 48)
        table.render_table(tab_summary, sys.stdout, 'txt')
        print()