From: mike@netwise.com (Mike Ernst)
Subject: Program for calculating returns
Reply-To: mike@netwise.com ()
Organization: Netwise, Inc.  Boulder, CO
Distribution: usa
Date: Thu, 16 Apr 1992 19:00:52 GMT

    I got a number of requests for this program, so here it is.  This
    program was written and posted to the net in 1989 by Dennis Bednar.
    It can be used to compute the returns on securities given price
    and dividend information.  There is some documentation in the README
    file, and some comments in the source files.  I'm willing to provide
    a minimal amount of support; however I did not write the program.
    
    The rest of this posting is in the form a shell archive, so save the
    file to a directory where you want to build the program, strip off
    the header to the CUT line, then type 'sh <save-file-name>'.  It will
    create four files (returns.c, rate.c, Makefile and README) and then
    run make to build the program.

-+-+-+-+-+-+-+-+-+-+-+-+-+-+- CUT HERE -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

echo "Creating returns.c"
cat << END > returns.c
/*
 * returns.c
 * dennis bednar 08 15 88
 *
 * Program to analyze performance (total return) of mutual funds.
 * Total return includes dividends as well as price appreciation.
 *
 * This program prints total return by using the methods originally
 * written up in nav.c.
 * This program prints % per year return using the methods originally
 * written in the roi.c program.
 *
 * returns nav change
 * returns [-sstartdate] [-eenddate] -f files ... 
 * prints percent change.
 * where files contain
 * start_date start_nav
 * ...
 * end_date end_nav
 * comments begin with # in line.
 * X dividend lines are of the form
 * X	dividend	price_at_which_redisributed.
 *
 * For example,
 * X 2	8
 * means that a $2/sh was redistributed at $8/share.
 * You should put the X lines BEFORE the nav for the same date!
 * Input file contains dates in cronological order (err msg if not).
 * 
 * BUGS:
 *
 *	An ex-dividend should preceed a date, even if it is for the
 *	same date.
 *
 *	You should not put multiple X lines for several distributions
 *	on the same day if they were all reinvested at the same NAV/share,
 *	rather you should sum up the distributions and put only one X
 *	line for that date.  Example: 2.00/sh gain and .25/share dividend
 *	reinvested at 12.00/share would be declared as:
 *	X 2.25 12.00
 *	rather than two separate lines of:
 *	X 2.00 12.00
 *	X  .25 12.00
 */
#include <stdio.h>
#include <math.h>
#include <malloc.h>

#define FLOAT double
#define STR_SAME !strcmp
#define UNDEF (-1.0)

extern	FLOAT	atof();

/* in rate.c */
extern	long	strtoday();
extern	FLOAT	get_rate();
extern	char	*cmd;	/* for error() */


char	*ST_date, *END_date;

FLOAT
getarg( msg )
	char	*msg;
{
	FLOAT	answer;

	printf( "%s ? ", msg );
	fflush( stdout );
	scanf( "%lf", &answer );
	return answer;
}

main( argc, argv )
	int	argc;
	char	*argv[];
{
	FLOAT	nav, change;
	int	i;
	cmd = argv[0];

	for (i = 1; i < argc; ++i)
		{
		if (strncmp( argv[i], "-s", 2) == 0)
			ST_date = argv[i]+2;
		else if (strncmp( argv[i], "-e", 2) == 0)
			END_date = argv[i]+2;
		else if (strncmp( argv[i], "-f", 2) == 0)
			{
			for(++i; i < argc; ++i)
				data_file( argv[i] );
			exit(0);
			}
		else if (argv[i][0] == '-')
			usage();
		else
			break;
		}

	argc -= (i-1);
	argv += (i-1);


	if (argc == 3)
		{
		nav = atof( argv[1] );
		change = atof ( argv[2] );
		pr_compute( nav, change );
		}
	else if (argc == 1)
		{
		while( 1 )
			{
			nav = getarg( "Current NAV" );
			change = getarg( "Change In NAV" );
			pr_compute( nav, change );
			}
		}
	else
		usage();

}



