Check your stocks with your Blackberry or cell phone

StockChecker -- A simple application of Finance::Quote



This all started out as an attempt to make a stock quotes page that would work well for "reduced capacity browsers" like those on the Blackberry platform and even cell phone browsers.

It started with a request from a user who was looking for a very simple web page where he could get stock quotes and keep an eye on his favorite ticker symbols. He didn't want to wait for 100 KB of advertisements, graphics and fluff to download and he didn't want to pay for the extra/useless stuff on a per-byte basis. I was looking for an excuse to try out the Finance::Quote Perl module so I decided to see what I could do with a CGI page that met his requirements. Below are the results of a couple hours of gross hacking. No elegance. No cutesy, efficient code. Give me a break, I started from scratch and spent all of two hours on this. Yes, I know about cascading style sheets. Yes, I know about color schemes and accessibility. Unfortunately, a Blackberry DOESN'T!

These quotes are as close as you could get by going to Yahoo Finance or BigCharts or any other financial site that doesn't offer real-time quotes. As far as I know, these are not real-time quotes, I believe they are the standard 20-minute delayed.

As I started researching Finance::Quote, I came across Finance::QuoteHist which gives historical quotes. I thought this might be nice to have also because I could give him a very coarse history of the stock price for each equity. Unfortunately, as I'll explain later, Finance::QuoteHist has given me more problems that the entire effort of the CGI.

A screen shot can be seen here. Woooooo! Pretty impressive, eh?

Also, while I was experimenting with the web page, I decided it might be nice to have an E-mail version that could be used without a web interface, perhaps by a cell phone or at other times when a browser is not available or desirable. I took the same CGI script and set it up for plain text. Then I made an autoresponder so you could mail your requests in and get quotes E-mailed back to you. More on this below.

What you see below is not rocket science. It is pretty much the author's example code from each module crammed into a CGI format to get it on a web page. Some notes about this stuff below:

[bubba@stinky bubba]$ cat /var/www/html/users/stocks
GE
CSCO
INTC
F
MSFT
[bubba@stinky bubba]$


E-mail Stock Checker Autoresponder

I also have a version below that can be used for E-mail. It uses the same database of favorites as the webpage but there is no way to modify the database via E-mail. You can send the script special look-up requests by simply putting them in the subject of the E-mail. Separate multiple ticker symbols with spaces or commas. If the subject of the E-mail is blank, or contains something other than letters, commas and spaces, the autoresponder will simply return the watchlist of favorites and ignore the subject field. The body of the stimulus E-mail is always ignored.

Below are the instructions for setting up the autoresponder with Sendmail. I assume other mail programs have similar capabilities. Most of the work is done in the script, however you will need to send (at least) the header to the script via some means.

Again, for Sendmail:

  1. Put a copy of your script into /usr/bin/ or another place that has execution privileges.
  2. Create a symbolic link in /etc/smrsh which is the remote shell directory for Sendmail. Do this with ln -s /usr/bin/mailquotes.pl mailquotes. The /etc/smrsh/ directory houses scripts that can be executed securely by Sendmail. As far as I know, you cannot use any other location for these scripts without some kind of sandbox and SUID scheme. I think MajorDomo uses something like this. Anyway....
  3. Make sure the file is executable: chmod 755 /usr/bin/mailquotes.pl
  4. Set up an alias in the /etc/aliases file which pipes the mail to the script. We'll use "stocks" for the name, but you can use anything you want.
ftp:	root
games:	root
stocks:	|/etc/smrsh/mailquotes
  1. Remember to run /etc/newaliases to make the change show through to Sendmail.

That's it. Now when your user sends an E-mail to stocks@example.com he will get quotes sent back to him, assuming his E-mail has a valid From: header. A few notes about the code:


The CGI Webpage Code [Click to download text version or copy/paste from below]


#! /usr/bin/perl -T

