From 71fde62bbcd9d8a14cf24d304339c91bb357cba8 Mon Sep 17 00:00:00 2001 From: blais Date: Sat, 7 Jun 2008 15:57:57 -0700 Subject: [PATCH] Started working on a hack to display total value and change for a Ledger --- bin/bean-assets | 224 +++++++++++++++++++++++ lib/python/beancount/cmdline.py | 17 +- lib/python/beancount/test/test_wallet.py | 10 + lib/python/beancount/wallet.py | 13 ++ 4 files changed, 259 insertions(+), 5 deletions(-) create mode 100755 bin/bean-assets diff --git a/bin/bean-assets b/bin/bean-assets new file mode 100755 index 000000000..7ed394f15 --- /dev/null +++ b/bin/bean-assets @@ -0,0 +1,224 @@ +#!/usr/bin/env python +""" +Given a series of Ledger files, figure out the sum of assets held by these +ledgers at balance, and fetch and display the market value and movement for the +current period. +""" + +# stdlib imports +import re, md5, urllib +import cPickle as pickle +from os.path import * +from decimal import Decimal + +# other imports +from BeautifulSoup import BeautifulSoup + +# beancount imports +from beancount import cmdline +from beancount.utils import render_tree +from beancount.ledger import compute_balsheet +from beancount.wallet import Wallet + + + +class FileValueCache(object): + "A cache for a value derived from a file." + + COMPUTE = None + + + def __init__(self, cachefn): + self.cachefn = '/tmp/bean-assets.cache' + self.load() + + # Saved computed values for files checked. + self.saved = {} + + def load(self): + try: + self.cache = pickle.load(open(self.cachefn)) + except (IOError, EOFError): + self.cache = {} + self.cache_orig = self.cache.copy() + + def save(self): + if self.cache != self.cache_orig: + pickle.dump(self.cache, open(self.cachefn, 'w')) + + def get(self, fn): + + try: + timestamp, size, crc, value = self.cache[fn] + ctimestamp = getmtime(fn) + csize = getsize(fn) + compute = (ctimestamp > timestamp or + csize != size) + if not compute: + m = md5.new() + m.update(open(fn).read()) + ccrc = m.hexdigest() + compute = ccrc != crc + else: + ccrc = None + + self.saved[fn] = (ctimestamp, csize, ccrc) + except KeyError: + compute = 1 + + if compute: + raise KeyError("Value needs to be recomputed.") + else: + return value + + def update(self, fn, value): + try: + timestamp, size, crc = self.saved[fn] + except KeyError: + timestamp, size, crc = None, None, None + + if timestamp is None: + timestamp = getmtime(fn) + if size is None: + size = getsize(fn) + if crc is None: + m = md5.new() + m.update(open(fn).read()) + crc = m.hexdigest() + + self.cache[fn] = (timestamp, size, crc, value) + + + +market_currency = { + 'NYSE': 'USD', + 'TSE': 'CAD', + } + + + +url_google = 'http://finance.google.com/finance?q=%s' + +def getquote_google(sym): + ssym = sym.strip().lower() + f = urllib.urlopen(url_google % ssym) + soup = BeautifulSoup(f) + el = soup.find('span', 'pr') + if el is not None: + # Find the quote currency. + h1 = soup.find('h1') + mstr = h1.next.next + mstr = mstr.replace(' ', '').replace('\n', '') + mstring = '\\(([A-Za-z]+),\\s+([A-Z]+):%s\\)' % ssym.upper() + mo = re.match(mstring, mstr) + if mo is not None: + market = mo.group(2) + comm = market_currency[market] + else: + raise ValueError("Unknown market: %s for %s" % (mstr, ssym)) + price = Decimal(el.contents[0]) + + chg = soup.find('span', 'bld') + else: + comm, price, chg = None, None + + + url = '' % (symbol, stat) + return urllib.urlopen(url).read().strip().strip('"') + +url_yahoo = 'http://finance.yahoo.com/d/quotes.csv?s=%s&f=l1c1' + + +def specDecimal(s): + if s == 'N/A': + return Decimal() + else: + return Decimal(s) + +def getquote_yahoo(sym, pcomm): + ssym = sym.strip().lower() + if pcomm == 'CAD': + ssym += '.TO' + f = urllib.urlopen(url_yahoo % ssym) + contents = f.read().strip() + price, change = [specDecimal(x) for x in contents.split(',')] + return (price, change) + +getquote = getquote_yahoo + + + +class Position(object): + + def __init__(self, comm, units, pcomm): + + # Position's commodity and number of units in that commodity. + self.comm = comm + self.units = units + + # The quote commodity. + self.pcomm = pcomm + + # Price, change, etc. + self.price = None + self.change = None + + def __cmp__(self, other): + return cmp(self.comm, other.comm) + + + +currencies = ['USD', 'CAD', 'JPY', 'EUR', 'AUD', 'CHF', 'BRL'] + +def main(): + import optparse + parser = optparse.OptionParser(__doc__.strip()) + + fvcache = FileValueCache('/tmp/bean-assets.cache') + + totassets = Wallet() + totpricedmap = {} + opts, _, args = cmdline.main(parser, no=0) + for fn in args: + try: + balance, pricedmap = fvcache.get(fn) + except KeyError: + # Compute the balance. + ledger = cmdline.load_ledger(fn, opts) + compute_balsheet(ledger, 'local_balance', 'balance') + acc = ledger.get_account('Assets') + balance = acc.balance + pricedmap = ledger.pricedmap + fvcache.update(fn, (balance, pricedmap)) + totassets += balance + totpricedmap.update(pricedmap) + fvcache.save() + + positions = {} + for comm, units in totassets.iteritems(): + pcomm_set = totpricedmap.get(comm, set()) + if pcomm_set: + pcomm = pcomm_set.pop() + else: + pcomm = None + pos = Position(comm, units, pcomm) + positions[comm] = pos + + for pos in sorted(positions.itervalues()): + if pos.comm in currencies: + pass + else: + price, change = getquote(pos.comm, pos.pcomm) + if price is not None: + pos.price = price + pos.change = change + + for pos in sorted(positions.itervalues()): + print pos.comm, pos.units, pos.pcomm, pos.price, pos.change + print + + + +if __name__ == '__main__': + main() + diff --git a/lib/python/beancount/cmdline.py b/lib/python/beancount/cmdline.py index abbb0cb1c..228e86ed5 100644 --- a/lib/python/beancount/cmdline.py +++ b/lib/python/beancount/cmdline.py @@ -17,7 +17,9 @@ from beancount import install_psyco -def main(parser): +MANY=-1 + +def main(parser, no=1): "Parse the cmdline as a list of ledger source files and return a Ledger." logging.basicConfig(level=logging.INFO, @@ -55,10 +57,15 @@ def main(parser): if not opts.no_psyco: install_psyco() - fn, args = args[0], args[1:] - - ledger = load_ledger(fn, opts) - return opts, ledger, args + if no == 1: + fn, args = args[0], args[1:] + ledger = load_ledger(fn, opts) + return opts, ledger, args + elif no == MANY: + ledgers = [load_ledger(fn, opts) for fn in args] + return opts, ledgers, args + elif no == 0: + return opts, None, args def load_ledger(fn, opts): # Parse the file. diff --git a/lib/python/beancount/test/test_wallet.py b/lib/python/beancount/test/test_wallet.py index 34c82e549..8597cde41 100644 --- a/lib/python/beancount/test/test_wallet.py +++ b/lib/python/beancount/test/test_wallet.py @@ -95,4 +95,14 @@ def test_only(self): assert w.only('CAD') == Wallet(CAD='17.1343843') + def test_price(self): + w = Wallet(AAPL='20') + w.price('AAPL', 'USD', Decimal('10')) + assert w == Wallet(USD='200') + + w = Wallet(AAPL='20', MSFT='10.1') + w.price('AAPL', 'USD', Decimal('10')) + assert w == Wallet(USD='200', MSFT='10.1') + + diff --git a/lib/python/beancount/wallet.py b/lib/python/beancount/wallet.py index 7d9c2d12a..09032d99d 100644 --- a/lib/python/beancount/wallet.py +++ b/lib/python/beancount/wallet.py @@ -195,6 +195,19 @@ def commodity_key(kv): k = kv[0] return (comm_importance.get(k, len(k)), k) + def price(self, comm, ucomm, price): + """ Replace all the units of 'comm' by units of 'ucomm' at the given + price. """ + try: + units = self[comm] + except KeyError: + return + wdiff = Wallet() + wdiff[comm] = -units + wdiff[ucomm] = units * price + self += wdiff + + # Order of important for commodities. comm_importance = {