forked from beancount/beancount
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Started working on a hack to display total value and change for a Ledger
- Loading branch information
blais
committed
Jun 7, 2008
1 parent
1d6a8c9
commit 71fde62
Showing
4 changed files
with
259 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
||
|
||
if __name__ == '__main__': | ||
main() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters