Skip to content

Instantly share code, notes, and snippets.

@romuloceccon
Last active November 6, 2016 15:31
Show Gist options
  • Select an option

  • Save romuloceccon/15f5f9c35f14cb43a58de371a116a6bf to your computer and use it in GitHub Desktop.

Select an option

Save romuloceccon/15f5f9c35f14cb43a58de371a116a6bf to your computer and use it in GitHub Desktop.
Fetch financial statements from Google Finance
#! /usr/bin/env ruby
require 'bigdecimal'
require 'csv'
require 'net/http'
require 'nokogiri'
class CompanyStats
attr_reader :name
def initialize(name)
@name = name
@values = {}
end
def add_value(key, val)
@values[key] = val
end
def dump
@values.each_pair do |k, v|
puts "#{k}: #{v && v.to_s('F')}"
end
end
def value_of(item)
if v = @values[item]
v.to_s('F')
end
end
end
class Fetcher
def initialize
@http = Net::HTTP.new('www.google.com', 443)
@http.use_ssl = true
end
def fetch(name, fstype)
path = "/finance?q=BVMF%3A#{name}"
path += "&fstype=#{fstype}" if fstype
response = @http.get(path)
unless response.is_a?(Net::HTTPSuccess)
return [false, response.code]
end
[true, response.body]
end
end
class Parser
def initialize(html)
@doc = Nokogiri::HTML(html)
end
protected
def str_to_decimal(s)
s = strip(s)
s.nil? || s.empty? || s == '-' ? nil : BigDecimal(s.gsub(',', ''))
end
def strip(s)
s.match(/^[\u00a0\s]*(.*?)[\u00a0\s]*$/)[1]
end
end
class SummaryParser < Parser
def parse(stats)
price = @doc.xpath('//span[@class="pr"]')
stats.add_value('Price', str_to_decimal(price.first.text))
snap = {}
@doc.xpath('//div[@class="snap-panel"]//tr').each do |row|
cols = row.xpath('./td').map { |x| x.text }
snap[strip(cols[0])] = cols[1]
end
stats.add_value('P/E', str_to_decimal(snap['P/E']))
stats.add_value('Market capitalization', snap_to_decimal(snap['Mkt cap']))
end
def valid?
@doc.xpath('//div[@id="market-data-div"]').size > 0
end
private
def multiplier(s)
{ 'M' => 1e6, 'B' => 1e9 }[s]
end
def snap_to_decimal(s)
if m = s.match(/^(.+)([MB])$/)
str_to_decimal(m[1]) * multiplier(m[2])
end
end
end
class FinancialsParser < Parser
def parse(stats)
['inc', 'bal', 'cas'].each do |s|
table = '//div[@id="%s"]//table[@id="%s"]' % ["#{s}annualdiv", "fs-table"]
@doc.xpath(table + '/tbody/tr').each do |row|
cols = row.xpath('./td').map { |x| x.text.strip }
val = str_to_decimal(cols[1])
stats.add_value(cols[0], val && val * 1e6)
end
end
end
def valid?
@doc.xpath('//div[@id="fs-type-tabs"]').size > 0
end
end
class TableOutput
def initialize(io, columns)
@io = io
@columns = columns
@csv = CSV.new(io)
@csv << [''] + columns
end
def add(stats)
@csv << [stats.name] + @columns.map { |x| stats.value_of(x) }
end
def close
@csv.close
end
end
if __FILE__ == $0
output = TableOutput.new(STDOUT, ['Total Revenue', 'P/E', 'Market capitalization'])
begin
fetcher = Fetcher.new
while ARGV.size > 0
ticker = ARGV.shift
stats = CompanyStats.new(ticker)
[[nil, SummaryParser], ['ii', FinancialsParser]].each do |(fstype, klass)|
success, data = fetcher.fetch(ticker, fstype)
unless success
STDERR.puts("HTTP error while fetching #{ticker}: #{data}")
next
end
parser = klass.new(data)
unless parser.valid?
STDERR.puts("Invalid data for #{ticker}")
next
end
parser.parse(stats)
end
output.add(stats)
end
ensure
output.close
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment