#!/usr/bin/env ruby require 'rubygems' require 'builder' require 'mongrel' require 'mechanize' ANN_BALANCE_SHEET = "http://finance.yahoo.com/q/bs?annual&s=" Q_BALANCE_SHEET = "http://finance.yahoo.com/q/bs?s=" KEY_ST = "http://finance.yahoo.com/q/ks?s=" ANN_CASH_FLOW = "http://finance.yahoo.com/q/cf?annual&s=" Q_CASH_FLOW = "http://finance.yahoo.com/q/cf?s=" ANN_INCOME_STATEMENT = "http://finance.yahoo.com/q/is?annual&s=" Q_INCOME_STATEMENT = "http://finance.yahoo.com/q/is?s=" FIELDS = %w( name ticker ev ann_ebit ann_total_assets ann_current_liabilities ann_da ann_roc ann_ey q_ebit q_total_assets q_current_liabilities q_da q_roc q_ey latest_q ) class Stock def initialize(symbol) @symbol = symbol @agent = WWW::Mechanize.new @agent.user_agent_alias = 'Mac Safari' @ann_balance_sheet = @agent.get(ANN_BALANCE_SHEET + @symbol) @q_balance_sheet = @agent.get(Q_BALANCE_SHEET + @symbol) @key_st = @agent.get(KEY_ST + @symbol) @ann_cash_flow = @agent.get(ANN_CASH_FLOW + @symbol) @q_cash_flow = @agent.get(Q_CASH_FLOW + @symbol) @ann_income_statement = @agent.get(ANN_INCOME_STATEMENT + @symbol) @q_income_statement = @agent.get(Q_INCOME_STATEMENT + @symbol) end def name(h) @name = @key_st.search("/html/head/title").inner_text.sub(/.*Key Statistics for ([^-]+) - Yahoo.*/, '\1') h.td { h.a(@name, :href => "#{KEY_ST}#{@symbol}") } end def ticker @symbol end def ev if (not(@ev)) tmp_ev = @key_st.search("//tr").inner_html.sub(/.*Enterprise Value .\d[^<]+3<.sup><.font>:<.td>([^<]+)<.td>.*/mi, '\1') last_char = tmp_ev[-1].chr tmp_ev = tmp_ev[0..-2].to_f if last_char == "M" tmp_ev = tmp_ev * 1000 elsif last_char == "B" tmp_ev = tmp_ev * 1000000 end @ev = tmp_ev end return @ev end def ann_ebit if (not(@ann_ebit)) @ann_ebit = @ann_income_statement.search("//tr").inner_html.sub(/.*Income Before Tax<.td>([^<]+).*/mi, '\1').gsub(/.nbsp;/, '') end return @ann_ebit end def ann_total_assets if (not(@ann_total_assets)) @ann_total_assets = @ann_balance_sheet.search("//tr").inner_html.sub(/.*Total Assets<.b><.td>([^<]+).*/mi, '\1').gsub(/.nbsp;/, '') end return @ann_total_assets end def ann_current_liabilities if (not(@ann_current_liabilities)) @ann_current_liabilities = @ann_balance_sheet.search("//tr").inner_html.sub(/.*Total Current Liabilities<.b><.td>([^<]+).*/mi, '\1').gsub(/.nbsp;/, '') end return @ann_current_liabilities end def ann_da if (not(@ann_da)) @ann_da = @ann_cash_flow.search("//tr").inner_html.sub(/.*Depreciation<.td>([^<]+).*/mi, '\1').gsub(/.nbsp;/, '') end return @ann_da end def ann_roc #EBIT/(TA-CL-D&A) ebit = to_num(ann_ebit()).to_f ann_roc = (ebit / (to_num(ann_total_assets()) - to_num(ann_current_liabilities()) - to_num(ann_da()))) * 100 return (sprintf("%4.3f", ann_roc) + "%") end def ann_ey ebit = to_num(ann_ebit()) ann_ev = (ebit / ev()) * 100 return (sprintf("%4.3f", ann_ev) + "%") end def q_ebit if (not(@q_ebit)) @q_ebit = @q_income_statement.search("//tr").inner_html.sub(/.*Income Before Tax<.td>([^<]+).*/mi, '\1').gsub(/.nbsp;/, '') end return @q_ebit end def q_total_assets if (not(@q_total_assets)) @q_total_assets = @q_balance_sheet.search("//tr").inner_html.sub(/.*Total Assets<.b><.td>([^<]+).*/mi, '\1').gsub(/.nbsp;/, '') end return @q_total_assets end def q_current_liabilities if (not(@q_current_liabilities)) @q_current_liabilities = @q_balance_sheet.search("//tr").inner_html.sub(/.*Total Current Liabilities<.b><.td>([^<]+).*/mi, '\1').gsub(/.nbsp;/, '') end return @q_current_liabilities end def q_da if (not(@q_da)) @q_da = @q_cash_flow.search("//tr").inner_html.sub(/.*Depreciation<.td>([^<]+).*/mi, '\1').gsub(/.nbsp;/, '') end return @q_da end def q_roc #EBIT/(TA-CL-D&A) ebit = to_num(q_ebit()).to_f q_roc = (ebit / (to_num(q_total_assets()) - to_num(q_current_liabilities()) - to_num(q_da()))) * 100 return (sprintf("%4.3f", q_roc) + "%") end def q_ey ebit = to_num(q_ebit()) q_ev = (ebit / ev()) * 100 return (sprintf("%4.3f", q_ev) + "%") end def latest_q return @q_income_statement.search("//tr").inner_html.sub(/.*PERIOD ENDING<.b><.small><.TD>([^<]+)<.b>.*/im, '\1') end private def to_num(arg) arg.gsub!(/,/, '') if arg =~ /^\(/ arg = arg[1..-2] elsif arg =~ / -/ arg = "0" end return arg.to_i end end class MagicHandler < Mongrel::HttpHandler PORT = 4115 SERVER = "0.0.0.0" def process(request, response) begin query = Mongrel::HttpRequest::query_parse(request.params["QUERY_STRING"]) response.start(200) {|head, out| head["Content-Type"] = "text/html" builder = Builder::XmlMarkup.new(:target=>out) #, :indent=>2) # so that we could validate this in the future... builder.declare! :DOCTYPE, :html, :PUBLIC, "-//W3C//DTD XHTML 1.0 Strict//EN", "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" builder.html(:xmlns => "http://www.w3.org/1999/xhtml", :"xml:lang" => "en") {|h| h.head { h.meta(:"http-equiv" => "Content-Type", :content => "text/html; #charset=utf-8") h.title("Sybaris Research Report") } # delegate to a separate method for each subsection h.body { if query.has_key?("symbols") h.table(:border => "1") { FIELDS.each {|column_name| column_prettyname = column_name.to_s.split('_').map {|x| x.capitalize}.join(' ') h.th(column_prettyname) } query["symbols"].split(",").each {|s| sym = CGI::unescape(s) stock = Stock.new(sym) h.tr { FIELDS.each {|func_name| if func_name == "name" stock.name(h) else val = stock.send(func_name.to_sym) h.td(val) end } } } } else h.p("No symbols") end } } } rescue => e return response.start(500) {|head, out| out.print "server error (#{e})"} end end end h = Mongrel::HttpServer.new(MagicHandler::SERVER, MagicHandler::PORT) h.register("/magic", MagicHandler.new) puts "Started server on #{MagicHandler::SERVER}:#{MagicHandler::PORT}" trap("INT"){ h.stop } h.run.join