/*! * copyright (c) Able to Pay LLC 2015-16 * version 4.1 * June 7, 2016 * * requires also using investReturns-a2p-JSv4.txt & constants-a2p-JSv3.txt * */ function CAPErate( y ) { var r = 0; var weight = 17.8; // parameters for half-cubic CAPE 1920-2015 // optimal maxGain-minLoss weighting as of 5-26-2016 var SEhmean = 2.082167E-02; // harmonic mean of estimated SEs var SEmult = 3.954038E-01; // mulitiplier for estimated SE var SEpower = -1.011328E+00; // exponent for estimated SE if ( y > 0 ) { r = SEhmean / ( SEmult * Math.pow( Math.min(y+1,41), SEpower ) ); r *= lnCAPEfactor( y ); r *= weight; }; return r; // see function calcAltSlices } var adjBonds = function( a ) { var r = a; var b = a.tbonds + a.cbonds; if ( a.stocks < toCBonds ) { r.tbonds = 0; r.cbonds = b; } else { r.tbonds = b; r.cbonds = 0; }; return r; }; var getAllocation = function( stocks, tbonds, notes, cash, year ) { var a = new allocator( stocks, tbonds, 0, notes, cash, 1, 1 ); a = adjBonds( a ); a = getReturns( a, year ); return a; }; var calcSlices = function( long, fluc, maxYear ) { // revised for new getReturns on 1-18-2016 var lag = 3; // approx year when stock% reaches 50% of asymptote var minStocks = 0.1; // minimum stock% var asymptote = 0.35 + (0.15*long) + (0.05*fluc); // highest stock% // updated 6-75-2016 from 0.4 + 0.15*long + 0.05*fluc var nYear1 = 0.80 - (0.15*fluc); // notes% at year=1 var toNotes = 14; // year to start moving from 10Y-treas to 2Y-note var rampRate = Math.pow( 1 - nYear1/(1-minStocks), 1/toNotes ); //controls rate of moving to 2Y-notes var s = 0; var tb = 0; var n = 0; var slices = []; // allocations from year = 0 to maxYear // a slice is one year of a multi-year plan slices[0] = getAllocation( 0, 0, 0, 1, 0 ); for ( y = 1; y <= maxYear; y++ ) { s = minStocks + (asymptote * ( 1 - Math.exp( -(y-1)/lag ))); tb = (1-s) * Math.pow( rampRate, Math.max(0, toNotes-y+1) ); n = Math.max( 0, 1-s-tb ); slices[y] = getAllocation( s, tb, n, 0, y ); }; return slices; }; var calcAltSlices = function( maxYear, preSlices ) { var s = 0; var tb = 0; var n = 0; var postSlices = []; // slices from year = 0 to maxYear, with CAPE-adjusted allocations postSlices[0] = preSlices[0]; for ( y = 1; y <= maxYear; y++ ) { s = preSlices[y].stocks; s = Math.log( Math.min(0.995, s) / Math.max(0.005, 1-s) ); // convert from percent to logit s += CAPErate( y ) * lnCAPEnow; // add log CAPE adjustment s = Math.exp( s ); // revert to percentage, step 1 s = Math.max( 0, Math.min( 1, s / (1+s) ) ); // revert to percentage, step 2 tb = preSlices[y].tbonds + preSlices[y].cbonds; tb /= Math.max(0.005, preSlices[y].notes + tb); tb *= (1-s); n = Math.max( 0, 1-s-tb ); postSlices[y] = getAllocation( s, tb, n, 0, y ); }; return postSlices; }; var calcMultiYearPlan = function( slices, start, maxYear, doAdjust, doFull ) { // allocation plans, one per year; each plan aggregates // consecutive slices from then-current thru final year var plan = []; var startYear = start; var stopYear = maxYear; var stockSum = 0; var tbondSum = 0; var cbondSum = 0; var noteSum = 0; var cashSum = 0; var weight = 1; var weightSum = 0; var sliceRet = 1; var weightRet = new allocator( 0, 0, 0, 0, 1, 1, 1 ); for ( yearNow = 0; yearNow <= maxYear; yearNow++ ){ for ( sliceNum = startYear; sliceNum <= stopYear; sliceNum++ ){ weightRet = getReturns( slices[sliceNum], maxYear - stopYear + sliceNum ); if ( doAdjust ) { weight = 1/weightRet.adjusted; } else { weight = 1/weightRet.expected; }; weightSum += weight; stockSum += slices[sliceNum].stocks * weight; tbondSum += slices[sliceNum].tbonds * weight; cbondSum += slices[sliceNum].cbonds * weight; noteSum += slices[sliceNum].notes * weight; cashSum += slices[sliceNum].cash * weight; }; sliceRet = (stopYear - startYear + 1) / weightSum; plan[yearNow] = new allocator( stockSum/weightSum, tbondSum/weightSum, cbondSum/weightSum, noteSum/weightSum, cashSum/weightSum, 1, sliceRet ); if ( !doAdjust ) { plan[yearNow].expected = plan[yearNow].adjusted; plan[yearNow].adjusted = 1; }; if ( doFull ) { startYear = Math.max( 0, startYear-1 ); stopYear = Math.max( 0, stopYear-1 ); stockSum = 0; tbondSum = 0; cbondSum = 0; noteSum = 0; cashSum = 0; weightSum = 0; } else { yearNow = maxYear + 1; }; }; plan[0] = adjBonds( plan[0] ); plan[0] = getReturns( plan[0], 1 ); // returns in year 1 of holding period // document.write( " exp " + plan[0].expected + " adj " + plan[0].adjusted ); return plan; }; var trimTo5Pct = function( raw ) { var trim = raw; var low = 0.045; var corrector = 1; if ( trim.stocks > 0 && trim.stocks <= low ) { corrector = trim.stocks + trim.tbonds + trim.cbonds + trim.notes; corrector /= trim.tbonds + trim.cbonds + trim.notes; trim.tbonds *= corrector; trim.cbonds *= corrector; trim.notes *= corrector; trim.stocks = 0; }; if ( trim.cash > 0 && trim.cash <= low ) { corrector = trim.tbonds + trim.cbonds + trim.notes + trim.cash - low; corrector /= trim.tbonds + trim.cbonds + trim.notes; trim.tbonds *= corrector; trim.cbonds *= corrector; trim.notes *= corrector; trim.cash = low; }; if ( trim.notes > 0 && trim.notes <= low ) { corrector = trim.tbonds + trim.cbonds + trim.notes; corrector /= trim.tbonds + trim.cbonds; trim.tbonds *= corrector; trim.cbonds *= corrector; trim.notes = 0; }; return trim; }; function sumUp( a ) { return a.stocks + a.tbonds + a.cbonds + a.notes + a.cash; } var roundTo100 = function( raw ) { var smoother = new allocator( 0, 0, 0, 0, 0, raw.expected, raw.adjusted ); var rounder = 100; var trimmed = trimTo5Pct( raw ); smoother.stocks = Math.round( rounder*trimmed.stocks ); smoother.tbonds = Math.round( rounder*trimmed.tbonds ); smoother.cbonds = Math.round( rounder*trimmed.cbonds ); smoother.notes = Math.round( rounder*trimmed.notes ); smoother.cash = Math.round( rounder*trimmed.cash ); var total = sumUp( smoother ); if( total < 100) { rounder = 100.5; } else if ( total > 100 ) { rounder = 99.5; }; if( total != 100 ) { smoother.stocks = Math.round(rounder*raw.stocks); if( sumUp( smoother ) != 100 ) { smoother.tbonds = Math.round(rounder*raw.tbonds); if( sumUp( smoother ) != 100 ) { smoother.cbonds = Math.round(rounder*raw.cbonds); if( sumUp( smoother ) != 100 ) { smoother.notes = Math.round(rounder*raw.notes); if( sumUp( smoother ) != 100 ) { smoother.cash = Math.max(0, 100 - sumUp( smoother) + smoother.cash); }; }; }; }; }; return smoother; };