[SUMMARY] Stock Portfolios (#41)

R

Ruby Quiz

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
# (e-mail address removed)

# 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",
:eek:pen=>"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 }.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...
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,057
Latest member
KetoBeezACVGummies

Latest Threads

Top