A Stock Portfolio Manager

Let's look at an example program that illustrates just how much time you can save using Perl modules that are already written. The example application is a stock portfolio manager. It maintains any number of portfolios, each of which can contain any number of securities. The portfolio manager downloads the latest share prices for all the stocks in your portfolio from Yahoo! Finance every time you view a portfolio.

First, the program creates a list of portfolios. From that page, you can add a new portfolio or view or delete an existing portfolio. When you view a portfolio, the latest prices for those stocks will be displayed. You can also delete stocks from the portfolio, or add new stocks to the portfolio from that page as well.

One thing this program demonstrates is how you can save yourself a lot of pain and suffering by using modules to take care of a lot of the grunt work in your script. In this example, the CGI modules LWP::Simple and XML::Simple were used. Both were introduced in earlier lessons. I use the LWP module to download stock prices from the Yahoo! Finance Web site. The CGI module takes care of everyday stuff such as processing query parameters on HTTP requests. All the persistent data is stored in an XML file, which is accessed using the XML::Simple module.

The Data File

Let's look at how the data is stored from request to request. Bear in mind that this data file is generated by the XML::Simple module from the data structure used in the program. I actually start out by writing the XML file, use the XML::Simple to read it in, and then write it back out every time the script runs to be sure to catch any changes that were made. Listing 21.1 contains a data listing.

Listing 21.1. The Data for the Stock Portfolio Program
<opt>
  <portfolio name="red">
    <stock lastfetch="1015292886" lastprice="48.65" company="DU PONT CO"
        name="dd" />
    <stock lastfetch="1015279312" lastprice="17.81" company="YAHOO INC"
        name="yhoo" />
  </portfolio>
  <portfolio name="foo">
    <stock lastfetch="1015280450" lastprice="63.30" company="MICROSOFT CP"
        name="msft" />
    <stock lastfetch="1015280613" lastprice="17.81" company="YAHOO INC"
        name="yhoo" />
    <stock lastfetch="1015280455" lastprice="16.05" company="AT&amp;T CORP"
        name="t" />
  </portfolio>
  <portfolio name="bar">
    <stock lastfetch="1015296403" lastprice="13.67" company="ORACLE CORP"
        name="orcl" />
    <stock lastfetch="1015296404" lastprice="17.81" company="YAHOO INC"
        name="yhoo" />
    <stock lastfetch="1015296404" lastprice="24.29" company="APPLE COMP INC"
        name="aapl" />
    <stock lastfetch="1015296405" lastprice="16.05" company="AT&amp;T CORP"
        name="t" />
  </portfolio>
  <portfolio name="baz">
    <stock lastfetch="1015278274" lastprice="31.85" company="INTEL CORP"
        name="intc" />
  </portfolio>
</opt>
						

When XML::Simple reads this file, it turns it into a structure of nested hashes (using references). One of the challenges when reading the program is figuring out how the data in this file maps to the data structure used throughout.

How the Program Works

Let's look at how this program is structured. The first thing to point out is that this program actually responds to many different kinds of requests. It is used to view a list of portfolios, view individual portfolios, and handle the addition and removal of both portfolios and stocks. The program expects a parameter named c (for command), which will indicate what sort of action should be taken. If the c parameter is not supplied, the program assumes that you want a list of portfolios.

Depending on the command sent to the program, other parameters might also be expected. The two optional parameters are the name of the portfolio and the ticker symbol of the stock. For example, if the command is del and the p parameter (for portfolio) is foo, the program knows you want to delete the portfolio named foo.

The core of the program is an if construct that checks which command was issued, and performs the tasks associated with that command. For example, if the command is add_stock, the program performs some basic error checking, calls the add_stock subroutine, and then displays the updated portfolio and the add stock form (using the view_portfolio and display_add_stock_form subroutines).

The commands that the program supports are

  • list— Lists the portfolios available.

  • view— Views the stocks in a particular portfolio.

  • del— Deletes the specified portfolio.

  • del_stock— Deletes the specified stock from the specified portfolio.

  • add— Adds a portfolio with the name specified.

  • add_stock— Adds the stock with the ticker specified to the current portfolio.

Before the conditional statement that drives the program, some modules were imported, some variables were initialized, and the parts of the page were printed out that are consistent regardless of which command was used. The footer of the page was printed out following the conditional statement.

Setting Up

Before I get into the meat of the program, some things have to be set up. This means importing the CGI, CGI::Carp (for better error reporting), XML::Simple, and LWP::Simple modules. Then, some global variables for this package that will be used throughout the script are set up. For example, the URLs for downloading stock information from Yahoo! Finance are hard coded, so that if those URLs change, they can be updated at the top of my program.