#
#   stockchecker.pl
#
# stockchecker -- perl script to generate VERY simple
# CGI page for Blackberry and cell phones
# stockchecker (C) 2005, pettingers.org, All Rights reserved under GPL license:
# http://www.gnu.org/licenses/gpl.txt
#
# Complete documentation for this code can be found at:
#  http://www.pettingers.org/code/stockchecker.html
#

use strict;
use CGI;
use Finance::Quote;
use Finance::QuoteHist::Yahoo;


$| = 1;
#
####
# User Defined Variables
#
###############################

our $stocklist = "/var/www/html/users/stocks"; #Location of user's watchlist

###############################
# End User Defined Variables
###
#

our $query = new CGI;
my ($tickers, $marker) = '';
my (@lists, @additional) = '';

# Do initial pull-in from the user's favorite tickers list
$tickers = listload();

# Set up web page using CGI module

print $query->header();
print $query->start_html(-title=>"Your Stock Checker Page", -BGCOLOR=>'#FFFFF0', -LEFTMARGIN=>'10', -MARGINWIDTH=>'10');
print '<font face=Verdana, Arial, Sans>';
print "\<CENTER\>\<BIG\>";
print "StockChecker\n\n\n";
print "\<\/BIG\>\n";
print $query->br;
print "\<\/CENTER\>\n";
print $query->startform();
print $query->textfield(-name=>'newitems', -default=>"$tickers", -size=>20, -maxlength=>80);
print $query->submit(-value=>'Add/Remove');
print $query->defaults('Clear');
print $query->br;
print $query->textfield(-name=>'looknew', -default=>'', -size=>20, -maxlength=>80);
print $query->submit(-value=>'Lookup');
print $query->defaults('Clear');
print $query->endform;


if ($query->param('newitems'))
{

        @additional = usercheck($query->param('newitems'));
        open (USEROUT, ">$stocklist");
        foreach $marker(@additional){
        print USEROUT "$marker\n";
        }
        close (USEROUT);

}

print $query->br;
print $query->hr;
if ($query->param('looknew'))

{
  @additional = usercheck($query->param('looknew'));

displayone(@additional);
print $query->br;
print $query->hr;

}

displayall();

print '</font>';

print $query->end_html;
exit;

#####################################################################
sub displayall
{
#
# Used to build the message body with quotes from the watchlist
#

use Finance::Quote;
my $quoter = Finance::Quote->new;
my ($stock);
my @lists;
$quoter->timeout(30);

        open (USERIN, $stocklist);
        @lists = <USERIN>;
        close (USERIN);


my %info = $quoter->fetch("usa", @lists);

foreach $stock (@lists) {
        chomp ($stock);
        unless ($info{$stock, "success"}) {
                warn "Lookup of $stock failed - ".$info{$stock, "errormsg"}.
                     "\n";
                next;
        }
        print $query->br;
        print '<u>';
        print $info{$stock, "name"}, " \($stock\):\n\n";
        print '</u>';
        print $query->br;

  # Call for historical quotes
        historical($stock,'3 weeks ago','3 Weeks Ago','2','1','0');
        historical($stock,'2 Weeks ago','2 Weeks Ago','2','1','0');
        historical($stock,'1 Week ago','1 Week Ago ','2','1','0');

        print '***Current: &nbsp;',
              "Net Change: ", $info{$stock, "net"}, " \(",
              $info{$stock, "p_change"}, "\%\)", '&nbsp;&nbsp;',
              "Price: " , $info{$stock, "price"}, "\n";


        print $query->hr;
}

} # End displayall
##################################################################

