[gnome-applets] [invest-applet] adding currency support, fixes bug #339127 and #609547



commit 3a8c4d4034c6c09f7c39d5943f37824da0919edb
Author: Enrico Minack <enrico-minack gmx de>
Date:   Tue Jul 6 22:29:14 2010 +0200

    [invest-applet] adding currency support, fixes bug #339127 and #609547
    
    Currencies of stocks are show in the applet. A target currency can be given
    in preferences so that all currencies are automatically converted to that one.

 invest-applet/data/prefs-dialog.ui  |   35 +++++
 invest-applet/invest/Makefile.am    |    1 +
 invest-applet/invest/__init__.py    |   51 ++++++-
 invest-applet/invest/preferences.py |  245 +++++++++++++++++++++++++++--------
 invest-applet/invest/quotes.py      |  205 +++++++++++++++++++++++++----
 invest-applet/invest/widgets.py     |   12 +-
 6 files changed, 453 insertions(+), 96 deletions(-)
---
diff --git a/invest-applet/data/prefs-dialog.ui b/invest-applet/data/prefs-dialog.ui
index db12c24..0beee9a 100644
--- a/invest-applet/data/prefs-dialog.ui
+++ b/invest-applet/data/prefs-dialog.ui
@@ -58,6 +58,7 @@
                 <child>
                   <object class="GtkAlignment" id="alignment16">
                     <property name="visible">True</property>
+                    <property name="bottom_padding">18</property>
                     <property name="left_padding">18</property>
                     <child>
                       <object class="GtkVBox" id="vbox156">
@@ -140,6 +141,40 @@
                     <property name="position">2</property>
                   </packing>
                 </child>
+                <child>
+                  <object class="GtkLabel" id="currencylabel">
+                    <property name="visible">True</property>
+                    <property name="xalign">0</property>
+                    <property name="label" translatable="yes">Currency</property>
+                    <property name="use_underline">True</property>
+                    <property name="justify">center</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                    <property name="position">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkAlignment" id="alignment1">
+                    <property name="visible">True</property>
+                    <property name="left_padding">18</property>
+                    <child>
+                      <object class="GtkEntry" id="currency">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="invisible_char">&#x25CF;</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="position">4</property>
+                  </packing>
+                </child>
               </object>
               <packing>
                 <property name="padding">6</property>
diff --git a/invest-applet/invest/Makefile.am b/invest-applet/invest/Makefile.am
index 308149c..ede7a61 100644
--- a/invest-applet/invest/Makefile.am
+++ b/invest-applet/invest/Makefile.am
@@ -11,6 +11,7 @@ invest_PYTHON = \
 	about.py \
 	applet.py \
 	chart.py \
+	currencies.py \
 	widgets.py \
 	quotes.py \
 	networkmanager.py \
diff --git a/invest-applet/invest/__init__.py b/invest-applet/invest/__init__.py
index c642bbd..29fb7d6 100644
--- a/invest-applet/invest/__init__.py
+++ b/invest-applet/invest/__init__.py
@@ -67,8 +67,8 @@ GCONF_DIR = "/apps/invest"
 # Preload gconf directories
 #GCONF_CLIENT.add_dir(GCONF_DIR, gconf.CLIENT_PRELOAD_RECURSIVE)
 
-# tests whether the given stocks are in the old format
-def old_stock_format(stocks):
+# tests whether the given stocks are in the old labelless format
+def labelless_stock_format(stocks):
 	if len(stocks) == 0:
 		return False
 
@@ -79,8 +79,8 @@ def old_stock_format(stocks):
 	# there is no list, so it is already the new stock file format
 	return False
 
-# converts the given stocks from the old format into the new one
-def update_stock_format(stocks):
+# converts the given stocks from the labelless format into the one with labels
+def update_to_labeled_stock_format(stocks):
 	new = {}
 
 	for k, l in stocks.items():
@@ -89,15 +89,43 @@ def update_stock_format(stocks):
 
 	return new
 