Also $command is initialized so that if the user fails to supply a command, list is used as the default. Probably the biggest job here, though, is reading in my data file. Amazingly, that part seems simple:

my $portfolios = XMLin("./portfolio.xml", forcearray => 1);

This code reads in a file called portfolio.xml, parses the XML data, and produces a reference to a data structure, which is assigned to a variable called $portfolios. An argument is passed to the XMLin() subroutine, forcearray, which basically tells it to treat all data as though it were in an array instead of handling lists of one item differently. This eliminates problems when a portfolio contains only one stock.

Displaying a List of Portfolios with list_portfolios()

Most of the list_portfolios() subroutine is concerned with printing out HTML, but there's some stuff that's worth inspecting here. For one thing, it introduces the data structure in the XML file. On the first line of the subroutine, a reference to the value in the top-level anonymous hash is created in my datastructure with the key portfolio, using this statement:

my $hashref = $portfolios->{portfolio} ;

The reference that's returned is to a hash containing all the portfolios in my file. On line 87, the keys in that anonymous hash are looped over, like this:

foreach my $port (keys %$hashref)

A list of the portfolios is printed out, along with links to view and delete them.

Displaying the Contents of a Single Portfolio Using view_portfolio()

This subroutine is a bit more complex. First, I read the argument passed to the subroutine and assign it to $port. Then, I create a reference to the hash containing the stocks in the named portfolio, like this:

my $hashref = $portfolios->{portfolio} ->{$port} ->{stock} ;

This reference points to a hash that contains hashes for each stock in the portfolio. The hashes are keyed on the stock's ticker symbol. Each of the stocks are then looped over using a foreach loop (just like the previous subroutine):

foreach my $stock_ticker (keys %$hashref)

In the body of the loop, the first thing to do is create a reference to the stock currently being processed, like this:

my $stock = $hashref->{$stock_ticker} ;

Then, some variables are set up that will be used to display this stock, print out the name of the company, and a delete link. Then, the get_current_price() subroutine is called to fetch the current price of the stock from Yahoo! Finance. I'll discuss that subroutine in a bit. After the price is fetched, a couple of temporary variables are set up so that the data can be used to update the record (which I do immediately after):

$stock->{lastprice}  = $current_price;
$stock->{lastfetch}  = time;

These two lines update the data structure so that the current price and time will be written to the data file as the previous price and time. At this point, all that's left is to print out the rest of the information about the stock.

Deleting Stocks and Portfolios Using delete_stock() and delete_portfolio()

When a user clicks on a delete link, the appropriate subroutine is called depending on whether they want to delete a stock or a portfolio. The statement used to delete a portfolio is

delete $portfolios->{portfolio} ->{$portfolio_name} ;

And here's the statement that deletes a stock:

delete $portfolios->{portfolio} ->{$portfolio_name} ->{stock} ->{$ticker} ;

As you can see, these statements simply delete values from hashes in the large data structure. When the data is saved, the changes will automatically be applied to the XML data file.

Adding a Portfolio Using add_portfolio()

Adding a portfolio is simple, you just test to make sure that the portfolio name entered doesn't already exist, and add a new empty hash to the data structure, like this:

$portfolios->{portfolio} ->{$portfolio_name}  = {} ;

That's all there is to it. The user can populate the new portfolio. They just have to view it and start adding stocks to it.

Adding a Stock Using add_stock()

Okay, here's where the real action is. This subroutine demonstrates how error handling is dealt with, how to access the data structure, and it pulls information down from the Web. I'll go ahead and list the source code for this subroutine so that it can be examined closely:

1:  sub add_stock
2:  {
3:      my $portfolio = shift(@_);
4:      my $ticker = shift(@_);
5:      my $stock_name = "";
6:      my $stock_price = 0;
7:
8:      if (exists $portfolios->{portfolio} ->{$portfolio} ->{stock} ->{$ticker} )
9:      {
10:         $errors .= "<li>That stock is already in the portfolio.</li>
";
11:         return;
12:     }
13:
14:     my $url = $stock_info_url . "$ticker";
15:     my $content = get($url);
16:
17:     if (!$content)
18:     {
19:         $errors .= "<li>Couldn't retrieve stock information.</li>
";
20:         return;
21:     }
22:
23:     if ($content =~ /No such ticker symbol/i)
24:     {
25:         $errors .= "<li>Invalid ticker symbol.</li>
";
26:         return;
27:     }
28:
29:     if ($content =~ /<td colspan=7><font(.+?)><b>(.+?)s+</b>/i)
30:     {
31:         $stock_name = $2;
32:     }
33:
34:     if ($content =~ /Last Trade<br>(.+?)<b>([d.]+)</b>/i)
35:     {
36:         $stock_price = $2;
37:     }
38:
39:     my $hashref = $portfolios->{portfolio} ->{$portfolio} ->{stock} ;
40:
41:     $hashref->{$ticker}  = { lastfetch => time, lastprice => $stock_price,
42:         company => $stock_name } ;
43: }
							