sub displayone
{
#
# Used to display quote information for one ore more stocks
# that were placed in the Lookup form field.
#

use Finance::Quote;
my $quoter = Finance::Quote->new;
my ($stock);
my @lists = @_;
$quoter->timeout(30);


my %info = $quoter->fetch("usa", @lists);

foreach $stock (@lists) {
        chomp ($stock);
        $stock =~ tr/a-z/A-Z/;
        unless ($info{$stock, "success"}) {
                warn "Lookup of $stock failed - ".$info{$stock, "errormsg"}.
                     "\n";
                next;
        }
        print $query->br;
        print '<u>';
        print $info{$stock, "name"}, " \($stock\):\n\n";
        print '</u>';
        print $query->br;
        print '52-wk Range: ', $info{$stock, "year_range"};
        print $query->br;

  # Call for historical quotes
        historical($stock,'3 months ago','3 Months Ago','2','1','0');
        historical($stock,'2 months ago','2 Months Ago','2','1','0');
        historical($stock,'2 days ago','2 Days Ago  ','2','1','0');

        print '***Current: &nbsp;',
              "Net Change: ", $info{$stock, "net"}, " \(",
              $info{$stock, "p_change"}, "\%\)", '&nbsp;&nbsp;',
              "Price: " , $info{$stock, "price"}, "\n";
        print $query->br;
        print '***Current: &nbsp;',
              "Vol: ", $info{$stock, "volume"}, '&nbsp;&nbsp;',
              "Day Range: ", $info{$stock, "day_range"}, '&nbsp;&nbsp;',
              "Open: ", $info{$stock, "open"};


        print $query->hr;
}


} # End displayone
####################################
sub usercheck
{
my ($entered) = @_;
my @adds;
 for ($entered) {
        s/,/ /g;
        s/^\s+//;
        s/\s+$//;
        s/\s+/ /g;
 }
 $entered =~ tr/a-z/A-Z/;
 unless (($entered =~ m/^[\w ]+$/) and ($entered =~ m/^[A-Z]{1,5}[, ]*/)){
        $entered = '';

 }

$entered = substr($entered,0,100);
@adds = split(/ /,$entered);
return @adds;

} # End usercheck

###############################################################

sub listload
{
my ($new, $tickers, @lists) = '';

open (USERIN, $stocklist);
@lists = <USERIN>;
close USERIN;
foreach $tickers(@lists){
        chomp($tickers);
        $new .= " " . $tickers;
}
return $new;


} # End listload

################################################################

sub historical
{

# Used to provide historical quotes from Yahoo.
#
# Pass follwing:
#
#       ticker
#       date of interest (Date::Manip format)
#       coloquial name for date (e.g. 2 weeks ago)
#       decimal place precision
#       attempt number time out for requests
#       full information (1) or just close price (0)
#

use Finance::QuoteHist::Yahoo;

my ($stock, $datein, $datename, $prec, $attempt, $full) = @_;
my ($row, $symbol, $date, $open, $high, $low, $close, $volume) = '';

my $q = Finance::QuoteHist::Yahoo->new
      (
       symbols    => $stock,
       start_date => $datein,
       end_date   => $datein,
       quote_precision => $prec,
       attempts => $attempt
      );

   foreach $row ($q->quotes()) {
       ($symbol, $date, $open, $high, $low, $close, $volume) = @$row;
        if ($full) {
          print "$datename:  Open: $open  High: $high  Low: $low  Close:
$close  Vol: $volume\n";
        }
        else {
          print "$datename:  $close\n";
        }
        print $query->br;

   }
} # end historical


#end



The E-mail Autoresponder Code [Click to download text version or copy/paste from below]


#! /usr/bin/perl

# mailquotes.pl
#
# mailquotes -- perl script to generate E-mail
# autoresponder for stock quotes
# mailquotes (C) 2005, pettingers.org, All Rights reserved under GPL
# license:
# http://www.gnu.org/licenses/gpl.txt
#
#
# Full documentation for this program can be found at:
#  http://www.pettingers.org/code/stockchecker.html
#


use strict;
use MIME::Lite;
use Finance::Quote;
use Finance::QuoteHist::Yahoo;

