• About
  • Process
  • Strategies
  • Insights
  • Get In Touch
  • Login

On this page

  • How Strategy Composition Affects Returns

Portfolio Composition Explorer

Visualize how different strategy blends affect realized return distributions

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)}`
    })
  ]
})
How to read this chart

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.

What this shows

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.

 

Invest Vegan LLC DBA Ethical Capital - Utah Registered Investment Adviser Disclosures | Privacy Policy | Terms of Use | GitHub 90 N 400 E, Provo, UT 84606 | hello@ethicic.com | Get In Touch

This website is for informational purposes only and does not constitute investment advice. Past performance does not guarantee future results.