[SUMMARY] Stock Portfolios (#41)

(James Edward Gray II) #1

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' )

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:


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")
  # ...

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 = {

      def initialize(symbols, options = [:symbol, :name,
                                         :last_trade, :last_trade_date,
          @symbols = symbols
          @options = options
          @data = nil

      def each
          data.each do |row|
              struct = OpenStruct.new(Hash[*(@options.zip(row).flatten)])
              yield struct

      def each_hash
          data.each do |row|
              hash = Hash[*(@options.zip(row).flatten)]
              yield hash

      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

      def data
          refresh unless @data

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

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

      def cost
          @price * @shares

      def to_s
          ((@shares > 0) ? "Bought":"Sold") +
          " #{shares.abs} on #{date} for #{cost.abs}, at #{price}"
  # ...

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

Next class:

  # ...
  class StockHistory
      attr_reader :symbol, :name, :history

      def initialize(symbol, name)
          @symbol = symbol
          @name = name
          @history = []

      def net_shares
          history.inject(0){|shares, transaction|
              shares + transaction.shares

      def net_balance
          history.inject(0){|balance, transaction|
              balance + transaction.cost

      def started
          history.first.date unless history.empty?

      def buy(shares, price)
          if(shares > 0 and price > 0)
              history << StockTransaction.new(shares.abs, price)
              puts "Could not buy #{shares} of #{name || symbol}, " +
                   "you only have #{net_shares} shares."

      def sell(shares, price)
          if(net_shares >= shares and shares > 0 and price > 0)
              history << StockTransaction.new(shares.abs*-1, price)
              puts "Could not sell #{shares} of #{name || symbol}, " +
                   "you only have #{net_shares} shares."

      def to_s
          lines = []
          lines << "#{name}(#{symbol})"
          history.each do |t|
              lines << t.to_s
          lines.join "\n"

  # ...

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

Last stock data class, coming up:

  # ...
  class StockPortfolio
      DEFAULT_INFO = [:symbol, :name, :last_trade]
      attr :stocks

      def initialize()
          @stocks = {} #stocks by symbol

      # 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],
                  puts "Couldn't find #{stock.symbol}."

      def buy(purchases)
          transaction(purchases){|history, shares, price|
              history.buy(shares, price)

      def sell(purchases)
          transaction(purchases){|history, shares, price|
              history.sell(shares, price)

      def history(symbol=nil)
          if (symbol)
              puts stocks[symbol]
              stocks.keys.each{|s| history s unless s.nil?}

      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) -
                  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 ""
  # ...

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

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 )
              puts "Starting a new portfolio..."
              @portfolio = StockPortfolio.new()
          @path = path

      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"

                  when REPORT

                  when BUY
                      purchases = parse_purchases($1)
                      @portfolio.buy purchases

                  when SELL
                      purchases = parse_purchases($1)
                      @portfolio.sell purchases

                  when VIEW
                      symbols = ($1).split
                      options = [:symbol, :name, :last_trade]
                      data = StockData.new(symbols, options)
                      data.each do |stock|
                          puts "#{stock.name} (#{stock.symbol} " +

                  when HISTORY
                      symbol = $1 ? ($1).upcase : nil

                  when HELP

                      puts "Enter: 'help' for help, or 'exit' to quit."


      def parse_purchases(str)
          purchases = {}
          str.scan(/(\d+)\s+(\w+)/){|pair| purchases[$2.upcase] = $1.to_i}

      def help
          puts <<END_OF_HELP
  [buy]: Purchase Stocks
      buy Shares Symbol[, Shares Symbol...]
      example: buy 30 GOOG, 10 MSFT

  [sell]: Sell Stocks
      sell Shares Symbol[, Shares Symbol...]
      example: sell 30 GOOG, 10 MSFT

  [history]: View your transaction history
      history [Symbol]
      example: history GOOG

  [report]: View a report of your current stocks

  [view]: View the current price of a stock
      view Symbol[, Symbol...]
      example: view GOOG, MSFT

  [exit]: Quit the stock application (also quit)



  # ...

While that looks like a lot of code, there's really not a lot of fancy stuff
going on.

The constructor just opens an existing portfolio file, using YAML, if one
exists. You can point it at a file, or it will default. The path of this file
is saved, so the file can be updated on exit.

The two biggest methods are run() and help(). The run() method just read
commands from STDIN, parses them using Regexps (you can see these at the top of
the class) in a large case statement, and executes the proper methods on the
classes we've been looking at in response. If you need more details on how any
of these work, glance at the other big method, help(), which is just a heredoc

The parse_purchase() method is a helper for the BUY and SELL commands that
extracts all the symbols and shares entered by the user.

Here's the last little chunk of code that creates and runs the application:

  # ...
  if __FILE__ == $0
      app = if ARGV.length > 0

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...