How Strategy Composition Affects Returns
This chart shows actual historical 1-year rolling returns for each strategy, plus computed overlays showing how blending them changes the distribution of outcomes.
Code
d3 = require ("d3@7" )
Plot = require ("@observablehq/plot@0.6" )
Inputs = await import ("https://cdn.jsdelivr.net/npm/@observablehq/inputs@0.12.0/+esm" )
Code
growthData = d3. csv ("../../assets/data/growth-performance.csv" , d3. autoType )
incomeData = d3. csv ("../../assets/data/income-performance.csv" , d3. autoType )
diversificationData = d3. csv ("../../assets/data/diversification-performance.csv" , d3. autoType )
Code
// Parse percentage strings to decimals
parsePercent = (str) => {
if (! str) return null ;
return parseFloat (str. replace (/ [%+] /g , '' )) / 100 ;
}
Code
// Calculate rolling 1-year returns
calculateRolling1YearReturns = (data, strategyName) => {
const returns = data. map (d => ({
month : new Date (d. Month ),
return : parsePercent (d. Strategy )
})). filter (d => d. return !== null ). reverse ();
const rolling = [];
for (let i = 11 ; i < returns. length ; i++ ) {
let compoundReturn = 1 ;
for (let j = 0 ; j < 12 ; j++ ) {
compoundReturn *= (1 + returns[i - j]. return );
}
rolling. push ({
endMonth : returns[i]. month ,
annualizedReturn : compoundReturn - 1 ,
strategy : strategyName
});
}
return rolling;
}
Code
growthRolling = calculateRolling1YearReturns (await growthData, "Growth" )
incomeRolling = calculateRolling1YearReturns (await incomeData, "Income" )
diversificationRolling = calculateRolling1YearReturns (await diversificationData, "Diversification" )
Code
// Combine all rolling returns
allRolling = [... growthRolling, ... incomeRolling, ... diversificationRolling]
Code
viewof growthWeight = Inputs. range ([0 , 100 ], {
value : 60 ,
step : 5 ,
label : "Growth %"
})
viewof incomeWeight = Inputs. range ([0 , 100 ], {
value : 20 ,
step : 5 ,
label : "Income %"
})
viewof diversificationWeight = Inputs. range ([0 , 100 ], {
value : 20 ,
step : 5 ,
label : "Diversification %"
})
Code
totalWeight = growthWeight + incomeWeight + diversificationWeight
Code
normalizedWeights = ({
growth : growthWeight / totalWeight,
income : incomeWeight / totalWeight,
diversification : diversificationWeight / totalWeight
})
Code
// Calculate blended returns
blendedReturns = (() => {
const monthMap = new Map ();
// Group by month
growthRolling. forEach (d => {
if (! monthMap. has (d. endMonth . getTime ())) {
monthMap. set (d. endMonth . getTime (), {});
}
monthMap. get (d. endMonth . getTime ()). growth = d. annualizedReturn ;
});
incomeRolling. forEach (d => {
if (! monthMap. has (d. endMonth . getTime ())) {
monthMap. set (d. endMonth . getTime (), {});
}
monthMap. get (d. endMonth . getTime ()). income = d. annualizedReturn ;
});
diversificationRolling. forEach (d => {
if (! monthMap. has (d. endMonth . getTime ())) {
monthMap. set (d. endMonth . getTime (), {});
}
monthMap. get (d. endMonth . getTime ()). diversification = d. annualizedReturn ;
});
// Calculate blended returns
return Array . from (monthMap. entries ())
. filter (([_, returns]) =>
returns. growth !== undefined &&
returns. income !== undefined &&
returns. diversification !== undefined
)
. map (([timestamp, returns]) => ({
endMonth : new Date (timestamp),
annualizedReturn :
returns. growth * normalizedWeights. growth +
returns. income * normalizedWeights. income +
returns. diversification * normalizedWeights. diversification ,
strategy : `Blend ( ${ Math . round (normalizedWeights. growth * 100 )} / ${ Math . round (normalizedWeights. income * 100 )} / ${ Math . round (normalizedWeights. diversification * 100 )} )`
}));
})()
Code
// Combine for plotting
plotData = [... allRolling, ... blendedReturns]
Code
Plot. plot ({
width : 1000 ,
height : 600 ,
marginLeft : 60 ,
marginBottom : 60 ,
x : {
label : "Time (end of 1-year period) →" ,
tickFormat : d3. timeFormat ("%Y" )
},
y : {
label : "↑ 1-Year Return" ,
tickFormat : d3. format (".0%" ),
grid : true
},
color : {
domain : ["Growth" , "Income" , "Diversification" , `Blend ( ${ Math . round (normalizedWeights. growth * 100 )} / ${ Math . round (normalizedWeights. income * 100 )} / ${ Math . round (normalizedWeights. diversification * 100 )} )` ],
range : ["#581c87" , "#0ea5e9" , "#64748b" , "#f59e0b" ]
},
marks : [
Plot. ruleY ([0 ], {stroke : "#94a3b8" , strokeWidth : 1 }),
Plot. dot (plotData, {
x : "endMonth" ,
y : "annualizedReturn" ,
fill : "strategy" ,
r : 4 ,
opacity : d => d. strategy . startsWith ("Blend" ) ? 0.8 : 0.5 ,
tip : true ,
title : d => ` ${ d. strategy }\n${ d3. timeFormat ("%b %Y" )(d. endMonth )}\n${ d3. format ("+.2%" )(d. annualizedReturn )} `
})
]
})
Each dot represents a realized 1-year return ending in that month.
Purple dots : Growth strategy returns
Blue dots : Income strategy returns
Gray dots : Diversification strategy returns
Orange dots : Your custom blend based on the sliders above
Use the sliders to explore how different allocations would have performed historically. Notice how blending strategies changes both the typical return and the range of outcomes.
This isn’t about finding the “optimal” blend - it’s about understanding the trade-offs. Higher growth allocation tends to increase both upside potential and volatility. More income/diversification typically dampens swings but may reduce growth potential.
Your ideal blend depends on your timeline and willingness to bear short-term volatility.