Public
Edited
Mar 15, 2024
Insert cell
Insert cell
// https://observablehq.com/@observablehq/module-require-debugger
tabletojson = import('https://cdn.skypack.dev/tabletojson@3.0.0?min')
Insert cell
// annualReturns is map from portfolioNumYears to array of output portfolios, where output portfolio is
// Object {name, startYear, endYear, annualReturn, inflationAdjustedAnnualReturn}
annualReturns = {
const annualReturns = new Map();

// Add annual returns to annualReturns map.
// inputPortfolios is Array of Objects. Objects have fields name, percentUS, percentIntl, percentBond, shouldRebalance.
function addAnnualReturns(startYear, endYear, inputPortfolios, shouldRebalance, retryAttempt) {
let url = `https://www.portfoliovisualizer.com/backtest-asset-class-allocation?s=y&mode=1&timePeriod=4&startYear=${startYear}&firstMonth=1&endYear=${endYear}&lastMonth=12&calendarAligned=true&includeYTD=false&initialAmount=1000000&annualOperation=0&annualAdjustment=0&inflationAdjusted=true&annualPercentage=0.0&frequency=4&rebalanceType=${shouldRebalance?1:0}&absoluteDeviation=5.0&relativeDeviation=25.0&leverageType=0&leverageRatio=0.0&debtAmount=0&debtInterest=0.0&maintenanceMargin=25.0&leveragedBenchmark=false&portfolioNames=false&portfolioName1=Portfolio+1&portfolioName2=Portfolio+2&portfolioName3=Portfolio+3&asset1=TotalStockMarket&allocation1_1=${inputPortfolios[0].percentUS}&asset2=IntlStockMarket&allocation2_1=${inputPortfolios[0].percentIntl}&asset3=TotalBond&allocation3_1=${inputPortfolios[0].percentBond}`;
if (inputPortfolios.length == 2) {
url += `&allocation1_2=${inputPortfolios[1].percentUS}&allocation2_2=${inputPortfolios[1].percentIntl}&allocation3_2=${inputPortfolios[1].percentBond}`;
}

return tabletojson.tabletojson.convertUrl(url,
// Needed to get inflation-adjusted annual return from tooltip
{ stripHtmlFromCells: false }
).then(async (tablesAsJson) => {
// tabletojson.convertUrl() doesn't throw an error on 429, so we have to detect here
if (tablesAsJson.length == 0) {
console.log(`Got 429, sleeping and retrying. ${startYear}-${endYear} rebalance=${shouldRebalance} retryAttempt ${retryAttempt}`);
await new Promise(r => setTimeout(r, 2000));
await addAnnualReturns(startYear, endYear, inputPortfolios, shouldRebalance, retryAttempt + 1);
return;
}

for (let i=0; i < inputPortfolios.length; i++) {
// tablesAsJson[inputPortfolios.length][i] is the "Performance Summary" table.
// cagrCell is the CAGR table cell for this portfolio.
const cagrCell = tablesAsJson[inputPortfolios.length][i].CAGR;
const annualReturn = Number(cagrCell.slice(0, cagrCell.search("%")));
const inflationAdjustedAnnualReturn = Number(cagrCell.substring(cagrCell.search("is ") + 3, cagrCell.search("%\"")));
// Initialize annualReturns map value, if neccessary.
if (!annualReturns.get(endYear - startYear)) {
annualReturns.set(endYear - startYear, new Array());
}
let outputPortfolios = annualReturns.get(endYear - startYear);
outputPortfolios.push({
name: `${inputPortfolios[i].name} - ${inputPortfolios[i].percentUS} US / ${inputPortfolios[i].percentIntl} Intl / ${inputPortfolios[i].percentBond} Bond`,
startYear: startYear,
annualReturn: annualReturn,
inflationAdjustedAnnualReturn: inflationAdjustedAnnualReturn,
});
}
}).catch ((e) => {
console.log(`Error: ${e}`);
});
}

// Data gathering 1: Go as far back as possible at the expense of starting portfolioNumYears. Stored in https://gist.github.com/melissachang/b4a888c550a2e02406af12cf7252ee2b
// endYear must be at least 1987; otherwise get error "Not enough data for Total US Bond Market
// for the selected time period. Available data range is Jan 1987 - Sep 2023". Start with
// portfolioNumYears = 15 (so firstYearOnPortfolioVisualizer can go as far back as possible) because 1972 + 15 = 1987.
// const firstYearOnPortfolioVisualizer = 1972;
// const lastYearOnPortfolioVisualizer = 2023;
// for (var portfolioNumYears = 15; portfolioNumYears <= 50; portfolioNumYears++) {

// Data gathering 2: Make firstYearOnPortfolioVisualizer later so portfolioNumYears can go as low as 1.
// I want to show Allan 2018-2023.
// Stored in https://gist.github.com/melissachang/0a5a77ffa4ed2ca7cd3112a29478824b
const firstYearOnPortfolioVisualizer = 1986;
const lastYearOnPortfolioVisualizer = 2023;
for (var portfolioNumYears = 1; portfolioNumYears <= 50; portfolioNumYears++) {

for (var startYear = firstYearOnPortfolioVisualizer; startYear <= lastYearOnPortfolioVisualizer - portfolioNumYears; startYear++) {
await addAnnualReturns(
startYear,
startYear + portfolioNumYears,
[
{ name: "High risk - no rebalance", percentUS: 60, percentIntl: 30, percentBond: 10},
{ name: "All stock", percentUS: 100, percentIntl: 0, percentBond: 0},
],
/* shouldRebalance */ false,
/* retryAttempt */ 0,
);
await addAnnualReturns(
startYear,
startYear + portfolioNumYears,
[
{ name: "High risk - with rebalance", percentUS: 60, percentIntl: 30, percentBond: 10},
{ name: "Moderate risk - with rebalance", percentUS: 40, percentIntl: 20, percentBond: 40},
],
/* shouldRebalance */ true,
/* retryAttempt */ 0,
);
}
}

return annualReturns;
}
Insert cell
// Can't serialize Map to JSON; do this instead. See https://stackoverflow.com/a/67201268/6447189
JSON.stringify(Array.from(annualReturns.entries()))
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more