FLOAT
compute( nav, change )
	FLOAT	nav, change;
{
	FLOAT	oldnav, percent;
	oldnav = nav - change;

	if ((nav != UNDEF) && (oldnav != UNDEF) && (oldnav != 0.0))
		percent = (change/oldnav) * 100.0;
	else
		percent = 0.0;	/* not really correct, prevent divide by zero */
	return percent;
}


pr_compute( nav, change )
	FLOAT	nav, change;
{
	FLOAT	percent;
	percent = compute( nav, change );
	printf("%.2f%%\n", percent);
}

usage()
{
	fprintf(stderr, "Usage: returns [nav change]\n");
	fprintf(stderr, "Usage: returns [-sstart_date] [-eenddate] -f files ...\n"); 
	exit(1);
}


char *
str_save( s )
	char	*s;
{
	char	*p;

	p = malloc( strlen(s)  + 1 );
	if (!p)
		return (char *)0;
	strcpy( p, s );
	return p;
}

/*
 *	   EndNAV     (     distshare )
 * Gain = --------  * ( 1 + --------- )
 *	  StartNAV    (     distprice )
 *
 * This is what should be the case regarding date processing:
 *
 * Process only dates in the requested range of dates (does not
 * have to be inclusive for this to occur).
 * Conditions:
 *	Start	End	Requires
 *	Y	Y	Start <= date <= End
 *	Y	N	Start <= date
 *	N	Y	date <= End
 *	N	N	always accept date
 *
 * Termination condition (finding last date):
 *	End given, stop when End <= date (TOTHINK: should we look at rest of
 * 	file for cronological ordering, or what?).
 */