#####################################
####### User Defined Variables ######
#
my $limit = 20000; # Max characters for outbound message minus header
our $stocklist = "/var/www/html/users/stocks"; # Watchlist of stocks
my $fromaddr = '"Mail Quotes" <no-reply@example.com>'; # Who the message will come from
my $celladdr = '1235551234@mobile.att.com'; # Pager or cell address of admin/monitor
my $notify = 1; # set to false (0) for no activity notification
my $abuse = 'abuse@example.com'; # Abuse or admin E-mail for your server
my $subj = 'Your Quotes!'; # Subject for the outbound message

#####################################
### End user Difined Variables ###
#
undef $/;
my %hdrs;
my @additional;
my ($infrom, $insubject, $data, $message, $msg) = '';

$message = <>;

$message =~ s/\n\s+/ /g;
%hdrs   =  (UNIX_FROM => split /^(\S*?):\s*/m, $message);

chop ($infrom = $hdrs{"From"});  # Find out who its from and chop the \n
chop ($insubject = $hdrs{"Subject"});  # Get the subject and chop the \n

# Be very picky about what is in the From: field.  If it looks goofy, bail out.
if ( $infrom =~ m/\<.+\@.+\>/ )
  {
   if ($infrom =~ m/.*\<(.+)\>/ )
        {$infrom = $1;}
   else {$infrom = '';}
  }
else {exit;}

$data = "This message is an automatically generated response to a request from $infrom\.\n";
$data .= "If you received this E-mail in error or did not request it, please contact us \n";
$data .= "immediately at: $abuse\n\n";

 $insubject =~ tr/a-z/A-Z/;
# Do sanity check on subject, then do more sanity checks!
 if (($insubject =~ m/^[\w, ]+$/) and ($insubject =~ m/^[A-Z]{1,5}[, ]*/)){

   @additional = usercheck($insubject);
# If we've got a good subject, do a custom lookup on the ticker symbol(s)
   $data .= displayone(@additional);
 }

# Build up the message body with the watchlist
$data .= displayall();

#   Package up the message with a max length of $limit
$data = substr($data, 0, $limit);

#  Format the message to send with MIME::Lite package
$msg = MIME::Lite->new(
        From    =>$fromaddr,
        To      =>$infrom,
        Subject =>$subj,
        Data    =>$data
        );
#   Send it best way possible (usually sendmail)
$msg->send;

#
# Build up message for admin/monitor.
# set $notify to 0 above if you don't care to have this sent.
#
if ($notify)
{
        $msg = MIME::Lite->new(
                From    =>$fromaddr,
                To      =>$celladdr,
                Subject =>"Quotes sent",
                Data    =>"User-- $infrom"
                );

#   Send it best way possible (usually sendmail)
$msg->send;
}

exit;


#########################################################

sub displayone
{
#
#
# Used to display quote information for one or more stocks
# that were originally placed in the subject of the requesting E-mail.
#

use Finance::Quote;
my $quoter = Finance::Quote->new;
my ($stock, $build) = '';
my @lists = @_;
$quoter->timeout(30);

# Load custom, one-time quotes into %info.
my %info = $quoter->fetch("usa", @lists);

foreach $stock (@lists) {
        chomp ($stock);
        $stock =~ tr/a-z/A-Z/; # one last check, probably not needed.
        unless ($info{$stock, "success"}) {
                warn "Lookup of $stock failed - ".$info{$stock, "errormsg"}.
                     "\n";
                next;
        }
        $build .= "\n\n" . $info{$stock, "name"}. " \($stock\):\n";
        $build .= '=====================' ."\n";

        $build .= '52-wk Range: '. $info{$stock, "year_range"} . "\n";

  # Call for historical quotes
        $build .= historical($stock,'3 months ago','3 Months Ago','2','1','0');
        $build .= historical($stock,'2 months ago','2 Months Ago','2','1','0');
        $build .=  historical($stock,'2 days ago','2 Days Ago  ','2','1','0');


        $build .= '***Current: '.
              "Net Change: ". $info{$stock, "net"} . " \(".
              $info{$stock, "p_change"} . "\%\)". '  '.
              "Price: " . $info{$stock, "price"}. "\n";

        $build .= '***Current:  '.
              "Vol: ". $info{$stock, "volume"}. '  '.
              "Day Range: ". $info{$stock, "day_range"}. '  '.
              "Open: ". $info{$stock, "open"} . "\n";

}

return $build; # Send the (partially) completed body back to the calling main

} # End displayone

