#!/usr/bin/perl -wT use strict; # # Copyright 1998-2002 Eric Hammond # BEGIN { # Set envariables for -T tainting. $ENV{'PATH'} = ''; $ENV{'ENV'} = ''; } BEGIN { # Extract path and program name. use vars qw($path $prog); $0 =~ m%(.*)[/\\]([^/\\]*)%; ($path, $prog) = ($1 || '.', $2 || $0); } use Getopt::Long; use sigtrap qw(die normal-signals); use sigtrap 'handler', sub { exit -1 }, 'PIPE'; use IO::File; # Version is extracted from CVS/RCS revision. my $REVISION = '$Revision: 1.4 $'; use vars qw($VERSION); ($VERSION = $REVISION) =~ s%^\$.evision: (.*?) \$$%$1%; # Ticker lookup URL (%s for stock ticker). my $TICKER_URL_FORMAT = "http://quote.yahoo.com/d/quotes.csv?s=%s&f=l1"; use vars qw($debug); $debug = 0; my $help = 0; use vars qw($quiet); $quiet = 0; my $version = 0; my $noaction = 0; my $as_of_date = this_month(); my $price = 10.00; my $nofuture = 0; Getopt::Long::config('no_ignore_case'); GetOptions( 'debug' => \$debug, 'help' => \$help, 'quiet' => \$quiet, 'version' => \$version, 'noaction' => \$noaction, 'as-of=s' => \$as_of_date, 'm=s' => \$as_of_date, 'price=s' => \$price, 'p=s' => \$price, 'nofuture' => \$nofuture, 'f' => \$nofuture, ) or die_usage(); STDOUT->autoflush(1); STDERR->autoflush(1); $quiet or warn "$prog v$VERSION\n"; die_usage() if $help; exit 0 if $version; my @grants = (); # day of month => array of grants which vest on that day. my %grant_days = (0 => []); # Price is a ticker symbol if it has a letter in it. my $ticker = ''; if ( $price =~ m%[a-z]%i ) { $ticker = $price; $price = lookup_ticker($ticker) } print "as of date: $as_of_date\n"; print "stock ticker: $ticker\n" if $ticker; printf "stock price: \$%.2f\n", $price; my $arg; while ( $arg = shift ) { # args: 1000:0.75:1996/06:48:12:200 ... die_usage() unless $arg =~ m#^(\d+):([\d.]+):(\d\d\d\d/\d\d(?:/(\d\d))?):(\d+)(?:/(\d+))?(?::(\d+))?(?::(\d+))?$#; my ($count, $strike, $start, $day, $months, $increment, $cliff, $exercised) = ($1, $2, $3, $4, $5, $6, $7, $8); $cliff ||= 0; $exercised ||= 0; $day ||= 0; $increment ||= 1; my $underwater = ($price < $strike); printf "%d options at \$%.2f, vest %s for %d mo", $count, $strike, $start, $months, ; printf ", %d mo cliff", $cliff if $cliff; printf ", every %d mo", $increment if $increment > 1; printf ", %d exercised", $exercised if $exercised; printf " [UNDERWATER]" if $underwater; print "\n"; next if $underwater; my $grant = Grant->new( 'count' => $count, 'strike' => $strike, 'start' => $start, 'months' => $months, 'increment' => $increment, 'cliff' => $cliff, 'exercised' => $exercised, 'day' => $day, ); push @grants, $grant; $grant_days{$day} = [] if not defined $grant_days{$day}; push @{$grant_days{$day}}, $grant; } print "\n"; my $total_count = 0; my $total_cost = 0; my $total_exercised = 0; my $total_count_vested = 0; my $total_count_unvested = 0; my $total_cost_exercised = 0; my $total_cost_vested = 0; my $total_cost_unvested = 0; my $total_value = 0; my $total_value_exercised = 0; my $total_value_vested = 0; my $total_value_unvested = 0; # The month where everything is vested. my $total_last_vest_month_scalar = 0; #--- Show summary to the current month of interest. print " Remaining\n"; print " Total Exercised Vested Unvested\n"; print " -------- --------- --------- --------\n"; foreach my $grant ( @grants ) { my $count_vested = $grant->count_vested($as_of_date); my $count_unvested = $grant->count_unvested($as_of_date); my $strike = $grant->strike(); my $start = $grant->start(); my $cost = $grant->cost(); my $cost_vested = $grant->cost_vested($as_of_date); my $cost_unvested = $grant->cost_unvested($as_of_date); $total_count += $grant->count(); $total_cost += $grant->cost(); $total_exercised += $grant->exercised(); $total_count_vested += $grant->count_vested($as_of_date); $total_count_unvested += $grant->count_unvested($as_of_date); $total_cost_exercised += $grant->cost_exercised(); $total_cost_vested += $grant->cost_vested($as_of_date); $total_cost_unvested += $grant->cost_unvested($as_of_date); $total_value += $grant->value($price); $total_value_exercised+= $grant->value_exercised($price); $total_value_vested += $grant->value_vested($as_of_date, $price); $total_value_unvested += $grant->value_unvested($as_of_date, $price); # The month where everything is vested. my $last_vest_month_scalar = month_scalar($grant->start()) + $grant->months(); $total_last_vest_month_scalar = $last_vest_month_scalar if $last_vest_month_scalar > $total_last_vest_month_scalar; printf " %7s/%2s x %2s - %7s = %7s %7s\n", commify($grant->count()), $grant->months(), $grant->months_vested($as_of_date), commify($grant->exercised()), commify($grant->count_vested($as_of_date)), commify($grant->count_unvested($as_of_date)), ; } print " -------- --------- --------- --------\n"; printf " %7s %7s %7s %7s options\n", commify($total_count), commify($total_exercised), commify($total_count_vested), commify($total_count_unvested), ; print "\n"; printf "%14s %14s %14s %14s value (%s)\n", cents($total_value), cents($total_value_exercised), cents($total_value_vested), cents($total_value_unvested), cents($price), ; printf "%14s %14s %14s %14s cost\n", '-'.cents($total_cost, 1), '-'.cents($total_cost_exercised, 1), '-'.cents($total_cost_vested, 1), '-'.cents($total_cost_unvested, 1), ; print "-------------- -------------- -------------- --------------\n"; printf "%14s %14s %14s %14s earnings\n", cents($total_value - $total_cost), cents($total_value_exercised - $total_cost_exercised), cents($total_value_vested - $total_cost_vested), cents($total_value_unvested - $total_cost_unvested), ; #---- Show the earnings each month until we vest completely. exit 0 if $nofuture; print "\n"; print " options value cumulative\n"; printf "date vesting %9s cost earnings earnings\n", '('.cents($price).')'; print "---------- ------- ---------- -------- ---------- -----------\n"; my $month_scalar_start = month_scalar($as_of_date); my $as_of_day = day_of($as_of_date); # If we didn't define a day for the "as of" date, then we just show whole # months, otherwise we list each date that grants vest. my @days_to_show = sort keys %grant_days; if ( $days_to_show[0] == 0 ) { push @days_to_show, unshift @days_to_show; } if ( not defined $as_of_day ) { @days_to_show = (0); # day of month "0" means whole month. %grant_days = (0 => \@grants); ++ $month_scalar_start; } # Reset for cumulative earnings. $total_value_vested = 0; $total_cost_vested = 0; # For each month in the future... for ( my $month_scalar = $month_scalar_start; $month_scalar <= $total_last_vest_month_scalar; ++$month_scalar ) { my $month = scalar_to_month($month_scalar); foreach my $day ( @days_to_show ) { my $date = $month; $date .= sprintf("/%02d", $day) if $day > 0; my $vested_on_date = 0; my $cost = 0; foreach my $grant ( @{$grant_days{$day}} ) { # Skip if we already counted this grant in the top summary. next if $day > 0 and defined $as_of_day and $month_scalar == $month_scalar_start and $day <= $as_of_day; $vested_on_date += $grant->vested_on_month($month); $cost += $grant->vested_on_month($month) * $grant->strike(); } next if $vested_on_date == 0; my $value = $vested_on_date * $price; $total_value_vested += $value; $total_cost_vested += $cost; printf "%-10s %6d %10s - %8s = %10s %11s\n", $date, $vested_on_date, dollars($value), dollars($cost), dollars($value - $cost), dollars($total_value_vested - $total_cost_vested), ; } } sub dollars { my ($amount) = @_; return '$'.commify(int($amount + 0.5)); } sub cents { my ($amount, $omit_sign) = @_; return ($omit_sign ? '' : '$') . commify(sprintf('%.2f', $amount)); } # From the Perl Cookbook sub commify { my $text = reverse $_[0]; $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; return scalar reverse $text; } exit 0; #---- Functions sub this_month { my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time); my $this_month = sprintf("%04d/%02d", $year + 1900, $mon + 1); $this_month; } sub this_day { my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time); $mday; } sub month_scalar { my ($yyyymm) = @_; use Carp; if ( not defined $yyyymm ) { confess(); } my ($yyyy, $mm) = ($1, $2) if $yyyymm =~ m%^(\d\d\d\d)/(\d\d)(?:/\d\d)?$%; die "$main::prog: invalid date $yyyymm" unless defined $mm; $yyyy * 12 + $mm - 1; } sub scalar_to_month { my ($month_scalar) = @_; my $yyyy = int($month_scalar / 12); my $mm = $month_scalar % 12 + 1; my $yyyymm = sprintf "%04d/%02d", $yyyy, $mm; $yyyymm; } sub lookup_ticker { my ($ticker) = @_; use LWP::Simple; my $price = get( sprintf($TICKER_URL_FORMAT, $ticker) ); return '0.00' unless defined $price; chomp $price; $price || '0.00'; } sub is_past { my ($day1, $day2) = @_; return 1 if not defined $day1; return 0 if not defined $day2; return $day1 >= $day2; } sub day_of { my ($yyyymmdd) = @_; my $day = $1 if $yyyymmdd =~ m%^\d\d\d\d/\d\d/(\d\d)$%; $day; } #---- package Grant { package Grant; use Carp; # constructor. sub new { my $class = shift; my $self = bless {@_}, $class; $self; } sub count { my $self = shift; $self->{'count'}; } sub strike { my $self = shift; $self->{'strike'}; } sub start { my $self = shift; $self->{'start'}; } sub day { my $self = shift; $self->{'day'}; } sub months { my $self = shift; $self->{'months'}; } sub increment { my $self = shift; $self->{'increment'}; } sub cliff { my $self = shift; $self->{'cliff'}; } sub exercised { my $self = shift; $self->{'exercised'}; } # number of months worth of options which are vested. sub months_vested { my $self = shift; my ($as_of_date) = @_; my $months = $self->months(); my $cliff = $self->cliff(); my $elapsed = main::month_scalar($as_of_date) - main::month_scalar($self->start()); --$elapsed if $elapsed > 0 and not main::is_past(main::day_of($as_of_date), $self->day()); my $increment = $self->increment(); $elapsed = $increment * int($elapsed / $increment); $months = $elapsed if $elapsed < $months; $months = 0 if $elapsed < $cliff; $months; } # number of options vested as of a given month. sub count_vested { my $self = shift; my ($as_of_date) = @_; my $count = $self->count(); my $months = $self->months(), my $months_vested = $self->months_vested($as_of_date); my $exercised = $self->exercised(); # Account for zero month (immediate) vesting. my $count_vested = $months == 0 ? $count : int($count / $months * $months_vested) if $months; $count_vested -= $exercised; $count_vested = 0 if $count_vested < 0; $count_vested; } # Number of options remaining unvested as of a given month. sub count_unvested { my $self = shift; my ($as_of_date) = @_; my $count = $self->count(); my $count_vested = $self->count_vested($as_of_date); my $exercised = $self->exercised(); my $count_unvested = $count - $count_vested - $exercised; $count_unvested; } # Cost of exercising options. sub cost { my $self = shift; $self->strike() * $self->count(); } sub cost_exercised { my $self = shift; $self->strike() * $self->exercised(); } sub cost_vested { my $self = shift; my ($as_of_date) = @_; $self->strike() * $self->count_vested($as_of_date); } sub cost_unvested { my $self = shift; my ($as_of_date) = @_; $self->strike() * $self->count_unvested($as_of_date); } # Value of stock at a price. sub value { my $self = shift; my ($price) = @_; $self->count() * $price; } sub value_exercised { my $self = shift; my ($price) = @_; $self->exercised() * $price; } sub value_vested { my $self = shift; my ($as_of_date, $price) = @_; $self->count_vested($as_of_date) * $price; } sub value_unvested { my $self = shift; my ($as_of_date, $price) = @_; $self->count_unvested($as_of_date) * $price; } # number of options which will vest on a given month. sub vested_on_month { my $self = shift; my ($month) = @_; my $last_month = main::scalar_to_month(main::month_scalar($month) - 1); $self->count_vested($month) - $self->count_vested($last_month); } } # package Grant #---- Functions # # die_usage - Print usage string from manpage at end of file and die # sub die_usage { my $usage; open(PROG, "< $0") or die "$prog: Unable to open $0 to print usage"; local($/) = undef; $usage = ; close(PROG); $usage =~ s%^.*? =head1\sSYNOPSIS\s+ (.*?)\s+ =head1\sOPTIONS\s*\n (.*?)\s* =head1.*$ %Usage: $1\n$2\n%xs; die $usage; } =head1 NAME option-vest - Stock option vesting calculator =head1 SYNOPSIS option-vest [opts] options:strike:vest-start:vest-months[:cliff[:exer]]... =head1 OPTIONS -d --debug Debug mode. -h --help Print help and exit. -q --quiet Quiet mode. -v --version Print version and exit. -p --price N Stock price (or ticker symbol). -m --as-of YYYY/MM Month of interest (defaults to current month). =head1 ARGUMENTS options Number of options strike Strike price vest-start Vesting start month in format YYYY/MM (e.g., 1998/05) vest-months Number of months to complete vesting. cliff Number of months 'till cliff. exer Number of shares exercises =head1 DOWNLOAD The option-vest Perl script is available here: http://www.anvilon.com/software/download/option-vest A nice web interface is available here: http://www.anvilon.com/option-vest/ =head1 DESCRIPTION The option-vest program accepts information about one or more stock option grants and calculates how many options are vested and unvested. It also shows the value of the options at a given stock price and calculates the earnings after the cost of the options (the strike price) is taken into consideration. =head1 EXAMPLES Here is a sample command and its output. Note that although the montly options earned states "6", it is actually a fraction more which is reflected in the value, cost, and earnings columns. See CAVEATS below for comments about rounding and display. $ option-vest -as-of 1998/07 -price 12.00 \ 1000:3.00:1996/06:48:12 \ 500:9.75:1998/05:12 as of date: 1998/07 stock price: $12.00 1000 options at $3.00, vest 1996/06 for 48 mo, 12 mo cliff 500 options at $9.75, vest 1998/05 for 12 mo Remaining Total Exercised Vested Unvested -------- --------- --------- -------- 1,000/48 x 25 - 0 = 520 480 500/12 x 2 - 0 = 83 417 -------- --------- --------- -------- 1,500 0 603 897 options $18,000.00 $0.00 $7,236.00 $10,764.00 value ($12.00) -7,875.00 -0.00 -2,369.25 -5,505.75 cost -------------- -------------- -------------- -------------- $10,125.00 $0.00 $4,866.75 $5,258.25 earnings options value cumulative date vesting ($12.00) cost earnings earnings ---------- ------- ---------- -------- ---------- ----------- 1998/08 63 $756 - $473 = $284 $284 1998/09 62 $744 - $463 = $281 $565 1998/10 63 $756 - $473 = $284 $848 1998/11 63 $756 - $473 = $284 $1,132 1998/12 62 $744 - $463 = $281 $1,413 1999/01 62 $744 - $470 = $275 $1,688 1999/02 63 $756 - $473 = $284 $1,971 1999/03 62 $744 - $463 = $281 $2,252 1999/04 63 $756 - $473 = $284 $2,536 1999/05 63 $756 - $473 = $284 $2,819 1999/06 21 $252 - $63 = $189 $3,008 1999/07 20 $240 - $60 = $180 $3,188 1999/08 21 $252 - $63 = $189 $3,377 1999/09 21 $252 - $63 = $189 $3,566 1999/10 21 $252 - $63 = $189 $3,755 1999/11 21 $252 - $63 = $189 $3,944 1999/12 21 $252 - $63 = $189 $4,133 2000/01 20 $240 - $60 = $180 $4,313 2000/02 21 $252 - $63 = $189 $4,502 2000/03 21 $252 - $63 = $189 $4,691 2000/04 21 $252 - $63 = $189 $4,880 2000/05 21 $252 - $63 = $189 $5,069 2000/06 21 $252 - $63 = $189 $5,258 =head1 CAVEATS This program may have bugs or errors in it. The user should verify all calculations by hand or using other software before making decisions. There are quite a few rounding operations which take place, which may affect the accuracy of final numbers. Some of the displayed numbers may not show all significant digits, especially in the case of monthly value, cost, and earnings. Trading costs, and tax considerations are not taken into account. For more information, please talk to a qualified accountant, lawyer, broker, and psychiatrist. Some of your option grants may vest at different times during the month. This program groups all of these together, so you may have to wait until the middle or end of a month to be able to excercise them. =head1 AUTHOR Eric Hammond $Id: option-vest,v 1.4 2004/04/07 02:26:13 erich Exp $ =cut