data_file( fname )
	char	*fname;
{
	int	lineno;
	char	*start, *end, *date;
	FLOAT	begin_nav, end_nav, nav, change, percent;
	FILE	*ifp;
	char	linebuf[ 200 ];
	char	date_buf[ 20 ];	/* holds date from input line */
	char	*cp;
	FLOAT	mult;		/* multipler for dividends */
	FLOAT	distshare, distprice, truepercent;
	static	int	firsttime = 1;
	long	longstart, longend, days;	/* start day, end day, differenc */
	long	thisdate, pastdate;	/* checks dates for in order */
	FLOAT	ann_percent;
	char	ann_buffer[100];
	
	mult = 1.0;
	pastdate = 0;
	longend = longstart = 0;

	if (firsttime) {
		/* 9 in last two because %% data makes 8 column 9 */
		printf("%-14.14s %10s %10s %8s %8s %7s %7s %9s\n",
		"Investment", "BeginDat", "EndDate", "BegNAV",
		"EndNAV", "%Change", "%Return", "%PerYr");
		firsttime = 0;
		}

	ifp = fopen(fname, "r");
	if (!ifp)
		error( "Cannot open filename %s", fname );

	/* don't have start or end dates yet */
	start = end = NULL;
	begin_nav = end_nav = UNDEF;

	lineno = 0;
	while(fgets(linebuf, sizeof(linebuf), ifp ))
		{
		++lineno;
		cp = linebuf;
		if ((*cp == ' ') || (*cp == '\t') ||
		    (*cp == '\n') || (*cp == '#'))
			continue;
		if (*cp == 'X')
			{
			/* skip dividends before start date */
			/* also this means that the first line of the
			 * file cannot be an "X" line.
			 */
			if (!start)	/* haven't reached start date yet */
				continue;
			if(sscanf( linebuf, "X %lf %lf\n", &distshare, &distprice) != 2)
				error("Line %d, %s: expected X distshare distprice", lineno, fname);
			mult *= (1.0 + (distshare/distprice) );
#if 0
			printf("distshare=%.2f, distprice=%.2f\n", distshare, distprice);
			printf("multiplier now %.2f\n", mult);
#endif
			continue;
			}
		if(sscanf( linebuf, "%s %lf", date_buf, &nav ) != 2)
			error("Line %d, %s: expected date nav", lineno, fname );
		if (nav == 0.0)
			continue;	/* skip dates with nav no money */
		thisdate = strtoday( date_buf );
		if (thisdate <= pastdate)
			error("Line %d, %s: date '%s' out of order", lineno,
			fname, date_buf);
		else
			pastdate = thisdate;	/* remember latest day */
		if (!start)
			{
			if ((ST_date && STR_SAME(ST_date, date_buf)) ||
/* allow 1/1/88 to match 1/1/1988 */
			    (ST_date && strtoday(ST_date) ==
					strtoday(date_buf)) ||
/* also, allow start date in file after -s date to match */
/* this code is getting UGLY, fix up later ! */
/* test one.  this date is between ST_date and END_date */
			    (ST_date && END_date &&
				(strtoday(ST_date) <= strtoday(date_buf)) &&
				(strtoday(date_buf) <= strtoday(END_date))
			    ) || 
/* test two.  this date is after ST_date, but no END_date given */
			    (ST_date && !END_date &&
				(strtoday(ST_date) <= strtoday(date_buf))
			    ) || 
			    (!ST_date))
				{
				/* first date or matches wanted start date */
				start = str_save(date_buf);
				longstart = strtoday( start );
				begin_nav = nav;
				/* if don't find end date in file, and this
				 * is the only date in range, use data
				 * for the end date.
				 */
				longend = longstart;
				end_nav = nav;
				}
			else
				;
			}
		if (1)	/* was else, end date could be start date */
			{
/* 11/7/88: add 2nd check if -s only and all dates are after start date, ie
 * don't use enddate of last date in file because BEFORE start date */
			if ((!END_date && start) ||
/* allow 1/1/88 to match 1/1/1988 */
/* if -s1/4/88 -e2/5/88 but only date of 2/1/88 in file, 2/1/88 is end date */
			    (END_date && strtoday(date_buf) <=
					 strtoday(END_date)) ||
			    (END_date && STR_SAME(END_date, date_buf))
			   )
				{
				/* either last one date found so far, or
				 * matches end date wanted */
				end = str_save(date_buf);
				longend = strtoday( end );
				end_nav = nav;
/* don't break out of loop if date is before END_date */
			    	if (END_date && strtoday(END_date) ==
					 strtoday(date_buf))
					/* user specified end date */
					break;	/* don't read dividends past end date */
				}
			else
				;
			}
		}
	fclose( ifp );
/*
	printf("debug start=%s end=%s bdate = %.2f edate = %.2f\n",
		start, end, begin_nav, end_nav);
*/

	change = end_nav - begin_nav;
	percent = compute( end_nav, change );

	truepercent = (end_nav / begin_nav) * mult;
	truepercent -= 1.0;
	truepercent *= 100.0;

	days = longend - longstart;
	if (days < 0)
		error("Line %d, %s: days cannot be negative", lineno, fname);
	ann_percent = get_rate( begin_nav, end_nav * mult, days );
	if (ann_percent > 1000000 )	/* bigger than 1 million percent a yr */
		sprintf( ann_buffer, "%8s", "HUGE" );
	else
		sprintf( ann_buffer, "%8.2f%%", ann_percent );


	printf("%-14.14s %10s %10s %8.2f %8.2f %6.2f%% %6.2f%% %s\n", fname, start, end, begin_nav, end_nav, percent, truepercent, ann_buffer );
}
END

echo "Creating rate.c"
cat << END > rate.c
/*
 * rate.c
 * Compute annual rate of return for old value to grow to new value.
 * code borrowed from roi.c
 * Entry points:
 *	strtoday	convert MM/DD/YY string to number that represents days
 *	get_rate	get annual rate of return in percent
 */
#include <stdio.h>
#include <signal.h>
#include <malloc.h>
#include <setjmp.h>
#include <math.h>	/* maximum HUGE defined there */

#define FLOAT	double
#define UNDEF	(-1.0)
#define STATIC
#define CATCHFPE

#ifdef CATCHFPE
/* fw refs to non-int functions */
int	fpe_error();
#endif

