From: Ruby Quiz Date: 2005-08-11T21:59:20+09:00 Subject: [SUMMARY] Stock Portfolios (#41) The first thing this quiz requires is a source of stock data. The only essential piece of data for the program shown in the quiz is a current share price. Jeffrey Moss shows what is probably the easiest way to get exactly that: require 'soap/rpc/driver' driver = SOAP::RPC::Driver.new( 'http://services.xmethods.com/soap', 'urn:xmethods-delayed-quotes' ) driver.add_method( 'getQuote', 'a_string' ) driver.getQuote('GOOG') That code fetches a current quote for Google (symbol "GOOG"), though it doesn't do anything with it. It uses the standard SOAP library to retrieve the quote from a web service provider. There are, of course, other ways to fetch stock data. You could always scrape it from any of the numerous provider sites across the web. Peter Verhage gave us another interesting option with a link posted to Ruby Talk: http://www.gummy-stuff.org/Yahoo-data.htm The page describes how to feed Yahoo! custom URL's to which it will respond with and impressive array of stock data in Comma Separated Value (CSV) format. A couple of solutions put this to use. Let's examine Adam Sanderson's code below: require 'open-uri' require 'ostruct' require 'csv' require 'yaml' # I used the methods outlined at http://www.gummy-stuff.org/Yahoo-data.htm # to fetch and manage data, it works quite well. # # StockData encapsulates Yahoo's service and generates OpenStructs # which have the requested fields. A StockTransaction records the # purchase or sale of stocks with a timestamp. StockHistory aggregates # StockTransactions. The StockPortfolio manages a user's stocks. Finaly # the StockApp provides a text UI. StockApp isn't very polished, but it # does the trick. # # Usage: # ruby stocks.rb [filename] # # .adam sanderson # netghost@gmail.com # To make things easier. overide the way Time is printed. class Time def to_s strftime("%m/%d/%Y %I:%M%p") end end # ... You can see that the code starts by requesting four standard libraries be loaded. The open-uri library makes it trivially easy to read from a URL, ostruct gives us an objectified Hash interface, csv can parse/write CSV data, and YAML is an easy and powerful data language for persistent storage. Learning about the various libraries Ruby ships with can put a lot of powerful tools at your finger tips. The rest of the above snippet is mostly a comment that describes the code to follow. The Time class is also altered to print date and time information as this code prefers. (I'm not sure how much value the Time hack has, since the user code could just call strftime() instead. You be the judge.) Here's the first class used in the solution: # http://www.gummy-stuff.org/Yahoo-data.htm class StockData include Enumerable SOURCE_URL = "http://finance.yahoo.com/d/quotes.csv" #These are the symbols I understand, which are limited OPTIONS = { :symbol=>"s", :name=>"n", :last_trade=>"l1", :last_trade_date=>"d1", :last_trade_time=>"t1", :open=>"o", :high=>"h", :low=>"g", :high_52_week=>"k", :low_52_week=>"j" } def initialize(symbols, options = [:symbol, :name, :last_trade, :last_trade_date, :last_trade_time]) @symbols = symbols @options = options @data = nil end def each data.each do |row| struct = OpenStruct.new(Hash[*(@options.zip(row).flatten)]) yield struct end end def each_hash data.each do |row| hash = Hash[*(@options.zip(row).flatten)] yield hash end end def refresh symbol_fragment = @symbols.join "+" option_fragment = @options.map{|s| OPTIONS[s] }.join "" url = SOURCE_URL + "?s=#{symbol_fragment}&f=#{option_fragment}" @data = [] CSV.parse open(url).read do |row| @data << row end end def data refresh unless @data @data end end This StockData class is a wrapper for the Yahoo! service I described earlier. It begins by initializing a few constants for the URL of the service and some of the options provided by the service. The constructor takes an Array of stock ticker symbols you want to fetch data for and an Array of options indicating the data you wish to fetch. Skip down now to the refresh() method, which actually does the data fetching. This methods just does what the previously mentioned link tells you to: join() all the symbols with "+", string the options together, form a URL of all that, and read the CSV data from it. Note that data is loaded into @data, row by row. The other three methods are how you get the data. Let's start with data(), because the other two rely on it. I like it when people remember that you don't have to write an accessor with the attr_... methods, and you can do clever tricks when you code them yourself. For example, this method makes sure the data is refresh()ed, if the instance variable is still empty. I think that's handy. The other two methods allow you to iterate over the rows of data. You can use each_hash(), to receive each row as a Hash pairing the requested option name and the fetched value. Note the smooth use of zip() and flatten() there to rapidly build the Hash. The each() method works exactly the same, same that it yields OpenStruct objects instead of Hashes. In other words, you can choose to access your data with row[:last_trade] or row.last_trade, as you prefer. (OpenStruct seems to be the preferred choice here though since all the Enumerable methods use it.) On to the next class: # ... class StockTransaction attr_reader :shares, :price, :date def initialize(shares, price) @shares = shares @price = price @date = Time.now end def cost @price * @shares end def to_s ((@shares > 0) ? "Bought":"Sold") + " #{shares.abs} on #{date} for #{cost.abs}, at #{price}" end end # ... This is a simple data class. It takes a number of shares (maybe negative, for sale transactions) and a price. It also records the creation time. Given that, you can ask it for the total cost() or a pretty String describing the transaction. Next class: # ... class StockHistory attr_reader :symbol, :name, :history def initialize(symbol, name) @symbol = symbol @name = name @history = [] end def net_shares history.inject(0){|shares, transaction| shares + transaction.shares } end def net_balance history.inject(0){|balance, transaction| balance + transaction.cost } end def started history.first.date unless history.empty? end def buy(shares, price) if(shares > 0 and price > 0) history << StockTransaction.new(shares.abs, price) else puts "Could not buy #{shares} of #{name || symbol}, " + "you only have #{net_shares} shares." end end def sell(shares, price) if(net_shares >= shares and shares > 0 and price > 0) history << StockTransaction.new(shares.abs*-1, price) else puts "Could not sell #{shares} of #{name || symbol}, " + "you only have #{net_shares} shares." end end def to_s lines = [] lines << "#{name}(#{symbol})" history.each do |t| lines << t.to_s end lines.join "\n" end end # ... This class is just a collection of the StockTransactions we just examined. You initialize() it with a symbol and name, then add transactions with buy() and sell(). Those methods construct StockTransaction objects and add them to the internal history Array. Once you have a StockHistory started, you can query it for net_shares(), net_balance(), and a started() date. Finally, to_s() will build a human readable summary using StockTransaction's to_s() to build each line. Last stock data class, coming up: # ... class StockPortfolio DEFAULT_INFO = [:symbol, :name, :last_trade] attr :stocks def initialize() @stocks = {} #stocks by symbol end # Takes a hash of symbols to shares, yields history, price, # quantity requested def transaction(purchases, &block) data = StockData.new(purchases.keys, DEFAULT_INFO) data.each do |stock| price = stock.last_trade.to_f if not price == 0 history = @stocks[ stock.symbol ] ||= StockHistory.new(stock.symbol, stock.name) yield [history, purchases[stock.symbol], stock.last_trade.to_f] else puts "Couldn't find #{stock.symbol}." end end end def buy(purchases) transaction(purchases){|history, shares, price| history.buy(shares, price) } end def sell(purchases) transaction(purchases){|history, shares, price| history.sell(shares, price) } end def history(symbol=nil) if (symbol) puts stocks[symbol] else stocks.keys.each{|s| history s unless s.nil?} end end def report() data = StockData.new(stocks.keys, DEFAULT_INFO) data.each do |stock| history = stocks[stock.symbol] if (history) gain = (history.net_shares * stock.last_trade.to_f) - history.net_balance puts "#{stock.name}(#{stock.symbol}), " + "Started #{history.started}" puts " Gain = Shares x Price - Balance:" puts " $#{gain} = #{history.net_shares} x " + "$#{stock.last_trade.to_f} - $#{history.net_balance}" puts "" end end end end # ... This class manages a portfolio, which is basically a collection of StockHistory objects. The buy() and sell() methods are the primary interface here, but they both just delegate to transaction(). That method, which might be better as a private instance method, takes a Hash of symbol keys and shares to buy or sell values. It also requires a block, though it doesn't use its "block" parameter. First, transaction() uses a StockData object (with the hardcoded DEFAULT_INFO selection of options) to lookup a current price for each of the symbols passed in. From that, it constructs price data and a matching StockHistory object, either by looking it up or creating a new one. The history, passed in share count, and price are then yielded to the block, which buy() and sell() use to add the transactions to the history. The other two methods report on the data. The history() method will print a single transaction history for a requested symbol or all known histories by symbol. To see how all the owned stocks are holding up, you can call report(). It fetches current prices, again using StockData, then outputs gain/loss information for each symbol in the portfolio. Note that these methods print data directly to STDOUT and thus wouldn't play too nice with non-console interfaces. Finally, here's the application itself: # ... class StockApp QUIT = /^exit|^quit/ BUY = /^buy\s+((\d+\s+\w+)(\,\s*\d+\s+\w+)*)\s*$/ SELL = /^sell\s+((\d+\s+\w+)(\,\s*\d+\s+\w+)*)\s*$/ HISTORY = /^history\s*(\w+)?\s*$/ REPORT = /^report\s*$/ VIEW = /^view\s+((\w+)(\,\s*\w+)*)\s*$/ HELP = /^help|^\?/ def initialize(path="stock_data.yaml") if File.exist? path puts "Loading Portfolio from #{path}" @portfolio = YAML.load( open(path).read ) @portfolio.report else puts "Starting a new portfolio..." @portfolio = StockPortfolio.new() end @path = path end def run command = nil while(STDOUT << ">"; command = gets.chomp) case command when QUIT puts "Saving data..." open(@path,"w"){|f| f << @portfolio.to_yaml} puts "Good bye" break when REPORT @portfolio.report when BUY purchases = parse_purchases($1) @portfolio.buy purchases @portfolio.report when SELL purchases = parse_purchases($1) @portfolio.sell purchases @portfolio.report when VIEW symbols = ($1).split options = [:symbol, :name, :last_trade] data = StockData.new(symbols, options) data.each do |stock| puts "#{stock.name} (#{stock.symbol} " + "$#{stock.last_trade})" end when HISTORY symbol = $1 ? ($1).upcase : nil @portfolio.history(symbol) when HELP help() else puts "Enter: 'help' for help, or 'exit' to quit." end end end def parse_purchases(str) purchases = {} str.scan(/(\d+)\s+(\w+)/){|pair| purchases[$2.upcase] = $1.to_i} purchases end def help puts < 0 StockApp.new(ARGV.pop) else StockApp.new() end app.run end My thanks to those who delved into the land of Wall Street and made some trades. Hopefully you're now well on your way to a balanced portfolio. Tomorrow we will try a submitted problem from Hans Fugal that should be plenty of fun for all you algorithm junkies out there...