diff options
Diffstat (limited to 'invest-applet/invest')
-rw-r--r-- | invest-applet/invest/Makefile.am | 41 | ||||
-rw-r--r-- | invest-applet/invest/__init__.py | 213 | ||||
-rw-r--r-- | invest-applet/invest/about.py | 35 | ||||
-rw-r--r-- | invest-applet/invest/applet.py | 185 | ||||
-rw-r--r-- | invest-applet/invest/chart.py | 257 | ||||
-rw-r--r-- | invest-applet/invest/currencies.py | 145 | ||||
-rw-r--r-- | invest-applet/invest/defs.py.in | 8 | ||||
-rw-r--r-- | invest-applet/invest/help.py | 8 | ||||
-rwxr-xr-x | invest-applet/invest/invest-applet.py | 98 | ||||
-rwxr-xr-x | invest-applet/invest/invest-chart | 15 | ||||
-rw-r--r-- | invest-applet/invest/networkmanager.py | 58 | ||||
-rw-r--r-- | invest-applet/invest/preferences.py | 287 | ||||
-rw-r--r-- | invest-applet/invest/quotes.py | 414 | ||||
-rwxr-xr-x | invest-applet/invest/test.py | 30 | ||||
-rw-r--r-- | invest-applet/invest/widgets.py | 250 |
15 files changed, 2044 insertions, 0 deletions
diff --git a/invest-applet/invest/Makefile.am b/invest-applet/invest/Makefile.am new file mode 100644 index 00000000..286b974c --- /dev/null +++ b/invest-applet/invest/Makefile.am @@ -0,0 +1,41 @@ +invest-applet: invest-applet.py + sed -e "s|\@PYTHONDIR\@|$(pythondir)/|" $< > $@ + +bin_SCRIPTS = invest-chart + +libexec_SCRIPTS = invest-applet + +investdir = $(pythondir)/invest +invest_PYTHON = \ + __init__.py \ + about.py \ + help.py \ + applet.py \ + chart.py \ + currencies.py \ + widgets.py \ + quotes.py \ + networkmanager.py \ + preferences.py +nodist_invest_PYTHON = \ + defs.py + +BUILT_SOURCES = \ + invest-applet + +CLEANFILES = \ + $(BUILT_SOURCES) + +DISTCLEANFILES = \ + defs.py \ + $(CLEANFILES) + +EXTRA_DIST = \ + defs.py.in \ + invest-applet.py \ + invest-chart \ + test.py + +#TESTS = test.py + +-include $(top_srcdir)/git.mk diff --git a/invest-applet/invest/__init__.py b/invest-applet/invest/__init__.py new file mode 100644 index 00000000..b86ce8ec --- /dev/null +++ b/invest-applet/invest/__init__.py @@ -0,0 +1,213 @@ +import os, sys +from os.path import join, exists, isdir, isfile, dirname, abspath, expanduser +from types import ListType +import datetime + +import gtk, gtk.gdk, mateconf, gobject +import cPickle + +import networkmanager + +# Autotools set the actual data_dir in defs.py +from defs import * + +DEBUGGING = False + +# central debugging and error method +def debug(msg): + if DEBUGGING: + print "%s: %s" % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), msg) + +def error(msg): + print "%s: ERROR: %s" % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), msg) + + +# Allow to use uninstalled invest --------------------------------------------- +UNINSTALLED_INVEST = False +def _check(path): + return exists(path) and isdir(path) and isfile(path+"/Makefile.am") + +name = join(dirname(__file__), '..') +if _check(name): + UNINSTALLED_INVEST = True + +# Sets SHARED_DATA_DIR to local copy, or the system location +# Shared data dir is most the time /usr/share/invest-applet +if UNINSTALLED_INVEST: + SHARED_DATA_DIR = abspath(join(dirname(__file__), '..', 'data')) + BUILDER_DATA_DIR = SHARED_DATA_DIR + ART_DATA_DIR = join(SHARED_DATA_DIR, 'art') +else: + SHARED_DATA_DIR = join(DATA_DIR, "mate-applets", "invest-applet") + BUILDER_DATA_DIR = BUILDERDIR + ART_DATA_DIR = SHARED_DATA_DIR + +USER_INVEST_DIR = expanduser("~/.mate2/invest-applet") +if not exists(USER_INVEST_DIR): + try: + os.makedirs(USER_INVEST_DIR, 0744) + except Exception , msg: + error('Could not create user dir (%s): %s' % (USER_INVEST_DIR, msg)) +# ------------------------------------------------------------------------------ + +# Set the cwd to the home directory so spawned processes behave correctly +# when presenting save/open dialogs +os.chdir(expanduser("~")) + +#Gconf client +MATECONF_CLIENT = mateconf.client_get_default() + +# MateConf directory for invest in window mode and shared settings +MATECONF_DIR = "/apps/invest" + +# MateConf key for list of enabled handlers, when uninstalled, use a debug key to not conflict +# with development version +#MATECONF_ENABLED_HANDLERS = MATECONF_DIR + "/enabled_handlers" + +# Preload mateconf directories +#MATECONF_CLIENT.add_dir(MATECONF_DIR, mateconf.CLIENT_PRELOAD_RECURSIVE) + +# tests whether the given stocks are in the old labelless format +def labelless_stock_format(stocks): + if len(stocks) == 0: + return False + + # take the first element of the dict and check if its value is a list + if type(stocks[stocks.keys()[0]]) is ListType: + return True + + # there is no list, so it is already the new stock file format + return False + +# 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(): + d = {'label':"", 'purchases':l} + new[k] = d + + 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 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 = {} + +#STOCKS = { +# "AAPL": { +# "amount": 12, +# "bought": 74.94, +# "comission": 31, +# }, +# "INTC": { +# "amount": 30, +# "bought": 25.85, +# "comission": 31, +# }, +# "GOOG": { +# "amount": 1, +# "bought": 441.4, +# "comission": 31, +# }, +#} + +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 + +# borrowed from Ross Burton +# http://burtonini.com/blog/computers/postr +# extended by exception handling and retry scheduling +def get_mate_proxy(client): + sleep = 10 # sleep between attempts for 10 seconds + attempts = 3 # try to get configuration from mateconf at most three times + get_mate_proxy_retry(client, attempts, sleep) + +def get_mate_proxy_retry(client, attempts, sleep): + # decrease attempts counter + attempts -= 1 + + # sanity check if we still need to look for proxy configuration + global PROXY + if PROXY != None: + return + + # try to get config from mateconfd + try: + if client.get_bool("/system/http_proxy/use_http_proxy"): + host = client.get_string("/system/http_proxy/host") + port = client.get_int("/system/http_proxy/port") + if host is None or host == "" or port == 0: + # mate proxy is not valid, stop here + return + + if client.get_bool("/system/http_proxy/use_authentication"): + user = client.get_string("/system/http_proxy/authentication_user") + password = client.get_string("/system/http_proxy/authentication_password") + if user and user != "": + url = "http://%s:%s@%s:%d" % (user, password, host, port) + else: + url = "http://%s:%d" % (host, port) + else: + url = "http://%s:%d" % (host, port) + + # proxy config found, memorize + PROXY = {'http': url} + + except Exception, msg: + error("Failed to get proxy configuration from MateConfd:\n%s" % msg) + # we did not succeed, schedule retry + if attempts > 0: + error("Retrying to contact MateConfd in %d seconds" % sleep) + gobject.timeout_add(sleep * 1000, get_mate_proxy_retry, client, attempts, sleep) + +# use mateconf to get proxy config +client = mateconf.client_get_default() +get_mate_proxy(client) + + +# connect to Network Manager to identify current network connectivity +nm = networkmanager.NetworkManager() diff --git a/invest-applet/invest/about.py b/invest-applet/invest/about.py new file mode 100644 index 00000000..e2829e86 --- /dev/null +++ b/invest-applet/invest/about.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from os.path import join +from gettext import gettext as _ +from invest.defs import VERSION +import invest +import gtk, gtk.gdk + +invest_logo = None +try: + invest_logo = gtk.gdk.pixbuf_new_from_file_at_size(join(invest.ART_DATA_DIR, "invest_neutral.svg"), 96, 96) +except Exception, msg: + pass + +def show_about(): + about = gtk.AboutDialog() + infos = { + "program-name" : _("Invest"), + "logo" : invest_logo, + "version" : VERSION, + "comments" : _("Track your invested money."), + "copyright" : "Copyright © 2004-2005 Raphael Slinckx.\nCopyright © 2009-2010 Enrico Minack." + } + + about.set_authors(["Raphael Slinckx <[email protected]>", "Enrico Minack <[email protected]>"]) +# about.set_artists([]) +# about.set_documenters([]) + + #translators: These appear in the About dialog, usual format applies. + about.set_translator_credits( _("translator-credits") ) + + for prop, val in infos.items(): + about.set_property(prop, val) + + about.connect ("response", lambda self, *args: self.destroy ()) + about.show_all() diff --git a/invest-applet/invest/applet.py b/invest-applet/invest/applet.py new file mode 100644 index 00000000..c1689586 --- /dev/null +++ b/invest-applet/invest/applet.py @@ -0,0 +1,185 @@ +import os, time +from os.path import * +import mateapplet, gtk, gtk.gdk, mateconf, gobject +gobject.threads_init() +from gettext import gettext as _ +import mateconf + +import invest, invest.about, invest.chart, invest.preferences, invest.defs +from invest.quotes import QuoteUpdater +from invest.widgets import * + +gtk.window_set_default_icon_from_file(join(invest.ART_DATA_DIR, "invest_neutral.svg")) + +class InvestApplet: + def __init__(self, applet): + self.applet = applet + self.applet.setup_menu_from_file ( + None, "Invest_Applet.xml", + None, [("About", self.on_about), + ("Help", self.on_help), + ("Prefs", self.on_preferences), + ("Refresh", self.on_refresh) + ]) + + evbox = gtk.HBox() + self.applet_icon = gtk.Image() + self.set_applet_icon(0) + self.applet_icon.show() + evbox.add(self.applet_icon) + self.applet.add(evbox) + self.applet.connect("button-press-event",self.button_clicked) + self.applet.show_all() + self.new_ilw() + + def new_ilw(self): + self.quotes_updater = QuoteUpdater(self.set_applet_icon, + self.set_applet_tooltip) + self.investwidget = InvestWidget(self.quotes_updater) + self.ilw = InvestmentsListWindow(self.applet, self.investwidget) + + def reload_ilw(self): + self.ilw.destroy() + self.new_ilw() + + def button_clicked(self, widget,event): + if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1: + # Three cases... + if len (invest.STOCKS) == 0: + # a) We aren't configured yet + invest.preferences.show_preferences(self, _("<b>You have not entered any stock information yet</b>")) + self.reload_ilw() + elif not self.quotes_updater.quotes_valid: + # b) We can't get the data (e.g. offline) + alert = gtk.MessageDialog(buttons=gtk.BUTTONS_CLOSE) + alert.set_markup(_("<b>No stock quotes are currently available</b>")) + alert.format_secondary_text(_("The server could not be contacted. The computer is either offline or the servers are down. Try again later.")) + alert.run() + alert.destroy() + else: + # c) Everything is normal: pop-up the window + self.ilw.toggle_show() + + def on_about(self, component, verb): + invest.about.show_about() + + def on_help(self, component, verb): + invest.help.show_help() + + def on_preferences(self, component, verb): + invest.preferences.show_preferences(self) + self.reload_ilw() + + def on_refresh(self, component, verb): + self.quotes_updater.refresh() + + def set_applet_icon(self, change): + if change == 1: + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(join(invest.ART_DATA_DIR, "invest-22_up.png"), -1,-1) + elif change == 0: + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(join(invest.ART_DATA_DIR, "invest-22_neutral.png"), -1,-1) + else: + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(join(invest.ART_DATA_DIR, "invest-22_down.png"), -1,-1) + self.applet_icon.set_from_pixbuf(pixbuf) + + def set_applet_tooltip(self, text): + self.applet_icon.set_tooltip_text(text) + +class InvestmentsListWindow(gtk.Window): + def __init__(self, applet, list): + gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) + self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DOCK) + self.stick() + self.set_resizable(False) + self.set_border_width(6) + + self.applet = applet # this is the widget we want to align with + self.alignment = self.applet.get_orient () + + self.add(list) + list.show() + + # boolean variable that identifies if the window is visible + # show/hide is triggered by left-clicking on the applet + self.hidden = True + + def toggle_show(self): + if self.hidden == True: + self.update_position() + self.show() + self.hidden = False + elif self.hidden == False: + self.hide() + self.hidden = True + + def update_position (self): + """ + Calculates the position and moves the window to it. + """ + self.realize() + + # Get our own dimensions & position + #(wx, wy) = self.get_origin() + (ax, ay) = self.applet.window.get_origin() + + (ww, wh) = self.get_size () + (aw, ah) = self.applet.window.get_size () + + screen = self.applet.window.get_screen() + monitor = screen.get_monitor_geometry (screen.get_monitor_at_window (self.applet.window)) + + if self.alignment == mateapplet.ORIENT_LEFT: + x = ax - ww + y = ay + + if (y + wh > monitor.y + monitor.height): + y = monitor.y + monitor.height - wh + + if (y < 0): + y = 0 + + if (y + wh > monitor.height / 2): + gravity = gtk.gdk.GRAVITY_SOUTH_WEST + else: + gravity = gtk.gdk.GRAVITY_NORTH_WEST + + elif self.alignment == mateapplet.ORIENT_RIGHT: + x = ax + aw + y = ay + + if (y + wh > monitor.y + monitor.height): + y = monitor.y + monitor.height - wh + + if (y < 0): + y = 0 + + if (y + wh > monitor.height / 2): + gravity = gtk.gdk.GRAVITY_SOUTH_EAST + else: + gravity = gtk.gdk.GRAVITY_NORTH_EAST + + elif self.alignment == mateapplet.ORIENT_DOWN: + x = ax + y = ay + ah + + if (x + ww > monitor.x + monitor.width): + x = monitor.x + monitor.width - ww + + if (x < 0): + x = 0 + + gravity = gtk.gdk.GRAVITY_NORTH_WEST + elif self.alignment == mateapplet.ORIENT_UP: + x = ax + y = ay - wh + + if (x + ww > monitor.x + monitor.width): + x = monitor.x + monitor.width - ww + + if (x < 0): + x = 0 + + gravity = gtk.gdk.GRAVITY_SOUTH_WEST + + self.move(x, y) + self.set_gravity(gravity) diff --git a/invest-applet/invest/chart.py b/invest-applet/invest/chart.py new file mode 100644 index 00000000..c6caff74 --- /dev/null +++ b/invest-applet/invest/chart.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python + +import gtk, gtk.gdk +import gobject +import os +import invest +from gettext import gettext as _ +from invest import * +import sys +from os.path import join +import urllib +from threading import Thread +import time + +AUTOREFRESH_TIMEOUT = 20*60*1000 # 15 minutes + +# based on http://www.johnstowers.co.nz/blog/index.php/2007/03/12/threading-and-pygtk/ +class _IdleObject(gobject.GObject): + """ + Override gobject.GObject to always emit signals in the main thread + by emmitting on an idle handler + """ + def __init__(self): + gobject.GObject.__init__(self) + + def emit(self, *args): + gobject.idle_add(gobject.GObject.emit,self,*args) + +class ImageRetriever(Thread, _IdleObject): + """ + Thread which uses gobject signals to return information + to the GUI. + """ + __gsignals__ = { + "completed": ( + gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []), + # FIXME: should we be making use of this? + #"progress": ( + # gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [ + # gobject.TYPE_FLOAT]) #percent complete + } + + def __init__(self, image_url): + Thread.__init__(self) + _IdleObject.__init__(self) + self.image_url = image_url + self.retrieved = False + + def run(self): + self.image = gtk.Image() + try: sock = urllib.urlopen(self.image_url, proxies = invest.PROXY) + except Exception, msg: + invest.debug("Error while opening %s: %s" % (self.image_url, msg)) + else: + loader = gtk.gdk.PixbufLoader() + loader.connect("closed", lambda loader: self.image.set_from_pixbuf(loader.get_pixbuf())) + loader.write(sock.read()) + sock.close() + loader.close() + self.retrieved = True + self.emit("completed") + +# p: +# eX = Exponential Moving Average +# mX = Moving Average +# b = Bollinger Bands Overlay +# v = Volume Overlay +# p = Parabolic SAR overlay +# s = Splits Overlay +# q: +# l = Line +# c = Candles +# b = Bars +# l: +# on = Logarithmic +# off = Linear +# z: +# l = Large +# m = Medium +# t: +# Xd = X Days +# Xm = X Months +# Xy = X Years +# a: +# fX = MFI X days +# ss = Slow Stochastic +# fs = Fast Stochastic +# wX = W%R X Days +# mX-Y-Z = MACD X Days, Y Days, Signal +# pX = ROC X Days +# rX = RSI X Days +# v = Volume +# vm = Volume +MA +# c: +# X = compare with X +# + +class FinancialChart: + def __init__(self, ui): + self.ui = ui + + #Time ranges of the plot + self.time_ranges = ["1d", "5d", "3m", "6m", "1y", "5y", "my"] + + # Window Properties + win = ui.get_object("window") + win.set_title(_("Financial Chart")) + + try: + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(join(invest.ART_DATA_DIR, "invest_neutral.svg"), 96,96) + self.ui.get_object("plot").set_from_pixbuf(pixbuf) + except Exception, msg: + invest.debug("Could not load 'invest-neutral.svg' file: %s" % msg) + pass + + # Defaut comboboxes values + for widget in ["t", "q"]: + ui.get_object(widget).set_active(0) + + # Connect every option widget to its corresponding change signal + symbolentry = ui.get_object("s") + refresh_chart_callback = lambda w: self.on_refresh_chart() + + for widgets, signal in [ + (("pm5","pm10","pm20","pm50","pm100","pm200", + "pe5","pe10", "pe20","pe50","pe100","pe200", + "pb","pp","ps","pv", + "ar","af","ap","aw","am","ass","afs","av","avm"), "toggled"), + (("t", "q"), "changed"), + (("s",), "activate"), + ]: + for widget in widgets: + ui.get_object(widget).connect(signal, refresh_chart_callback) + + ui.get_object("progress").hide() + + # Connect auto-refresh widget + self.autorefresh_id = 0 + ui.get_object("autorefresh").connect("toggled", self.on_autorefresh_toggled) + + def on_refresh_chart(self, from_timer=False): + tickers = self.ui.get_object("s").get_text() + + if tickers.strip() == "": + return True + + # FIXME: We don't just do US stocks, so we can't be this + # simplistic about it, but it is a good idea. + #if from_timer and not ustime.hour_between(9, 16): + # return True + + tickers = [ticker.strip().upper() for ticker in tickers.split(' ') if ticker != ""] + + # Update Window Title ------------------------------------------------------ + win = self.ui.get_object("window") + title = _("Financial Chart - %s") + titletail = "" + for ticker in tickers: + titletail += "%s / " % ticker + title = title % titletail + + win.set_title(title[:-3]) + + # Detect Comparison or simple chart ---------------------------------------- + opt = "" + for ticker in tickers[1:]: + opt += "&c=%s" % ticker + + # Create the overlay string ------------------------------------------------ + p = "" + for name, param in [ + ("pm5", 5), + ("pm10", 10), + ("pm20", 20), + ("pm50", 50), + ("pm100", 100), + ("pm200", 200), + ("pe5", 5), + ("pe10", 10), + ("pe20", 20), + ("pe50", 50), + ("pe100", 100), + ("pe200", 200), + ("pb", ""), + ("pp", ""), + ("ps", ""), + ("pv", ""), + ]: + if self.ui.get_object(name).get_active(): + p += "%s%s," % (name[1], param) + + # Create the indicators string --------------------------------------------- + a = "" + for name, param in [ + ("ar", 14), + ("af", 14), + ("ap", 12), + ("aw", 14), + ("am", "26-12-9"), + ("ass", ""), + ("afs", ""), + ("av", ""), + ("avm", ""), + ]: + if self.ui.get_object(name).get_active(): + a += "%s%s," % (name[1:], param) + + # Create the image URL ----------------------------------------------------- + chart_base_url = "http://ichart.europe.yahoo.com/z?s=%(s)s&t=%(t)s&q=%(q)s&l=%(l)s&z=%(z)s&p=%(p)s&a=%(a)s%(opt)s" + url = chart_base_url % { + "s": tickers[0], + "t": self.time_ranges[self.ui.get_object("t").get_active()], + "q": self.ui.get_object("q").get_active_text(), + "l": "off", + "z": "l", + "p": p, + "a": a, + "opt": opt, + } + + # Download and display the image ------------------------------------------- + progress = self.ui.get_object("progress") + progress.set_text(_("Opening Chart")) + progress.show() + + image_retriever = ImageRetriever(url) + image_retriever.connect("completed", self.on_retriever_completed) + image_retriever.start() + + # Update timer if needed + self.on_autorefresh_toggled(self.ui.get_object("autorefresh")) + return True + + def on_retriever_completed(self, retriever): + self.ui.get_object("plot").set_from_pixbuf(retriever.image.get_pixbuf()) + progress = self.ui.get_object("progress") + if retriever.retrieved == True: + progress.set_text(_("Chart downloaded")) + else: + progress.set_text(_("Chart could not be downloaded")) + + def on_autorefresh_toggled(self, autorefresh): + if self.autorefresh_id != 0: + gobject.source_remove(self.autorefresh_id) + self.autorefresh_id = 0 + + if autorefresh.get_active(): + self.autorefresh_id = gobject.timeout_add(AUTOREFRESH_TIMEOUT, self.on_refresh_chart, True) + +def show_chart(tickers): + ui = gtk.Builder(); + ui.add_from_file(os.path.join(invest.BUILDER_DATA_DIR, "financialchart.ui")) + chart = FinancialChart(ui) + ui.get_object("s").set_text(' '.join(tickers)) + chart.on_refresh_chart() + return ui.get_object("window") + diff --git a/invest-applet/invest/currencies.py b/invest-applet/invest/currencies.py new file mode 100644 index 00000000..0eaa075c --- /dev/null +++ b/invest-applet/invest/currencies.py @@ -0,0 +1,145 @@ +class Currencies: + currencies = { + "BZD" : "Belize Dollar", + "NLG" : "Dutch Guilder", + "SLL" : "Sierra Leone Leone", + "FRF" : "French Franc", + "NGN" : "Nigerian Naira", + "CRC" : "Costa Rican Colon", + "LAK" : "Laos Kip", + "CLP" : "Chilean Peso", + "DZD" : "Algerian Dinar", + "SZL" : "Swaziland Lilangeni", + "MUR" : "Mauritius Rupee", + "WST" : "Western Samoa Tala", + "MMK" : "Myanmar Kyat", + "IDR" : "Indonesian Rupiah", + "GTQ" : "Guatemala Quetzal", + "CAD" : "Canadian Dollar", + "AWG" : "Aruban Florin", + "TTD" : "Trinidad Dollar", + "PKR" : "Pakistani Rupee", + "XCD" : "East Caribbean Dollar", + "VUV" : "Vanuatu Vatu", + "XOF" : "CFA Franc (BCEAO)", + "ROL" : "Romanian Leu", + "KMF" : "Comoros Franc", + "SIT" : "Slovenian Tolar", + "VEB" : "Venezuelan Bolivar", + "ANG" : "Netherlands Antilles Guilder", + "MNT" : "Mongolian Tugrik", + "LBP" : "Lebanese Pound", + "KES" : "Kenyan Shilling", + "BTN" : "Bhutan Ngultrum", + "GBP" : "British Pound", + "SEK" : "Swedish Krona", + "ZMK" : "Zambia Kwacha", + "SKK" : "Slovak Koruna", + "DKK" : "Danish Krone", + "AFA" : "Afganistan Afghani", + "CYP" : "Cypriot Pound", + "SCR" : "Seychelles Rupee", + "FJD" : "Fiji Dollar", + "SRG" : "Surinam Guilder", + "SHP" : "St. Helena Pound", + "ALL" : "Albanian Lek", + "TOP" : "Tonga Isl Pa'anga", + "UGX" : "Ugandan Shilling", + "OMR" : "Oman Rial", + "DJF" : "Djibouti Franc", + "BND" : "Brunei Dollar", + "TND" : "Tunisian Dinar", + "PTE" : "Portuguese Escudo", + "IEP" : "Irish Punt", + "SBD" : "Salomon Islands Dollar", + "GNF" : "Guinea Franc", + "BOB" : "Bolivian Boliviano", + "CVE" : "Cape Verde Escudo", + "ARS" : "Argentinian Peso", + "GMD" : "Gambia Dalasi", + "ZWD" : "Zimbabwean Dollar", + "MWK" : "Malawi Kwacha", + "BDT" : "Bangladesh Taka", + "GRD" : "Greek Drachma", + "KWD" : "Kuwaiti Dinar", + "EUR" : "Euro", + "TRL" : "Turkish Lira", + "CHF" : "Swiss Franc", + "DOP" : "Dominican Peso", + "PEN" : "Peruvian Sol", + "SVC" : "El Salvador Colon", + "SGD" : "Singapore Dollar", + "TWD" : "Taiwan New Dollar", + "USD" : "US Dollar", + "BGN" : "Bulgarian Lev", + "MAD" : "Moroccan Dirham", + "SAR" : "Saudi Arabian Riyal", + "AUD" : "Australian Dollar", + "KYD" : "Cayman Islands Dollar", + "GHC" : "Ghanaian Cedi", + "KRW" : "South Korean Won", + "GIP" : "Gibraltar Pound", + "NAD" : "Namibian Dollar", + "CZK" : "Czech Koruna", + "JMD" : "Jamaican Dollar", + "MXN" : "Mexican Peso", + "BWP" : "Botswana Pula", + "GYD" : "Guyana Dollar", + "EGP" : "Egyptian Pound", + "THB" : "Thai Baht", + "AED" : "United Arab Emirates Dirham", + "JPY" : "Japanese Yen", + "JOD" : "Jordanian Dinar", + "HRK" : "Croatian Kuna", + "ZAR" : "South African Rand", + "CUP" : "Cuban Peso", + "BBD" : "Barbados Dollar", + "PGK" : "Papua New Guinea Kina", + "LKR" : "Sri Lanka Rupee", + "BEF" : "Belgian Franc", + "PLN" : "Polish Zloty", + "MYR" : "Malaysian Ringgit", + "FIM" : "Finnish Markka", + "CNY" : "Renmimbi Yuan", + "SDD" : "Sudanese Dinar", + "LVL" : "Latvian Lats", + "ITL" : "Italian Lira", + "INR" : "Indian Rupee", + "NIO" : "Nicaraguan Cordoba", + "PHP" : "Philippines Peso", + "HNL" : "Honduras Lempira", + "HKD" : "Hong Kong Dollar", + "NZD" : "New Zealand Dollar", + "BRL" : "Brazilian Real", + "MTL" : "Maltese Pound", + "ATS" : "Austrian Schilling", + "EEK" : "Estonian Kroon", + "NOK" : "Norwegian Krone", + "ISK" : "Iceland Krona", + "ILS" : "Israeli Shekel", + "LSL" : "Lesotho Loti", + "HUF" : "Hungarian Forint", + "ESP" : "Spanish Peseta", + "UAH" : "Ukraine Hryvnia", + "RUB" : "Russian Ruble", + "BMD" : "Bermuda Dollar", + "MVR" : "Maldives Rufiyan", + "QAR" : "Qatari Rial", + "VND" : "Vietnam Dong", + "MRO" : "Mauritania Ouguiya", + "MZM" : "Mozambique Metical", + "NPR" : "Nepal Rupee", + "COP" : "Colombian Peso", + "TZS" : "Tanzanian Shilling", + "MGF" : "Malagasy Franc", + "KHR" : "Cambodian Riel", + "SYP" : "Syria Pound", + "HTG" : "Haitian Gourde", + "DEM" : "German Mark", + "BHD" : "Bahraini Dinar", + "XAF" : "CFA Franc(BEAC)", + "STD" : "Sao Tome & Principe Dobra", + "LTL" : "Lithuanian Litas", + "ETB" : "Ethiopian Birr", + "XPF" : "CFP Franc", +} diff --git a/invest-applet/invest/defs.py.in b/invest-applet/invest/defs.py.in new file mode 100644 index 00000000..c674b1b4 --- /dev/null +++ b/invest-applet/invest/defs.py.in @@ -0,0 +1,8 @@ +DATA_DIR = "@DATADIR@" +LIB_DIR = "@LIBDIR@" +VERSION = "@VERSION@" +PACKAGE = "@PACKAGE@" +PYTHONDIR = "@PYTHONDIR@" +GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@" +MATELOCALEDIR = "@MATELOCALEDIR@" +BUILDERDIR = "@BUILDERDIR@" diff --git a/invest-applet/invest/help.py b/invest-applet/invest/help.py new file mode 100644 index 00000000..7dfaabf0 --- /dev/null +++ b/invest-applet/invest/help.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +import gtk, gtk.gdk + +def show_help(): + gtk.show_uri(None, "ghelp:invest-applet", gtk.gdk.CURRENT_TIME) + +def show_help_section(id): + gtk.show_uri(None, "ghelp:invest-applet?%s" % id, gtk.gdk.CURRENT_TIME) diff --git a/invest-applet/invest/invest-applet.py b/invest-applet/invest/invest-applet.py new file mode 100755 index 00000000..884bcba8 --- /dev/null +++ b/invest-applet/invest/invest-applet.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# + +import gobject +import gtk, mateapplet + +import getopt, sys +from os.path import * + +# Allow to use uninstalled +def _check(path): + return exists(path) and isdir(path) and isfile(path+"/Makefile.am") + +name = join(dirname(__file__), '..') +if _check(name): + print 'Running uninstalled invest, modifying PYTHONPATH' + sys.path.insert(0, abspath(name)) +else: + sys.path.insert(0, abspath("@PYTHONDIR@")) + +# Now the path is set, import our applet +import invest, invest.applet, invest.defs, invest.help + +# Prepare i18n +import gettext, locale +gettext.bindtextdomain(invest.defs.GETTEXT_PACKAGE, invest.defs.MATELOCALEDIR) +gettext.textdomain(invest.defs.GETTEXT_PACKAGE) +locale.bindtextdomain(invest.defs.GETTEXT_PACKAGE, invest.defs.MATELOCALEDIR) +locale.textdomain(invest.defs.GETTEXT_PACKAGE) + +from gettext import gettext as _ + +def applet_factory(applet, iid): + invest.debug('Starting invest instance: %s %s'% ( applet, iid )) + invest.applet.InvestApplet(applet) + return True + +# Return a standalone window that holds the applet +def build_window(): + app = gtk.Window(gtk.WINDOW_TOPLEVEL) + app.set_title(_("Invest Applet")) + app.connect("destroy", gtk.main_quit) + app.set_property('resizable', False) + + applet = mateapplet.Applet() + applet_factory(applet, None) + applet.reparent(app) + + app.show_all() + + return app + + +def usage(): + print """=== Invest applet: Usage +$ invest-applet [OPTIONS] + +OPTIONS: + -h, --help Print this help notice. + -d, --debug Enable debug output (default=off). + -w, --window Launch the applet in a standalone window for test purposes (default=no). + """ + sys.exit() + +if __name__ == "__main__": + standalone = False + + try: + opts, args = getopt.getopt(sys.argv[1:], "hdw", ["help", "debug", "window"]) + except getopt.GetoptError: + # Unknown args were passed, we fallback to bahave as if + # no options were passed + opts = [] + args = sys.argv[1:] + + for o, a in opts: + if o in ("-h", "--help"): + usage() + elif o in ("-d", "--debug"): + invest.DEBUGGING = True + invest.debug("Debugging enabled") + # these messages cannot be turned by invest.DEBUGGING at their originating location, + # because that variable was set here to be True + invest.debug("Data Dir: %s" % invest.SHARED_DATA_DIR) + invest.debug("Detected PROXY: %s" % invest.PROXY) + elif o in ("-w", "--window"): + standalone = True + + if standalone: + build_window() + gtk.main() + else: + mateapplet.matecomponent_factory( + "OAFIID:Invest_Applet_Factory", + mateapplet.Applet.__gtype__, + invest.defs.PACKAGE, + invest.defs.VERSION, + applet_factory) diff --git a/invest-applet/invest/invest-chart b/invest-applet/invest/invest-chart new file mode 100755 index 00000000..37df77cb --- /dev/null +++ b/invest-applet/invest/invest-chart @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +import sys, invest.chart, gtk + +# Prepare i18n +import gettext, locale +gettext.bindtextdomain(invest.defs.GETTEXT_PACKAGE, invest.defs.MATELOCALEDIR) +gettext.textdomain(invest.defs.GETTEXT_PACKAGE) +locale.bindtextdomain(invest.defs.GETTEXT_PACKAGE, invest.defs.MATELOCALEDIR) +locale.textdomain(invest.defs.GETTEXT_PACKAGE) + +if __name__ == "__main__": + win = invest.chart.show_chart(sys.argv[1:]) + win.connect("destroy", lambda x: gtk.main_quit()) + gtk.main() diff --git a/invest-applet/invest/networkmanager.py b/invest-applet/invest/networkmanager.py new file mode 100644 index 00000000..415e9ce4 --- /dev/null +++ b/invest-applet/invest/networkmanager.py @@ -0,0 +1,58 @@ +import invest +from dbus.mainloop.glib import DBusGMainLoop +import dbus + +# possible states, see http://projects.mate.org/NetworkManager/developers/spec-08.html#type-NM_STATE +STATE_UNKNOWN = dbus.UInt32(0) +STATE_ASLEEP = dbus.UInt32(1) +STATE_CONNECTING = dbus.UInt32(2) +STATE_CONNECTED = dbus.UInt32(3) +STATE_DISCONNEDTED = dbus.UInt32(4) + +class NetworkManager: + def __init__(self): + self.state = STATE_UNKNOWN + self.statechange_callback = None + + try: + # get an event loop + loop = DBusGMainLoop() + + # get the NetworkManager object from D-Bus + invest.debug("Connecting to Network Manager via D-Bus") + bus = dbus.SystemBus(mainloop=loop) + nmobj = bus.get_object('org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager') + nm = dbus.Interface(nmobj, 'org.freedesktop.NetworkManager') + + # connect the signal handler to the bus + bus.add_signal_receiver(self.handler, None, + 'org.freedesktop.NetworkManager', + 'org.freedesktop.NetworkManager', + '/org/freedesktop/NetworkManager') + + # get the current status of the network manager + self.state = nm.state() + invest.debug("Current Network Manager status is %d" % self.state) + except Exception, msg: + invest.error("Could not connect to the Network Manager: %s" % msg ) + + def online(self): + return self.state == STATE_UNKNOWN or self.state == STATE_CONNECTED + + def offline(self): + return not self.online() + + # the signal handler for signals from the network manager + def handler(self,signal=None): + if isinstance(signal, dict): + state = signal.get('State') + if state != None: + invest.debug("Network Manager change state %d => %d" % (self.state, state) ); + self.state = state + + # notify about state change + if self.statechange_callback != None: + self.statechange_callback() + + def set_statechange_callback(self,handler): + self.statechange_callback = handler diff --git a/invest-applet/invest/preferences.py b/invest-applet/invest/preferences.py new file mode 100644 index 00000000..3c48ee42 --- /dev/null +++ b/invest-applet/invest/preferences.py @@ -0,0 +1,287 @@ +from gettext import gettext as _ +import locale +from os.path import join +import gtk, gobject, mateconf +import invest +import currencies +import cPickle + +class PrefsDialog: + def __init__(self, applet): + self.ui = gtk.Builder() + self.ui.add_from_file(join(invest.BUILDER_DATA_DIR, "prefs-dialog.ui")) + + 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.ui.get_object("help").connect('clicked', self.on_help) + 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, 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 + + 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): + 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: + if purchase.has_key("exchange"): + exchange = purchase["exchange"] + else: + exchange = 0.0 + store.append([key, label, purchase["amount"], purchase["bought"], purchase["comission"], exchange]) + + 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) + self.dialog.show_all() + if explanation == "": + expl.hide() + # returns 1 if help is clicked + while self.dialog.run() == 1: + pass + self.dialog.destroy() + + invest.STOCKS = {} + + def save_symbol(model, path, iter): + #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) + 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) + + 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, 0]) + 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)) + + def on_help(self, w): + invest.help.show_help_section("invest-applet-usage") + + def on_tree_keypress(self, w, event): + if event.keyval == 65535: + self.on_remove_stock(w) + + 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 new file mode 100644 index 00000000..e8f6145b --- /dev/null +++ b/invest-applet/invest/quotes.py @@ -0,0 +1,414 @@ +from os.path import join +import mateapplet, gtk, gtk.gdk, mateconf, gobject +from gettext import gettext as _ +import csv +import locale +from urllib import urlopen +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=snc4l1d1t1c1ohgv&e=.csv" + +# 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): + """ + Override gobject.GObject to always emit signals in the main thread + by emmitting on an idle handler + """ + def __init__(self): + gobject.GObject.__init__(self) + + def emit(self, *args): + gobject.idle_add(gobject.GObject.emit,self,*args) + +class QuotesRetriever(Thread, _IdleObject): + """ + Thread which uses gobject signals to return information + to the GUI. + """ + __gsignals__ = { + "completed": ( + gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []), + # FIXME: We don't monitor progress, yet ... + #"progress": ( + # gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [ + # gobject.TYPE_FLOAT]) #percent complete + } + + def __init__(self, tickers): + Thread.__init__(self) + _IdleObject.__init__(self) + self.tickers = tickers + self.retrieved = False + self.data = [] + self.currencies = [] + + def run(self): + quotes_url = QUOTES_URL % {"s": self.tickers} + try: + quotes_file = urlopen(quotes_url, proxies = invest.PROXY) + self.data = quotes_file.readlines () + quotes_file.close () + except Exception, msg: + invest.debug("Error while retrieving quotes data (url = %s): %s" % (quotes_url, msg)) + else: + self.retrieved = True + self.emit("completed") + + +class QuoteUpdater(gtk.ListStore): + 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, gtk.gdk.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.SORT_ASCENDING) + self.refresh() + + # tell the network manager to notify me when network status changes + invest.nm.set_statechange_callback(self.nm_state_changed) + + def set_update_interval(self, interval): + if self.timeout_id != None: + invest.debug("Canceling refresh timer") + gobject.source_remove(self.timeout_id) + self.timeout_id = None + if interval > 0: + invest.debug("Setting refresh timer to %s:%02d.%03d" % ( interval / 60000, interval % 60000 / 1000, interval % 1000) ) + self.timeout_id = gobject.timeout_add(interval, self.refresh) + + def nm_state_changed(self): + # when nm is online but we do not have an update timer, create it and refresh + if invest.nm.online(): + if self.timeout_id == None: + self.set_update_interval(AUTOREFRESH_TIMEOUT) + self.refresh() + + def refresh(self): + invest.debug("Refreshing") + + # when nm tells me I am offline, stop the update interval + if invest.nm.offline(): + invest.debug("We are offline, stopping update timer") + self.set_update_interval(0) + return False + + if len(invest.STOCKS) == 0: + return True + + tickers = '+'.join(invest.STOCKS.keys()) + quotes_retriever = QuotesRetriever(tickers) + quotes_retriever.connect("completed", self.on_retriever_completed) + quotes_retriever.start() + + return True + + + # 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 on_retriever_completed(self, retriever): + if retriever.retrieved == False: + tooltip = [_('Invest could not connect to Yahoo! Finance')] + if self.last_updated != None: + # Translators: %s is an hour (%H:%M) + tooltip.append(_('Updated at %s') % self.last_updated.strftime("%H:%M")) + self.set_tooltip_callback('\n'.join(tooltip)) + else: + self.populate(self.parse_yahoo_csv(csv.reader(retriever.data))) + self.updated = True + self.last_updated = datetime.datetime.now() + 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): + result = {} + for fields in csvreader: + if len(fields) == 0: + continue + + result[fields[0]] = {} + for i, field in enumerate(QUOTES_CSV_FIELDS): + if type(field) == tuple: + try: + result[fields[0]][field[0]] = field[1](fields[i]) + except: + result[fields[0]][field[0]] = 0 + else: + result[fields[0]][field] = fields[i] + # calculated fields + try: + result[fields[0]]['variation_pct'] = result[fields[0]]['variation'] / float(result[fields[0]]['trade'] - result[fields[0]]['variation']) * 100 + except ZeroDivisionError: + 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.statistics = {} + + for ticker, val in quote_items: + pb = None + + # 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: + self.simple_quotes_count += 1 + row = self.insert(0, [ticker, label, val["currency"], True, 0, 0, val["trade"], val["variation_pct"], pb]) + simple_quotes_change += val['variation_pct'] + 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"]) + + if len(ticker.split('.')) == 2: + url = 'http://ichart.europe.yahoo.com/h?s=%s' % ticker + else: + 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() + + if self.simple_quotes_count > 0: + self.avg_simple_quotes_change = simple_quotes_change/float(self.simple_quotes_count) + else: + self.avg_simple_quotes_change = 0 + + if self.avg_simple_quotes_change != 0: + simple_quotes_change_sign = self.avg_simple_quotes_change / abs(self.avg_simple_quotes_change) + else: + simple_quotes_change_sign = 0 + + # change icon + if self.simple_quotes_count > 0: + self.change_icon_callback(simple_quotes_change_sign) + else: + positions_balance_sign = self.positions_balance/abs(self.positions_balance) + self.change_icon_callback(positions_balance_sign) + + # mark quotes to finally be valid + self.quotes_valid = True + + except Exception, msg: + invest.debug("Failed to populate quotes: %s" % msg) + 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()) + + # 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 + +if gtk.pygtk_version < (2,8,0): + gobject.type_register(QuoteUpdater) diff --git a/invest-applet/invest/test.py b/invest-applet/invest/test.py new file mode 100755 index 00000000..aea5f5f3 --- /dev/null +++ b/invest-applet/invest/test.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import unittest +from os.path import * +import sys + +# Make sure we run the local version +sys.path.insert(0, abspath(dirname(__file__) + "/..")) +print sys.path + +import quotes +import invest + +def null_function (*args): + pass + +class TestQuotes (unittest.TestCase): + def testQuoteUpdater_populate (self): + qu = quotes.QuoteUpdater (null_function, null_function) + invest.STOCKS = {'GOGO': {'label': "Google Inc.", 'purchases': [{'amount' : 1, 'comission' : 0.0, 'bought': 0.0}]}, 'JAVA': {'label':"Sun Microsystems Inc.", 'purchases': [{'amount' : 1, 'comission' : 0.0, 'bought': 0.0}]}} + quote = { 'GOGO': { "ticker": 'GOGO', "trade": 386.91, "time": "10/3/2008", "date": "4.00pm", "variation": -3.58, "open": 397.14, "variation_pct": 10 }} + qu.populate (quote) + self.assertEqual (qu.quotes_valid, True) + # In response to bug 554425, try a stock that isn't in our database + quote = { "clearlyFake": { "ticker": "clearlyFake", "trade": 386.91, "time": "10/3/2008", "date": "4.00pm", "variation": -3.58, "open": 397.14, "variation_pct": 10 }} + qu.populate (quote) + self.assertEqual (qu.quotes_valid, False) + +if __name__ == '__main__': + unittest.main () diff --git a/invest-applet/invest/widgets.py b/invest-applet/invest/widgets.py new file mode 100644 index 00000000..3f5bc91b --- /dev/null +++ b/invest-applet/invest/widgets.py @@ -0,0 +1,250 @@ +import os, time +from os.path import * +import mateapplet, gtk, gtk.gdk, mateconf, gobject, pango +from gettext import gettext as _ +import locale +import csv +import invest, invest.about, invest.chart + +COLORSCALE_POSITIVE = [ + "white", + "#ad7fa8", + "#75507b", + "#5c3566", + "#729fcf", + "#3465a4", + "#204a87", + "#8ae234", + "#73d216", + "#4e9a06", +] +COLORSCALE_NEGATIVE = [ + "white", + "#fce94f", + "#e9b96e", + "#fcaf3e", + "#c17d11", + "#f57900", + "#ce5c00", + "#ef2929", + "#cc0000", + "#a40000", +] +LIGHT = -3 +MEDIUM = -1 + +TICKER_TIMEOUT = 10000#3*60*1000# + +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) + + simple_quotes_only = quotes_updater.simple_quotes_only() + + # model: SYMBOL, LABEL, TICKER_ONLY, BALANCE, BALANCE_PCT, VALUE, VARIATION_PCT, PB + # Translators: these words all refer to a stock. Last is short + # for "last price". Gain is referring to the gain since the + # stock was purchased. + col_names = [_('Ticker'), _('Last'), _('Change %'), _('Chart'), _('Gain'), _('Gain %')] + col_cellgetdata_functions = [self._getcelldata_label, self._getcelldata_value, + self._getcelldata_variation, None, self._getcelldata_balance, + self._getcelldata_balancepct] + for i, col_name in enumerate(col_names): + if i < 3: + cell = gtk.CellRendererText() + if i > 0: + cell.set_property("xalign", 1.0) + column = gtk.TreeViewColumn (col_name, cell) + if i == 0: + column.set_sort_column_id(quotes_updater.LABEL) + elif i == 2: + column.set_sort_column_id(quotes_updater.VARIATION_PCT) + column.set_cell_data_func(cell, col_cellgetdata_functions[i]) + self.append_column(column) + elif i == 3: + cell_pb = gtk.CellRendererPixbuf() + column = gtk.TreeViewColumn (col_name, cell_pb, pixbuf=quotes_updater.PB) + self.append_column(column) + else: + # add the last two column only if we have any positions + if simple_quotes_only == False: + cell = gtk.CellRendererText() + cell.set_property("xalign", 1.0) + column = gtk.TreeViewColumn (col_name, cell) + if i == 4: + column.set_sort_column_id(quotes_updater.BALANCE) + elif i == 5: + column.set_sort_column_id(quotes_updater.BALANCE_PCT) + 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) + + + # locale-aware formatting of the value as monetary, without currency symbol, using 2 decimal digits + 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, 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], model[iter][model.CURRENCY])) + + def is_selected(self, model, iter): + m, it = self.get_selection().get_selected() + return it != None and model.get_path(iter) == m.get_path(it) + + def get_color(self, model, iter, field): + palette = COLORSCALE_POSITIVE + intensity = MEDIUM + if model[iter][field] < 0: + palette = COLORSCALE_NEGATIVE + if self.is_selected(model, iter): + intensity = LIGHT + return palette[intensity] + + def _getcelldata_variation(self, column, cell, model, iter): + color = self.get_color(model, iter, model.VARIATION_PCT) + change_pct = self.format_percent(model[iter][model.VARIATION_PCT]) + cell.set_property('markup', + "<span foreground='%s'>%s</span>" % + (color, change_pct)) + + def _getcelldata_balance(self, column, cell, model, iter): + is_ticker_only = model[iter][model.TICKER_ONLY] + color = self.get_color(model, iter, model.BALANCE) + if is_ticker_only: + cell.set_property('text', '') + else: + balance = self.format_difference(model[iter][model.BALANCE], model[iter][model.CURRENCY]) + cell.set_property('markup', + "<span foreground='%s'>%s</span>" % + (color, balance)) + + def _getcelldata_balancepct(self, column, cell, model, iter): + is_ticker_only = model[iter][model.TICKER_ONLY] + color = self.get_color(model, iter, model.BALANCE_PCT) + if is_ticker_only: + cell.set_property('text', '') + else: + balance_pct = self.format_percent(model[iter][model.BALANCE_PCT]) + cell.set_property('markup', + "<span foreground='%s'>%s</span>" % + (color, balance_pct)) + + def on_row_activated(self, treeview, path, view_column): + ticker = self.get_model()[self.get_model().get_iter(path)][0] + if ticker == None: + return + + invest.chart.show_chart([ticker]) + +#class InvestTicker(gtk.Label): +# def __init__(self): +# gtk.Label.__init__(self, _("Waiting...")) +# +# self.quotes = [] +# gobject.timeout_add(TICKER_TIMEOUT, self.scroll_quotes) +# +# get_quotes_updater().connect('quotes-updated', self.on_quotes_update) +# +# def on_quotes_update(self, updater): +# self.quotes = [] +# updater.foreach(self.update_quote, None) +# +# def update_quote(self, model, path, iter, user_data): +# color = GREEN +# if model[iter][model.BALANCE] < 0: +# color = RED +# +# self.quotes.append( +# "%s: <span foreground='%s'>%+.2f (%+.2f%%)</span> %.2f" % +# (model[iter][model.SYMBOL], color, model[iter][model.BALANCE], model[iter][model.BALANCE_PCT], model[iter][model.VALUE])) +# +# def scroll_quotes(self): +# if len(self.quotes) == 0: +# return True +# +# q = self.quotes.pop() +# self.set_markup("<span face='Monospace'>%s</span>" % q) +# self.quotes.insert(0, q) +# +# return True +# +#gobject.type_register(InvestTicker) + +class InvestTrend(gtk.Image): + def __init__(self): + gtk.Image.__init__(self) + self.pixbuf = None + self.previous_allocation = (0,0) + self.connect('size-allocate', self.on_size_allocate) + get_quotes_updater().connect('quotes-updated', self.on_quotes_update) + + def on_size_allocate(self, widget, allocation): + if self.previous_allocation == (allocation.width, allocation.height): + return + + self.pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, allocation.height, allocation.height) + self.set_color("grey") + self.previous_allocation = (allocation.width, allocation.height) + + def set_color(self, color, opacity=0xFF): + if self.pixbuf != None: + try: + color = pango.Color(color) + factor = float(0xFF)/0xFFFF + self.pixbuf.fill( + int(color.red*factor)<<24|int(color.green*factor)<<16|int(color.blue*factor)<<8|opacity) + self.set_from_pixbuf(self.pixbuf) + except Exception, msg: + invest.error("Could not set color: %s" % msg) + + def on_quotes_update(self, updater): + start_total = 0 + now_total = 0 + for row in updater: + # Don't count the ticker only symbols in the color-trend + if row[updater.TICKER_ONLY]: + continue + + var = row[updater.VARIATION]/100 + now = row[updater.VALUE] + + start = now / (1 + var) + + portfolio_number = sum([purchase["amount"] for purchase in invest.STOCKS[row[updater.SYMBOL]]["purchases"]]) + start_total += start * portfolio_number + now_total += now * portfolio_number + + day_var = 0 + if start_total != 0: + day_var = (now_total - start_total) / start_total * 100 + + color = int(2*day_var) + opacity = min(0xFF, int(abs(127.5*day_var))) + if day_var < 0: + color = COLORSCALE_NEGATIVE[min(len(COLORSCALE_NEGATIVE)-1, abs(color))] + else: + color = COLORSCALE_POSITIVE[min(len(COLORSCALE_POSITIVE)-1, abs(color))] + + self.set_color(color, opacity) + +gobject.type_register(InvestTrend) |