/* external functions */
extern	FLOAT	atof();
extern	long	atol();
extern	FLOAT	pow();	/* double = pow( double base, double exponent) */

/* globals */
#ifdef CATCHFPE
jmp_buf fpe_jump;
#endif
char	*cmd;
static unsigned maxd[] = {
    0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
    };

STATIC
double dexp(d, exp)
    double d;
    register long exp;
    {
    static double result;

    result = 1.0;	    /* init multiplicative acc */

    if (exp < 0)	    /* exponent negative? */
	{
	d = 1/d;
	exp = -exp;
	}

    while (exp != 0) {
	if (exp & 1) result *= d;   /* fold this factor */
	exp >>= 1;	    /* shift exponent */
	d *= d;     /* square running multiplier */
	}

    return result;
    }

/* this alorithm from the original roi.c posted by Alan Wendt Aug 87 */
/*  convert "MM/DD/YY" or "MMDDYY" into # of days using Jan 1, 1601 = day 1
 *  ref: CACM 11, 657 (1968) - valid back to 24 Nov -4713
 */
long strtoday(dd)
    char *dd;
    {
    long		strtol();
    char		*ddend;
    int			c = 0, slash = 0;
    long		jdn;
	long	n[3];
#define month (n[0])
#define day (n[1])
#define year (n[2])
    static int		m;

    jdn = strtol(dd, &ddend, 10);		/* convert mmddyy */
    month = jdn;

    if (*ddend == '/') {
	slash = 1;				/* got mm/dd/yy   */
	goto middle;
	}

    else if (*ddend) return -1;

    for (; c <= 2; c++) {
	if (slash) {
	    n[c] = strtol(dd, &ddend, 10);	/* convert a number */
	    if (*ddend != "//"[c])		/* check proper terminator */
baddy:		return -1;
middle:	    dd = ddend + 1;
	    }
	else {
	    n[2 - c] = jdn % 100;
	    jdn /= 100;
	    }
	}

    if (year <= 99) year += 1900;	/* correct for c */
    maxd[2] = 28;
    if ((!(year & 3) && (year % 100)) || !(year % 400)) 
	maxd[2] = 29;
    if (month < 1 || month > 12 || day < 1 || day > maxd[month]) goto baddy;

    m = (month <= 2) ? -1 : 0;
    year += m;
    jdn = (long)day - 2337888L;
    jdn += 1461L * ((long)year + 4800L) / 4L;
    jdn += 367L * (month - 2L - (long)m * 12L) / 12L;
    jdn -= 3L * (((long)year + 4900L) / 100L) / 4L;

    return (double)jdn;
    }

