<div>
  |
  <i><b>Code by <a href="https://twitter.com/clarler"  target="_blank">Clarence Leung</a></b></i>
  | <i><b>Nest Egg Calculator by <a href="http://supplychenmanagement.com/about/"  target="_blank">Wayne Chen</a></b></i>
  |

</div>
<form>
  <div class="form-group">
    <label>Current Age</label>
    <input type="number" class="form-control" id="age" placeholder="30">
  </div>
  <div class="form-group">
    <label>Starting Balance ($)</label>
    <input type="number" class="form-control" id="balance" placeholder="1000.00">
  </div>
  <div class="form-group">
    <label>Yearly Contribution ($)</label>
    <input type="number" class="form-control" id="contribution" placeholder="3000.00">
  </div>
  <button id="submit" type="submit" class="btn btn-default">Simulate Again</button>
</form>

<table class="table table-striped">
  <thead>
    <tr>
      <th>Estimated Net Worth</th>
      <th>25th Percentile</th>
      <th>50th Percentile</th>
      <th>75th Percentile</th>
      <th>Average RoR</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

<script id="template" type="text/template">
  <% for (var i = 0; i < data.length; i++) { %>
    <tr>
      <th scope="row">At Age <%- data[i].age %></td>
      <td><%- accounting.formatMoney(data[i].firstQuartile) %></td>
      <td><%- accounting.formatMoney(data[i].secondQuartile) %></td>
      <td><%- accounting.formatMoney(data[i].thirdQuartile) %></td>
      <td><%- _.round(data[i].averageRoR * 100, 2) %>%</td>
    </tr>
  <% } %>
</script>
body {
  padding-left: 10px;
  font-size: 11px;
}

form {
  width: 20%;
  float: left;
}

.form-group {
  margin: 2px;
}

.table {
  margin-left: 20px;
  width: 70%;
  float: left;
}

#submit {
  margin-top: 3px;
}
var template = document.getElementById('template').innerHTML;
var compiled = _.template(template);

// Normal distribution with mean of 6% gain, and std dev= .13 (n=50)
function rateOfReturn() {
  var distribution = gaussian(1.06, Math.pow(0.13, 2));
  return distribution.ppf(Math.random()) - 1;
}

// Get data from input fields
function getValue(id) {
  return document.getElementById(id).value;
}

function getInput() {
  var age = getValue('age') !== '' ? parseInt(getValue('age'), 10) : 30;
  var balance = getValue('balance') !== '' ? parseFloat(getValue('balance')) : 1000.00;
  var contribution = getValue('contribution') !== '' ? parseFloat(getValue('contribution')) : 3000.00;
  return {
    "age": age,
    "balance": balance,
    "contribution": contribution
  };
}

function displayData() {
  var input = getInput();
  var ages = _.filter([45, 50, 55, 60, 65], function(age) {
    return age >= input.age;
  });;
  var simulations = runSimulations(50);
  var data = ages.map(function(age) {
    return {
      age: age,
      firstQuartile: getNetWorth(simulations, age, 25),
      secondQuartile: getNetWorth(simulations, age, 50),
      thirdQuartile: getNetWorth(simulations, age, 75),
      averageRoR: getAverageRoR(simulations, age)
    }
  });
      
  var results = compiled({ data: data });
  
  document.querySelector('tbody').innerHTML = results;
}

function addHandlers() {
  var button = document.getElementById('submit');
  var ageField = document.getElementById('age');
  var balanceField = document.getElementById('balance');
  var contributionField = document.getElementById('contribution');
  
  button.addEventListener('click', function(e) {
    e.preventDefault();
    displayData();
  });
  
  ageField.addEventListener('blur', function(e) {
    displayData();
  });
  
  balanceField.addEventListener('blur', function(e) {
    displayData();
  });
  
  contributionField.addEventListener('blur', function(e) {
    displayData();
  });
}

function simulateRetirement(age, balance, contribution) {
  var initialRoR = rateOfReturn();
  var data = [{
    "age": age,
    "balance": balance,
    "contribution": contribution,
    "ror": initialRoR,
    "total": balance + contribution + (initialRoR * (balance + contribution))
  }];
  
  for (var i = 1; i <= (65 - age); i++) {
    var nextRoR = rateOfReturn();
    var nextBalance = data[i - 1].total;
    var nextYear = {
      "age": age + i,
      "balance": nextBalance,
      "contribution": contribution,
      "ror": nextRoR,
      "total": nextBalance + contribution + (nextRoR * (nextBalance + contribution))
    };
    
    data.push(nextYear);
  }
  
  return data;
}

function runSimulations(n) {
  var n = n || 50;
  var simulations = [];
  var options = getInput();
  for (var i = 0; i < n; i++) {
    var simulation = simulateRetirement(options.age, options.balance, options.contribution);
    simulations.push(simulation);
  }
  
  return simulations;
}

function dataAtAge(simulations, age) {
  return simulations.map(function(simulation) {
    return _.find(simulation, { age: age });
  });
}

function getNetWorth(simulations, age, percent) {
  var totalsAtAge = dataAtAge(simulations, age).map(function(data) {
    return data.total;
  })
  
  return percentile(percent, totalsAtAge);
}

function getAverageRoR(simulations, age) {
  var rorAtAge = dataAtAge(simulations, age).map(function(data) {
    return data.ror;
  });
  
  return _.mean(rorAtAge);
}

displayData();
addHandlers();
Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css

External JavaScript

  1. https://unpkg.com/[email protected]
  2. https://unpkg.com/[email protected]/lib/index.js
  3. https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js
  4. https://rawgit.com/openexchangerates/accounting.js/master/accounting.min.js