+# tests whether the given stocks are in the format without exchange information
+def exchangeless_stock_format(stocks):
+	if len(stocks) == 0:
+		return False
+
+	# take the first element of the dict and check if its value is a list
+	for symbol, data in stocks.items():
+		purchases = stocks[symbol]["purchases"]
+		if len(purchases) > 0:
+			purchase = purchases[0]
+			if not purchase.has_key("exchange"):
+				return True
+
+	return False
+
+# converts the given stocks into format with exchange information
+def update_to_exchange_stock_format(stocks):
+	for symbol, data in stocks.items():
+		purchases = data["purchases"]
+		for purchase in purchases:
+			purchase["exchange"] = 0
+
+	return stocks
+
 STOCKS_FILE = join(USER_INVEST_DIR, "stocks.pickle")
 
 try:
 	STOCKS = cPickle.load(file(STOCKS_FILE))
 
-	# if the stocks file is in the old stocks format,
-	# then we need to convert it into the new format
-	if old_stock_format(STOCKS):
-		STOCKS = update_stock_format(STOCKS);
+	# if the stocks file is in the stocks format without labels,
+	# then we need to convert it into the new labeled format
+	if labelless_stock_format(STOCKS):
+		STOCKS = update_to_labeled_stock_format(STOCKS);
+
+	# if the stocks file does not contain exchange rates, add them
+	if exchangeless_stock_format(STOCKS):
+		STOCKS = update_to_exchange_stock_format(STOCKS);
 except Exception, msg:
 	error("Could not load the stocks from %s: %s" % (STOCKS_FILE, msg) )
 	STOCKS = {}
@@ -120,6 +148,13 @@ except Exception, msg:
 #	},
 #}
 
+CONFIG_FILE = join(USER_INVEST_DIR, "config.pickle")
+try:
+	CONFIG = cPickle.load(file(CONFIG_FILE))
+except Exception, msg:
+	CONFIG = {}       # default configuration
+
+
 # set default proxy config
 PROXY = None
 
diff --git a/invest-applet/invest/preferences.py b/invest-applet/invest/preferences.py
index cde62bf..5eb1aeb 100644
--- a/invest-applet/invest/preferences.py
+++ b/invest-applet/invest/preferences.py
@@ -3,6 +3,7 @@ import locale
 from os.path import join
 import gtk, gobject, gconf
 import invest
+import currencies
 import cPickle
 
 class PrefsDialog:
@@ -12,79 +13,59 @@ class PrefsDialog:
 
 		self.dialog = self.ui.get_object("preferences")
 		self.treeview = self.ui.get_object("stocks")
+		self.currency = self.ui.get_object("currency")
+		self.currency_code = None
+		self.currencies = currencies.Currencies.currencies
 
 		self.ui.get_object("add").connect('clicked', self.on_add_stock)
 		self.ui.get_object("add").connect('activate', self.on_add_stock)
 		self.ui.get_object("remove").connect('clicked', self.on_remove_stock)
 		self.ui.get_object("remove").connect('activate', self.on_remove_stock)
 		self.treeview.connect('key-press-event', self.on_tree_keypress)
+		self.currency.connect('key-press-event', self.on_entry_keypress)
+		self.currency.connect('activate', self.on_activate_entry)
+		self.currency.connect('focus-out-event', self.on_focus_out_entry)
 
-		self.typs = (str, str, float, float, float)
-		self.names = (_("Symbol"), _("Label"), _("Amount"), _("Price"), _("Commission"))
+		self.typs = (str, str, float, float, float, float)
+		self.names = (_("Symbol"), _("Label"), _("Amount"), _("Price"), _("Commission"), _("Currency Rate"))
 		store = gtk.ListStore(*self.typs)
 		store.set_sort_column_id(0, gtk.SORT_ASCENDING)
 		self.treeview.set_model(store)
 		self.model = store
 
-		def on_cell_edited(cell, path, new_text, col, typ):
-			try:
-				if col == 0:    # stock symbols must be uppercase
-					new_text = str.upper(new_text)
-				if col < 2:
-					store[path][col] = new_text
-				else:
-					value = locale.atof(new_text)
-					store[path][col] = value
-			except:
-				pass
-
-		def format(fmt, value):
-			return locale.format(fmt, value, True)
-
-		def get_cell_data(column, cell, model, iter, data):
-			typ, col = data
-			if typ == int:
-				cell.set_property('text', "%d" % typ(model[iter][col]))
-			elif typ == float:
-				# provide float numbers with at least 2 fractional digits
-				val = model[iter][col]
-				digits = fraction_digits(val)
-				fmt = "%%.%df" % max(digits, 2)
-				cell.set_property('text', format(fmt, val))
-			else:
-				cell.set_property('text', typ(model[iter][col]))
-
-		# determine the number of non zero digits in the fraction of the value
-		def fraction_digits(value):
-			text = "%g" % value	# do not use locale here, so that %g always is rendered to a number with . as decimal separator
-			if text.find(".") < 0:
-				return 0
-			return len(text) - text.find(".") - 1
-
-		def create_cell (view, column, name, typ):
-			cell_description = gtk.CellRendererText ()
-			if typ == float:
-				cell_description.set_property("xalign", 1.0)
-			cell_description.set_property("editable", True)
-			cell_description.connect("edited", on_cell_edited, column, typ)
-			column_description = gtk.TreeViewColumn (name, cell_description)
-			if typ == str:
-				column_description.set_attributes (cell_description, text=column)
-				column_description.set_sort_column_id(column)
-			if typ == float:
-				column_description.set_cell_data_func(cell_description, get_cell_data, (float, column))
-			view.append_column(column_description)
+		completion = gtk.EntryCompletion()
+		self.currency.set_completion(completion)
+		liststore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
+		completion.set_model(liststore)
+		completion.set_text_column(0)
+		for code, label in self.currencies.items():
+			liststore.append([self.format_currency(label, code), code])
+		completion.set_match_func(self.match_func, 0)
+		completion.connect("match-selected", self.on_completion_selection, 1)
 
+		if invest.CONFIG.has_key("currency"):
+			code = invest.CONFIG["currency"];
+			if self.currencies.has_key(code):
+				self.currency_code = code;
+				currency = self.format_currency(self.currencies[self.currency_code], self.currency_code)
+				self.currency.set_text(currency)
 
 		for n in xrange (0, 5):
-			create_cell (self.treeview, n, self.names[n], self.typs[n])		
+			self.create_cell (self.treeview, n, self.names[n], self.typs[n])
+		if self.currency_code != None:
+			self.add_exchange_column()
+
 		stock_items = invest.STOCKS.items ()
 		stock_items.sort ()
 		for key, data in stock_items:
 			label = data["label"]
 			purchases = data["purchases"]
 			for purchase in purchases:
-				store.append([key, label, purchase["amount"], purchase["bought"], purchase["comission"]])
+				if purchase.has_key("exchange"):
+					exchange =  purchase["exchange"]
+				else:
+					exchange = 0.0
+				store.append([key, label, purchase["amount"], purchase["bought"], purchase["comission"], exchange])
 
 		try:
 			pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(join(invest.ART_DATA_DIR, "invest-16.png"), -1,-1)
@@ -95,6 +76,63 @@ class PrefsDialog:
 
 		self.sync_ui()
 
+	def on_cell_edited(self, cell, path, new_text, col, typ):
+		try:
+			if col == 0:    # stock symbols must be uppercase
+				new_text = str.upper(new_text)
+			if col < 2:
+				self.model[path][col] = new_text
+			else:
+				value = locale.atof(new_text)
+				self.model[path][col] = value
+		except Exception, msg:
+			invest.error('Exception while processing cell change: %s' % msg)
+			pass
+
+	def format(self, fmt, value):
+		return locale.format(fmt, value, True)
+
+	def get_cell_data(self, column, cell, model, iter, data):
+		typ, col = data
+		if typ == int:
+			cell.set_property('text', "%d" % typ(model[iter][col]))
+		elif typ == float:
+			# provide float numbers with at least 2 fractional digits
+			val = model[iter][col]
+			digits = self.fraction_digits(val)
+			fmt = "%%.%df" % max(digits, 2)
+			cell.set_property('text', self.format(fmt, val))
+		else:
+			cell.set_property('text', typ(model[iter][col]))
+
+	# determine the number of non zero digits in the fraction of the value
+	def fraction_digits(self, value):
+		text = "%g" % value	# do not use locale here, so that %g always is rendered to a number with . as decimal separator
+		if text.find(".") < 0:
+			return 0
+		return len(text) - text.find(".") - 1
+
+	def create_cell (self, view, column, name, typ):
+		cell_description = gtk.CellRendererText ()
+		if typ == float:
+			cell_description.set_property("xalign", 1.0)
+		cell_description.set_property("editable", True)
+		cell_description.connect("edited", self.on_cell_edited, column, typ)
+		column_description = gtk.TreeViewColumn (name, cell_description)
+		if typ == str:
+			column_description.set_attributes (cell_description, text=column)
+			column_description.set_sort_column_id(column)
+		if typ == float:
+			column_description.set_cell_data_func(cell_description, self.get_cell_data, (float, column))
+		view.append_column(column_description)
+
+	def add_exchange_column(self):
+		self.create_cell (self.treeview, 5, self.names[5], self.typs[5])
+
+	def remove_exchange_column(self):
+		column = self.treeview.get_column(5)
+		self.treeview.remove_column(column)
+
 	def show_run_hide(self, explanation = ""):
 		expl = self.ui.get_object("explanation")
 		expl.set_markup(explanation)