FLOAT
get_rate( oldvalue, newvalue, days)
	FLOAT	oldvalue,
		newvalue;
	long	days;
{
	FLOAT	limit[2];
	FLOAT	rate;
	FLOAT	testtot;
	int	which;
	int	count;
	FLOAT	total;

#ifdef DEBUG
	printf("get_rate(oldvalue=%f, newvalue=%f, days=%ld) called\n",
	oldvalue, newvalue, days );
#endif
	/* if no time length, 0% rate of return */
	/* also prevents divide by zero errors */
	if (days == 0L)
		return 0.0;

	/* one of the values is unknown */
	if ((oldvalue == UNDEF) || (newvalue == UNDEF))
		/* so rate of return is unknown */
		return UNDEF;

	total = newvalue;	/* total we want to have */

	/* Calculate daily rate of return by binary search.
  	 * Don't use a big number like 1.5 (50% per day) because
	 * raising the base to a huge number of days might cause
	 * an overflow.  So make an educated guess what the
	 * daily rate is. We can do this, which is different
	 * than roi.c because there is only one PV and one FV.
	 */
	limit[0] = limit[1] = pow( (newvalue/oldvalue), (1/(FLOAT)days));
	limit[0] -= .01;	/* decrease lower guess */
	limit[1] += .01;	/* increase upper guess */
#ifdef DEBUG
printf("low = %f, high limit=%f\n", limit[0], limit[1]);
#endif

	count = 0;		/* times thru binary search loop */
#ifdef CATCHFPE
	signal(SIGFPE, fpe_error);
#endif
	for (;;) {
		/* compute midpoint rate */
		rate = (limit[1] + limit[0]) / 2;

#ifdef DEBUG
printf("\nmidrate = %.12f, low = %.12f, high = %.12f\n", rate, limit[0], limit[1]);
		{
		double junkrate;
#ifdef CATCHFPE
		if (setjmp(fpe_jump) == 0)
			{
			junkrate = 100.0 * (dexp(rate, 365L) - 1.0);
			printf("Trying Annual Rate = %.6f\n", junkrate);
			}
		else
			{
			printf("Caught FPE trying to print Annual Rate\n");
			}
#else
		junkrate = 100.0 * (dexp(rate, 365L) - 1.0);
		printf("Trying Annual Rate = %.6f\n", junkrate);
#endif /* CATCHFPE */
		}
#endif /* DEBUG */

#ifdef CATCHFPE
		if (setjmp(fpe_jump) == 0)
			;
		else
			{
			/* dexp() failed */
			/* the midpoint rate was too big, so make the high the midpoint */
			/* BAD: it might be too low! */
#ifdef DEBUG
 			printf("Overflow caught, with daily mid rate = %f\n", rate);
			printf("Subtracting .05 of diff of midrate and low\n");
 
#endif
			limit[1] = (rate-limit[0]) * .05;

			continue;
			}
#endif
		/* guess rate for entire days */
		testtot = oldvalue * dexp(rate, days);

		/* if testtot too big, rate too big, so make high(1) the
		 * current mid rate.
		 */
		which = (total < testtot);
#ifdef DEBUG
		printf("Testtot = %.2f: too %s\n", testtot, which ? "big" : "small");
#endif

		if ((which != 0) && (which != 1))
			{
			perror("BAD which");
			exit(1);
			}

		if ((limit[0] >= limit[1]) ||
		    (limit[which] == rate) ||
#define MAXGUESS 100
		    (++count > MAXGUESS)
		   ){
#ifdef CATCHFPE
			/* if the daily interest is 100% a day ($5 to $20
			 * in two days, the rate is 2.0, which if raised
			 * to the 365 power, would overflow.
			 */
			if (setjmp(fpe_jump) == 0)
				;
			else
				return HUGE;	/* error indication */
#endif
#if 0
			/* commented out, old way was not that accurate,
			 * because it assumed 365 days per year.  Not
			 * bad for a few years, but progressively worse
			 * as the number of years increased a lot.
			 */
			rate = 100.0 * (dexp(rate, 365L) - 1.0);
#else
			/* every 4 years is a leap year except certain
			 * centuries divisible by 400.  The 365.245
			 * number was obtained from 200 years starting
			 * 1/1/88, ending 1/1/2188, and dividing the
			 * total number of days by 200. It was chosen
			 * emperically, but the answer is very close
			 * to a pocket calculator.
			 */
			if (days > 36500)	/* more than 100 years */
				rate = 100.0 * (pow(rate, (FLOAT)365.245) - 1.0);
			else
				rate = 100.0 * (pow(rate, (FLOAT)365.25) - 1.0);
#endif
/*
			printf("Annual Rate = %.6f\n", rate);
			exit(0);
*/
#ifdef DEBUG 
			if (count > MAXGUESS)
				fprintf(stderr, "get_rate: too many guesses\n");
#endif
			return rate;
		}

		limit[which] = rate;
	}
}


#ifdef CATCHFPE
/*
 * floating point overflow
 */
STATIC
fpe_error( sig )
	int	sig;
{
	signal( SIGFPE, fpe_error );	/* rearm */
	longjmp( fpe_jump, 1);		/* indicate the overflow */
}
#endif



#define MANYARGS msg, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9
typedef char	*CHARSTAR;

/*
 * print error message and exit
 * Error message does *not* have a newline at the end.
 */
