added portfolio section
[stockthis] / stockthis.py
1 #!/usr/bin/env python
2 # -*- coding: UTF8 -*-
3 # Copyright (C) 2008 by Daniel Martin Yerga
4 # <dyerga@gmail.com>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18 #
19 # StocksPy: Application to get stocks data from Yahoo Finance.
20 # Version 0.1
21 #
22
23 import urllib2
24 import gtk, gobject
25 import os
26 import hildon
27
28 #import osso
29 #osso_c = osso.Context("net.yerga.stockthis", "0.3", False)
30
31 from marketdata import markets, idmarket, localmarkets, localids
32 import settings
33
34
35 #detect if is ran locally or not
36 import sys
37 runningpath = sys.path[0]
38
39 if '/usr/share' in runningpath:
40     runninglocally = False
41 else:
42     runninglocally = True
43
44 HOME = os.path.expanduser("~")
45
46 settingsdb, imgdir, configdir, logfile = \
47     settings.define_paths(runninglocally, HOME)
48
49
50 fhsize = gtk.HILDON_SIZE_FINGER_HEIGHT
51 horbtn = hildon.BUTTON_ARRANGEMENT_HORIZONTAL
52 ui_normal = gtk.HILDON_UI_MODE_NORMAL
53 ui_edit = gtk.HILDON_UI_MODE_EDIT
54 winprogind = hildon.hildon_gtk_window_set_progress_indicator
55
56 gtk.gdk.threads_init()
57
58 class StocksPy:
59
60     def __init__(self):
61         self.program = hildon.Program()
62         self.program.__init__()
63         gtk.set_application_name("StockThis")
64         self.window = hildon.StackableWindow()
65         self.window.set_default_size(800, 480)
66         self.program.add_window(self.window)
67         self.window.connect("destroy", gtk.main_quit)
68
69         menu = hildon.AppMenu()
70         self.window.set_app_menu(menu)
71         button = gtk.Button("About")
72         button.connect("clicked", self.on_about)
73         menu.append(button)
74         menu.show_all()
75
76         vbox = gtk.VBox()
77         toolbar = self.main_toolbar(False, False)
78
79         parea = hildon.PannableArea()
80         tv = hildon.GtkTreeView(ui_normal)
81         inmodel = self.__create_model(markets, idmarket)
82         tv.connect("row-activated", self.show_instrument_view, inmodel)
83         tv.set_model(inmodel)
84         self._tv_columns(tv)
85         parea.add(tv)
86
87         vbox.pack_start(parea, True, True, 0)
88         vbox.pack_start(gtk.HSeparator(), False, False, 5)
89         vbox.pack_start(toolbar, False, False, 0)
90
91         self.window.add(vbox)
92         self.window.show_all()
93
94     def show_instrument_view(self, widget, path, column, inmodel):
95         market = inmodel[path][0]
96
97         names = localmarkets[idmarket.index(market)]
98         ids = localids[idmarket.index(market)]
99
100         window = hildon.StackableWindow()
101
102         vbox = gtk.VBox()
103         toolbar = self.main_toolbar(False, False)
104
105         parea = hildon.PannableArea()
106         tv = hildon.GtkTreeView(ui_normal)
107         model = self.__create_model(names, ids)
108         tv.connect("row-activated", self.show_quotes_view, model, False)
109         tv.set_model(model)
110         self._tv_columns(tv)
111         parea.add(tv)
112
113         vbox.pack_start(parea, True, True, 0)
114         vbox.pack_start(gtk.HSeparator(), False, False, 5)
115         vbox.pack_start(toolbar, False, False, 0)
116
117         window.add(vbox)
118         window.show_all()
119
120
121     def show_quotes_view(self, widget, path, column, model, portfolio):
122         quote = model[path][0], model[path][1]
123
124         self.stocks_id = quote[0]
125         self.stocks_name = quote[1]
126         print quote
127
128         self.quotes_win = hildon.StackableWindow()
129
130         vbox = gtk.VBox()
131         toolbar = self.main_toolbar(True, portfolio)
132
133
134         self.titlelbl = gtk.Label('')
135         self.titlelbl.set_markup('<b><big>' + quote[1].replace('&', '') +
136                                  '</big></b>')
137         color = gtk.gdk.color_parse("#03A5FF")
138         self.titlelbl.modify_fg(gtk.STATE_NORMAL, color)
139
140         parea = hildon.PannableArea()
141
142         vbox1 = gtk.VBox()
143
144         hbox = gtk.HBox()
145         label = gtk.Label('')
146         label.set_markup('<b><big>Price:</big></b>')
147         self.lprice = gtk.Label('')
148         hbox.pack_start(label, False, False, 20)
149         hbox.pack_start(self.lprice, False, False, 245)
150         vbox1.pack_start(hbox, True, True, 0)
151
152         hbox = gtk.HBox()
153         label = gtk.Label('')
154         label.set_markup('<b><big>Change:</big></b>')
155         self.lchange = gtk.Label('')
156         self.lpercent = gtk.Label('')
157         hbox.pack_start(label, False, False, 20)
158         hbox.pack_start(self.lchange, False, False, 205)
159         hbox.pack_start(self.lpercent, False, False, 0)
160         vbox1.pack_start(hbox, True, True, 0)
161
162         hbox = gtk.HBox()
163         label = gtk.Label('')
164         label.set_markup('<b><big>Volume:</big></b>')
165         self.lvolume = gtk.Label('')
166         hbox.pack_start(label, False, False, 20)
167         hbox.pack_start(self.lvolume, False, False, 207)
168         vbox1.pack_start(hbox, True, True, 0)
169
170         hbox = gtk.HBox()
171         label = gtk.Label('')
172         label.set_markup('<b><big>52 week high:</big></b>')
173         self.l52whigh = gtk.Label('')
174         hbox.pack_start(label, False, False, 20)
175         hbox.pack_start(self.l52whigh, False, False, 110)
176         vbox1.pack_start(hbox, True, True, 0)
177
178         hbox = gtk.HBox()
179         label = gtk.Label('')
180         label.set_markup('<b><big>52 week low:</big></b>')
181         self.l52wlow = gtk.Label('')
182         hbox.pack_start(label, False, False, 20)
183         hbox.pack_start(self.l52wlow, False, False, 125)
184         vbox1.pack_start(hbox, True, True, 0)
185
186         hbox = gtk.HBox()
187         button1 = hildon.PickerButton(fhsize, horbtn)
188         data = ["50", "100", "200", "300", "400", "500", "600", "700", "800",
189                 "900", "1000"]
190         selector = self.create_selector(data, True)
191         button1.set_selector(selector)
192         button1.set_title("Shares")
193         #FIXME: Improve as it's shown you have a component in your portfolio
194         shares = self.get_shares_from_symbol()
195         button1.set_value(shares)
196         hbox.pack_start(button1, True, True, 0)
197
198         button = hildon.Button(fhsize, horbtn)
199         button.set_title("Add to Portfolio")
200         button.connect("clicked", self.add_to_portfolio, button1)
201         hbox.pack_start(button, True, True, 0)
202
203         hbox1 = gtk.HBox()
204         label = gtk.Label('')
205         label.set_markup('<b><big>Shares:</big></b>')
206         self.shares = gtk.Label(shares)
207         hbox1.pack_start(label, False, False, 20)
208         hbox1.pack_start(self.shares, False, False, 215)
209
210         hbox2 = gtk.HBox()
211         label = gtk.Label('')
212         label.set_markup('<b><big>Holdings Value:</big></b>')
213         self.holdingsvalue = gtk.Label("")
214         hbox2.pack_start(label, False, False, 20)
215         hbox2.pack_start(self.holdingsvalue, False, False, 85)
216
217         hbox3 = gtk.HBox()
218         label = gtk.Label('')
219         label.set_markup("<b><big>Day's Value Change:</big></b>")
220         self.dayvaluechange = gtk.Label("")
221         hbox3.pack_start(label, False, False, 20)
222         hbox3.pack_start(self.dayvaluechange, False, False, 10)
223
224         if not portfolio:
225             vbox1.pack_start(hbox, False, False, 0)
226         else:
227             vbox1.pack_start(hbox1, True, True, 0)
228             vbox1.pack_start(hbox2, True, True, 0)
229             vbox1.pack_start(hbox3, True, True, 0)
230
231         parea.add_with_viewport(vbox1)
232
233         vbox.pack_start(self.titlelbl, False, False, 0)
234         vbox.pack_start(gtk.HSeparator(), False, False, 0)
235         vbox.pack_start(parea, True, True, 0)
236         vbox.pack_start(gtk.HSeparator(), False, False, 5)
237         vbox.pack_start(toolbar, False, False, 0)
238
239         self.quotes_win.add(vbox)
240         self.quotes_win.show_all()
241         self.show_data(quote[0], self.quotes_win, shares)
242
243     def get_shares_from_symbol(self):
244         portfolio_data = settings.load_portfolio(settingsdb)
245         shares = "0"
246         for item in portfolio_data :
247             if self.stocks_id in item:
248                 shares = item[2]
249         return shares
250
251     def add_to_portfolio(self, widget, button):
252         shares = button.get_value()
253         self.stocks_id
254         self.stocks_name
255
256         portfolio = settings.load_portfolio(settingsdb)
257         index = "None"
258         for item in portfolio:
259             if self.stocks_id in item:
260                 index = portfolio.index(item)
261
262         item = [self.stocks_id, self.stocks_name, shares, '-']
263
264         if index is "None":
265             settings.insert_new_item_to_portfolio(settingsdb, item)
266         else:
267             settings.delete_item_from_portfolio(settingsdb, self.stocks_id)
268             settings.insert_new_item_to_portfolio(settingsdb, item)
269
270         self.show_info_banner(widget, "Added to portfolio")
271
272
273     def create_selector(self, data, entry):
274         if entry:
275             selector = hildon.TouchSelectorEntry(text=True)
276         else:
277             selector = hildon.hildon_touch_selector_new_text()
278         for i in range(len(data)):
279             selector.append_text(data[i])
280
281         return selector
282
283     def show_data(self, quote, win, shares):
284         import thread
285         winprogind(win, 1)
286         thread.start_new_thread(self.get_data, (quote, win, shares))
287
288     def get_data(self, quote, win, shares):
289         from ystockquote import ystockquote as yt
290         try:
291             data = yt.get_all(quote)
292         except:
293             print 'Failed to get internet data'
294             data = {'price': 'N/A', 'change': 'N/A', 'volume':'N/A',
295                     '52_week_high': 'N/A', '52_week_low': 'N/A'}
296             self.titlelbl.set_markup('<b><big>Failed to get data</big></b>')
297
298         try:
299             ch_percent = \
300                     100.0 * float(data['change'])/(float(data['price']) - \
301                     float(data['change']))
302         except ValueError:
303             ch_percent = 0.0
304
305         self.lprice.set_label(data['price'])
306         self.lchange.set_label(data['change'])
307         self.lpercent.set_label('%6.2f %%' % ch_percent)
308
309         if '-' in data['change']:
310             color = gtk.gdk.color_parse("#FF0000")
311         else:
312             color = gtk.gdk.color_parse("#16EB78")
313
314         self.lpercent.modify_fg(gtk.STATE_NORMAL, color)
315         self.lchange.modify_fg(gtk.STATE_NORMAL, color)
316
317         self.lvolume.set_label(data['volume'])
318         self.l52whigh.set_label(data['52_week_high'])
319         self.l52wlow.set_label(data['52_week_low'])
320
321         try:
322             daychange = float(shares)*float(data['change'])
323         except ValueError:
324             daychange = 'N/A'
325         try:
326             holdvalue = float(shares)*float(data['price'])
327         except ValueError:
328             holdvalue = 'N/A'
329
330         self.dayvaluechange.set_label(str(daychange))
331         self.holdingsvalue.set_label(str(holdvalue))
332
333         winprogind(win, 0)
334
335     def refresh_stock_data(self, widget, portfolio):
336         if portfolio:
337             shares = self.get_shares_from_symbol()
338         else:
339             shares = "0"
340
341         self.show_data(self.stocks_id, self.quotes_win, shares)
342
343     def show_graph_view(self, widget):
344         win = hildon.StackableWindow()
345
346         vbox = gtk.VBox()
347         toolbar = self.main_toolbar(False, True)
348
349         self.graphs_title = gtk.Label(self.stocks_name)
350         color = gtk.gdk.color_parse("#03A5FF")
351         self.graphs_title.modify_fg(gtk.STATE_NORMAL, color)
352
353         parea = hildon.PannableArea()
354
355         hbox = gtk.HBox()
356         hbox.set_homogeneous(True)
357
358         button = hildon.Button(fhsize, horbtn)
359         button.set_label('1d')
360         button.connect("clicked", self.show_graph, '1d', win)
361         hbox.pack_start(button)
362
363         button = hildon.Button(fhsize, horbtn)
364         button.set_label('5d')
365         button.connect("clicked", self.show_graph, '5d', win)
366         hbox.pack_start(button)
367
368         button = hildon.Button(fhsize, horbtn)
369         button.set_label('3m')
370         button.connect("clicked", self.show_graph, '3m', win)
371         hbox.pack_start(button)
372
373         button = hildon.Button(fhsize, horbtn)
374         button.set_label('6m')
375         button.connect("clicked", self.show_graph, '6m', win)
376         hbox.pack_start(button)
377
378         button = hildon.Button(fhsize, horbtn)
379         button.set_label('1y')
380         button.connect("clicked", self.show_graph, '1y', win)
381         hbox.pack_start(button)
382
383         button = hildon.Button(fhsize, horbtn)
384         button.set_label('2y')
385         button.connect("clicked", self.show_graph, '2y', win)
386         hbox.pack_start(button)
387
388         button = hildon.Button(fhsize, horbtn)
389         button.set_label('5y')
390         button.connect("clicked", self.show_graph, '5y', win)
391         hbox.pack_start(button)
392
393         button = hildon.Button(fhsize, horbtn)
394         button.set_label('Max')
395         button.connect("clicked", self.show_graph, 'max', win)
396         hbox.pack_start(button)
397
398         vbox1 = gtk.VBox()
399         vbox1.pack_start(hbox, False, False, 0)
400
401         self.graph = gtk.Image()
402         vbox1.pack_start(self.graph, True, True, 0)
403
404         parea.add_with_viewport(vbox1)
405
406         vbox.pack_start(self.graphs_title, False, False, 0)
407         vbox.pack_start(gtk.HSeparator(), False, False, 0)
408         vbox.pack_start(parea, True, True, 0)
409         vbox.pack_start(gtk.HSeparator(), False, False, 5)
410         vbox.pack_start(toolbar, False, False, 0)
411
412         win.add(vbox)
413         win.show_all()
414
415         self.show_graph(None, '1d', win)
416
417     def show_graph(self, widget, option, win):
418         import thread
419         winprogind(win, 1)
420         thread.start_new_thread(self.get_graph_data, (option, win))
421
422     def get_graph_data(self, option, win):
423         kind = self.stocks_id
424         if option == '1d':
425             url = 'http://uk.ichart.yahoo.com/b?s=%s' % kind
426         elif option == '5d':
427             url = 'http://uk.ichart.yahoo.com/w?s=%s' % kind
428         elif option == '3m':
429             url = 'http://chart.finance.yahoo.com/c/3m/s/%s' % kind.lower()
430         elif option == '6m':
431             url = 'http://chart.finance.yahoo.com/c/6m/s/%s' % kind.lower()
432         elif option == '1y':
433             url = 'http://chart.finance.yahoo.com/c/1y/s/%s' % kind.lower()
434         elif option == '2y':
435             url = 'http://chart.finance.yahoo.com/c/2y/s/%s' % kind.lower()
436         elif option == '5y':
437             url = 'http://chart.finance.yahoo.com/c/5y/s/%s' % kind.lower()
438         elif option == 'max':
439             url = 'http://chart.finance.yahoo.com/c/my/s/%s' % kind.lower()
440
441         try:
442             myimg = urllib2.urlopen(url)
443             imgdata = myimg.read()
444
445             pbl = gtk.gdk.PixbufLoader()
446             pbl.write(imgdata)
447
448             pbuf = pbl.get_pixbuf()
449             pbl.close()
450             self.graph.set_from_pixbuf(pbuf)
451             winprogind(win, 0)
452         except:
453             winprogind(win, 0)
454             self.graphs_title.set_label('Failed to get data')
455             self.graph.destroy()
456
457     def _tv_columns(self, treeview):
458         column = gtk.TreeViewColumn('ID', gtk.CellRendererText(), text=0)
459         column.set_visible(False)
460         treeview.append_column(column)
461
462         column = gtk.TreeViewColumn('Name', gtk.CellRendererText(), text=1)
463         treeview.append_column(column)
464
465     def __create_model(self, names, ids):
466         lstore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
467         for item in range(len(names)):
468             iter = lstore.append()
469             lstore.set(iter, 0, ids[item], 1, names[item])
470         return lstore
471
472     def main_toolbar(self, quotesview, portfolio):
473         toolbar = gtk.HBox()
474         toolbar.set_homogeneous(True)
475
476         portfolio_btn = hildon.Button(fhsize, horbtn)
477         portfolio_btn.set_title("Portfolio")
478         portfolio_btn.connect("clicked", self.show_portfolio_view)
479
480         graph_btn = hildon.Button(fhsize, horbtn)
481         graph_btn.set_title("Graph")
482         graph_btn.connect("clicked", self.show_graph_view)
483
484         refresh_btn = hildon.Button(fhsize, horbtn)
485         refresh_btn.set_title("Refresh")
486         refresh_btn.connect("clicked", self.refresh_stock_data, portfolio)
487
488         if not portfolio:
489             toolbar.pack_start(portfolio_btn)
490         if quotesview:
491             toolbar.pack_start(graph_btn)
492             toolbar.pack_start(refresh_btn)
493
494         toolbar.show_all()
495
496         return toolbar
497
498     def show_portfolio_view(self, widget):
499         data = settings.load_portfolio(settingsdb)
500
501         win = hildon.StackableWindow()
502
503         vbox = gtk.VBox()
504
505         parea = hildon.PannableArea()
506         tv = hildon.GtkTreeView(ui_normal)
507         tv.set_headers_visible(True)
508         self.portfolio_model = self._create_portfolio_model(data)
509         tv.connect("row-activated", self.show_quotes_view, self.portfolio_model, True)
510         tv.set_model(self.portfolio_model)
511         self._tv_portfolio_columns(tv)
512         parea.add(tv)
513
514         hbox = gtk.HBox()
515         button = hildon.Button(fhsize, horbtn)
516         button.set_title("Refresh All")
517         button.connect("clicked", self.refresh_portfolio, tv, win)
518         hbox.pack_start(button, True, True, 0)
519
520         button = hildon.Button(fhsize, horbtn)
521         button.set_title("Remove")
522         button.connect("clicked", self.remove_item)
523         hbox.pack_start(button, True, True, 0)
524
525         vbox.pack_start(parea, True, True, 0)
526         vbox.pack_start(hbox, False, False, 0)
527         win.add(vbox)
528         win.show_all()
529
530     def remove_item(self, widget):
531         win = hildon.StackableWindow()
532         win.fullscreen()
533         toolbar = hildon.EditToolbar("Choose items to delete", "Delete")
534         win.set_edit_toolbar(toolbar)
535
536         vbox = gtk.VBox()
537         parea = hildon.PannableArea()
538         tv = hildon.GtkTreeView(ui_edit)
539         selection = tv.get_selection()
540         selection.set_mode(gtk.SELECTION_MULTIPLE)
541         tv.set_model(self.portfolio_model)
542         self._tv_remove_portfolio_columns(tv)
543         parea.add(tv)
544
545         toolbar.connect("button-clicked", self.delete_from_portfolio, win, tv,
546                         selection)
547         toolbar.connect_object("arrow-clicked", gtk.Window.destroy, win)
548
549         vbox.pack_start(parea, True, True, 0)
550         win.add(vbox)
551         win.show_all()
552
553     def delete_from_portfolio(self, widget, win, tv, selection):
554         if not self.is_treeview_selected(tv):
555             return
556
557         conf = self.show_confirmation(win, "Delete items?")
558
559         if conf:
560             selmodel, selected = selection.get_selected_rows()
561             iters = [selmodel.get_iter(path) for path in selected]
562             for i in iters:
563                 symbol = selmodel.get_value(i, 0)
564                 settings.delete_item_from_portfolio(settingsdb, symbol)
565                 selmodel.remove(i)
566
567     def _tv_remove_portfolio_columns(self, treeview):
568         column = gtk.TreeViewColumn('ID', gtk.CellRendererText(), text=0)
569         column.set_visible(False)
570         treeview.append_column(column)
571
572         column = gtk.TreeViewColumn('Name', gtk.CellRendererText(), text=1)
573         column.set_property("expand", True)
574         treeview.append_column(column)
575
576         column = gtk.TreeViewColumn('Shares', gtk.CellRendererText(), text=2)
577         column.set_visible(False)
578         treeview.append_column(column)
579
580         column = gtk.TreeViewColumn('Price', gtk.CellRendererText(), text=3)
581         column.set_visible(False)
582         treeview.append_column(column)
583
584     def refresh_portfolio(self, widget, tv, win):
585         data = settings.load_portfolio(settingsdb)
586         import thread
587         winprogind(win, 1)
588         thread.start_new_thread(self._do_refresh_portfolio, (data, tv, win))
589
590     def _do_refresh_portfolio(self, data, tv, win):
591         for item in data:
592             item[3] = self.get_price(item[0])
593
594         self.portfolio_model = self._create_portfolio_model(data)
595         tv.set_model(self.portfolio_model)
596         winprogind(win, 0)
597
598     def get_price(self, symbol):
599         from ystockquote import ystockquote as yt
600         price = yt.get_price(symbol)
601
602         return price
603
604     def _create_portfolio_model(self, data):
605         lstore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING,
606                                 gobject.TYPE_STRING, gobject.TYPE_STRING)
607         for item in data:
608             iter = lstore.append()
609             lstore.set(iter, 0, item[0], 1, item[1], 2, item[2], 3, item[3])
610         return lstore
611
612     def _tv_portfolio_columns(self, treeview):
613         column = gtk.TreeViewColumn('ID', gtk.CellRendererText(), text=0)
614         column.set_visible(False)
615         treeview.append_column(column)
616
617         column = gtk.TreeViewColumn('Name', gtk.CellRendererText(), text=1)
618         column.set_property("expand", True)
619         treeview.append_column(column)
620
621         column = gtk.TreeViewColumn('Shares', gtk.CellRendererText(), text=2)
622         treeview.append_column(column)
623
624         column = gtk.TreeViewColumn('Price', gtk.CellRendererText(), text=3)
625         treeview.append_column(column)
626
627     def show_confirmation(self, window, msg):
628         dialog = hildon.hildon_note_new_confirmation(window, msg)
629         dialog.show_all()
630         result = dialog.run()
631         if result == gtk.RESPONSE_OK:
632             dialog.destroy()
633             return True
634
635         dialog.destroy()
636         return False
637
638     def show_info_banner(self, widget, msg):
639         hildon.hildon_banner_show_information(widget, 'qgn_note_infoprint', msg)
640
641     def is_treeview_selected(self, treeview):
642         selection = treeview.get_selection()
643         if selection.count_selected_rows() == 0:
644             self.show_info_banner(treeview, 'No selected item')
645             return False
646         else:
647             return True
648
649     def on_about(self, widget):
650         dialog = gtk.AboutDialog()
651         dialog.set_name("StockThis")
652         dialog.set_version("0.3")
653         dialog.set_copyright("Copyright © 2009")
654         dialog.set_website("http://stockthis.garage.maemo.org")
655         dialog.set_authors(["Daniel Martin Yerga <dyerga@gmail.com>"])
656         logo = gtk.gdk.pixbuf_new_from_file(imgdir + "stockthis.png")
657         dialog.set_logo(logo)
658         dialog.set_license("This program is released under the GNU\nGeneral Public License. Please visit \nhttp://www.gnu.org/copyleft/gpl.html\nfor details.")
659         dialog.set_artists(["Logo by Daniel Martin Yerga"])
660         dialog.run()
661         dialog.destroy()
662
663 if __name__ == "__main__":
664     stockspy = StocksPy()
665     gtk.gdk.threads_enter()
666     gtk.main()
667     gtk.gdk.threads_leave()