First, on lines 3 through 6, set up some variables. The first two variables take their values from the arguments passed to the subroutine, and the next two are set to default values. I'll get the real values from Yahoo! Finance, assume everything goes as planned.

Next, check to ensure that the stock the user is trying to add isn't already in the portfolio. If it is, add an error message, $errors, and return. Next, append the stock ticker to the URL for retrieving information about the stock. Then, use the get subroutine (which is part of LWP::Simple) to request the page associated with that URL. If the page was requested successfully, the HTML code that makes up the page is assigned to $content. Just in case it isn't, a test is done to see whether $content evaluates as true, and if it doesn't, raise an error and return.

If it isn't empty, look for a particular string named No such ticker symbol. This text only appears on the page if the ticker symbol is invalid. If it's there, they've requested a stock using a ticker symbol that doesn't exist, so raise an error and return.

Now we're ready to start grabbing data using regular expressions. First, grab the stock name, and then the current price. By carefully examining the source code of the page, I wrote the regular expressions used to extract this information (and nothing extraneous) from the page. The catch here is that if Yahoo! changes the appearance of this page, there's a pretty good chance that these regular expressions will stop matching what they're supposed to, and therefore, you'll have to update your script. When you extract information from Web pages like this, that's just something you have to contend with. One bit of advice is that you should use as small an expression as possible, which makes it less likely that someone will come along and break your script.

After these two bits of data have been extracted from the page, access the data structure to insert a new anonymous hash into the portfolio. When the data is written to disk, this new stock will be included.

Getting the Current Stock Price Using get_current_price()

I explained add_stock() in detail, therefore, I don't have to go into that much detail for this subroutine because it's sort of like add_stock(), except simpler. Like the previous subroutine, take the ticker symbol as an argument, append it to the URL where stock quotes are found, and then fetch the page containing the pertinent information. If, for some reason, the page isn't found, I raise an error and return from the subroutine.

What we then have is a large regular expression that extracts the current stock price from the HTML that was returned. This is a different regular expression than was used before. The same one could have been used, but because this is an example program, I wanted to use multiple pages and expressions. After the price has been extracted, it's returned.

Writing the Data Using write_data()

One of the last things the program does before exiting is write its data to disk. Transforming the data in $portfolios to XML is easy with XML::Simple, just use the following subroutine call:

my $xml = XMLout($portfolios);

After the XML is stored in the variable $xml, open a filehandle and write the data out to disk.

The Source Code

Listing 21.2 contains the source code for the portfolio.cgi program.

