#!/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 = <<EOF;
# Comments are lines beginning with a '#', like this one.
# Each line must contain information about one stock. The fields on one
# line must be separated by a delimiter.
Company A	ABC	12.06	49.11		2002/03/08
Lossmaker Inc.	XY	3.19	13.90	44.341	1981/12/16

# Blank lines are ignored.
# Field #0, required, must contain the name of the company.
# Field #1, required, must contain the ticker symbol of the stock.
# Field #2, required, must contain the number of shares you own.
# Field #3, required, must contain the rate at which you bought them.
# Field #4, optional, must contain the product of fields 2 and 3.
# Field #5, optional, must contain the purchase date in yyyy/mm/dd format.
# Field #6, computed, must contain the current rate.
# Field #7, computed, must contain the product of fields 2 and 6.
# Field #8, computed, must contain the % change between fields 7 and 4.
EOF
$opt_delim = "\t";

sub Usage
{
    print STDERR <<EOF;
Usage: $MyName [-help] [-debug] [-about] [-v[erbose]]
	[-delim <str>] [-sample] [-summary] <file1> <file2> ...
EOF

	if ($#_ > -1)
	{
		print STDERR <<EOF;

$MyName calculates current values of stock portfolios. Options are:
    -help                   : Print this help screen.
    -about                  : Print information about the author.
    -v[erbose]              : Turn verbose mode on.
    -debug                  : Turn debug mode on. Warning! Copious output.
    -delim <str>            : Specify delimiter for fields in file.
                              Default: \"$opt_delim\".
    -sample                 : Generate a sample portfolio.
    -summary                : Generate a summary portfolio.
    <file>                  : Find portfolio in <file>. 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 <file>.<date> and, if -summary is specified,
in <file>.<date>.summary as well.

EOF
	}
    exit 0;
}

sub About
{
    print STDERR <<EOF;

$MyName calculates the worth of a stock portfolio.

$MyName is authored by Anand Natrajan.
URL: www.anandnatrajan.com.

$MyName is freeware. Please use it as you wish.
Naturally, the author is not liable if it breaks.
Send suggestions, comments and criticism to author.

EOF
	exit 0;
}

# Print only if verbose mode is on.
sub Vprint
{
	foreach my $line (@_) { print STDERR $line if ($opt_v); }
}

# Print only if debug mode is on.
sub Dprint
{
	foreach my $line (@_) { print STDERR $line if ($opt_debug); }
}

# Check if passed argument is defined and non-blank.
sub ispresent
{
	my $var = shift;
	return (defined($var) and $var ne "");
}

# Remove whitespace before and after passed argument.
sub prune
{
	my $str = shift;
	return $str if (!&ispresent($str));
	chomp($str);
	$str =~ s/^\s+//;
	$str =~ s/\s+$//;
	return $str;
}

### MAIN ###

&GetOptions("help" => \$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 = <FOLIO>;
	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 = <FOLIO>;
	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:
		# <td width="60" valign="top"><a href="http://quotes.nasdaq.com/asp/summaryquote.asp?symbol=XXXXX">XXXXX</a></td>
		# <td width="65" valign="top" nowrap>$&nbsp;50</td>
		# <td align="right" class="green">0.23</td>
		# <td valign="bottom"><img src="/images/greenArrowSmall.gif" align="absmiddle" border="0" hspace="2" height="11" width="11" alt=""></td>
		# <td class="green">0.46%</td>
		# <td width="95" align="right">34,669,800</td>
		# 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</)
				{ shift(@info); }
			# If we couldn't find this line, flag an error.
			if ($#info == -1)
			{
				warn "Could not find quote for ticker $ticker.\n"
					. "The presentation of the NASDAQ page may have changed\n"
					. "\tor the ticker $ticker is not listed on NASDAQ.\n";
				next;
			}
			shift(@info);

			# Parse the next few lines carefully to get exactly what we want.
			while ($#info >= 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;([^<]*)</;
			my $currrate = $1;
			shift(@info);

			# Parse the following fields out for later use.
			my $change = "";
			my $changepercent = "";
			my $volume = "";
			while ($#info >= 0 and $info[0] =~ /^\s*$/) { shift(@info); }
			if ($#info > -1)
			{
				if ($info[0] =~ /.*class="([^>]*)">([^<]*)</)
				{
					$change = $2 if ($1 eq "green");
					$change = "-" . $2 if ($1 eq "red");
					shift(@info);
					shift(@info);
					$info[0] =~ m/.*class="([^>]*)">([^<]*)</;
					$changepercent = $2 if ($1 eq "green");
					$changepercent = "-" . $2 if ($1 eq "red");
					shift(@info);
				}
				elsif ($info[0] =~ />unch</)
				{
					$change = 0;
					$changepercent = "0%";
					shift(@info);
				}
			}
			while ($#info >= 0 and $info[0] =~ /^\s*$/) { shift(@info); }
			if ($#info > -1)
			{
				$info[0] =~ m/.*right"[^>]*>([^<]*)</;
				$volume = $1;
				if ($#info > -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;
		}
	}
}
