#!/usr/cs/bin/perl -w # # Anand Natrajan (www.anandnatrajan.com) # Fri Aug 16 12:58:12 PDT 2002 # # Program to calculate current values of stocks listed on the NASDAQ. use File::Basename; use File::Path; use POSIX ":sys_wait_h"; use Getopt::Long; use Time::Local; use IO::Handle; use LWP::UserAgent; $| = 1; $MyName = basename($0); $Sample = <] [-sample] [-summary] ... EOF if ($#_ > -1) { print STDERR < : Specify delimiter for fields in file. Default: \"$opt_delim\". -sample : Generate a sample portfolio. -summary : Generate a summary portfolio. : Find portfolio in . Can be specified multiply. $MyName requires input files, each containing a listing of the stocks that are part of that portfolio. Format for the input file must be as in the sample generated if you specify the -sample option. $MyName outputs its results in . and, if -summary is specified, in ..summary as well. EOF } exit 0; } sub About { print STDERR < \$opt_help, "about" => \$opt_about, "debug", "v|verbose", "delim", "sample" => \$opt_sample, "summary" => \$opt_summary) or &Usage; &Usage(1) if ($opt_help); &About if ($opt_about); $opt_v = 1 if (defined($opt_debug)); if (defined($opt_sample)) { print $Sample; exit 0; } @opt_folios = @ARGV; &Usage() if ($#opt_folios < 0); # We're going to parse each folio twice. The first time, we'll merely get # the list of the ticker symbols so that we can ask the NASDAQ site for # quotes in bulk. The second time, we'll parse the folio in detail to # report for each line and also calculate the summary. # First pass. %Quote = (); foreach my $folio (@opt_folios) { &Vprint("Scanning portfolio $folio for tickers.\n"); if (!open(FOLIO, $folio)) { warn "Could not read folio file $folio: $!\n" . "\tIgnoring this file.\n"; next; } my @lines = ; close FOLIO; &Dprint("Found " . ($#lines+1) . " lines in $folio.\n"); my $linecnt = 0; foreach my $line (@lines) { chomp($line); $linecnt++; # Ignore blank lines and comments. next if ($line =~ /^\s*$/ or $line =~ /^\s*#/); # Here's where we parse the really strict format of the folio, but # this time around, all we need is the symbol. my ($name, $ticker, $shares, $purchrate, $purchval, $purchdate, $currrate, $currval, $changeval) = split(/$opt_delim/, $line); # For now, we'll ignore whitespace in the ticker symbol. $ticker = &prune($ticker); if (!&ispresent($ticker)) { warn "Ticker symbol missing on line $linecnt in folio $folio.\n" . "\tIgnoring this line.\n"; next; } if (!defined($Quote{$ticker})) { my %stockrec = (currrate => 0, change => 0, changepercent => "", volume => ""); &Dprint("Found ticker $ticker.\n"); $Quote{$ticker} = { %stockrec }; } } } &GetQuotes(\%Quote); # Second pass. foreach my $folio (@opt_folios) { &Vprint("Researching portfolio $folio.\n"); if (!open(FOLIO, $folio)) { warn "Could not read folio file $folio: $!\n" . "\tIgnoring this file.\n"; next; } my @lines = ; close FOLIO; &Dprint("Found " . ($#lines+1) . " lines in $folio.\n"); $output = ""; # Output string for printing at the end. my %Summary = (); # Summary information for this portfolio. my $linecnt = 0; foreach my $line (@lines) { chomp($line); $linecnt++; # Ignore blank lines and comments. if ($line =~ /^\s*$/ or $line =~ /^\s*#/) { $output .= "$line\n"; # Let it be. next; } # Here's where we parse the really strict format of the folio. my ($name, $ticker, $shares, $purchrate, $purchval, $purchdate, $currrate, $currval, $changeval) = split(/$opt_delim/, $line); # Sanity-check the line to see if required fields are present. $name = &prune($name); # For now, we'll ignore whitespace in the ticker symbol. $ticker = &prune($ticker); if (!&ispresent($ticker)) { warn "Ticker missing on line $linecnt in folio $folio.\n" . "\tIgnoring this line.\n"; $output .= "$line\n"; next; } &Vprint("Researching company $name.\n"); $shares = &prune($shares); $purchrate = &prune($purchrate); $purchval = &prune($purchval); if (!&ispresent($shares)) { if (!&ispresent($purchrate)) { warn "Number of shares and purchase rate missing" . " on line $linecnt in folio $folio.\n" . "\tIgnoring this line.\n"; $output .= "$line\n"; next; } if (!&ispresent($purchval)) { warn "Number of shares and purchase value missing" . " on line $linecnt in folio $folio.\n" . "\tIgnoring this line.\n"; $output .= "$line\n"; next; } if ($purchrate == 0) { warn "Purchase rate is 0" . " on line $linecnt in folio $folio.\n" . "\tIgnoring this line.\n"; $output .= "$line\n"; next; } $shares = $purchval / $purchrate; # Really complicated math, eh? } else { if (!&ispresent($purchrate)) { if (!&ispresent($purchval)) { warn "Purchase rate and value missing" . " on line $linecnt in folio $folio.\n" . "\tIgnoring this line.\n"; $output .= "$line\n"; next; } else { if ($shares == 0) { warn "Number of shares is 0" . " on line $linecnt in folio $folio.\n" . "\tIgnoring this line.\n"; $output .= "$line\n"; next; } $purchrate = $purchval / $shares; } } else { if (!&ispresent($purchval)) { $purchval = $shares * $purchrate; } else { # If the user has kept a purchase value, we'll just use # it even if the product is not right because the user # may have added commissions or fees. } } } $purchdate = &prune($purchdate); $currrate = $Quote{$ticker}{'currrate'}; # Get the actual quote. if (!&ispresent($currrate)) { warn "Current rate not found for company $name" . " on line $linecnt in folio $folio.\n" . "\tIgnoring this line.\n"; $output .= "$line\n"; next; } $currval = $shares * $currrate; $changeval = ($purchval == 0) ? "inf" : (($currval - $purchval) * 100 / $purchval); &Dprint("For line $linecnt in folio $folio:\n" . "\tCompany Name : $name\n" . "\tTicker Symbol : $ticker\n" . "\tNumber of Shares : $shares\n" . "\tPurchase Rate : $purchrate\n" . "\tPurchase Value : $purchval\n" . "\tPurchase Date : $purchdate\n" . "\tToday's Rate : $currrate\n" . "\tToday's Value : $currval\n" . "\tChange in Value : $changeval\n" ); # Okay, so only if we can actually get a quote are we going to # modify the line and print what we want. $output .= join($opt_delim, ($name, $ticker, sprintf("%.4f", $shares), sprintf("%.4f", $purchrate), sprintf("%.4f", $purchval), $purchdate, sprintf("%.4f", $currrate), sprintf("%.4f", $currval), ($changeval eq "inf") ? "inf" : sprintf("%.4f%%", $changeval))) . "\n"; # Accumulate summary information for this portfolio. if (!defined($Summary{$ticker})) { # Create empty record. my %stockrec = (name => $name, # not particularly important... shares => 0, purchvalue => 0, currrate => $currrate); $Summary{$ticker} = { %stockrec }; } $Summary{$ticker}{'shares'} += $shares; $Summary{$ticker}{'purchval'} += $purchval; } ($_, $_, $_, my $mday, my $mon, my $year, $_, $_, $_) = localtime; my $today = ($year + 1900) . "-" . ($mon + 1) . "-$mday"; # Write out this portfolio's current status. my $filename = "$folio.$today"; &Vprint("Writing portfolio $filename.\n"); if (!open(FOLIO, ">$filename")) { warn "Could not write folio file $filename: $!\n" . "\tIgnoring this file.\n"; next; } print FOLIO $output; close FOLIO; my $summary = ""; # Summary string; my $totshares = 0; my $totpurchval = 0; my $totcurrval = 0; foreach my $ticker (sort keys %Summary) { # The summary line looks a lot like the actual line in the # portfolio. Indeed, the summary file can later be used as a # portfolio file. my $name = $Summary{$ticker}{'name'}; my $shares = $Summary{$ticker}{'shares'}; $shares = 0 if ($shares < 0.0001); # Catch rounding errors. my $purchval = $Summary{$ticker}{'purchval'}; $purchval = 0 if ($shares == 0); # So that we don't skew the totals. my $purchrate = ($shares != 0 ? $purchval / $shares : 0); my $purchdate = ""; my $currrate = $Summary{$ticker}{'currrate'}; my $currval = $shares * $currrate; my $changeval = ($purchval == 0) ? "inf" : (($currval - $purchval) * 100 / $purchval); $summary .= join($opt_delim, ($name, $ticker, sprintf("%.4f", $shares), sprintf("%.4f", $purchrate), sprintf("%.4f", $purchval), $purchdate, sprintf("%.4f", $currrate), sprintf("%.4f", $currval), ($changeval eq "inf") ? "inf" : sprintf("%.4f%%", $changeval))) . "\n"; $totshares += $shares; $totpurchval += $purchval; $totcurrval += $currval; } my $totpurchrate = ($totshares != 0 ? $totpurchval / $totshares : 0); my $totcurrrate = ($totshares != 0 ? $totcurrval / $totshares : 0); my $totchangeval = ($totpurchval == 0) ? "inf" : (($totcurrval - $totpurchval) * 100 / $totpurchval); # Add the grand total to the summary, but comment it since it's not # really a ticker. $summary .= join($opt_delim, ("# TOTAL", "", sprintf("%.4f", $totshares), sprintf("%.4f", $totpurchrate), sprintf("%.4f", $totpurchval), "", sprintf("%.4f", $totcurrrate), sprintf("%.4f", $totcurrval), ($totchangeval eq "inf") ? "inf" : sprintf("%.4f%%", $totchangeval))) . "\n"; # Write out this portfolio's summary status, if requested. if (defined($opt_summary)) { my $filename = "$folio.$today.summary"; &Vprint("Writing portfolio $filename.\n"); if (!open(FOLIO, ">$filename")) { warn "Could not write folio file $filename: $!\n" . "\tIgnoring this file.\n"; next; } print FOLIO $summary; close FOLIO; } } # Get the NASDAQ quotes for ticker symbol specified in a hash. Returns # the current rates for the symbols in the hash itself. sub GetQuotes { my $QuoteRef = shift; # Prepare the quote fetcher robot. my $rater = new LWP::UserAgent; $rater->agent("Quote Machine" . $rater->agent); # Get the list of the ticker symbols for which we want quotes. my @tickerlist = keys %$QuoteRef; my $batch = 10; # Maximum number of quotes we'll get in one shot. # NASDAQ-specific query string, with XXXXX for the ticker symbol. my $nasdaqurl = "http://quotes.nasdaq.com/Quote.dll?mode=stock&symbol=XXXXX&quick.x=0&quick.y=0"; while ($#tickerlist >= 0) { my $urlcopy = $nasdaqurl; my @sublist = (); # Sub-list depending on how many we want to batch. for (my $i = 0; $i < $batch and $#tickerlist >= 0; $i++) { push(@sublist, shift(@tickerlist)); } # NASDAQ-specific manipulation for multiple quotes. my $subststr = join("&symbol=", @sublist); $urlcopy =~ s/XXXXX/$subststr/; my $request = new HTTP::Request('GET', $urlcopy); $request->content_type('application/x-www-form-urlencoded'); $request->content('match=www&errors=0'); my $result = $rater->request($request); if ($result->is_error) { warn "Could not satisfy request for symbols:\n\t" . join($opt_delim, @sublist) . "\n"; next; } # Scroll down the returned page until we find the lines we want # for each ticker symbol. The lines currently look like this: # XXXXX # $ 50 # 0.23 # # 0.46% # 34,669,800 # with all sorts of blank lines and whitespace in between. foreach my $ticker (@sublist) { my @info = split(/\n/, $result->content); # Begin NASDAQ-specific parsing. while ($#info >= 0 and $info[0] !~ /quotes.nasdaq.com.*>$ticker= 0 and $info[0] =~ /^\s*$/) { shift(@info); } if ($#info == -1) { warn "Could not find current rate for ticker $ticker.\n" . "\tThe presentation of the NASDAQ page may have changed.\n"; next; } $info[0] =~ m/.*nbsp;([^<]*)= 0 and $info[0] =~ /^\s*$/) { shift(@info); } if ($#info > -1) { if ($info[0] =~ /.*class="([^>]*)">([^<]*)]*)">([^<]*)unch= 0 and $info[0] =~ /^\s*$/) { shift(@info); } if ($#info > -1) { $info[0] =~ m/.*right"[^>]*>([^<]*) -1) { shift(@info); } } # End NASDAQ-specific parsing. # Add all the fields retrieved to the quote record. &Dprint("For ticker $ticker:\n" . "\tToday's Rate : $currrate\n" . "\tToday's Change : $change\n" . "\tToday's Change % : $changepercent\n" . "\tToday's Volume : $volume\n" ); $QuoteRef->{$ticker}{'currrate'} = $currrate; $QuoteRef->{$ticker}{'change'} = $change; $QuoteRef->{$ticker}{'changepercent'} = $changepercent; $QuoteRef->{$ticker}{'volume'} = $volume; } } }