Listing 21.2. The Stock Portfolio Program
1:   #!/usr/local/bin/perl
2:
3:   use strict;
4:   use CGI;
5:   use CGI::Carp qw(fatalsToBrowser);
6:   use XML::Simple;
7:   use LWP::Simple;
8:
9:   my $portfolios = XMLin("./portfolio.xml", forcearray => 1);
10:  my $command = "list";
11:  my $query = new CGI;
12:  my $quote_url = 'http://finance.yahoo.com/q?d=v1&s=';
13:  my $stock_info_url = 'http://finance.yahoo.com/q?d=t&s=';
14:  my $errors = "";
15:
16:  if ($query->param('c'))
17:  {
18:      $command = $query->param('c'),
19:  }
20:
21:  print $query->header;
22:  print "<html><head><title>portfolio</title></head><body>
";
23:  print "<h1 align="center">Stock Portfolio</h1>
";
24:
25:  if ($command eq "list")
26:  {
27:      &list_portfolios;
28:      &display_add_form;
29:  }
30:  elsif ($command eq "view")
31:  {
32:      &view_portfolio($query->param('p'));
33:      &display_add_stock_form($query->param('p'));
34:  }
35:  elsif ($command eq "del")
36:  {
37:      &delete_portfolio($query->param('p'));
38:      &list_portfolios;
39:      &display_add_form;
40:  }
41:  elsif ($command eq "del_stock")
42:  {
43:      &delete_stock($query->param('p'), $query->param('s'));
44:      &view_portfolio($query->param('p'));
45:      &display_add_stock_form($query->param('p'));
46:  }
47:  elsif ($command eq "add")
48:  {
49:      if ($query->param('p'))
50:      {
51:          &add_portfolio($query->param('p'));
52:      }
53:      else
54:      {
55:          $errors .= "<li>You must enter a portfolio name.</li>
";
56:      }
57:
58:      &list_portfolios;
59:      &display_add_form;
60:  }
61:  elsif ($command eq "add_stock")
62:  {
63:      if ($query->param('s'))
64:      {
65:          &add_stock($query->param('p'), $query->param('s'));
66:      }
67:      else
68:      {
69:          $errors .= "<li>You must enter a ticker symbol.</li>
";
70:      }
71:
72:      &view_portfolio($query->param('p'));
73:      &display_add_stock_form($query->param('p'));
74:  }
75:
76:  # Write out the update data file.
77:  &write_data;
78:  print "<p><a href="portfolio.pl">return to portfolio list</a></p>
";
79:  print "</body></html>
";
80:
81:  sub list_portfolios
82:  {
83:      my $hashref = $portfolios->{portfolio} ;
84:
85:      print "<div align="center">
";
86:      print "<table cellpadding="8">
";
87:      foreach my $port (keys %$hashref)
88:      {
89:          my $encoded_port = &encode_string($port);
90:          my $view_link = "portfolio.pl?c=view&amp;p=$encoded_port";
91:          my $delete_link = "portfolio.pl?c=del&amp;p=$encoded_port";
92:          print "<tr>
";
93:          print "<td><b>$port</b></td>
";
94:          print "<td>";
95:          print "<a href="$view_link">view</a></td>
";
96:          print "<td><a href="$delete_link">delete</a></td>
";
97:          print "</tr>
";
98:      }
99:      print "</table>
";
100:     print "</div>
";
101: }
102:
103: sub view_portfolio
104: {
105:     my $port = shift(@_);
106:
107:     my $hashref = $portfolios->{portfolio} ->{$port} ->{stock} ;
108:
109:     print "<h3>$port</h3>";
110:     foreach my $stock_ticker (keys %$hashref)
111:     {
112:         my $stock = $hashref->{$stock_ticker} ;
113:         my $company_name = $stock->{company} ;
114:         my $delete_link = "portfolio.pl?c=del_stock&amp;p=$port";
115:         $delete_link .= "&s=" . $stock_ticker;
116:
117:         print "<p>
";
118:         print "<table border="1" cellspacing="0" cellpadding="5">
";
119:         print "<tr><td colspan="2"><b>", $company_name, "</b>";
120:         print "<br /><font size="1">";
121:         print "<a href="$delete_link">delete</a>";
122:         print "</font>";
123:         print "</td></tr>
";
124:
125:         my $current_price = &get_current_price($stock_ticker);
126:
127:         my $lastprice = $stock->{lastprice} ;
128:         my $lastfetch = $stock->{lastfetch} ;
129:
130:         # Move this soon.
131:         $stock->{lastprice}  = $current_price;
132:         $stock->{lastfetch}  = time;
133:
134:         print "<tr><td>ticker</td>";
135:         print "<td>", $stock_ticker, "</td></tr>
";
136:         print "<tr><td>current price:</td>";
137:         print "<td>", $current_price, "</td></tr>
";
138:         print "<tr><td>last price:</td>";
139:         print "<td>", $lastprice, "</td></tr>
";
140:         print "<tr><td>as of:</td>";
141:         print "<td>", scalar localtime($lastfetch), "</td></tr>
";
142:         print "</table>
";
143:         print "</p>
";
144:     }
145: }
146:
147: sub delete_portfolio
148: {
149:     my $portfolio_name = shift(@_);
150:
151:     delete $portfolios->{portfolio} ->{$portfolio_name} ;
152: }
153:
154: sub delete_stock
155: {
156:     my $portfolio_name = shift(@_);
157:     my $ticker = shift(@_);
158:
159:     delete $portfolios->{portfolio} ->{$portfolio_name} ->{stock} ->{$ticker} ;
160: }
161:
162: sub get_current_price
163: {
164:     my $ticker = shift(@_);
165:     my $url = $quote_url . "$ticker";
166:     my $content = get($url);
167:
168:     if (!$content)
169:     {
170:         return "request failed";
171:     }
172:
173:     my $current_price = "not found";
174:
175:     if ($content =~ /<td[^>]*><font[^>]*><a[^>]*>(w+?)</a></font><
/td><td[^>]*><font[^>]*>(.+?)</font></td><td[^>]*><font[^>]*><b>([d.]+?)</b></font><
/td>/i)
176:     {
177:         $current_price = $3;
178:     }
179:
180:     return $current_price;
181: }
182:
183: sub add_portfolio
184: {
185:     my $portfolio_name = shift(@_);
186:
187:     if (exists $portfolios->{portfolio} ->{$portfolio_name} )
188:     {
189:         $errors .= "<li>That portfolio name is already in use.</li>
";
190:         return;
191:     }
192:
193:     $portfolios->{portfolio} ->{$portfolio_name}  = {} ;
194: }
195:
196: sub add_stock
197: {
198:     my $portfolio = shift(@_);
199:     my $ticker = shift(@_);
200:     my $stock_name = "";
201:     my $stock_price = 0;
202:
203:     if (exists $portfolios->{portfolio} ->{$portfolio} ->{stock} ->{$ticker} )
204:     {
205:         $errors .= "<li>That stock is already in the portfolio.</li>
";
206:     }
207:
208:     my $url = $stock_info_url . "$ticker";
209:     my $content = get($url);
210:
211:     if (!$content)
212:     {
213:         $errors .= "<li>Couldn't retrieve stock information.</li>
";
214:         return;
215:     }
216:
217:     if ($content =~ /No such ticker symbol/i)
218:     {
219:         $errors .= "<li>Invalid ticker symbol.</li>
";
220:         return;
221:     }
222:
223:     if ($content =~ /<td colspan=7><font(.+?)><b>(.+?)s+</b>/i)
224:     {
225:         $stock_name = $2;
226:     }
227:
228:     if ($content =~ /Last Trade<br>(.+?)<b>([d.]+)</b>/i)
229:     {
230:         $stock_price = $2;
231:     }
232:
233:     my $hashref = $portfolios->{portfolio} ->{$portfolio} ->{stock} ;
234:
235:     $hashref->{$ticker}  = { lastfetch => time, lastprice => $stock_price,
236:         company => $stock_name } ;
237: }
238:
239: sub write_data
240: {
241:     my $xml = XMLout($portfolios);
242:     open (FILE,"> portfolio.xml")
243:         or die "Can't open data file: ";
244:     print FILE $xml;
245:     close FILE;
246: }
247:
248: sub display_add_form
249: {
250:     print "<h3 align="center">add a new portfolio</h3>
";
251:     if ($errors)
252:     {
253:         print "<div align="center"><table><tr><td>
";
254:         print "Please correct the following error:
";
255:         print "<ul>
", $errors, "</ul>
";
256:         print "</td></tr></table>
";
257:     }
258:     print "<form>
";
259:     print "<input type="hidden" name="c" value="add">
";
260:     print "<div align="center"><table>
";
261:     print "<tr><td>portfolio name:</td><td>";
262:     print "<input type="text" name="p" value="">";
263:     print "</td></tr>
";
264:     print "<tr><td colspan="2"><input type="submit"></td></tr>
";
265:     print "</table></div>
";
266:     print "</form>
";
267: }
268:
269: sub display_add_stock_form
270: {
271:     my $portfolio_name = shift(@_);
272:     print "<h3 align="center">add a new stock</h3>
";
273:     if ($errors)
274:     {
275:         print "<div align="center"><table><tr><td>
";
276:         print "Please correct the following error:
";
277:         print "<ul>
", $errors, "</ul>
";
278:         print "</td></tr></table>
";
279:     }
280:     print "<form>
";
281:     print "<input type="hidden" name="c" value="add_stock">
";
282:     print "<input type="hidden" name="p" value="$portfolio_name">
";
283:     print "<div align="center"><table>
";
284:     print "<tr><td>stock ticker symbol:</td><td>";
285:     print "<input type="text" name="s" value="">";
286:     print "</td></tr>
";
287:     print "<tr><td colspan="2"><input type="submit"></td></tr>
";
288:     print "</table></div>
";
289:     print "</form>
";
290: }
291:
292: sub encode_string
293: {
294:     my $string_to_encode = shift @_;
295:
296:     $string_to_encode =~ s/&/%26/g;
297:     $string_to_encode =~ s/+/%2B/g;
298:     $string_to_encode =~ s/?/%2B/g;
299:     $string_to_encode =~ s/ /+/g;
300:
301:     return $string_to_encode;
302: }
						

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset