#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import sys
import time
import re
from threading import Thread, Lock
from decimal import Decimal
from urllib import urlopen
from eagle import *
page_url = "http://www.personnaliteinvestnet.com.br/personnaliteinvestnet/fundos/rentabilidade/mensal/impressaorentabilidade.asp?tipo=per"
re_line = re.compile(
"""\
\
-(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?) | \
(?P.*?)% | \
(?:.*?) | \
\
""",
re.M)
current_month = time.localtime()[1] - 1 # 1..12 -> 0..11
month_str = ("Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec")
def populate_table(app):
"""Function to populate tables.
This function will be put in idle_add(), so it will always run in main,
graphics thread. We're unable to update GUI from secondary threads!
"""
hide_with_loss = app["hide_with_loss"]
pop = []
for row in app.table:
if not hide_with_loss:
pop.append(row)
else:
for col in row[1: 15]:
if col < 0:
break
else:
pop.append(row)
graph_items = []
for row in pop:
graph_items.append(row[0:4] + [False, row, None])
calc_items = []
for row in pop:
calc_items.append([row[0], Decimal(0)] + row[1:4] + [row])
app["table"][:] = pop
app["graph_items"][:] = graph_items
app["calc_items"][:] = calc_items
calc_changed(app)
return False
def add_status_msg(app, msg):
def f(app):
try:
ids = app.statusmsg
except AttributeError, e:
app.statusmsg = ids = []
ids.append(app.status_message(msg))
return False
app.idle_add(f)
def clear_status_msg(app):
def f(app):
try:
ids = app.statusmsg
app.statusmsg = []
except AttributeError, e:
return
for i in ids:
app.remove_status_message(i)
return False
app.idle_add(f)
def download_and_parse_unlocked(app):
"""Download and parse HTML page with data, populate tables.
This will not do any GUI stuff, it will add populate_table() to be
called in next idle time from GUI thread using idle_add()
"""
add_status_msg(app, "downloading url: %s" % page_url)
contents = urlopen(page_url).read()
clear_status_msg(app)
pos_remap = (0, 17, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 1, 2, 3, 16)
add_status_msg(app, "parsing document...")
table = []
results = re_line.finditer(contents)
for r in results:
row = [None] * len(pos_remap)
for i, v in enumerate(r.groups()):
v = v.strip()
if i == 0:
v = v.decode('latin1')
else:
try:
# 1.234,00 -> 1234.00
v = v.replace('.', '').replace(',', '.')
v = Decimal(v)
except Exception, e:
v = Decimal("0")
row[pos_remap[i]] = v
table.append(row)
def get_whole_col(table, col_idx):
return [row[col_idx] for row in table]
app.max_of = {}
app.min_of = {}
for i in xrange(1, 18):
whole_col = get_whole_col(table, i)
app.max_of[i] = max(*whole_col)
app.min_of[i] = min(*whole_col)
clear_status_msg(app)
app.table = table
app.idle_add(populate_table)
download_and_parse_lock = Lock()
def download_and_parse(app):
"""Ensure just one processing is done at time"""
download_and_parse_lock.acquire()
try:
try:
download_and_parse_unlocked(app)
except Exception, e:
import traceback
add_status_msg(app, "Error %s" % e)
traceback.print_tb(sys.exc_info()[2])
finally:
download_and_parse_lock.release()
def cell_format_func(app, table, row, col, value):
"""Highlight main table values"""
kargs = {}
if col > 0:
xalign = Table.CellFormat.XALIGN_RIGHT
if value == app.min_of[col]:
kargs["fgcolor"] = "red"
kargs["bold"] = True
elif value == app.max_of[col]:
kargs["fgcolor"] = "blue"
kargs["bold"] = True
else:
xalign = Table.CellFormat.XALIGN_LEFT
return Table.CellFormat(contents=str, xalign=xalign, **kargs)
def plot(canvas, min_val, max_val, values, color):
"""Plot data values"""
points = []
y_scale = (float(max_val) - float(min_val)) / canvas.height
dx = canvas.width / float(len(values) - 1)
x = 0
for v in values:
y = canvas.height - (float(v) - float(min_val)) / y_scale
points.append((int(x), int(y)))
x += dx
canvas.draw_lines(points, color=color)
def get_colors(n_items):
"""Dumb way to get different colors
@todo find a smarter color allocation algorithm
"""
n_var = n_items / 3
v_color = 255 / (n_var + 1)
colors = []
mult = 0
for i in xrange(n_items):
c = [255, 255, 255]
j = i % 3
if j == 0:
mult += 1
c[j] = c[j] - v_color * mult
colors.append(c)
return colors
def draw_text_shadow(canvas, text, x, y, color="white"):
canvas.draw_text(str(text), x + 1, y + 1,
font_options=canvas.FONT_OPTION_BOLD,
fgcolor="black")
canvas.draw_text(str(text), x, y,
font_options=canvas.FONT_OPTION_BOLD,
fgcolor=color)
def draw_graph_info(canvas, min_val, max_val, zero=0):
"""Draw vertical lines, month names and extreme values"""
color = "#444444"
month_pos = 20
x = 0
dx = float(canvas.width) / 11
month = current_month - 12
for i in xrange(12):
ix = int(x)
draw_text_shadow(canvas, month_str[month], ix + 2, month_pos, color)
x += dx
month += 1
canvas.draw_line(ix, 0, ix, canvas.height, color)
draw_text_shadow(canvas, "%0.2f" % max_val, 0, 0)
draw_text_shadow(canvas, "%0.2f" % min_val, 0, canvas.height - 20)
if max_val > min_val and min_val < zero < max_val:
zero_y = canvas.height * (zero - min_val)/(max_val - min_val)
y = canvas.height - zero_y
canvas.draw_line(0, y, canvas.width, y, color)
def redraw_graph(app):
"""Check selected values and plot them"""
selected = [x for x in app["graph_items"] if x[4]]
canvas = app["graph"]
# Calculate minimum and maximum values of selected items in order to
# get the scale
min_val = 100
max_val = 0
for x in selected:
item = x[5]
for m in item[4:16]:
min_val = min(m, min_val)
max_val = max(m, max_val)
canvas.clear()
if not selected:
return
draw_graph_info(canvas, min_val, max_val)
colors = get_colors(len(selected))
for color, x in zip(colors, selected):
item = x[5]
values = item[4:16]
values.reverse()
x[6] = color # remember graph color, to use in cell_format_func_graph()
plot(canvas, min_val, max_val, values, color)
def redraw_graph2(app):
"""Check selected values and plot them"""
selected = [x for x in app["graph_items"] if x[4]]
canvas = app["graph2"]
capital = []
# Calculate minimum and maximum values of selected items in order to
# get the scale
min_val = 1000
max_val = 1000
for x in selected:
capital.append([])
PV = 1000
item = x[5]
ble = item[4:16]
ble.reverse()
for m in ble:
PV *= 1+(m/100)
capital[-1].append(PV)
min_val = min(PV, min_val)
max_val = max(PV, max_val)
canvas.clear()
if not selected:
return
draw_graph_info(canvas, min_val, max_val, 1000)
i = 0
colors = get_colors(len(selected))
for color, x in zip(colors, selected):
values = capital[i]
x[6] = color # remember graph color, to use in cell_format_func_graph()
plot(canvas, min_val, max_val, values, color)
i = i + 1
def graph_selected(app, table, rows):
"""Toggle item selection"""
if not rows:
return
idx, row = rows[0]
v = not row[4]
table[idx][4] = v
if not v:
table[idx][6] = None
redraw_graph(app)
redraw_graph2(app)
def cell_format_func_graph(app, table, row, col, value):
"""If item is selected, change the background to match graph color"""
r = table[row]
kargs = {}
if r[4] and r[6]:
kargs["bgcolor"] = r[6]
if 1 <= col <= 3:
kargs["xalign"] = Table.CellFormat.XALIGN_RIGHT
return Table.CellFormat(**kargs)
def reload(app, button):
"""Launch a thread to do the download and parse"""
t = Thread(target=download_and_parse, args=(app,))
t.start()
def toggle_with_loss(app, checkbox, value):
populate_table(app)
def cell_format_func_calc(app, table, row, col, value):
kargs = {}
if col == 1:
kargs["contents"] = "%0.2f" % value
if abs(value - app.min_calc) < 10:
kargs["bold"] = True
kargs["fgcolor"] = "red"
elif abs(value - app.max_calc) < 10:
kargs["bold"] = True
kargs["fgcolor"] = "blue"
return Table.CellFormat(**kargs)
def calc_changed(app, *args):
app.min_calc = sys.maxint
app.max_calc = 0
n_months = app["n_months"]
for row in app["calc_items"]:
plan = row[5]
amount = Decimal(app["amount"])
for i in plan[4 : 4 + n_months]:
v = 1 + i / Decimal("100.00")
amount *= v
row[1] = amount
app.min_calc = min(amount, app.min_calc)
app.max_calc = max(amount, app.max_calc)
page_data = Tabs.Page(label="Data",
children=(Table(id="table",
label=None,
show_headers=True,
headers=("Description",
"Yield Year",
"Yield 12",
"Yield 36",
month_str[current_month - 1],
month_str[current_month - 2],
month_str[current_month - 3],
month_str[current_month - 4],
month_str[current_month - 5],
month_str[current_month - 6],
month_str[current_month - 7],
month_str[current_month - 8],
month_str[current_month - 9],
month_str[current_month - 10],
month_str[current_month - 11],
month_str[current_month - 12],
"Tax (%)",
"Equity (R$ MM 12month)"),
types=(str,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal),
cell_format_func=cell_format_func),
),
)
page_graph = Tabs.Page(label="Graph",
children=(Table(id="graph_items",
label=None,
show_headers=True,
expand_columns_indexes=0,
hidden_columns_indexes=(5, 6),
cell_format_func=cell_format_func_graph,
selection_callback=graph_selected,
headers=("Name",
"Yield Year",
"Yield 12",
"Yield 36",
"Graph it?",
"Object",
"Color"),
types=(str,
Decimal,
Decimal,
Decimal,
bool,
object,
object)),
Group(id="graficos", label="", children=(
Canvas(id="graph",
label=None,
width=500,
height=350,
scrollbars=False,
expand_policy=ExpandPolicy.Nothing()),
Canvas(id="graph2",
label=None,
width=500,
height=350,
scrollbars=False,
expand_policy=ExpandPolicy.Nothing()),), horizontal=True),
),
)
page_calc = Tabs.Page(label="Calculator",
children=(Table(id="calc_items",
label=None,
show_headers=True,
expand_columns_indexes=0,
hidden_columns_indexes=5,
cell_format_func=cell_format_func_calc,
headers=("Name",
"Result",
"Yield Year",
"Yield 12",
"Yield 36",
"Object"),
types=(str,
Decimal,
Decimal,
Decimal,
Decimal,
object)),
UIntSpin(id="n_months",
label="Months:",
value=12,
max=12,
callback=calc_changed),
UIntSpin(id="amount",
label="Initial Amount:",
value=5000,
step=1000,
callback=calc_changed),
),
)
app = App(title="Itaú Personnalité",
statusbar=True,
center=(Tabs(id="tabs",
children=(page_data,
page_graph,
page_calc,
),
),
CheckBox(id="hide_with_loss",
label="No loss acceptable",
callback=toggle_with_loss),
),
bottom=Button(id="reload", stock="refresh", callback=reload)
)
app.max_calc = None
app.min_calc = None
app.timeout_add(1000, reload, app["reload"])
run()