1 -- Authors: Andrea Rossato <arossato@istitutocolli.org>
2 -- License: GPL, version 2 or later
3 -- Last Changed: 2006-07-10
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.
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.
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
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
55 -- You can set "important" and "critical" thresholds for each meter.
58 -- If you want to monitor a portfolio you can set it up in the configuration with
59 -- something like this:
61 -- off_msg = "*MyStock",
64 -- ["IBZL.MI"] = 1500,
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.
73 -- Please report your feedback, bugs reports, features request, to the
74 -- above email address.
77 -- 2006-07-10 first release
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
99 -- Andrea Rossato arossato AT istitutocolli DOT org
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()"),
119 local function new_stock()
126 interval
= 5 * 60 * 1000, -- check every 5 minutes
128 susp_msg
= "(Stock suspended)",
129 susp_msg_hint
= "critical",
144 status_timer
= ioncore
.create_timer(),
145 url
= "http://finance.yahoo.com/d/quotes.csv",
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
160 function math
.dpr(number, signs_q
)
161 local pattern
= "%d+"
162 if signs_q
== nil 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()
177 local c
= ioncore
.read_savefile("cfg_statusd")
179 this
.config
= table.merge(this
.config
, c
.stock
)
181 c
= ioncore
.read_savefile("cfg_stock")
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
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
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
)
226 this
.StatusBar
:set_template_table(new_sb
)
229 -- restores the statusbar with the original meter
230 function this
.restore_sb(meter
)
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
237 if item
.meter
== "end_insert_by_"..meter
then
242 local new_sb
= old_sb
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"
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")
264 os
.execute("rm "..this
.paths
.sessiondir
.."/"..string.gsub(t
,"%^" ,"")..".cvs")
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
, }
278 this
.data
[t
].difference
,
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)
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
306 elseif imp
and tonumber(val
) >= imp
then
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
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
326 -- notifies data to statusbar
327 function this
.notify()
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
342 mod_statusbar
.inform("stock",this
.config
.off_msg
)
343 mod_statusbar
.inform("stock_hint", "important")
345 mod_statusbar
.inform("stock",this
.config
.off_msg
)
346 mod_statusbar
.inform("stock_hint", "critical")
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
362 if this
.status_timer
~= nil and mod_statusbar
~= nil then
365 this
.status_timer
:set(this
.config
.interval
, this
.loop
)
371 function this
.update()
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()
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
397 this
.config
.portfolio
[str
] = nil
398 ioncore
.write_savefile("cfg_stock", this
.config
.portfolio
)
399 this
.data
[string.gsub(str
,"[%^%.]" ,"")] = nil
402 mod_query
.query(mplex
, TR("Delete a ticket (format: tickename e.g. ^N225):"), nil, handler
,
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()
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
)
425 this
.config
.toggle
= "on"
432 this
.process_config()
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
,
446 config
= this
.config
,
451 StockMonitor
= new_stock()