I'm a member of a local stock club. For those of you not familiar with such clubs, they're just a group of people who pool resources (primarily knowledge, research time, and finances) to purchase stocks.
My particular club makes use of a lot of technology for information and communication. A recent request was a tool to practice investing. This is especially helpful to inexperienced members. The idea is that they can pretend to purchase a few stocks (preferably after studying their choices!) and then see how they do over time.
This week's Ruby Quiz is to build this simple tool.
Here's a sample run to get the ideas flowing:
$ ./portfolio
Buy (symbol shares/dollars): PIXR $1000
You purchased 23 shares of PIXR for $991.53.
Buy (symbol shares/dollars): GOOG 3
You purchased 3 shares of GOOG for $887.55.
Buy (symbol shares/dollars):
+--------+--------+-----------+----------------+---------------+-----------+
| Symbol | Shares | Buy Price | Buy Date | Current Price | Gain/Loss |
+--------+--------+-----------+----------------+---------------+-----------+
| GOOG | 3 | $ 295.85 | 07/25/05 19:53 | $ 295.85 | $ 0.00 |
+--------+--------+-----------+----------------+---------------+-----------+
| PIXR | 23 | $ 43.11 | 07/25/05 19:53 | $ 43.11 | $ 0.00 |
+--------+--------+-----------+----------------+---------------+-----------+
Later, I can see how my shares are doing:
$ ./portfolio
+--------+--------+-----------+----------------+---------------+-----------+
| Symbol | Shares | Buy Price | Buy Date | Current Price | Gain/Loss |
+--------+--------+-----------+----------------+---------------+-----------+
| GOOG | 3 | $ 295.85 | 07/25/05 19:53 | $ 293.29 | $ -7.68 |
+--------+--------+-----------+----------------+---------------+-----------+
| PIXR | 23 | $ 43.11 | 07/25/05 19:53 | $ 43.44 | $ 7.59 |
+--------+--------+-----------+----------------+---------------+-----------+
Don't feel tied to this exact display or interface. Some interesting ideas might be to show historical data before asking a user to confirm their choice, or plot changes over regular intervals instead of just showing the initial and current prices. If you think of something else, go for it.
I think this is a great quiz for beginners wanting to learn more about Ruby's standard library. If that describes you, I encourage you to give it a try. (Hint: Many web sites offer stock data, all you need do is figure out how to get it...)
Quiz Summary
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:
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:
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 '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:
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 <<END_OF_HELP
Commands:
[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
report
[view]: View the current price of a stock
view Symbol[, Symbol...]
example: view GOOG, MSFT
[exit]: Quit the stock application (also quit)
END_OF_HELP
end
end
# ...
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 String.
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
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...