[gnome-applets] invest applet: introduces index value expansion and tree organization of quotes - indices like the N
- From: Enrico Minack <eminack src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-applets] invest applet: introduces index value expansion and tree organization of quotes - indices like the N
- Date: Mon, 11 Jul 2011 19:43:52 +0000 (UTC)
commit bc1169f981551fa96417a03fea08ea2b8099907a
Author: Enrico Minack <enrico-minack gmx de>
Date: Mon Jul 11 21:35:30 2011 +0200
invest applet: introduces index value expansion and tree organization of quotes
- indices like the NASDAQ can automatically be expanded to its contained stocks
- stocks can be arranged into groups and sub-groups, allowing tree organization
invest-applet/data/prefs-dialog.ui | 59 ++++++-
invest-applet/invest/__init__.py | 34 +++-
invest-applet/invest/applet.py | 9 +
invest-applet/invest/preferences.py | 184 ++++++++++++++----
invest-applet/invest/quotes.py | 361 ++++++++++++++++++++++++++---------
invest-applet/invest/widgets.py | 35 +++-
6 files changed, 534 insertions(+), 148 deletions(-)
---
diff --git a/invest-applet/data/prefs-dialog.ui b/invest-applet/data/prefs-dialog.ui
index fed2e2a..ae3c5c7 100644
--- a/invest-applet/data/prefs-dialog.ui
+++ b/invest-applet/data/prefs-dialog.ui
@@ -128,6 +128,7 @@
<property name="can_focus">True</property>
<property name="reorderable">True</property>
<property name="rules_hint">True</property>
+ <property name="enable_tree_lines">True</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview-selection1"/>
</child>
@@ -147,7 +148,7 @@
<property name="spacing">6</property>
<property name="homogeneous">True</property>
<child>
- <object class="GtkButton" id="add">
+ <object class="GtkButton" id="addstock">
<property name="label">gtk-add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
@@ -162,6 +163,20 @@
</packing>
</child>
<child>
+ <object class="GtkButton" id="addgroup">
+ <property name="label" translatable="yes" context=" " comments="Instead of adding a single stock to the list of stocks, the 'Add Group' button adds a group (kind of a sub folder) to which numerous stocks can be added. A group here refers to a group of stocks.">Add Group</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
<object class="GtkButton" id="remove">
<property name="label">gtk-remove</property>
<property name="visible">True</property>
@@ -173,7 +188,7 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
- <property name="position">1</property>
+ <property name="position">2</property>
</packing>
</child>
</object>
@@ -184,6 +199,43 @@
</packing>
</child>
<child>
+ <object class="GtkCheckButton" id="indexexpansion">
+ <property name="label" translatable="yes" comments="An index value (for instance the NASDAQ Composite) is based on a number of stocks. This option allows to also show the quotes of the stocks an index is based on. ">Show stocks of index values</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="has_tooltip">True</property>
+ <property name="tooltip_markup" translatable="yes">An index value, for instance the <i>NASDAQ Composite</i> (^IXIC), is based on a number of stocks. This option allows to also show the quotes of the <i><b>stocks</b></i> an index is based on.</property>
+ <property name="use_action_appearance">False</property>
+ <property name="xalign">0</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="hidecharts">
+ <property name="label" translatable="yes">Hide charts in quotes list</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">A small chart image is shown next to each quote. The retrieval of each chart image causes network traffic. Hiding charts reduces the network bandwidth demand significantly.</property>
+ <property name="use_action_appearance">False</property>
+ <property name="relief">none</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
<object class="GtkLabel" id="default_info">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -194,7 +246,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
- <property name="position">2</property>
+ <property name="position">4</property>
</packing>
</child>
</object>
@@ -233,6 +285,7 @@
<object class="GtkEntry" id="currency">
<property name="visible">True</property>
<property name="can_focus">True</property>
+ <property name="tooltip_text" translatable="yes">Type the target currency to which all stock quotes will be converted to.</property>
</object>
</child>
</object>
diff --git a/invest-applet/invest/__init__.py b/invest-applet/invest/__init__.py
index 014d42d..986284d 100644
--- a/invest-applet/invest/__init__.py
+++ b/invest-applet/invest/__init__.py
@@ -122,22 +122,38 @@ def update_to_exchange_stock_format(stocks):
return stocks
+# converts the given stocks from the dict format into a list
+def update_to_list_stock_format(stocks):
+ new = []
+
+ for ticker, stock in stocks.items():
+ stock['ticker'] = ticker
+ new.append(stock)
+
+ return new
+
STOCKS_FILE = join(USER_INVEST_DIR, "stocks.pickle")
try:
STOCKS = cPickle.load(file(STOCKS_FILE))
- # 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 contains a list, the subsequent tests are obsolete
+ if type(STOCKS) != list:
+ # 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)
+
+ # here, stocks is a most up-to-date dict, but we need it to be a list
+ STOCKS = update_to_list_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 = {}
+ STOCKS = []
#STOCKS = {
# "AAPL": {
@@ -164,7 +180,9 @@ except Exception, msg:
error("Could not load the configuration from %s: %s" % (CONFIG_FILE, msg) )
CONFIG = {} # default configuration
+CURRENCIES_FILE = join(USER_INVEST_DIR, "currencies.csv")
QUOTES_FILE = join(USER_INVEST_DIR, "quotes.csv")
+INDEX_QUOTES_FILE_TEMPLATE = "quotes.#.csv"
# set default proxy config
diff --git a/invest-applet/invest/applet.py b/invest-applet/invest/applet.py
index a4adff2..ac013c2 100644
--- a/invest-applet/invest/applet.py
+++ b/invest-applet/invest/applet.py
@@ -99,6 +99,9 @@ class InvestmentsListWindow(Gtk.Window):
def __init__(self, applet, list):
Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL)
self.list = list
+ self.list.connect('row-collapsed', self.on_collapsed)
+ self.list.connect('row-expanded', self.on_expanded)
+
self.set_type_hint(Gdk.WindowTypeHint.DOCK)
self.stick()
self.set_border_width(WidgetBorderWidth)
@@ -124,6 +127,12 @@ class InvestmentsListWindow(Gtk.Window):
self.hide()
self.hidden = True
+ def on_collapsed(self, treeview, iter, path):
+ self.update_position()
+
+ def on_expanded(self, treeview, iter, path):
+ self.update_position()
+
def update_position (self):
"""
Calculates the position and moves the window to it.
diff --git a/invest-applet/invest/preferences.py b/invest-applet/invest/preferences.py
index 0aadc63..caff3c4 100644
--- a/invest-applet/invest/preferences.py
+++ b/invest-applet/invest/preferences.py
@@ -16,9 +16,22 @@ class PrefsDialog:
self.currency = self.ui.get_object("currency")
self.currency_code = None
self.currencies = currencies.Currencies.currencies
+ self.indexexpansion = self.ui.get_object("indexexpansion")
+ if invest.CONFIG.has_key('indexexpansion'):
+ self.indexexpansion.set_active(invest.CONFIG['indexexpansion'])
+ else:
+ self.indexexpansion.set_active(False)
+
+ self.hidecharts = self.ui.get_object("hidecharts")
+ if invest.CONFIG.has_key('hidecharts'):
+ self.hidecharts.set_active(invest.CONFIG['hidecharts'])
+ else:
+ self.hidecharts.set_active(False)
- 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("addstock").connect('clicked', self.on_add, False)
+ self.ui.get_object("addstock").connect('activate', self.on_add, False)
+ self.ui.get_object("addgroup").connect('clicked', self.on_add, True)
+ self.ui.get_object("addgroup").connect('activate', self.on_add, True)
self.ui.get_object("remove").connect('clicked', self.on_remove_stock)
self.ui.get_object("remove").connect('activate', self.on_remove_stock)
self.ui.get_object("help").connect('clicked', self.on_help)
@@ -29,7 +42,7 @@ class PrefsDialog:
self.typs = (str, str, float, float, float, float)
self.names = (_("Symbol"), _("Label"), _("Amount"), _("Price"), _("Commission"), _("Currency Rate"))
- store = Gtk.ListStore(*self.typs)
+ store = Gtk.TreeStore(*self.typs)
store.set_sort_column_id(0, Gtk.SortType.ASCENDING)
self.treeview.set_model(store)
self.model = store
@@ -56,23 +69,22 @@ class PrefsDialog:
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:
- if purchase.has_key("exchange"):
- exchange = purchase["exchange"]
- else:
- exchange = 0.0
- store.append([key, label, float(purchase["amount"]), float(purchase["bought"]), float(purchase["comission"]), float(exchange)])
+ self.add_to_store(store, None, invest.STOCKS)
self.sync_ui()
+ def is_group(self, iter):
+ return self.model[iter][1] == None
+
+ def is_stock(self, iter):
+ return not self.is_group(iter)
+
def on_cell_edited(self, cell, path, new_text, col, typ):
+ if col != 0 and self.is_group(self.model.get_iter(path)):
+ return
+
try:
- if col == 0: # stock symbols must be uppercase
+ if col == 0 and self.is_stock(self.model.get_iter(path)): # stock symbols must be uppercase
new_text = str.upper(new_text)
if col < 2:
self.model[path][col] = new_text
@@ -88,16 +100,24 @@ class PrefsDialog:
def get_cell_data(self, column, cell, model, iter, data):
typ, col = data
+ if self.is_group(iter):
+ if col == 0:
+ val = model[iter][col]
+ cell.set_property('markup', "<b>%s</b>" % typ(val))
+ else:
+ cell.set_property('text', "")
+ return
+
+ val = model[iter][col]
if typ == int:
- cell.set_property('text', "%d" % typ(model[iter][col]))
+ cell.set_property('text', "%d" % typ(val))
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]))
+ cell.set_property('text', typ(val))
# determine the number of non zero digits in the fraction of the value
def fraction_digits(self, value):
@@ -114,10 +134,8 @@ class PrefsDialog:
cell_description.connect("edited", self.on_cell_edited, column, typ)
column_description = Gtk.TreeViewColumn (name, cell_description)
if typ == str:
- column_description.add_attribute(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))
+ column_description.set_cell_data_func(cell_description, self.get_cell_data, (typ, column))
view.append_column(column_description)
def add_exchange_column(self):
@@ -138,49 +156,133 @@ class PrefsDialog:
pass
self.dialog.destroy()
- invest.STOCKS = {}
-
- def save_symbol(model, path, iter, data):
- #if int(model[iter][1]) == 0 or float(model[iter][2]) < 0.0001:
- # return
-
- if not model[iter][0] in invest.STOCKS:
- invest.STOCKS[model[iter][0]] = { 'label': model[iter][1], 'purchases': [] }
-
- invest.STOCKS[model[iter][0]]["purchases"].append({
- "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, None)
+
+ # transform the stocks treestore into the STOCKS list
+ invest.STOCKS = self.to_list(self.model.get_iter_first())
+
+ # store the STOCKS into the pickles file
try:
cPickle.dump(invest.STOCKS, file(invest.STOCKS_FILE, 'w'))
invest.debug('Stocks written to file')
except Exception, msg:
invest.error('Could not save stocks file: %s' % msg)
+ # store the CONFIG (currency, index expansion) into the config file
invest.CONFIG = {}
if self.currency_code != None and len(self.currency_code) == 3:
invest.CONFIG['currency'] = self.currency_code
+ invest.CONFIG['indexexpansion'] = self.indexexpansion.get_active()
+ invest.CONFIG['hidecharts'] = self.hidecharts.get_active()
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 to_list(self, iter):
+ stocks = {}
+ groups = []
+
+ while iter != None:
+ if self.is_stock(iter):
+ ticker = self.model[iter][0]
+ if not ticker in stocks:
+ stocks[ticker] = {
+ 'ticker': ticker,
+ 'label': self.model[iter][1],
+ 'purchases': []
+ }
+ stocks[ticker]["purchases"].append({
+ "amount": float(self.model[iter][2]),
+ "bought": float(self.model[iter][3]),
+ "comission": float(self.model[iter][4]),
+ "exchange": float(self.model[iter][5])
+ })
+
+ else:
+ name = self.model[iter][0]
+ list = self.to_list(self.model.iter_children(iter))
+ groups.append({ 'name': name, 'list': list })
+
+ iter = self.model.iter_next(iter)
+
+ groups.extend(stocks.values())
+ return groups
+
+ def add_to_store(self, store, parent, stocks):
+ for stock in stocks:
+ if not stock.has_key('ticker'):
+ name = stock['name']
+ list = stock['list']
+ row = store.append(parent, [name, None, None, None, None, None])
+ self.add_to_store(store, row, list)
+ else:
+ ticker = stock['ticker']
+ label = stock["label"]
+ purchases = stock["purchases"]
+ for purchase in purchases:
+ if purchase.has_key("exchange"):
+ exchange = purchase["exchange"]
+ else:
+ exchange = 0.0
+ store.append(parent, [ticker, label, float(purchase["amount"]), float(purchase["bought"]), float(purchase["comission"]), float(exchange)])
+
def sync_ui(self):
pass
- def on_add_stock(self, w):
- iter = self.model.append(["GOOG", "Google Inc.", 0.0, 0.0, 0.0, 0.0])
- path = self.model.get_path(iter)
+ def on_add(self, w, group = False):
+ # get the selected row
+ (model, parent) = self.treeview.get_selection().get_selected()
+
+ # if the selected row is a stock, move to its parent
+ while parent != None and self.is_stock(parent):
+ parent = model.iter_parent(parent)
+
+ # add a new row as child of the selected row
+ if group:
+ iter = self.model.append(parent, [_("Stock Group"), None, None, None, None, None])
+ # select the group and add a stock
+ path = self.model.get_path(iter)
+ self.treeview.set_cursor(path, None, False)
+ self.on_add(w, False)
+ # make sure the new group is elapsed
+ self.treeview.expand_row(path, False)
+ # select the new group for editing
+ else:
+ iter = self.model.append(parent, ["YHOO", "Yahoo! Inc.", 0.0, 0.0, 0.0, 0.0])
+ # make sure the new stock's parent is elapsed
+ if parent != None:
+ path = self.model.get_path(parent)
+ self.treeview.expand_row(path, False)
+ # select the new row for editing
+ path = self.model.get_path(iter)
self.treeview.set_cursor(path, self.treeview.get_column(0), True)
+
def on_remove_stock(self, w):
model, paths = self.treeview.get_selection().get_selected_rows()
for path in paths:
- model.remove(model.get_iter(path))
+ iter = model.get_iter(path)
+ if model.iter_n_children(iter) > 0:
+ # translators: Asks the user to confirm deletion of a group of stocks
+ dialog = Gtk.Dialog(_("Delete entire stock group?"),
+ None,
+ Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
+ Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT))
+ # translators: stocks can be grouped together into a "stock group".
+ # The user wants to delete a group, but the group still contains stocks.
+ # By deleting the group, also all stocks will be removed from configuration
+ label = Gtk.Label(_("This removes all stocks contained in this stock group!\nDo you really want to remove this stock group?"))
+ dialog.get_content_area().pack_start(label, True, True, 10)
+ label.show()
+ response = dialog.run()
+ dialog.destroy()
+ if response == Gtk.ResponseType.REJECT:
+ continue
+
+ model.remove(iter)
def on_help(self, w):
invest.help.show_help_section("invest-applet-usage")
diff --git a/invest-applet/invest/quotes.py b/invest-applet/invest/quotes.py
index 38eed6a..767b6ea 100644
--- a/invest-applet/invest/quotes.py
+++ b/invest-applet/invest/quotes.py
@@ -6,10 +6,13 @@ import locale
from urllib import urlopen
import datetime
from threading import Thread
-
+from os import listdir, unlink
+import re
import invest, invest.about, invest.chart
import currencies
+# TODO: start currency retrieval after _all_ index expansion completed !!!
+
CHUNK_SIZE = 512*1024 # 512 kB
AUTOREFRESH_TIMEOUT = 15*60*1000 # 15 minutes
@@ -51,11 +54,10 @@ class QuotesRetriever(Thread, _IdleObject):
self.retrieved = False
self.data = []
self.currencies = []
- invest.debug("QuotesRetriever created");
def run(self):
- invest.debug("QuotesRetriever started");
quotes_url = QUOTES_URL % {"s": self.tickers}
+ invest.debug("QuotesRetriever started: %s" % quotes_url);
try:
quotes_file = urlopen(quotes_url, proxies = invest.PROXY)
self.data = quotes_file.read ()
@@ -67,26 +69,28 @@ class QuotesRetriever(Thread, _IdleObject):
self.emit("completed")
-class QuoteUpdater(Gtk.ListStore):
+class QuoteUpdater(Gtk.TreeStore):
updated = False
last_updated = None
quotes_valid = False
timeout_id = None
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, GObject.TYPE_STRING, bool, float, float, float, float, GdkPixbuf.Pixbuf)
+ Gtk.TreeStore.__init__ (self, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, bool, float, float, float, float, GdkPixbuf.Pixbuf)
self.set_update_interval(AUTOREFRESH_TIMEOUT)
self.change_icon_callback = change_icon_callback
self.set_tooltip_callback = set_tooltip_callback
self.set_sort_column_id(1, Gtk.SortType.ASCENDING)
- self.load() # read the last cached quotes file
- self.refresh() # download a new quotes file, this may fail if disconnected
+ self.load_quotes() # read the last cached quotes file
+ self.load_all_index_quotes() # expand indices if requested
+ self.load_currencies() # convert currencies from cached file
+ self.refresh() # download a new quotes file, this may fail if disconnected
# tell the network manager to notify me when network status changes
invest.nm.set_statechange_callback(self.nm_state_changed)
# loads the cached csv file and its last-modification-time as self.last_updated
- def load(self):
+ def load_quotes(self):
invest.debug("Loading quotes");
try:
f = open(invest.QUOTES_FILE, 'r')
@@ -101,7 +105,7 @@ class QuoteUpdater(Gtk.ListStore):
invest.error("Could not load the cached quotes file %s: %s" % (invest.QUOTES_FILE, msg) )
# stores the csv content on disk so it can be used on next start up
- def save(self, data):
+ def save_quotes(self, data):
invest.debug("Storing quotes")
try:
f = open(invest.QUOTES_FILE, 'w')
@@ -110,6 +114,95 @@ class QuoteUpdater(Gtk.ListStore):
except Exception, msg:
invest.error("Could not save the retrieved quotes file to %s: %s" % (invest.QUOTES_FILE, msg) )
+
+
+ def expand_indices(self):
+ if not ( invest.CONFIG.has_key('indexexpansion') and invest.CONFIG['indexexpansion'] ):
+ # retrieve currencies immediately
+ self.retrieve_currencies()
+ return
+
+ # trigger retrieval for each index
+ for index in self.get_indices(invest.STOCKS):
+ quotes_retriever = QuotesRetriever("@%s" % index)
+ quotes_retriever.connect("completed", self.on_index_quote_retriever_completed, index)
+ quotes_retriever.start()
+
+ def on_index_quote_retriever_completed(self, retriever, index):
+ if retriever.retrieved == False:
+ invest.error("Failed to retrieve quotes for index %s!" % index)
+ else:
+ self.save_index_quotes(index, retriever.data)
+ self.load_index_quotes(index)
+ self.retrieve_currencies()
+
+ def save_index_quotes(self, index, data):
+ # store the index quotes
+ invest.debug("Storing quotes of index %s" % index)
+ try:
+ filename = invest.INDEX_QUOTES_FILE_TEMPLATE.replace('#', index)
+ filename = join(invest.USER_INVEST_DIR, filename)
+ f = open(filename, 'w')
+ f.write(data)
+ f.close()
+ except Exception, msg:
+ invest.error("Could not save the retrieved index quotes file of %s to %s: %s" % (index, filename, msg) )
+ return
+
+ def load_index_quotes(self, index):
+ # load the file
+ try:
+ filename = invest.INDEX_QUOTES_FILE_TEMPLATE.replace('#', index)
+ filename = join(invest.USER_INVEST_DIR, filename)
+ f = open(filename, 'r')
+ data = f.readlines()
+ f.close()
+ except Exception, msg:
+ invest.error("Could not load index quotes file %s of index %s: %s" % (filename, index, msg) )
+ return
+
+ # expand the index
+ self.expand_index(index, data)
+
+ def load_all_index_quotes(self):
+ if not ( invest.CONFIG.has_key('indexexpansion') and invest.CONFIG['indexexpansion'] ):
+ return
+
+ # load all existing index quotes files
+ files = listdir(invest.USER_INVEST_DIR)
+ for file in files:
+ # is this a index quote file?
+ m = re.match(invest.INDEX_QUOTES_FILE_TEMPLATE.replace('#', '([^.]+)'), file)
+ if m:
+ index = m.group(1)
+ filename = join(invest.USER_INVEST_DIR, file)
+
+ # load the file
+ f = open(filename, 'r')
+ data = f.readlines()
+ f.close()
+
+ # expand respective indices
+ if not self.expand_index(index, data):
+ # delete index file because the index is not used anymore
+ unlink(filename)
+
+ def expand_index(self, index, data):
+ invest.debug("Expanding index %s" % index)
+
+ quotes = self.parse_yahoo_csv(csv.reader(data))
+
+ nodes = self.find_stock(index)
+ for node in nodes:
+ for ticker in quotes.keys():
+ quote = self.get_quote(quotes, ticker)
+ row = self.insert(self.get_iter(node), 0, [ticker, quote["label"], quote["currency"], True, 0.0, 0.0, float(quote["trade"]), float(quote["variation_pct"]), None])
+ self.retrieve_image(ticker, row)
+
+ # indicate if index was found
+ return len(nodes) > 0
+
+
def set_update_interval(self, interval):
if self.timeout_id != None:
invest.debug("Canceling refresh timer")
@@ -139,16 +232,43 @@ class QuoteUpdater(Gtk.ListStore):
invest.debug("No stocks configured")
return True
- tickers = '+'.join(invest.STOCKS.keys())
- invest.debug("creating QuotesRetriever")
+ tickers = '+'.join( self.get_tickers(invest.STOCKS) )
quotes_retriever = QuotesRetriever(tickers)
quotes_retriever.connect("completed", self.on_retriever_completed)
- invest.debug("starting QuotesRetriever")
quotes_retriever.start()
- invest.debug("started QuotesRetriever")
return True
+ def get_tickers(self, stocks):
+ tickers = []
+ for stock in stocks:
+ if stock.has_key('ticker'):
+ ticker = stock['ticker']
+ tickers.append(ticker)
+ else:
+ tickers.extend(self.get_tickers(stock['list']))
+ return tickers
+
+ def get_indices(self, stocks):
+ indices = []
+ for stock in stocks:
+ if stock.has_key('ticker'):
+ ticker = stock['ticker']
+ if ticker.startswith('^'):
+ indices.append(ticker)
+ else:
+ indices.extend(self.get_indices(stock['list']))
+ return indices
+
+ def find_stock(self, symbol):
+ list = []
+ self.foreach(self.find_stock_cb, (symbol, list))
+ return list
+
+ def find_stock_cb(self, model, path, iter, data):
+ (symbol, list) = data
+ if model[path][0] == symbol:
+ list.append(str(path))
# locale-aware formatting of the percent float (decimal point, thousand grouping point) with 2 decimal digits
def format_percent(self, value):
@@ -160,22 +280,46 @@ class QuoteUpdater(Gtk.ListStore):
def on_retriever_completed(self, retriever):
if retriever.retrieved == False:
- invest.debug("QuotesRetriever failed");
+ invest.debug("QuotesRetriever failed for tickers '%s'" % retriever.tickers);
self.update_tooltip(_('Invest could not connect to Yahoo! Finance'))
else:
invest.debug("QuotesRetriever completed");
# cache the retrieved csv file
- self.save(retriever.data)
+ self.save_quotes(retriever.data)
# load the cache and parse it
- self.load()
+ self.load_quotes()
+ # expand index values if requested
+ self.expand_indices()
+
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.save_currencies(retriever.data)
+ self.load_currencies()
self.update_tooltip()
+ def save_currencies(self, data):
+ invest.debug("Storing currencies to %s" % invest.CURRENCIES_FILE)
+ try:
+ f = open(invest.CURRENCIES_FILE, 'w')
+ f.write(data)
+ f.close()
+ except Exception, msg:
+ invest.error("Could not save the retrieved currencies to %s: %s" % (invest.CURRENCIES_FILE, msg) )
+
+ def load_currencies(self):
+ invest.debug("Loading currencies from %s" % invest.CURRENCIES_FILE)
+ try:
+ f = open(invest.CURRENCIES_FILE, 'r')
+ data = f.readlines()
+ f.close()
+
+ self.convert_currencies(self.parse_yahoo_csv(csv.reader(data)))
+ except Exception, msg:
+ invest.error("Could not load the currencies from %s: %s" % (invest.CURRENCIES_FILE, msg) )
+
def update_tooltip(self, msg = None):
tooltip = []
if self.quotes_count > 0:
@@ -203,6 +347,10 @@ class QuoteUpdater(Gtk.ListStore):
if len(fields) == 0:
continue
+ if len(fields) != len(QUOTES_CSV_FIELDS):
+ invest.debug("CSV line has unexpected number of fields, expected %d, has %d: %s" % (len(QUOTES_CSV_FIELDS), len(fields), fields))
+ continue
+
result[fields[0]] = {}
for i, field in enumerate(QUOTES_CSV_FIELDS):
if type(field) == tuple:
@@ -264,73 +412,16 @@ class QuoteUpdater(Gtk.ListStore):
quote_items = quotes.items ()
quote_items.sort ()
- quotes_change = 0
+ self.quotes_change = 0
self.quotes_count = 0
self.statistics = {}
- for ticker, val in quote_items:
- pb = None
-
- # ignore unknown stocks
- if ticker not in invest.STOCKS.keys():
- invest.debug("Observed unknown stock: %s" % ticker)
- continue
-
- # get the label of this stock for later reuse
- label = invest.STOCKS[ticker]["label"]
- if len(label) == 0:
- 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
- for purchase in invest.STOCKS[ticker]["purchases"]:
- if purchase["amount"] != 0:
- is_simple_quote = False
- break
-
- if is_simple_quote:
- row = self.insert(0, [ticker, label, val["currency"], True, 0.0, 0.0, val["trade"], val["variation_pct"], pb])
- else:
- (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"])
-
- url = 'http://ichart.yahoo.com/h?s=%s' % ticker
-
- image_retriever = invest.chart.ImageRetriever(url)
- image_retriever.connect("completed", self.set_pb_callback, row)
- image_retriever.start()
-
- quotes_change += val['variation_pct']
- self.quotes_count += 1
+ # iterate over the STOCKS tree and build up this treestore
+ self.add_quotes(quotes, invest.STOCKS, None)
# we can only compute an avg quote change if there are quotes
if self.quotes_count > 0:
- self.avg_quotes_change = quotes_change/float(self.quotes_count)
+ self.avg_quotes_change = self.quotes_change/float(self.quotes_count)
# change icon
quotes_change_sign = 0
@@ -340,7 +431,6 @@ class QuoteUpdater(Gtk.ListStore):
else:
self.avg_quotes_change = 0
-
# mark quotes to finally be valid
self.quotes_valid = True
@@ -349,6 +439,8 @@ class QuoteUpdater(Gtk.ListStore):
invest.debug(quotes)
self.quotes_valid = False
+
+ def retrieve_currencies(self):
# start retrieving currency conversion rates
if invest.CONFIG.has_key("currency"):
target_currency = invest.CONFIG["currency"]
@@ -369,6 +461,90 @@ class QuoteUpdater(Gtk.ListStore):
quotes_retriever.connect("completed", self.on_currency_retriever_completed)
quotes_retriever.start()
+
+ def add_quotes(self, quotes, stocks, parent):
+ for stock in stocks:
+ if not stock.has_key('ticker'):
+ name = stock['name']
+ list = stock['list']
+ # here, the stock group name is used as the label,
+ # so in quotes, the key == None indicates a group
+ # in preferences, the label == None indicates this
+ try:
+ row = self.insert(parent, 0, [None, name, None, True, None, None, None, None, None])
+ except Exception, msg:
+ invest.debug("Failed to insert group %s: %s" % (name, msg))
+ self.add_quotes(quotes, list, row)
+ # Todo: update the summary statistics of row
+ else:
+ ticker = stock['ticker'];
+ if not quotes.has_key(ticker):
+ invest.debug("no quote for %s retrieved" % ticker)
+ continue
+
+ # get the quote
+ quote = self.get_quote(quotes, ticker)
+
+ # get the label of this stock for later reuse
+ label = stock["label"]
+ if len(label) == 0:
+ if len(quote["label"]) != 0:
+ label = quote["label"]
+ else:
+ label = ticker
+
+ # Check whether the symbol is a simple quote, or a portfolio value
+ try:
+ if self.is_simple_quote(stock):
+ row = self.insert(parent, 0, [ticker, label, quote["currency"], True, 0.0, 0.0, float(quote["trade"]), float(quote["variation_pct"]), None])
+ else:
+ (balance, change) = self.balance(stock["purchases"], quote["trade"])
+ row = self.insert(parent, 0, [ticker, label, quote["currency"], False, float(balance), float(change), float(quote["trade"]), float(quote["variation_pct"]), None])
+ self.add_balance_change(balance, change, quote["currency"])
+ except Exception, msg:
+ invest.debug("Failed to insert stock %s: %s" % (stock, msg))
+
+ self.quotes_change += quote['variation_pct']
+ self.quotes_count += 1
+
+ self.retrieve_image(ticker, row)
+
+ def retrieve_image(self, ticker, row):
+ if invest.CONFIG.has_key('hidecharts') and invest.CONFIG['hidecharts']:
+ return
+
+ url = 'http://ichart.yahoo.com/h?s=%s' % ticker
+ image_retriever = invest.chart.ImageRetriever(url)
+ image_retriever.connect("completed", self.set_pb_callback, row)
+ image_retriever.start()
+
+ def get_quote(self, quotes, ticker):
+ # the data for this quote
+ quote = quotes[ticker];
+
+ # make sure the currency field is upper case
+ quote["currency"] = quote["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"):
+ quote["currency"] = ticker[3:6]
+
+ # indices should not have a currency, though yahoo says so
+ if ticker.startswith("^"):
+ quote["currency"] = ""
+
+ # sometimes, funny currencies are returned (special characters), only consider known currencies
+ if len(quote["currency"]) > 0 and quote["currency"] not in currencies.Currencies.currencies:
+ invest.debug("Currency '%s' is not known, dropping" % quote["currency"])
+ quote["currency"] = ""
+
+ # if this is a currency not yet seen and different from the target currency, memorize it
+ if quote["currency"] not in self.currencies and len(quote["currency"]) > 0:
+ self.currencies.append(quote["currency"])
+
+ return quote
+
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"):
@@ -389,6 +565,9 @@ class QuoteUpdater(Gtk.ListStore):
iter = self.get_iter_first()
while iter != None:
currency = self.get_value(iter, self.CURRENCY)
+ if currency == None:
+ iter = self.iter_next(iter)
+ continue
symbol = self.get_value(iter, self.SYMBOL)
# ignore stocks that are currency conversions
# and only convert stocks that are not in the target currency
@@ -432,12 +611,18 @@ class QuoteUpdater(Gtk.ListStore):
self.set_value(row, self.PB, retriever.image.get_pixbuf())
# check if we have only simple quotes
- def simple_quotes_only(self):
- res = True
- for entry, data in invest.STOCKS.iteritems():
- purchases = data["purchases"]
- for purchase in purchases:
- if purchase["amount"] != 0:
- res = False
- break
- return res
+ def simple_quotes_only(self, stocks):
+ for stock in stocks:
+ if stock.has_key('purchases'):
+ if not self.is_simple_quote(stock):
+ return False
+ else:
+ if not self.simple_quotes_only(stock['list']):
+ return False
+ return True
+
+ def is_simple_quote(self, stock):
+ for purchase in stock["purchases"]:
+ if purchase["amount"] != 0:
+ return False
+ return True
diff --git a/invest-applet/invest/widgets.py b/invest-applet/invest/widgets.py
index 0442e0f..3177b36 100644
--- a/invest-applet/invest/widgets.py
+++ b/invest-applet/invest/widgets.py
@@ -39,10 +39,11 @@ class InvestWidget(Gtk.TreeView):
def __init__(self, quotes_updater):
Gtk.TreeView.__init__(self)
self.set_property("rules-hint", True)
- self.set_reorderable(True)
- self.set_hover_selection(True)
+# self.set_property("enable-grid-lines", True)
+# self.set_property("reorderable", True)
+ self.set_property("hover-selection", True)
- simple_quotes_only = quotes_updater.simple_quotes_only()
+ simple_quotes_only = quotes_updater.simple_quotes_only(invest.STOCKS)
# model: SYMBOL, LABEL, TICKER_ONLY, BALANCE, BALANCE_PCT, VALUE, VARIATION_PCT, PB
# Translators: these words all refer to a stock. Last is short
@@ -65,6 +66,8 @@ class InvestWidget(Gtk.TreeView):
column.set_cell_data_func(cell, col_cellgetdata_functions[i])
self.append_column(column)
elif i == 3:
+ if invest.CONFIG.has_key('hidecharts') and invest.CONFIG['hidecharts']:
+ continue
cell_pb = Gtk.CellRendererPixbuf()
column = Gtk.TreeViewColumn (col_name, cell_pb, pixbuf=quotes_updater.PB)
self.append_column(column)
@@ -81,9 +84,6 @@ class InvestWidget(Gtk.TreeView):
column.set_cell_data_func(cell, col_cellgetdata_functions[i])
self.append_column(column)
- if simple_quotes_only == True:
- self.set_property('headers-visible', False)
-
self.connect('row-activated', self.on_row_activated)
self.set_model(quotes_updater)
@@ -102,10 +102,19 @@ class InvestWidget(Gtk.TreeView):
def _getcelldata_label(self, column, cell, model, iter, userdata):
- cell.set_property('text', model[iter][model.LABEL])
+ label = model[iter][model.LABEL]
+ if self.is_stock(iter):
+ cell.set_property('text', label)
+ else:
+ cell.set_property('markup', "<b>%s</b>" % label)
def _getcelldata_value(self, column, cell, model, iter, userdata):
- cell.set_property('text', self.format_currency(model[iter][model.VALUE], model[iter][model.CURRENCY]))
+ value = model[iter][model.VALUE];
+ currency = model[iter][model.CURRENCY];
+ if value == None or currency == None:
+ cell.set_property('text', "")
+ else:
+ cell.set_property('text', self.format_currency(value, currency))
def is_selected(self, model, iter):
m, it = self.get_selection().get_selected()
@@ -121,6 +130,10 @@ class InvestWidget(Gtk.TreeView):
return palette[intensity]
def _getcelldata_variation(self, column, cell, model, iter, userdata):
+ if self.is_group(iter):
+ cell.set_property('text', '')
+ return
+
color = self.get_color(model, iter, model.VARIATION_PCT)
change_pct = self.format_percent(model[iter][model.VARIATION_PCT])
cell.set_property('markup',
@@ -156,6 +169,12 @@ class InvestWidget(Gtk.TreeView):
invest.chart.show_chart([ticker])
+ def is_group(self, iter):
+ return self.get_model()[iter][0] == None
+
+ def is_stock(self, iter):
+ return not self.is_group(iter)
+
#class InvestTicker(Gtk.Label):
# def __init__(self):
# Gtk.Label.__init__(self, _("Waiting..."))
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]