def main(): logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') parser = argparse.ArgumentParser(description=__doc__.strip()) parser.add_argument('filename', help='Ledger filename') args = parser.parse_args() with open('/tmp/ledger.rieg', 'wb') as outfile: with riegeli.RecordWriter(outfile) as writer: entries, errors, options_map = loader.load_file(args.filename) for entry in entries: if isinstance(entry, data.Transaction): pbent = convert_Transaction(entry) elif isinstance(entry, data.Open): pbent = convert_Open(entry) elif isinstance(entry, data.Close): pbent = convert_Close(entry) else: pbent = None if pbent is not None: #print(type(txn)) #print(txn) writer.write_message(pbent) if 0: print('-' * 100) printer.print_entry(entry) print(txn) print()
def export_v2_data(filename: str, output_filename: str, num_directives: Optional[int]): if output_filename.endswith(".pbtxt"): output = open(output_filename, 'w') writer = None def write(message): print(message, file=output) else: output = open(output_filename, 'wb') writer = riegeli.RecordWriter(output) write = writer.write_message #entries, errors, options_map = loader.load_file(filename) entries, errors, options_map = parser.parse_file(filename) entries = data.sorted(entries) if num_directives: entries = itertools.islice(entries, num_directives) for entry in entries: if isinstance(entry, data.Transaction): pbdir = convert_Transaction(entry) elif isinstance(entry, data.Open): pbdir = convert_Open(entry) elif isinstance(entry, data.Close): pbdir = convert_Close(entry) elif isinstance(entry, data.Commodity): pbdir = convert_Commodity(entry) elif isinstance(entry, data.Event): pbdir = convert_Event(entry) elif isinstance(entry, data.Note): pbdir = convert_Note(entry) elif isinstance(entry, data.Query): pbdir = convert_Query(entry) elif isinstance(entry, data.Price): pbdir = convert_Price(entry) elif isinstance(entry, data.Balance): pbdir = convert_Balance(entry) elif isinstance(entry, data.Pad): pbdir = convert_Pad(entry) else: pbdir = None if pbdir is not None: write("#---") write("# {}".format(pbdir.location.lineno)) write("#") write(pbdir) write("") if 0: print('-' * 80) printer.print_entry(entry) print(txn) print() if hasattr(writer, "close"): writer.close() output.close()
def assert_same_results(actuals: List[Directive], expecteds: List[Directive]): same, missings1, missings2 = compare_entries(actuals, expecteds) for missing in missings1: print('Unexpected entry:', file=sys.stderr) printer.print_entry(missing, file=sys.stderr) for missing in missings2: print('Missing entry:', file=sys.stderr) printer.print_entry(missing, file=sys.stderr) assert same
def print_unmatched(txn_postings, filename, regexp): if not txn_postings: return print() print() print('=== Ummatched from {}, "{}"'.format(filename, regexp)) print() for txn_posting in sorted(txn_postings, key=lambda tp: tp.txn.date): printer.print_entry(txn_posting.txn)
def main(): entries, errors, options = loader.load_file(sys.argv[1]) for entry in entries: printer.print_entry(entry) print(options, file=sys.stderr) for error in errors: printer.print_error(error, file=sys.stderr)
def test_parse_string(self): # TODO(blais): Remove, this is temporary, for testing locally. filename = os.getenv("L") assert filename builder = grammar.Builder() out = extmodule.parse(builder, filename) print(out) entries, errors, options_map = builder.finalize() #pprint.pprint(options_map) pprint.pprint((len(entries), errors)) if entries: printer.print_entry(entries[-1])
def add_ira_contribs(entries, options_map, config): """Add legs for 401k employer match contributions. See module docstring for an example configuration. Args: entries: a list of entry instances options_map: a dict of options parsed from the file config: A configuration string, which is intended to be a Python dict mapping match-accounts to a pair of (negative-account, position-account) account names. Returns: A tuple of entries and errors. """ # Parse and extract configuration values. config_obj = eval(config, {}, {}) if not isinstance(config_obj, dict): raise RuntimeError( "Invalid plugin configuration: should be a single dict.") currency = config_obj.pop('currency', 'UNKNOWN') flag = config_obj.pop('flag', None) account_transforms = config_obj.pop('accounts', {}) new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): orig_entry = entry for posting in entry.postings: if (posting.account in account_transforms and posting.position and (account_types.get_account_sign(posting.account) * posting.position.number) > 0): # Get the new account legs to insert. neg_account, pos_account = account_transforms[ posting.account] assert posting.position.lot.cost is None # Insert income/expense entries for 401k. entry = add_postings( entry, amount.Amount(abs(posting.position.number), currency), neg_account.format(year=entry.date.year), pos_account.format(year=entry.date.year), flag) if DEBUG and orig_entry is not entry: printer.print_entry(orig_entry) printer.print_entry(entry) new_entries.append(entry) return new_entries, []
def test_render_missing(self): # We want to make sure we never render with scientific notation. input_string = textwrap.dedent(""" 2019-01-19 * "Fitness First" "Last training session" Expenses:Sports:Gym:Martin Assets:Martin:Cash """) entries, errors, options_map = loader.load_string(input_string) txn = errors[0].entry oss = io.StringIO() printer.print_entry(txn, file=oss)
def main(): argparser = argparse.ArgumentParser(description=__doc__) argparser.add_argument('infile', type=argparse.FileType('r'), help='Filename or "-" for stdin') args = argparser.parse_args() # Read input from stdin or a given filename. entries, errors, options = loader.load_string(args.infile.read()) # Print out sorted entries. for entry in data.sorted(entries): printer.print_entry(entry)
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='Ledger filename') args = parser.parse_args() entries, errors, options_map = loader.load_file(args.filename) for entry in entries: if (isinstance(entry, data.Transaction) and any(posting.position.lot.lot_date for posting in entry.postings)): printer.print_entry(entry)
def main(): logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') parser = argparse.ArgumentParser(description=__doc__.strip()) parser.add_argument('filename', help='Ledger filename') args = parser.parse_args() entries, errors, options_map = loader.load_file(args.filename) for entry in entries: if isinstance(entry, data.Transaction): txn = convert_transaction(entry) if 0: print('-' * 100) printer.print_entry(entry) print(txn) print()
def render_entry_context(entries, options_map, entry): """Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. entry: The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. """ oss = io.StringIO() meta = entry.meta print("Hash:{}".format(compare.hash_entry(entry)), file=oss) print("Location: {}:{}".format(meta["filename"], meta["lineno"]), file=oss) # Get the list of accounts sorted by the order in which they appear in the # closest entry. order = {} if isinstance(entry, data.Transaction): order = { posting.account: index for index, posting in enumerate(entry.postings) } accounts = sorted(getters.get_entry_accounts(entry), key=lambda account: order.get(account, 10000)) # Accumulate the balances of these accounts up to the entry. balance_before, balance_after = interpolate.compute_entry_context( entries, entry) # Create a format line for printing the contents of account balances. max_account_width = max(map(len, accounts)) if accounts else 1 position_line = '{{:1}} {{:{width}}} {{:>49}}'.format( width=max_account_width) # Print the context before. print(file=oss) print("------------ Balances before transaction", file=oss) print(file=oss) before_hashes = set() for account in accounts: positions = balance_before[account].get_positions() for position in positions: before_hashes.add((account, hash(position))) print(position_line.format('', account, str(position)), file=oss) if not positions: print(position_line.format('', account, ''), file=oss) print(file=oss) # Print the entry itself. print(file=oss) print("------------ Transaction", file=oss) print(file=oss) dcontext = options_map['dcontext'] printer.print_entry(entry, dcontext, render_weights=True, file=oss) if isinstance(entry, data.Transaction): print(file=oss) # Print residuals. residual = interpolate.compute_residual(entry.postings) if not residual.is_empty(): # Note: We render the residual at maximum precision, for debugging. print('Residual: {}'.format(residual), file=oss) # Dump the tolerances used. tolerances = interpolate.infer_tolerances(entry.postings, options_map) if tolerances: print('Tolerances: {}'.format(', '.join( '{}={}'.format(key, value) for key, value in sorted(tolerances.items()))), file=oss) # Compute the total cost basis. cost_basis = inventory.Inventory(pos for pos in entry.postings if pos.cost is not None).reduce( convert.get_cost) if not cost_basis.is_empty(): print('Basis: {}'.format(cost_basis), file=oss) # Print the context after. print(file=oss) print("------------ Balances after transaction", file=oss) print(file=oss) for account in accounts: positions = balance_after[account].get_positions() for position in positions: changed = (account, hash(position)) not in before_hashes print(position_line.format('*' if changed else '', account, str(position)), file=oss) if not positions: print(position_line.format('', account, ''), file=oss) print(file=oss) return oss.getvalue()
def segment_periods(entries, accounts_value, accounts_intflows, date_begin=None, date_end=None): """Segment entries in terms of piecewise periods of internal flow. This function iterated through the given entries and computes balances at the beginning and end of periods without external flow entries. You should be able to then compute the returns from these informations. Args: entries: A list of directives. The list may contain directives other than than transactions as well as directives with no relation to the assets or internal flow accounts (the function simply ignores that which is not relevant). accounts_value: A set of the asset accounts in the related group. accounts_intflows: A set of the internal flow accounts in the related group. date_begin: A datetime.date instance, the beginning date of the period to compute returns over. date_end: A datetime.date instance, the end date of the period to compute returns over. Returns: A pair of periods: A list of period tuples, each of which contains: period_begin: A datetime.date instance, the first day of the period. period_end: A datetime.date instance, the last day of the period. balance_begin: An Inventory instance, the balance at the beginning of the period. balance_end: An Inventory instance, the balance at the end of the period. portfolio_entries: A list of the entries that we used in computing the portfolio. Raises: ValueError: If the dates create an impossible situation, the beginning must come before the requested end, if specified. """ logging.info("Segmenting periods.") logging.info("Date begin: %s", date_begin) logging.info("Date end: %s", date_end) if date_begin and date_end and date_begin >= date_end: raise ValueError("Dates are not ordered correctly: {} >= {}".format( date_begin, date_end)) accounts_related = accounts_value | accounts_intflows is_external_flow_entry = lambda entry: (isinstance( entry, data.Transaction) and any(posting.account not in accounts_related for posting in entry.postings)) # Create an iterator over the entries we care about. portfolio_entries = [ entry for entry in entries if getters.get_entry_accounts(entry) & accounts_value ] iter_entries = iter(portfolio_entries) entry = next(iter_entries) # If a beginning cut-off has been specified, skip the entries before then # (and make sure to accumulate the initial balance correctly). balance = inventory.Inventory() if date_begin is not None: period_begin = date_begin try: while True: if entry.date >= date_begin: break if date_end and entry.date >= date_end: break balance = sum_balances_for_accounts(balance, entry, accounts_value) entry = next(iter_entries) except StopIteration: # No periods found! Just return an empty list. return [(date_begin, date_end or date_begin, balance, balance)], [] else: period_begin = entry.date # Main loop over the entries. periods = [] entry_logger = misc_utils.LineFileProxy(logging.debug, ' ') done = False while True: balance_begin = copy.copy(balance) logging.debug( ",-----------------------------------------------------------") logging.debug(" Begin: %s", period_begin) logging.debug(" Balance: %s", balance_begin.units()) logging.debug("") # Consume all internal flow entries, simply accumulating the total balance. while True: period_end = entry.date if is_external_flow_entry(entry): break if date_end and entry.date >= date_end: period_end = date_end done = True break if entry: printer.print_entry(entry, file=entry_logger) balance = sum_balances_for_accounts(balance, entry, accounts_value) try: entry = next(iter_entries) except StopIteration: done = True if date_end: period_end = date_end break else: done = True balance_end = copy.copy(balance) ## FIXME: Bring this back in, this fails for now. Something about the ## initialization fails it. assert period_begin <= period_end, ## (period_begin, period_end) periods.append((period_begin, period_end, balance_begin, balance_end)) logging.debug(" Balance: %s", balance_end.units()) logging.debug(" End: %s", period_end) logging.debug( "`-----------------------------------------------------------") logging.debug("") if done: break # Absorb the balance of the external flow entry. assert is_external_flow_entry(entry), entry if entry: printer.print_entry(entry, file=entry_logger) balance = sum_balances_for_accounts(balance, entry, accounts_value) try: entry = next(iter_entries) except StopIteration: # If there is an end date, insert that final period to cover the end # date, with no changes. if date_end: periods.append((period_end, date_end, balance, balance)) break period_begin = period_end ## FIXME: Bring this back in, this fails for now. # assert all(period_begin <= period_end # for period_begin, period_end, _, _ in periods), periods return periods, portfolio_entries
def add_ira_contribs(entries, options_map, config_str): """Add legs for 401k employer match contributions. See module docstring for an example configuration. Args: entries: a list of entry instances options_map: a dict of options parsed from the file config_str: A configuration string, which is intended to be a Python dict mapping match-accounts to a pair of (negative-account, position-account) account names. Returns: A tuple of entries and errors. """ # Parse and extract configuration values. # FIXME: Use ast.literal_eval() here; you need to convert this code and the getters. # FIXME: Also, don't raise a RuntimeError, return an error object; review # this for all the plugins. # FIXME: This too is temporary. # pylint: disable=eval-used config_obj = eval(config_str, {}, {}) if not isinstance(config_obj, dict): raise RuntimeError( "Invalid plugin configuration: should be a single dict.") # Currency of the inserted postings. currency = config_obj.pop('currency', 'UNKNOWN') # Flag to attach to the inserted postings. insert_flag = config_obj.pop('flag', None) # A dict of account names that trigger the insertion of postings to pairs of # inserted accounts when triggered. accounts = config_obj.pop('accounts', {}) # Convert the key in the accounts configuration for matching. account_transforms = {} for key, config in accounts.items(): if isinstance(key, str): flag = None account = key else: assert isinstance(key, tuple) flag, account = key account_transforms[account] = (flag, config) new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): orig_entry = entry for posting in entry.postings: if (posting.units is not MISSING and (posting.account in account_transforms) and (account_types.get_account_sign(posting.account) * posting.units.number > 0)): # Get the new account legs to insert. required_flag, ( neg_account, pos_account) = account_transforms[posting.account] assert posting.cost is None # Check required flag if present. if (required_flag is None or (required_flag and required_flag == posting.flag)): # Insert income/expense entries for 401k. entry = add_postings( entry, amount.Amount(abs(posting.units.number), currency), neg_account.format(year=entry.date.year), pos_account.format(year=entry.date.year), insert_flag) if DEBUG and orig_entry is not entry: printer.print_entry(orig_entry) printer.print_entry(entry) new_entries.append(entry) return new_entries, []
#!/usr/bin/env python3 from datetime import date import sys from beancount import loader from beancount.core import compare,data from beancount.parser import printer from youqianDict import CategoryAll entries_existing=[] if len(sys.argv) > 1: filename = sys.argv[1] entries_existing, errors, options = loader.load_file(filename) entries_new=[] accounts_existing=[i.account for i in entries_existing] for account_youqian in sorted(set(CategoryAll.values())): if account_youqian not in accounts_existing: entries_new.append( data.Open( meta=None, booking=None, date=date(1970, 1, 1), account=account_youqian, currencies=["CNY"] ) ) for entry in entries_new: printer.print_entry(entry)
def addTransaction(self, transaction, fileName): with open(path.join(self.basedir, fileName), 'a') as output: printer.print_entry(transaction, file=output)
def get_trades(entries, options_map, symbols, date_end): """Process a series of entries and extract a list of processable trades. The returned rows include computed fees and proceeds, ready to be washed. The list of trades includes buy and sell types. Args: entries: A list of directives to be processed. options_map: An options dict, as per the parser. symbols: A set of currency strings for substantially identical stocks. date_end: The cutoff date after which to stop processing transactions. Returns: XXX """ acc_types = options.get_account_types(options_map) # Inventory of lots to accumulate. balances = inventory.Inventory() # A list of trade information. trades = [] for entry in entries: # Skip other directives. if not isinstance(entry, data.Transaction): continue # Skip entries after the relevant period. if entry.date > date_end: continue # Skip entries not relevant to the currency. if not any(posting.position.lot.currency in symbols for posting in entry.postings): continue # Calculate the fee amount and the total price, in order to split the # fee later on. fee_total = ZERO units_total = ZERO for posting in entry.postings: if account_types.get_account_type( posting.account) == acc_types.expenses: fee_total += posting.position.number if (account_types.get_account_type( posting.account) == acc_types.assets and posting.position.lot.cost is not None): units_total += posting.position.number # Loop through the postings and create trade entries for them, computing # proceeds and fees and all details required to wash sales later on. booked = False for posting in entry.postings: pos = posting.position if pos.lot.currency not in symbols: continue # Check that all sales have the sale price attached to them. if pos.number < ZERO: assert posting.price or re.search('Split', entry.narration, re.I) # Update the shared inventory. booked = True booked_pos = book_position(balances, entry.date, pos) # Determine the dates. if pos.number > ZERO: txn_type = 'BUY' acq_date = entry.date adj_acq_date = None sell_date = None number = pos.number cost = pos.lot.cost price = '' partial_fee = '' proceeds = '' pnl = '' else: # This only holds true because we book lot sales individually. assert len(booked_pos) <= 1, "Internal error." booked_position = booked_pos[0] txn_type = 'SELL' assert pos.number < ZERO acq_date = booked_position.lot.lot_date adj_acq_date = None sell_date = entry.date number = pos.number cost = pos.lot.cost price = posting.price if posting.price else cost partial_fee = fee_total * (number / units_total).quantize( D('0.01')) proceeds = -number * price.number - partial_fee pnl = proceeds - cost_basis cost_basis = -number * cost.number trades.append( (txn_type, acq_date, adj_acq_date, sell_date, number, pos.lot.currency, cost.number, cost_basis, price.number if price else '', proceeds, partial_fee, pnl)) if booked: printer.print_entry(entry) for pos in balances: number, rest = str(pos).split(' ', 1) print(' {:>16} {}'.format(number, rest)) print() print() return trades
def render_entry_context(entries, options_map, entry, parsed_entry=None): """Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. entry: The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) parsed_entry: An optional incomplete, parsed but not booked nor interpolated entry. If this is provided, this is used for inspecting the list of prior accounts and it is also rendered. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. """ oss = io.StringIO() pr = functools.partial(print, file=oss) header = "** {} --------------------------------" meta = entry.meta pr(header.format("Transaction Id")) pr() pr("Hash:{}".format(compare.hash_entry(entry))) pr("Location: {}:{}".format(meta["filename"], meta["lineno"])) pr() pr() # Get the list of accounts sorted by the order in which they appear in the # closest entry. order = {} if parsed_entry is None: parsed_entry = entry if isinstance(parsed_entry, data.Transaction): order = { posting.account: index for index, posting in enumerate(parsed_entry.postings) } accounts = sorted(getters.get_entry_accounts(parsed_entry), key=lambda account: order.get(account, 10000)) # Accumulate the balances of these accounts up to the entry. balance_before, balance_after = interpolate.compute_entry_context( entries, entry, additional_accounts=accounts) # Create a format line for printing the contents of account balances. max_account_width = max(map(len, accounts)) if accounts else 1 position_line = '{{:1}} {{:{width}}} {{:>49}}'.format( width=max_account_width) # Print the context before. pr(header.format("Balances before transaction")) pr() before_hashes = set() average_costs = {} for account in accounts: balance = balance_before[account] pc_balances = balance.split() for currency, pc_balance in pc_balances.items(): if len(pc_balance) > 1: average_costs[account] = pc_balance.average() positions = balance.get_positions() for position in positions: before_hashes.add((account, hash(position))) pr(position_line.format('', account, str(position))) if not positions: pr(position_line.format('', account, '')) pr() pr() # Print average cost per account, if relevant. if average_costs: pr(header.format("Average Costs")) pr() for account, average_cost in sorted(average_costs.items()): for position in average_cost: pr(position_line.format('', account, str(position))) pr() pr() # Print the entry itself. dcontext = options_map['dcontext'] pr(header.format("Unbooked Transaction")) pr() if parsed_entry: printer.print_entry(parsed_entry, dcontext, render_weights=True, file=oss) pr() pr(header.format("Transaction")) pr() printer.print_entry(entry, dcontext, render_weights=True, file=oss) pr() if isinstance(entry, data.Transaction): pr(header.format("Residual and Tolerances")) pr() # Print residuals. residual = interpolate.compute_residual(entry.postings) if not residual.is_empty(): # Note: We render the residual at maximum precision, for debugging. pr('Residual: {}'.format(residual)) # Dump the tolerances used. tolerances = interpolate.infer_tolerances(entry.postings, options_map) if tolerances: pr('Tolerances: {}'.format(', '.join( '{}={}'.format(key, value) for key, value in sorted(tolerances.items())))) # Compute the total cost basis. cost_basis = inventory.Inventory(pos for pos in entry.postings if pos.cost is not None).reduce( convert.get_cost) if not cost_basis.is_empty(): pr('Basis: {}'.format(cost_basis)) pr() pr() # Print the context after. pr(header.format("Balances after transaction")) pr() for account in accounts: positions = balance_after[account].get_positions() for position in positions: changed = (account, hash(position)) not in before_hashes print(position_line.format('*' if changed else '', account, str(position)), file=oss) if not positions: pr(position_line.format('', account, '')) pr() return oss.getvalue()