Skip to content

Commit

Permalink
Started working on a hack to display total value and change for a Ledger
Browse files Browse the repository at this point in the history
  • Loading branch information
blais committed Jun 7, 2008
1 parent 1d6a8c9 commit 71fde62
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 5 deletions.
224 changes: 224 additions & 0 deletions bin/bean-assets
Original file line number Diff line number Diff line change
@@ -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()

17 changes: 12 additions & 5 deletions lib/python/beancount/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions lib/python/beancount/test/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')


13 changes: 13 additions & 0 deletions lib/python/beancount/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit 71fde62

Please sign in to comment.