error(MANYARGS)
	CHARSTAR	MANYARGS;
{
	extern	char	*cmd;
	char	buffer[BUFSIZ];
	sprintf(buffer, MANYARGS);
	fprintf(stderr, "%s: error: %s. Exitting.\n", cmd, buffer);
	exit(1);
}

/*	@(#)strtol.c	2.1	*/
/*LINTLIBRARY*/
#include <ctype.h>
#define DIGIT(x)	(isdigit(x) ? (x) - '0' : \
			islower(x) ? (x) + 10 - 'a' : (x) + 10 - 'A')
#define MBASE	('z' - 'a' + 1 + 10)

long
strtol(str, ptr, base)
register char *str;
char **ptr;
register int base;
{
	register long val;
	register int c;
	int xx, neg = 0;

	if (ptr != (char **)0)
		*ptr = str; /* in case no number is formed */
	if (base < 0 || base > MBASE)
		return (0); /* base is invalid -- should be a fatal error */
	if (!isalnum(c = *str)) {
		while (isspace(c))
			c = *++str;
		switch (c) {
		case '-':
			neg++;
		case '+': /* fall-through */
			c = *++str;
		}
	}
	if (base == 0)
		if (c != '0')
			base = 10;
		else if (str[1] == 'x' || str[1] == 'X')
			base = 16;
		else
			base = 8;
	/*
	 * for any base > 10, the digits incrementally following
	 *	9 are assumed to be "abc...z" or "ABC...Z"
	 */
	if (!isalnum(c) || (xx = DIGIT(c)) >= base)
		return (0); /* no number formed */
	if (base == 16 && c == '0' && isxdigit(str[2]) &&
	    (str[1] == 'x' || str[1] == 'X'))
		c = *(str += 2); /* skip over leading "0x" or "0X" */
	for (val = -DIGIT(c); isalnum(c = *++str) && (xx = DIGIT(c)) < base; )
		/* accumulate neg avoids surprises near MAXLONG */
		val = base * val - xx;
	if (ptr != (char **)0)
		*ptr = str;
	return (neg ? val : -val);
}
END

echo "Creating Makefile"
cat << END > Makefile

returns: returns.o rate.o
	cc returns.o rate.o -lm -o returns

END

echo "Creating README"
cat << END > README

This program will compute absolute returns and annualized returns
given data for one or more securities.  I grabbed it from the net
in September of 89.  It was written and posted by Dennis Bednar.
I take no credit for this program, and have made no modifications
except for a name change.  (It was originally called 'dow').

Build the program by typeing 'make'.  I've built it on both Sun3
and Sun4 machines.

Data is assumed to be in a separate file for each security.  Entries
in the data file can have one of three forms:

    - a comment begins with a # and continues to the end of line
    - a NAV entry has the form
		<date> <NAV>
	    ex.
		4/14/92		11.28
    - a distribution entry has the form
		X	<div>	<NAV>
	    ex.
		X	0.07	11.1615

      A distribution entry is for capital gains or dividend payouts; you
      must know the amount paid, the share price at which it is reinvested,
      and the date of the distribution.  Distribution entries must be
      followed by a NAV entry for the dividend payment date.  Returns.c
      assumes that both dividends and gains are reinvested.

To use the returns program, you must at least specify one or more data files.
In addition, you can specify start and end dates to determine the performance
over a specified time period.  Some sample invocations:

    returns -f mutShares		# entire range of dates in file
    returns -s1/1/88 -f mutShares	# begin on Jan 1 88 to latest
    returns -s1/1/88 -e2/1/88 mutShares	# only for one month

Note that this program does not provide an Internal Rate of Return (IRR)
calculation (i.e. your actual return may differ if you make periodic
investments as in dollar cost averaging).  It does compute an APR that
can be used as a basis for comparison against other published data.

END

echo "Building the program 'returns'"
make

exit
-- 
    Mike Ernst				Netwise, Inc.
    mike@netwise.com			2477 Fifty Fifth St
    (303)442-8280			Boulder, Co 80301
Newsgroups: misc.invest