@@ -117,6 +155,7 @@ class PrefsDialog:
 				"amount": float(model[iter][2]),
 				"bought": float(model[iter][3]),
 				"comission": float(model[iter][4]),
+				"exchange": float(model[iter][5])
 			})
 		self.model.foreach(save_symbol)
 		try:
@@ -125,12 +164,20 @@ class PrefsDialog:
 		except Exception, msg:
 			invest.error('Could not save stocks file: %s' % msg)
 
+		invest.CONFIG = {}
+		if self.currency_code != None and len(self.currency_code) == 3:
+			invest.CONFIG['currency'] = self.currency_code
+		try:
+			cPickle.dump(invest.CONFIG, file(invest.CONFIG_FILE, 'w'))
+			invest.debug('Configuration written to file')
+		except Exception, msg:
+			invest.debug('Could not save configuration file: %s' % msg)
 
 	def sync_ui(self):
 		pass
 
 	def on_add_stock(self, w):
-		iter = self.model.append(["GOOG", "Google Inc.", 0, 0, 0])
+		iter = self.model.append(["GOOG", "Google Inc.", 0, 0, 0, 0])
 		path = self.model.get_path(iter)
 		self.treeview.set_cursor(path, self.treeview.get_column(0), True)
 
@@ -145,5 +192,97 @@ class PrefsDialog:
 
 		return False
 
+	def format_currency(self, label, code):
+		if code == None:
+			return label
+		return label + " (" + code + ")"
+
+	def on_entry_keypress(self, w, event):
+		# enter key was pressed
+		if event.keyval == 65293:
+			self.match_currency()
+		return False
+
+	# entry was activated (Enter)
+	def on_activate_entry(self, entry):
+		self.match_currency()
+
+	# entry left focus
+	def on_focus_out_entry(self, w, event):
+		self.match_currency()
+		return False
+
+	# tries to find a currency for the text in the currency entry
+	def match_currency(self):
+		# get the text
+		text = self.currency.get_text().upper()
+
+		# if there is none, finish
+		if len(text) == 0:
+			self.currency_code = None
+			self.pick_currency(None)
+			return
+
+		# if it is a currency code, take that one
+		if len(text) == 3:
+			# try to find the string as code
+			if self.currencies.has_key(text):
+				self.pick_currency(text)
+				return
+		else:
+			# try to find the code for the string
+			for code, label in self.currencies.items():
+				# if the entry equals to the full label,
+				# or the entry equals to the label+code concat
+				if label.upper() == text or self.format_currency(label.upper(), code) == text:
+					# then we take that code
+					self.pick_currency(code)
+					return
+
+		# the entry is not valid, reuse the old one
+		self.pick_currency(self.currency_code)
+
+	# pick this currency, stores it and sets the entry text
+	def pick_currency(self, code):
+		if code == None:
+			label = ""
+			if len(self.treeview.get_columns()) == 6:
+				self.remove_exchange_column()
+		else:
+			label = self.currencies[code]
+			if len(self.treeview.get_columns()) == 5:
+				self.add_exchange_column()
+		self.currency.set_text(self.format_currency(label, code))
+		self.currency_code = code
+
+	# finds matches by testing candidate strings to have tokens starting with the entered text's tokens
+	def match_func(self, completion, key, iter, column):
+		keys = key.split()
+		model = completion.get_model()
+		text = model.get_value(iter, column).lower()
+		tokens = text.split()
+
+		# each key must have a match
+		for key in keys:
+			found_key = False
+			# check any token of the completions start with the key
+			for token in tokens:
+				# remove the ( from the currency code
+				if token.startswith("("):
+					token = token[1:]
+				if token.startswith(key):
+					found_key = True
+					break
+			# this key does not have a match, this is not a completion
+			if not found_key:
+				return False
+		# all keys matched, this is a completion
+		return True
+
+	# stores the selected currency code
+	def on_completion_selection(self, completion, model, iter, column):
+		self.pick_currency(model.get_value(iter, column))
+
+
 def show_preferences(applet, explanation = ""):
 	PrefsDialog(applet).show_run_hide(explanation)
diff --git a/invest-applet/invest/quotes.py b/invest-applet/invest/quotes.py
index c123978..93b8b68 100644
--- a/invest-applet/invest/quotes.py
+++ b/invest-applet/invest/quotes.py
@@ -8,14 +8,15 @@ import datetime
 from threading import Thread
 
 import invest, invest.about, invest.chart
+import currencies
 
 CHUNK_SIZE = 512*1024 # 512 kB
 AUTOREFRESH_TIMEOUT = 15*60*1000 # 15 minutes
 
-QUOTES_URL="http://finance.yahoo.com/d/quotes.csv?s=%(s)s&f=sl1d1t1c1ohgv&e=.csv"
+QUOTES_URL="http://finance.yahoo.com/d/quotes.csv?s=%(s)s&f=snc4l1d1t1c1ohgv&e=.csv"
 
-# Sample (25/4/2008): UCG.MI,"4,86",09:37:00,2008/04/25,"0,07","4,82","4,87","4,82",11192336
-QUOTES_CSV_FIELDS=["ticker", ("trade", float), "time", "date", ("variation", float), ("open", float)]
+# Sample (09/2/2010): "UCG.MI","UNICREDIT","EUR","UCG.MI",1.9410,"2/9/2010","6:10am",+0.0210,1.9080,1.9810,1.8920,166691232
+QUOTES_CSV_FIELDS=["ticker", "label", "currency", ("trade", float), "date", "time", ("variation", float), ("open", float), ("high", float), ("low", float), ("volume", int)]
 
 # based on http://www.johnstowers.co.nz/blog/index.php/2007/03/12/threading-and-pygtk/
 class _IdleObject(gobject.GObject):
@@ -49,6 +50,7 @@ class QuotesRetriever(Thread, _IdleObject):
 		self.tickers = tickers
 		self.retrieved = False
 		self.data = []
+		self.currencies = []
 
 	def run(self):
 		quotes_url = QUOTES_URL % {"s": self.tickers}
@@ -68,9 +70,9 @@ class QuoteUpdater(gtk.ListStore):
 	last_updated = None
 	quotes_valid = False
 	timeout_id = None
-	SYMBOL, LABEL, TICKER_ONLY, BALANCE, BALANCE_PCT, VALUE, VARIATION_PCT, PB = range(8)
+	SYMBOL, LABEL, CURRENCY, TICKER_ONLY, BALANCE, BALANCE_PCT, VALUE, VARIATION_PCT, PB = range(9)
 	def __init__ (self, change_icon_callback, set_tooltip_callback):
-		gtk.ListStore.__init__ (self, gobject.TYPE_STRING, gobject.TYPE_STRING, bool, float, float, float, float, gtk.gdk.Pixbuf)
+		gtk.ListStore.__init__ (self, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, bool, float, float, float, float, gtk.gdk.Pixbuf)
 		self.set_update_interval(AUTOREFRESH_TIMEOUT)
 		self.change_icon_callback = change_icon_callback
 		self.set_tooltip_callback = set_tooltip_callback
