Expose XWarpPointer to lua as rootwin:warp_pointer, for region_do_warp_alt
[notion.git] / contrib / scripts / stock.lua
blobc44485d513631b8b4d06b5867b09d7a5f70798c5
1 -- Authors: Andrea Rossato <arossato@istitutocolli.org>
2 -- License: GPL, version 2 or later
3 -- Last Changed: 2006-07-10
4 --
5 -- stock.lua
7 -- ABOUT
8 -- An Ion3 applet for retrieving and displaying stock market information
9 -- from http://finance.yahoo.com. You can set up a portfolio and monitor
10 -- its intraday performance.
13 -- QUICKSTART
14 -- 1. In template of you cfg_statusbar.lua insert: "%stock" (without quotes)
15 -- 2. Insert, in you cfg_ion.lua or run: dopath("stock")
16 -- 3. press MOD1+F10 to get the menu
17 -- 4. Add a ticket: e.g. "^N225" (without quotes) to monitor the Nikkei index.
20 -- COMMANDS
21 -- Here's the list of available commands:
22 -- - add-a-ticket: add a Yahoo ticket to monitor (e.g. "^N225" -
23 -- without quotes). You can also insert the quantity, separated by a
24 -- a comma: "TIT.MI,100" will insert 100 shares of TIT.MI in your portfolio.
25 -- - delete-a-ticket: remove a ticket
26 -- - suspend-updates: to stop retrieving data from Yahoo
27 -- - resume-updates: to resume retrieving data from Yahoo
28 -- - toggle-visibility: short or data display. You can configure
29 -- the string for the short display.
30 -- - update: force monitor to update data.
32 -- CONFIGURATION AND SETUP
33 -- You may configure this applet in cfg_statusbar.cfg
34 -- In mod_statusbar.launch_statusd{) insert something like this:
36 -- -- stock configuration options
37 -- stock = {
38 -- tickets = {"^N225", "^SPMIB", "TIT.MI"},
39 -- interval = 5 * 60 * 1000, -- check every 5 minutes
40 -- off_msg = "*Stock*", -- string to be displayed in "short" mode
41 -- susp_msg = "(Stock suspended)", -- string to be displayed when data
42 -- -- retrieval is suspended
43 -- susp_msg_hint = "critical", -- hint for suspended mode
44 -- unit = {
45 -- delta = "%",
46 -- },
47 -- important = {
48 -- delta = 0,
49 -- },
50 -- critical = {
51 -- delta = 0,
52 -- }
53 -- },
55 -- You can set "important" and "critical" thresholds for each meter.
57 -- PORTFOLIO
58 -- If you want to monitor a portfolio you can set it up in the configuration with
59 -- something like this:
60 -- stock = {
61 -- off_msg = "*MyStock",
62 -- portfolio = {
63 -- ["TIT.MI"] = 2000,
64 -- ["IBZL.MI"] = 1500,
65 -- },
66 -- },
67 -- where numbers represent the quantities of shares you posses. When
68 -- visibility will be set to OFF, in the statusbar (if you use ONLY the
69 -- %stock meter) you will get a string ("MyStock" in the above example)
70 -- red or green depending on the its global performance.
72 -- FEEDBACK
73 -- Please report your feedback, bugs reports, features request, to the
74 -- above email address.
76 -- REVISIONS
77 -- 2006-07-10 first release
79 -- LEGAL
80 -- Copyright (C) 2006 Andrea Rossato
82 -- This program is free software; you can redistribute it and/or
83 -- modify it under the terms of the GNU General Public License
84 -- as published by the Free Software Foundation; either version 2
85 -- of the License, or (at your option) any later version.
87 -- This software is distributed in the hope that it will be useful,
88 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
89 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
90 -- GNU General Public License for more details.
92 -- You should have received a copy of the GNU General Public License
93 -- along with this program; if not, write to the Free Software
94 -- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
97 -- Have fun!
99 -- Andrea Rossato arossato AT istitutocolli DOT org
102 -- key bindings
103 -- you can (should) change the key bindings to your liking
104 defbindings("WMPlex", {
105 kpress(MOD1.."F10", "mod_query.query_menu(_, 'stockmenu', 'StockMonitor Menu: ')"),
106 kpress(MOD1.."Shift+F10", "StockMonitor.add_ticket(_)"),
108 defmenu("stockmenu", {
109 menuentry("update now", "StockMonitor.update()"),
110 menuentry("add a ticket", "StockMonitor.add_ticket(_)"),
111 menuentry("delete a Ticket", "StockMonitor.del_ticket(_)"),
112 menuentry("toggle visibility", "StockMonitor.toggle()"),
113 menuentry("suspend updates", "StockMonitor.suspend()"),
114 menuentry("resume updates", "StockMonitor.resume()"),
118 -- our stuff
119 local function new_stock()
120 local this = {
122 -- configuration
124 config = {
125 tickets = {},
126 interval = 5 * 60 * 1000, -- check every 5 minutes
127 off_msg = "*Stock*",
128 susp_msg = "(Stock suspended)",
129 susp_msg_hint = "critical",
130 toggle = "on",
131 portfolio = {},
132 unit = {
133 delta = "%",
135 important = {
136 delta = 0,
138 critical = {
139 delta = 0,
142 -- end configuration
144 status_timer = ioncore.create_timer(),
145 url = "http://finance.yahoo.com/d/quotes.csv",
146 data = { },
147 backup = { },
148 paths = ioncore.get_paths(),
151 -- some needed global functions
152 function table.merge(t1, t2)
153 local t=table.copy(t1, false)
154 for k, v in pairs(t2) do
155 t[k]=v
157 return t
160 function math.dpr(number, signs_q)
161 local pattern = "%d+"
162 if signs_q == nil then
163 signs_q = 2
165 if signs_q ~= 0 then
166 pattern = pattern.."%."
168 for i = 1, tonumber (signs_q) do
169 pattern = pattern.."%d"
171 return string.gsub (number, "("..pattern..")(.*)", "%1")
174 -- gets configuration values and store them in this.config (public)
175 function this.process_config()
176 this.get_sb()
177 local c = ioncore.read_savefile("cfg_statusd")
178 if c.stock then
179 this.config = table.merge(this.config, c.stock)
181 c = ioncore.read_savefile("cfg_stock")
182 if c then
183 this.config.portfolio = table.merge(this.config.portfolio, c)
185 this.process_portfolio()
188 -- gets tickets from portfolio
189 function this.process_portfolio()
190 for t,q in pairs(this.config.portfolio) do
191 if t then table.insert(this.config.tickets, t) end
195 -- gets the statusbar obj and makes a backup of the sb table (just in case)
196 function this.get_sb()
197 for _, sb in pairs(mod_statusbar.statusbars()) do
198 this.StatusBar = sb
200 ioncore.write_savefile("stock_sb", this.StatusBar:get_template_table())
203 -- removes a meter from the statusbar and inserts a new template chunk
204 function this.sb_insert_tmpl_chunk(chunk, meter)
205 local pos, old_sb_chunk
206 this.restore_sb(meter)
207 local old_sb = this.StatusBar:get_template_table()
208 for a, item in pairs(old_sb) do
209 if item.meter == meter then
210 pos = a
211 old_sb_chunk = item
212 break
215 this.backup[meter] = old_sb_chunk
216 mod_statusbar.inform("start_insert_by_"..meter, "")
217 mod_statusbar.inform("end_insert_by_"..meter, "")
218 local new_sb_chunk = mod_statusbar.template_to_table("%start_insert_by_"..meter.." "..
219 chunk.." %end_insert_by_"..meter)
220 local new_sb = old_sb
221 table.remove(new_sb, pos)
222 for i,v in pairs(new_sb_chunk) do
223 table.insert(new_sb, pos, v)
224 pos = pos + 1
226 this.StatusBar:set_template_table(new_sb)
229 -- restores the statusbar with the original meter
230 function this.restore_sb(meter)
231 local st, en
232 local old_sb = this.StatusBar:get_template_table()
233 for a, item in pairs(old_sb) do
234 if item.meter == "start_insert_by_"..meter then
235 st = a
237 if item.meter == "end_insert_by_"..meter then
238 en = a
239 break
242 local new_sb = old_sb
243 if en then
244 for a=st, en, 1 do
245 table.remove(new_sb, st)
247 table.insert(new_sb, st, this.backup[meter])
248 this.StatusBar:set_template_table(new_sb)
249 mod_statusbar.inform(meter, "")
253 -- gets ticket's data
254 function this.get_record(t)
255 local command = "wget -O "..this.paths.sessiondir.."/"..
256 string.gsub(t,"%^" ,"")..".cvs "..this.url.."?s="..
257 string.gsub(t,"%^" ,"%%5E")..
258 "\\&f=sl1d1t1c1ohgv\\&e=.csv"
259 os.execute(command)
260 local f = io.open(this.paths.sessiondir .."/"..string.gsub(t,"%^" ,"")..".cvs", "r")
261 if not f then return end
262 local s=f:read("*all")
263 f:close()
264 os.execute("rm "..this.paths.sessiondir .."/"..string.gsub(t,"%^" ,"")..".cvs")
265 return s
268 -- parses ticket's data and store them in this.data.ticketname
269 function this.process_record(s)
270 local _,_,t = string.find(s, '"%^?(.-)".*' )
271 t = string.gsub(t , "%.", "")
272 this.data[t] = { raw_data = s, }
273 _, _,
274 this.data[t].ticket,
275 this.data[t].quote,
276 this.data[t].date,
277 this.data[t].time,
278 this.data[t].difference,
279 this.data[t].open,
280 this.data[t].high,
281 this.data[t].low,
282 this.data[t].volume =
283 string.find(s, '"(.-)",(.-),"(.-)","(.-)",(.-),(.-),(.-),(.-),(.-)' )
284 if tonumber(this.data[t].difference) ~= nil then
285 this.data[t].delta = math.dpr((this.data[t].difference /
286 (this.data[t].quote - this.data[t].difference) * 100), 2)
287 else
288 this.data[t] = nil
292 -- updates tickets' data
293 function this.update_data()
294 for _, v in pairs(this.config.tickets) do
295 this.process_record(this.get_record(v))
299 -- gets threshold info
300 function this.get_hint(meter, val)
301 local hint = "normal"
302 local crit = this.config.critical[meter]
303 local imp = this.config.important[meter]
304 if crit and tonumber(val) < crit then
305 hint = "critical"
306 elseif imp and tonumber(val) >= imp then
307 hint = "important"
309 return hint
312 -- gets the unit (if any) of each meter
313 function this.get_unit(meter)
314 local unit = this.config.unit[meter]
315 if unit then return unit end
316 return ""
319 -- gets the quantity (if any) of each ticket
320 function this.get_quantity(t)
321 local quantity = this.config.portfolio[t]
322 if quantity then return quantity end
323 return 1
326 -- notifies data to statusbar
327 function this.notify()
328 local newtmpl = ""
329 local perf = 0
330 local base = 0
331 for i,v in pairs(this.data) do
332 newtmpl = newtmpl..v.ticket..": %stock_delta_"..i.." "
333 perf = perf + this.get_quantity(v.ticket) + (v.delta * this.get_quantity(v.ticket) / 100)
334 base = base + (this.get_quantity(v.ticket))
335 for ii,vv in pairs(this.data[i]) do
336 mod_statusbar.inform("stock_"..ii.."_"..i.."_hint", this.get_hint(ii, vv))
337 mod_statusbar.inform("stock_"..ii.."_"..i, vv..this.get_unit(ii))
340 if this.config.toggle == "off" then
341 if perf > base then
342 mod_statusbar.inform("stock",this.config.off_msg )
343 mod_statusbar.inform("stock_hint", "important")
344 else
345 mod_statusbar.inform("stock",this.config.off_msg )
346 mod_statusbar.inform("stock_hint", "critical")
348 else
349 this.sb_insert_tmpl_chunk(newtmpl, "stock")
351 mod_statusbar.update()
354 -- checks if the timer is set and ther restarts
355 function this.restart()
356 if not this.status_timer then this.resume() end
357 this.loop()
360 -- main loop
361 function this.loop()
362 if this.status_timer ~= nil and mod_statusbar ~= nil then
363 this.update_data()
364 this.notify()
365 this.status_timer:set(this.config.interval, this.loop)
369 -- public methods
371 function this.update()
372 this.loop()
375 function this.add_ticket(mplex)
376 local handler = function(mplex, str)
377 local _,_,t,_,q = string.find(str, "(.*)(,)(%d*)")
378 ioncore.write_savefile("debug", { ["q"] = q, ["t"] = t})
379 if q then this.config.portfolio[t] = tonumber(q)
380 else this.config.portfolio[str] = 1 end
381 ioncore.write_savefile("cfg_stock", this.config.portfolio)
382 this.process_portfolio()
383 this.restart()
385 mod_query.query(mplex, TR("Add a ticket (format: ticketname - e.g. ^N225 or tickename,quantity e.g: ^N225,100):"),
386 nil, handler, nil, "stock")
389 function this.del_ticket(mplex)
390 local handler = function(mplex, str)
391 for i,v in pairs(this.config.tickets) do
392 if this.config.tickets[i] == str then
393 this.config.tickets[i] = nil
394 break
397 this.config.portfolio[str] = nil
398 ioncore.write_savefile("cfg_stock", this.config.portfolio)
399 this.data[string.gsub(str,"[%^%.]" ,"")] = nil
400 this.restart()
402 mod_query.query(mplex, TR("Delete a ticket (format: tickename e.g. ^N225):"), nil, handler,
403 nil, "stock")
406 function this.suspend()
407 this.restore_sb("stock")
408 mod_statusbar.inform("stock",this.config.susp_msg )
409 mod_statusbar.inform("stock_hint", this.config.susp_msg_hint)
410 mod_statusbar.update()
411 this.status_timer = nil
414 function this.resume()
415 this.status_timer = ioncore.create_timer()
416 this.restart()
419 function this.toggle()
420 if this.config.toggle == "on" then
421 this.config.toggle = "off"
422 this.restore_sb("stock")
423 mod_statusbar.inform("stock",this.config.off_msg )
424 else
425 this.config.toggle = "on"
427 this.restart()
430 -- constructor
431 function this.init()
432 this.process_config()
433 this.loop()
436 this.init()
437 -- return this
438 return {
439 update = this.update,
440 add_ticket = this.add_ticket,
441 del_ticket = this.del_ticket,
442 toggle = this.toggle,
443 suspend = this.suspend,
444 resume = this.resume,
445 data = this.data,
446 config = this.config,
450 -- there we go!
451 StockMonitor = new_stock()