####################################

###################################
sub displayall
{
#
# Used to build the message body with quotes from the watchlist
#

use Finance::Quote;
my $quoter = Finance::Quote->new;
my ($stock, $build) = '';
my @lists;
$quoter->timeout(30);

        open (USERIN, $stocklist);
#    @lists = <USERIN>;   Doesn't work???
        $build = <USERIN>;
        close (USERIN);
@lists = split(/\n/, $build); #why do we need this?
$build = '';

my %info = $quoter->fetch("usa", @lists);

foreach $stock (@lists) {
        chomp ($stock);

        unless ($info{$stock, "success"}) {
                warn "Lookup of $stock failed - ".$info{$stock, "errormsg"}.
                     "\n";
                next;
        }
        $build .= "\n\n" . $info{$stock, "name"}. " \($stock\):\n";
        $build .= '=====================' ."\n";

        $build .= '52-wk Range: '. $info{$stock, "year_range"} . "\n";

  # Call for historical quotes
        $build .= historical($stock,'3 weeks ago','3 Weeks Ago','2','1','0');
        $build .= historical($stock,'2 Weeks ago','2 Weeks Ago','2','1','0');
        $build .= historical($stock,'1 Week ago','1 Week Ago ','2','1','0');


        $build .= '***Current: '.
              "Net Change: ". $info{$stock, "net"} . " \(".
              $info{$stock, "p_change"} . "\%\)". '  '.
              "Price: " . $info{$stock, "price"}. "\n";

        $build .= '***Current:  '.
              "Vol: ". $info{$stock, "volume"}. '  '.
              "Day Range: ". $info{$stock, "day_range"}. '  '.
              "Open: ". $info{$stock, "open"} . "\n";


}

return $build;

} # End displayall


##################################################################
sub usercheck
{
#
# Simple sanity checks for user entered lists
#

my ($entered) = @_;
my @adds;
 for ($entered) {
        s/,/ /g;
        s/^\s+//;
        s/\s+$//;
        s/\s+/ /g;
 }
 $entered =~ tr/a-z/A-Z/;
 unless ($entered =~ m/^[\w ]+$/) {
        $entered = '';

 }

$entered = substr($entered,0,100);
@adds = split(/ /,$entered);
return @adds;

} # End usercheck

###############################################################

sub historical
{

# Used to provide historical quotes from Yahoo.
#
# Pass follwing:
#
#       ticker
#       date of interest (Date::Manip format)
#       coloquial name for date (e.g. 2 weeks ago)
#       decimal place precision
#       attempt number time out for requests
#       full information (1) or just close price (0)
#

use Finance::QuoteHist::Yahoo;

my ($stock, $datein, $datename, $prec, $attempt, $full) = @_;
my ($buildh, $row, $symbol, $date, $open, $high, $low, $close, $volume) = '';

my $q = Finance::QuoteHist::Yahoo->new
      (

       symbols    => $stock,
       start_date => $datein,
       end_date   => $datein,
       quote_precision => $prec,
       attempts => $attempt
      );

   foreach $row ($q->quotes()) {
       ($symbol, $date, $open, $high, $low, $close, $volume) = @$row;
        if ($full) {
          $buildh .= "$datename: Open:$open  High:$high  Low:$low  Close:$close  Vol:$volume\n";
        }
        else {
          $buildh .= "$datename:  $close\n";
        }


   }
return $buildh;

} # end historical


# end

synch

Prerequisites



shoulder
Copyright 2005 Pettingers.org

Vectors at

pettingers.org