@@ -134,16 +136,31 @@ class QuoteUpdater(gtk.ListStore):
 			self.populate(self.parse_yahoo_csv(csv.reader(retriever.data)))
 			self.updated = True
 			self.last_updated = datetime.datetime.now()
-			tooltip = []
-			if self.simple_quotes_count > 0:
-				# Translators: This is share-market jargon. It is the percentage change in the price of a stock. The %% gets changed to a single percent sign and the %s gets replaced with the string value of the change (localized), including the percent sign.
-				tooltip.append(_('Quotes average change %%: %s') % self.format_percent(self.avg_simple_quotes_change))
-			if self.positions_count > 0:
-				# Translators: This is share-market jargon. It refers to the total difference between the current price and purchase price for all the shares put together. i.e. How much money would be earned if they were sold right now.
-				tooltip.append(_('Positions balance: %s') % self.format_difference(self.positions_balance))
-			tooltip.append(_('Updated at %s') % self.last_updated.strftime("%H:%M"))
-			self.set_tooltip_callback('\n'.join(tooltip))
+			self.update_tooltip()
 
+	def on_currency_retriever_completed(self, retriever):
+		if retriever.retrieved == False:
+			invest.error("Failed to retrieve currency rates!")
+		else:
+			self.convert_currencies(self.parse_yahoo_csv(csv.reader(retriever.data)))
+		self.update_tooltip()
+
+	def update_tooltip(self):
+		tooltip = []
+		if self.simple_quotes_count > 0:
+			# Translators: This is share-market jargon. It is the average percentage change of all stock prices. The %s gets replaced with the string value of the change (localized), including the percent sign.
+			tooltip.append(_('Average change: %s') % self.format_percent(self.avg_simple_quotes_change))
+		for currency, stats in self.statistics.items():
+			# get the statsitics
+			balance = stats["balance"]
+			paid = stats["paid"]
+			change = self.format_percent(balance / paid * 100)
+			balance = self.format_difference(balance)
+
+			# Translators: This is share-market jargon. It refers to the total difference between the current price and purchase price for all the shares put together for a particular currency. i.e. How much money would be earned if they were sold right now. The first string is the change value, the second the currency, and the third value is the percentage of the change, formatted using user's locale.
+			tooltip.append(_('Positions balance: %s %s (%s)') % (balance, currency, change))
+		tooltip.append(_('Updated at %s') % self.last_updated.strftime("%H:%M"))
+		self.set_tooltip_callback('\n'.join(tooltip))
 
 
 	def parse_yahoo_csv(self, csvreader):
@@ -168,20 +185,54 @@ class QuoteUpdater(gtk.ListStore):
 				result[fields[0]]['variation_pct'] = 0
 		return result
 
+	# Computes the balance of the given purchases using a certain current value
+	# and optionally a current exchange rate.
+	def balance(self, purchases, value, currentrate=None):
+		current = 0
+		paid = 0
+
+		for purchase in purchases:
+			if purchase["amount"] != 0:
+				buyrate = purchase["exchange"]
+				# if the buy rate is invalid, use 1.0
+				if buyrate <= 0:
+					buyrate = 1.0
+
+				# if no current rate is given, ignore buy rate
+				if currentrate == None:
+					buyrate = 1.0
+					rate = 1.0
+				else:
+					# otherwise, take use buy rate and current rate to compute the balance
+					rate = currentrate
+
+				# current value is the current rate * amount * value
+				current += rate * purchase["amount"] * value
+				# paid is buy rate * ( amount * price + commission )
+				paid += buyrate * (purchase["amount"] * purchase["bought"] + purchase["comission"])
+
+		balance = current - paid
+		if paid != 0:
+			change = 100*balance/paid
+		else:
+			change = 100 # Not technically correct, but it will look more intuitive than the real result of infinity.
+
+		return (balance, change)
+
 	def populate(self, quotes):
 		if (len(quotes) == 0):
 			return
 
 		self.clear()
-		
+		self.currencies = []
+
 		try:
 			quote_items = quotes.items ()
 			quote_items.sort ()
 
 			simple_quotes_change = 0
 			self.simple_quotes_count = 0
-			self.positions_balance = 0
-			self.positions_count = 0
+			self.statistics = {}
 
 			for ticker, val in quote_items:
 				pb = None
@@ -189,7 +240,31 @@ class QuoteUpdater(gtk.ListStore):
 				# get the label of this stock for later reuse
 				label = invest.STOCKS[ticker]["label"]
 				if len(label) == 0:
-					label = ticker
+					if len(val["label"]) != 0:
+						label = val["label"]
+					else:
+						label = ticker
+
+				# make sure the currency field is upper case
+				val["currency"] = val["currency"].upper();
+
+				# the currency of currency conversion rates like EURUSD=X is wrong in csv
+				# this can be fixed easily by reusing the latter currency in the symbol
+				if len(ticker) == 8 and ticker.endswith("=X"):
+					val["currency"] = ticker[3:6]
+
+				# indices should not have a currency, though yahoo says so
+				if ticker.startswith("^"):
+					val["currency"] = ""
+
+				# sometimes, funny currencies are returned (special characters), only consider known currencies
+				if len(val["currency"]) > 0 and val["currency"] not in currencies.Currencies.currencies:
+					invest.debug("Currency '%s' is not known, dropping" % val["currency"])
+					val["currency"] = ""
+
+				# if this is a currency not yet seen and different from the target currency, memorize it
+				if val["currency"] not in self.currencies and len(val["currency"]) > 0:
+					self.currencies.append(val["currency"])
 
 				# Check whether the symbol is a simple quote, or a portfolio value
 				is_simple_quote = True
@@ -200,19 +275,12 @@ class QuoteUpdater(gtk.ListStore):
 
 				if is_simple_quote:
 					self.simple_quotes_count += 1
-					row = self.insert(0, [ticker, label, True, 0, 0, val["trade"], val["variation_pct"], pb])
+					row = self.insert(0, [ticker, label, val["currency"], True, 0, 0, val["trade"], val["variation_pct"], pb])
 					simple_quotes_change += val['variation_pct']
 				else:
-					self.positions_count += 1
-					current = sum([purchase["amount"]*val["trade"] for purchase in invest.STOCKS[ticker]["purchases"] if purchase["amount"] != 0])
-					paid = sum([purchase["amount"]*purchase["bought"] + purchase["comission"] for purchase in invest.STOCKS[ticker]["purchases"] if purchase["amount"] != 0])
-					balance = current - paid
-					if paid != 0:
-						change = 100*balance/paid
-					else:
-						change = 100 # Not technically correct, but it will look more intuitive than the real result of infinity.
-					row = self.insert(0, [ticker, label, False, balance, change, val["trade"], val["variation_pct"], pb])
-					self.positions_balance += balance
+					(balance, change) = self.balance(invest.STOCKS[ticker]["purchases"], val["trade"])
+					row = self.insert(0, [ticker, label, val["currency"], False, balance, change, val["trade"], val["variation_pct"], pb])
+					self.add_balance_change(balance, change, val["currency"])
 
 				if len(ticker.split('.')) == 2:
 					url = 'http://ichart.europe.yahoo.com/h?s=%s' % ticker
@@ -248,6 +316,85 @@ class QuoteUpdater(gtk.ListStore):
 			invest.debug(quotes)
 			self.quotes_valid = False
 
