4 -- An Ion3 applet for retrieving and displaying stock market information
5 -- from http://finance.yahoo.com. You can set up a portfolio and monitor
6 -- its intraday performance.
10 -- 1. In template of you cfg_statusbar.lua insert: "%stock" (without quotes)
11 -- 2. Insert, in you cfg_ion.lua or run: dopath("stock")
12 -- 3. press MOD1+F10 to get the menu
13 -- 4. Add a ticket: e.g. "^N225" (without quotes) to monitor the Nikkei index.
17 -- Here's the list of available commands:
18 -- - add-a-ticket: add a Yahoo ticket to monitor (e.g. "^N225" -
19 -- without quotes). You can also insert the quantity, separated by a
20 -- a comma: "TIT.MI,100" will insert 100 shares of TIT.MI in your portfolio.
21 -- - delete-a-ticket: remove a ticket
22 -- - suspend-updates: to stop retrieving data from Yahoo
23 -- - resume-updates: to resume retrieving data from Yahoo
24 -- - toggle-visibility: short or data display. You can configure
25 -- the string for the short display.
26 -- - update: force monitor to update data.
28 -- CONFIGURATION AND SETUP
29 -- You may configure this applet in cfg_statusbar.cfg
30 -- In mod_statusbar.launch_statusd{) insert something like this:
32 -- -- stock configuration options
34 -- tickets = {"^N225", "^SPMIB", "TIT.MI"},
35 -- interval = 5 * 60 * 1000, -- check every 5 minutes
36 -- off_msg = "*Stock*", -- string to be displayed in "short" mode
37 -- susp_msg = "(Stock suspended)", -- string to be displayed when data
38 -- -- retrieval is suspended
39 -- susp_msg_hint = "critical", -- hint for suspended mode
51 -- You can set "important" and "critical" thresholds for each meter.
54 -- If you want to monitor a portfolio you can set it up in the configuration with
55 -- something like this:
57 -- off_msg = "*MyStock",
60 -- ["IBZL.MI"] = 1500,
63 -- where numbers represent the quantities of shares you posses. When
64 -- visibility will be set to OFF, in the statusbar (if you use ONLY the
65 -- %stock meter) you will get a string ("MyStock" in the above example)
66 -- red or green depending on the its global performance.
69 -- Please report your feedback, bugs reports, features request, to the
70 -- above email address.
73 -- 2006-07-10 first release
76 -- Copyright (C) 2006 Andrea Rossato
78 -- This program is free software; you can redistribute it and/or
79 -- modify it under the terms of the GNU General Public License
80 -- as published by the Free Software Foundation; either version 2
81 -- of the License, or (at your option) any later version.
83 -- This software is distributed in the hope that it will be useful,
84 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
85 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
86 -- GNU General Public License for more details.
88 -- You should have received a copy of the GNU General Public License
89 -- along with this program; if not, write to the Free Software
90 -- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
95 -- Andrea Rossato arossato AT istitutocolli DOT org
99 -- you can (should) change the key bindings to your liking
100 defbindings("WMPlex", {
101 kpress(MOD1
.."F10", "mod_query.query_menu(_, 'stockmenu', 'StockMonitor Menu: ')"),
102 kpress(MOD1
.."Shift+F10", "StockMonitor.add_ticket(_)"),
104 defmenu("stockmenu", {
105 menuentry("update now", "StockMonitor.update()"),
106 menuentry("add a ticket", "StockMonitor.add_ticket(_)"),
107 menuentry("delete a Ticket", "StockMonitor.del_ticket(_)"),
108 menuentry("toggle visibility", "StockMonitor.toggle()"),
109 menuentry("suspend updates", "StockMonitor.suspend()"),
110 menuentry("resume updates", "StockMonitor.resume()"),
115 local function new_stock()
122 interval
= 5 * 60 * 1000, -- check every 5 minutes
124 susp_msg
= "(Stock suspended)",
125 susp_msg_hint
= "critical",
140 status_timer
= ioncore
.create_timer(),
141 url
= "http://finance.yahoo.com/d/quotes.csv",
144 paths
= ioncore
.get_paths(),
147 -- some needed global functions
148 function table.merge(t1
, t2
)
149 local t
=table.copy(t1
, false)
150 for k
, v
in pairs(t2
) do
156 function math
.dpr(number, signs_q
)
157 local pattern
= "%d+"
158 if signs_q
== nil then
162 pattern
= pattern
.."%."
164 for i
= 1, tonumber (signs_q
) do
165 pattern
= pattern
.."%d"
167 return string.gsub (number, "("..pattern
..")(.*)", "%1")
170 -- gets configuration values and store them in this.config (public)
171 function this
.process_config()
173 local c
= ioncore
.read_savefile("cfg_statusd")
175 this
.config
= table.merge(this
.config
, c
.stock
)
177 c
= ioncore
.read_savefile("cfg_stock")
179 this
.config
.portfolio
= table.merge(this
.config
.portfolio
, c
)
181 this
.process_portfolio()
184 -- gets tickets from portfolio
185 function this
.process_portfolio()
186 for t
,q
in pairs(this
.config
.portfolio
) do
187 if t
then table.insert(this
.config
.tickets
, t
) end
191 -- gets the statusbar obj and makes a backup of the sb table (just in case)
192 function this
.get_sb()
193 for _
, sb
in pairs(mod_statusbar
.statusbars()) do
196 ioncore
.write_savefile("stock_sb", this
.StatusBar
:get_template_table())
199 -- removes a meter from the statusbar and inserts a new template chunk
200 function this
.sb_insert_tmpl_chunk(chunk
, meter
)
201 local pos
, old_sb_chunk
202 this
.restore_sb(meter
)
203 local old_sb
= this
.StatusBar
:get_template_table()
204 for a
, item
in pairs(old_sb
) do
205 if item
.meter
== meter
then
211 this
.backup
[meter
] = old_sb_chunk
212 mod_statusbar
.inform("start_insert_by_"..meter
, "")
213 mod_statusbar
.inform("end_insert_by_"..meter
, "")
214 local new_sb_chunk
= mod_statusbar
.template_to_table("%start_insert_by_"..meter
.." "..
215 chunk
.." %end_insert_by_"..meter
)
216 local new_sb
= old_sb
217 table.remove(new_sb
, pos
)
218 for i
,v
in pairs(new_sb_chunk
) do
219 table.insert(new_sb
, pos
, v
)
222 this
.StatusBar
:set_template_table(new_sb
)
225 -- restores the statusbar with the original meter
226 function this
.restore_sb(meter
)
228 local old_sb
= this
.StatusBar
:get_template_table()
229 for a
, item
in pairs(old_sb
) do
230 if item
.meter
== "start_insert_by_"..meter
then
233 if item
.meter
== "end_insert_by_"..meter
then
238 local new_sb
= old_sb
241 table.remove(new_sb
, st
)
243 table.insert(new_sb
, st
, this
.backup
[meter
])
244 this
.StatusBar
:set_template_table(new_sb
)
245 mod_statusbar
.inform(meter
, "")
249 -- gets ticket's data
250 function this
.get_record(t
)
251 local command
= "wget -O "..this
.paths
.sessiondir
.."/"..
252 string.gsub(t
,"%^" ,"")..".cvs "..this
.url
.."?s="..
253 string.gsub(t
,"%^" ,"%%5E")..
254 "\\&f=sl1d1t1c1ohgv\\&e=.csv"
256 local f
= io
.open(this
.paths
.sessiondir
.."/"..string.gsub(t
,"%^" ,"")..".cvs", "r")
257 if not f
then return end
258 local s
=f
:read("*all")
260 os
.execute("rm "..this
.paths
.sessiondir
.."/"..string.gsub(t
,"%^" ,"")..".cvs")
264 -- parses ticket's data and store them in this.data.ticketname
265 function this
.process_record(s
)
266 local _
,_
,t
= string.find(s
, '"%^?(.-)".*' )
267 t
= string.gsub(t
, "%.", "")
268 this
.data
[t
] = { raw_data
= s
, }
274 this
.data
[t
].difference
,
278 this
.data
[t
].volume
=
279 string.find(s
, '"(.-)",(.-),"(.-)","(.-)",(.-),(.-),(.-),(.-),(.-)' )
280 if tonumber(this
.data
[t
].difference
) ~= nil then
281 this
.data
[t
].delta
= math
.dpr((this
.data
[t
].difference
/
282 (this
.data
[t
].quote
- this
.data
[t
].difference
) * 100), 2)
288 -- updates tickets' data
289 function this
.update_data()
290 for _
, v
in pairs(this
.config
.tickets
) do
291 this
.process_record(this
.get_record(v
))
295 -- gets threshold info
296 function this
.get_hint(meter
, val
)
297 local hint
= "normal"
298 local crit
= this
.config
.critical
[meter
]
299 local imp
= this
.config
.important
[meter
]
300 if crit
and tonumber(val
) < crit
then
302 elseif imp
and tonumber(val
) >= imp
then
308 -- gets the unit (if any) of each meter
309 function this
.get_unit(meter
)
310 local unit
= this
.config
.unit
[meter
]
311 if unit
then return unit
end
315 -- gets the quantity (if any) of each ticket
316 function this
.get_quantity(t
)
317 local quantity
= this
.config
.portfolio
[t
]
318 if quantity
then return quantity
end
322 -- notifies data to statusbar
323 function this
.notify()
327 for i
,v
in pairs(this
.data
) do
328 newtmpl
= newtmpl
..v
.ticket
..": %stock_delta_"..i
.." "
329 perf
= perf
+ this
.get_quantity(v
.ticket
) + (v
.delta
* this
.get_quantity(v
.ticket
) / 100)
330 base
= base
+ (this
.get_quantity(v
.ticket
))
331 for ii
,vv
in pairs(this
.data
[i
]) do
332 mod_statusbar
.inform("stock_"..ii
.."_"..i
.."_hint", this
.get_hint(ii
, vv
))
333 mod_statusbar
.inform("stock_"..ii
.."_"..i
, vv
..this
.get_unit(ii
))
336 if this
.config
.toggle
== "off" then
338 mod_statusbar
.inform("stock",this
.config
.off_msg
)
339 mod_statusbar
.inform("stock_hint", "important")
341 mod_statusbar
.inform("stock",this
.config
.off_msg
)
342 mod_statusbar
.inform("stock_hint", "critical")
345 this
.sb_insert_tmpl_chunk(newtmpl
, "stock")
347 mod_statusbar
.update()
350 -- checks if the timer is set and ther restarts
351 function this
.restart()
352 if not this
.status_timer
then this
.resume() end
358 if this
.status_timer
~= nil and mod_statusbar
~= nil then
361 this
.status_timer
:set(this
.config
.interval
, this
.loop
)
367 function this
.update()
371 function this
.add_ticket(mplex
)
372 local handler
= function(mplex
, str
)
373 local _
,_
,t
,_
,q
= string.find(str
, "(.*)(,)(%d*)")
374 ioncore
.write_savefile("debug", { ["q"] = q
, ["t"] = t
})
375 if q
then this
.config
.portfolio
[t
] = tonumber(q
)
376 else this
.config
.portfolio
[str
] = 1 end
377 ioncore
.write_savefile("cfg_stock", this
.config
.portfolio
)
378 this
.process_portfolio()
381 mod_query
.query(mplex
, TR("Add a ticket (format: ticketname - e.g. ^N225 or tickename,quantity e.g: ^N225,100):"),
382 nil, handler
, nil, "stock")
385 function this
.del_ticket(mplex
)
386 local handler
= function(mplex
, str
)
387 for i
,v
in pairs(this
.config
.tickets
) do
388 if this
.config
.tickets
[i
] == str
then
389 this
.config
.tickets
[i
] = nil
393 this
.config
.portfolio
[str
] = nil
394 ioncore
.write_savefile("cfg_stock", this
.config
.portfolio
)
395 this
.data
[string.gsub(str
,"[%^%.]" ,"")] = nil
398 mod_query
.query(mplex
, TR("Delete a ticket (format: tickename e.g. ^N225):"), nil, handler
,
402 function this
.suspend()
403 this
.restore_sb("stock")
404 mod_statusbar
.inform("stock",this
.config
.susp_msg
)
405 mod_statusbar
.inform("stock_hint", this
.config
.susp_msg_hint
)
406 mod_statusbar
.update()
407 this
.status_timer
= nil
410 function this
.resume()
411 this
.status_timer
= ioncore
.create_timer()
415 function this
.toggle()
416 if this
.config
.toggle
== "on" then
417 this
.config
.toggle
= "off"
418 this
.restore_sb("stock")
419 mod_statusbar
.inform("stock",this
.config
.off_msg
)
421 this
.config
.toggle
= "on"
428 this
.process_config()
435 update
= this
.update
,
436 add_ticket
= this
.add_ticket
,
437 del_ticket
= this
.del_ticket
,
438 toggle
= this
.toggle
,
439 suspend
= this
.suspend
,
440 resume
= this
.resume
,
442 config
= this
.config
,
447 StockMonitor
= new_stock()