+		# start retrieving currency conversion rates
+		if invest.CONFIG.has_key("currency"):
+			target_currency = invest.CONFIG["currency"]
+			symbols = []
+
+			invest.debug("These currencies occur: %s" % self.currencies)
+			for currency in self.currencies:
+				if currency == target_currency:
+					continue
+
+				invest.debug("%s will be converted to %s" % ( currency, target_currency ))
+				symbol = currency + target_currency + "=X"
+				symbols.append(symbol)
+
+			if len(symbols) > 0:
+				tickers = '+'.join(symbols)
+				quotes_retriever = QuotesRetriever(tickers)
+				quotes_retriever.connect("completed", self.on_currency_retriever_completed)
+				quotes_retriever.start()
+
+	def convert_currencies(self, quotes):
+		# if there is no target currency, this method should never have been called
+		if not invest.CONFIG.has_key("currency"):
+			return
+
+		# reset the overall balance
+		self.statistics = {}
+
+		# collect the rates for the currencies
+		rates = {}
+		for symbol, data in quotes.items():
+			currency = symbol[0:3]
+			rate = data["trade"]
+			rates[currency] = rate
+
+		# convert all non target currencies
+		target_currency = invest.CONFIG["currency"]
+		iter = self.get_iter_first()
+		while iter != None:
+			currency = self.get_value(iter, self.CURRENCY)
+			symbol = self.get_value(iter, self.SYMBOL)
+			# ignore stocks that are currency conversions
+			# and only convert stocks that are not in the target currency
+			# and if we have a conversion rate
+			if not ( len(symbol) == 8 and symbol[6:8] == "=X" ) and \
+			   currency != target_currency and rates.has_key(currency):
+				# first convert the balance, it needs the original value
+				if not self.get_value(iter, self.TICKER_ONLY):
+					ticker = self.get_value(iter, self.SYMBOL)
+					value = self.get_value(iter, self.VALUE)
+					(balance, change) = self.balance(invest.STOCKS[ticker]["purchases"], value, rates[currency])
+					self.set(iter, self.BALANCE, balance)
+					self.set(iter, self.BALANCE_PCT, change)
+					self.add_balance_change(balance, change, target_currency)
+
+				# now, convert the value
+				value = self.get_value(iter, self.VALUE)
+				value *= rates[currency]
+				self.set(iter, self.VALUE, value)
+				self.set(iter, self.CURRENCY, target_currency)
+
+			else:
+				# consider non-converted stocks here
+				balance = self.get_value(iter, self.BALANCE)
+				change  = self.get_value(iter, self.BALANCE_PCT)
+				self.add_balance_change(balance, change, currency)
+
+			iter = self.iter_next(iter)
+
+	def add_balance_change(self, balance, change, currency):
+		if balance == 0 and change == 0:
+			return
+
+		if self.statistics.has_key(currency):
+			self.statistics[currency]["balance"] += balance
+			self.statistics[currency]["paid"] += balance/change*100
+		else:
+			self.statistics[currency] = { "balance" : balance, "paid" : balance/change*100 }
+
 	def set_pb_callback(self, retriever, row):
 		self.set_value(row, self.PB, retriever.image.get_pixbuf())
 
diff --git a/invest-applet/invest/widgets.py b/invest-applet/invest/widgets.py
index 37ed6f6..13a2ec9 100644
--- a/invest-applet/invest/widgets.py
+++ b/invest-applet/invest/widgets.py
@@ -89,23 +89,23 @@ class InvestWidget(gtk.TreeView):
 
 
 	# locale-aware formatting of the value as monetary, without currency symbol, using 2 decimal digits
-	def format_currency(self, value):
-		return locale.format("%.2f", value, True, True)
+	def format_currency(self, value, currency):
+		return locale.format("%.2f", value, True, True) + " " + currency
 
 	# locale-aware formatting of the percent float (decimal point, thousand grouping point) with 2 decimal digits
 	def format_percent(self, value):
 		return locale.format("%+.2f", value, True) + "%"
 
 	# locale-aware formatting of the float value (decimal point, thousand grouping point) with sign and 2 decimal digits
-	def format_difference(self, value):
-		return locale.format("%+.2f", value, True, True)
+	def format_difference(self, value, currency):
+		return locale.format("%+.2f", value, True, True) + " " + currency
 
 
 	def _getcelldata_label(self, column, cell, model, iter):
 		cell.set_property('text', model[iter][model.LABEL])
 
 	def _getcelldata_value(self, column, cell, model, iter):
-		cell.set_property('text', self.format_currency(model[iter][model.VALUE]))
+		cell.set_property('text', self.format_currency(model[iter][model.VALUE], model[iter][model.CURRENCY]))
 
 	def is_selected(self, model, iter):
 		m, it = self.get_selection().get_selected()
@@ -133,7 +133,7 @@ class InvestWidget(gtk.TreeView):
 		if is_ticker_only:
 			cell.set_property('text', '')
 		else:
-			balance = self.format_difference(model[iter][model.BALANCE])
+			balance = self.format_difference(model[iter][model.BALANCE], model[iter][model.CURRENCY])
 			cell.set_property('markup',
 				"<span foreground='%s'>%s</span>" %
 				(color, balance))



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]