+ How does mixing bonds and stocks look in practice?
+
+
+ Each line shows a possible one-year journey of €100. Parameters are exaggerated for clarity (bonds and stocks
+ move in opposite directions here). Watch how the mix (purple) is smoother than either alone.
+
+ Left: cumulative portfolio value over time. Right: distribution of daily returns — narrower means less
+ fluctuation. Notice how the purple mix is narrower than both bonds and stocks individually.
+
+ );
+}
+
+// ============================================================
+// Metadata
+// ============================================================
+export const meta = {
+ title: "Why is diversification of savings important?",
+ publishing_date: "2026-03-08",
+ tokenID: 200,
+ category: "others",
+ description:
+ "A blog post that makes the case for diversification of savings instead of putting all your money into a single asset class.",
+};
+
+// ============================================================
+// Blog Post
+// ============================================================
+export default function ETFDiversification() {
+ return (
+
+ {/* ============================== */}
+ {/* THE PARADOX */}
+ {/* ============================== */}
+
+
+ {`
+## Motivation for this post
+
+In numerous discussions we get to the question on how to invest savings. And quite
+often the people in the group fall into some of the following categories:
+
+- The "Housing Only" crowd: "I put all my money into my house, that's the safest bet!"
+- The "Stock Market" fans: "I invest into stocks. Look at the long-term returns, it's the best way to grow wealth!"
+- The "I do nothing" group: "I just keep my money in the bank, it's safe and I don't have to worry about it."
+- The "desperate quant" who tries to explain his friends about the "magic of diversification" but fails to make it intuitive.
+
+I likely fall into the last category, and I certainly fail all the time to get my point across more than once in a blue moon. So I want to
+try to make it more intuitive here and maybe it helps some of the "I do nothing" people or the "Housing Only" crowd to diversify a bit.
+
+## The Diversification Paradox
+
+So let's start with the common question. You have some job which allows you to put away some savings every month. But it is not enough to make the
+down payment for a house in the next 5 years. You want to grow your savings, but you also don't want to take too much risk. How do you invest it?
+The savings account is really not giving much of a return, but it feels riskless. So what are other options?
+
+- Bonds feel safe: steady, boring, predictable.
+- Stocks feel risky: volatile, unpredictable, scary.
+
+So maybe put all the money into the safe option — bonds? That way you won't lose money, right?
+
+Try the simulator below: drag the slider to mix bonds and stocks, then press **▶ Start** to watch what happens. Can you find the mix that fluctuates least?
+`}
+
+
+
+
+
+ {`
+
+If you played with the slider above, you probably noticed something surprising: a 50/50 mix (purple) fluctuates much less than the 100% stock portfolio (red) — but also less than the 100% bond portfolio (blue).
+In other words: when you combine assets that move independently, their random ups and downs partially cancel. The result: the mix fluctuates less than any single part.
+
+So, what should we do ? Think of it like packing for unpredictable weather. If you bring only an
+umbrella, you're covered for rain but stuck in sunshine. If you add sunscreen,
+you haven't made your bag heavier in any meaningful way — you've made it
+*more useful for more situations*. This effect is called **diversification**, and it's the only genuine free lunch
+in investing.
+
+## So Let's Find the Perfect Mix?
+
+Once someone accepts that it is helpful to diversify, they typically try to understand what would be an optimal mix.
+And optimal often means "maximizing return for a given level of risk". You only started to look into the whole investment
+thing to get a good return, right?
+
+This kind of optimization earned [Harry Markowitz a Nobel Prize in 1990](https://www.nobelprize.org/prizes/economic-sciences/1990/summary/). Banks and fund
+managers have tried his "mean-variance optimization" ever since. And if you tried it too, you would unfortunately discover
+that this optimization **doesn't work in practice.**
+
+The reason is simple. To find the optimal mix, you need two ingredients:
+1. How much each asset *fluctuates* (risk) — this we can measure reasonably well.
+2. How much each asset will *earn in the future* (expected return) — this we **cannot**.
+
+Estimating future returns from past data is like predicting next year's
+weather from last year's diary. The mathematician Robert Merton showed in 1980 that you'd need
+**80 to 100 years** of data to estimate stock returns with any confidence.
+
+> "Optimizing" with unreliable inputs doesn't give you the best portfolio —
+> it gives you the portfolio most sensitive to estimation errors.
+
+The good news? We *can* measure risk (volatility, correlations) much more
+reliably. So instead of chasing the "best return", we focus on
+the thing we *can* control: **risk**.
+
+## Allocating the risks
+
+So now you might wonder what we can do with this kind of information ? Actually,
+we can look into the risk contribution of each asset to the overall portfolio risk.
+And then we can see if this is in line what we are confortable with. Let's make it more concrete.
+
+I actually went through a number of different ETFs that are available in Europe. They allow you to invest
+in a number of really large asset classes within the the following clusters:
+
+- 🟦 EU Bonds: There you might invest into corporate or government bonds.
+- 🟥 US & Friends: US stocks and closely correlated markets like Canada.
+- 🟧 Regional Diversifiers: This includes ETFs that invest into regional markets like Europe, emerging market bonds, India, Australia, and Japan.
+
+I then went through the historic data of the last ten years and looked into the fluctuations of the ETFs. Based on
+this data, we can see how the risk contribution of each cluster looks like depending on your investment choice.
+
+`}
+
+
+
+
+
+ {`
+### What to notice
+
+- **Bonds are your shock absorber.** Even 20–30% bonds dramatically reduce
+ portfolio swings — the gauge drops fast.
+- **Weight ≠ Risk.** A 30% allocation to US stocks can easily contribute 50%
+ of total portfolio risk. The bars above show this mismatch.
+`}
+
+
+
+ {/* ============================== */}
+ {/* TAKEAWAYS */}
+ {/* ============================== */}
+
+
+ {`
+## What Does This Mean for You?
+
+Three takeaways from our data:
+
+**1. Diversify broadly — don't put all eggs in one basket.**
+Mixing assets that don't move in lockstep reduces risk more than you'd expect.
+Bonds + stocks is the classic pair, but adding different geographies
+(US, Europe, Emerging Markets) helps too.
+
+**2. Don't chase "optimal" allocations.**
+The math shows that expected returns are too noisy to optimize.
+No model, no AI, no guru can reliably predict which asset class will
+outperform next year. Focus on what's measurable: risk.
+
+**3. Keep it simple and cheap.**
+A broad mix of low-cost index ETFs — rebalanced once a year — captures
+most of the diversification benefit. Complexity adds fees, not returns.
+
+---
+
+*The analysis behind this post is based on 12 European-listed ETFs tracked
+from 2018 to 2026. Cluster assignments were determined by Ward-linkage
+hierarchical clustering on bootstrapped correlation matrices. Risk metrics
+use 252-day annualization. For the full methodology see the
+[analysis notebooks](https://github.com/fretchen/EuropeSmallCapAnalysis).*
+`}
+
+
+
+ );
+}
diff --git a/website/blog/multichain_technical_notes.md b/website/blog/multichain_technical_notes.md
index f42c2f1c6..abd41f6c2 100644
--- a/website/blog/multichain_technical_notes.md
+++ b/website/blog/multichain_technical_notes.md
@@ -34,6 +34,7 @@ const chain = getViemChain(network);
```
**Why CAIP-2?**
+
- Human-readable (`eip155:10` vs `10`)
- Standard across wallets, indexers, block explorers
- Type-safe with TypeScript (can't accidentally pass chainId where network expected)
@@ -59,6 +60,7 @@ monorepo/
### 1. Shared Package: `@fretchen/chain-utils`
**Exports:**
+
- `toCAIP2(chainId)` / `fromCAIP2(network)` - conversion utilities
- `getViemChain(network)` - returns viem Chain object
- `getGenAiNFTAddress(network)` - contract address lookup
@@ -67,9 +69,10 @@ monorepo/
- Contract ABIs
**Critical detail:** No `prepare` script. CI must explicitly build before consumers install:
+
```yaml
- run: cd shared/chain-utils && npm ci && npm run build
-- run: cd website && npm ci # Now chain-utils is built
+- run: cd website && npm ci # Now chain-utils is built
```
### 2. Backend: Network Parameter
@@ -87,12 +90,14 @@ const network = body.network; // "eip155:10" | "eip155:8453" | ...
### 3. Frontend: Multi-Chain Hooks
**New hook: `useMultiChainNFTs`**
+
```typescript
const { tokens, isLoading, reload } = useMultiChainUserNFTs();
// Returns NFTs from ALL supported chains, merged
```
**New component: `ChainBadge`**
+
```typescript
// Renders "Base" badge
```
@@ -109,6 +114,7 @@ const { tokens, isLoading, reload } = useMultiChainUserNFTs();
```
The `network` parameter flows through the entire stack:
+
- Frontend → Backend → Payment Facilitator → Smart Contract
## Testing Architecture (Key Learning)
@@ -148,6 +154,7 @@ it("should fail gracefully with invalid config", async () => {
```
**Why this matters:**
+
- Functional tests are 10x faster → run on every save
- Deployment tests catch CI/CD issues → run before deploy
- Clear separation prevents "test pollution" (ethers global state affecting viem tests)
@@ -185,14 +192,14 @@ vi.mock("../hooks/useMultiChainNFTs", () => ({
## Metrics
-| Metric | Value |
-|--------|-------|
-| Planning duration | ~4 weeks |
+| Metric | Value |
+| ----------------------- | ---------- |
+| Planning duration | ~4 weeks |
| Implementation duration | ~3-4 weeks |
-| Lines changed | ~2,500 |
-| New tests | ~50 |
-| Test coverage | >90% |
-| Breaking changes | 0 |
+| Lines changed | ~2,500 |
+| New tests | ~50 |
+| Test coverage | >90% |
+| Breaking changes | 0 |
## Common Pitfalls
@@ -201,6 +208,7 @@ vi.mock("../hooks/useMultiChainNFTs", () => ({
2. **Nonce race conditions:** Parallel requests can cause "nonce too low" errors. Viem auto-manages nonces, but add retry logic for resilience.
3. **Type casting with wagmi:** `chainId` from `fromCAIP2()` returns `number`, but wagmi expects specific chain IDs:
+
```typescript
const chainId = fromCAIP2(network) as SupportedChainId;
```
@@ -212,4 +220,4 @@ vi.mock("../hooks/useMultiChainNFTs", () => ({
- [CAIP-2 Specification](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md)
- [Viem Multi-Chain Guide](https://viem.sh/docs/clients/chains.html)
- [ERC-8004 (Agent Authorization)](https://eips.ethereum.org/EIPS/eip-8004)
-- [Implementation Proposal](/website/MULTICHAIN_EXPANSION_PROPOSAL.md)
\ No newline at end of file
+- [Implementation Proposal](/website/MULTICHAIN_EXPANSION_PROPOSAL.md)
From 180b8ee93cd507b51f18511f07778d01ec094734 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 12 Mar 2026 11:46:10 -0400
Subject: [PATCH 02/17] Update etf_diversification_interactive.tsx
---
.../blog/etf_diversification_interactive.tsx | 77 ++++++++++++-------
1 file changed, 48 insertions(+), 29 deletions(-)
diff --git a/website/blog/etf_diversification_interactive.tsx b/website/blog/etf_diversification_interactive.tsx
index a422dc94a..1ca192656 100644
--- a/website/blog/etf_diversification_interactive.tsx
+++ b/website/blog/etf_diversification_interactive.tsx
@@ -259,6 +259,7 @@ const PEDAGOGY = {
function DiversificationRandomWalk() {
const [stockPct, setStockPct] = useState(50);
+ const [hasMovedSlider, setHasMovedSlider] = useState(false);
const [paths, setPaths] = useState(null);
const [visibleSteps, setVisibleSteps] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
@@ -279,6 +280,25 @@ function DiversificationRandomWalk() {
// Cleanup on unmount
useEffect(() => stopAnimation, [stopAnimation]);
+ // Auto-start animation on mount
+ useEffect(() => {
+ const fresh = generatePaths(sigBond, sigStock, rho);
+ setPaths(fresh);
+ setVisibleSteps(0);
+ setIsAnimating(true);
+ let step = 0;
+ animRef.current = setInterval(() => {
+ step = Math.min(step + ANIM_BATCH, STEPS);
+ setVisibleSteps(step);
+ if (step >= STEPS) {
+ clearInterval(animRef.current!);
+ animRef.current = null;
+ setIsAnimating(false);
+ }
+ }, ANIM_INTERVAL);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const startAnimation = useCallback(
(newPaths?: Paths) => {
stopAnimation();
@@ -350,6 +370,7 @@ function DiversificationRandomWalk() {
backgroundColor: "transparent",
borderWidth: 2,
pointRadius: 0,
+ pointStyle: "line" as const,
tension: 0.1,
},
{
@@ -359,6 +380,7 @@ function DiversificationRandomWalk() {
backgroundColor: "transparent",
borderWidth: 2,
pointRadius: 0,
+ pointStyle: "line" as const,
tension: 0.1,
},
{
@@ -368,6 +390,7 @@ function DiversificationRandomWalk() {
backgroundColor: "transparent",
borderWidth: 3,
pointRadius: 0,
+ pointStyle: "line" as const,
tension: 0.1,
},
],
@@ -378,7 +401,7 @@ function DiversificationRandomWalk() {
maintainAspectRatio: false,
animation: { duration: 0 },
plugins: {
- legend: { position: "top" as const, labels: { font: { size: 12 } } },
+ legend: { position: "top" as const, labels: { font: { size: 12 }, usePointStyle: true, pointStyleWidth: 20 } },
title: { display: false },
},
scales: {
@@ -438,7 +461,7 @@ function DiversificationRandomWalk() {
marginBottom: "1rem",
})}
>
- Each line shows a possible one-year journey of €100. Parameters are exaggerated for clarity (bonds and stocks
+ Each line shows a possible two-year journey of €100. Parameters are exaggerated for clarity (bonds and stocks
move in opposite directions here). Watch how the mix (purple) is smoother than either alone.
@@ -475,13 +498,14 @@ function DiversificationRandomWalk() {
value={stockPct}
onChange={(e) => {
setStockPct(parseInt(e.target.value, 10));
+ setHasMovedSlider(true);
}}
className={css({ width: "100%" })}
/>
{/* Button */}
-
+
+ {showVol && !hasMovedSlider && (
+
+ 👆 Move the slider to see how the mix changes
+
+ )}
{/* Chart + side histogram */}
{/* Line chart */}
- {paths ? (
-
- ) : (
-
- Press ▶ Start to watch the paths grow
-
- )}
+
{/* Rotated histogram on the right */}
@@ -531,14 +543,21 @@ function DiversificationRandomWalk() {
width: "90px",
flexShrink: 0,
height: "300px",
+ display: "flex",
+ flexDirection: "column",
})}
>
-
+
+ Daily return spread
+
+
+
+
)}
@@ -568,7 +587,7 @@ function DiversificationRandomWalk() {
marginBottom: "0.5rem",
})}
>
- Measured volatility of this particular random path:
+ How bumpy was this ride? (lower = smoother)
{[
From 920cd90f1ef23f107b77afe2b6d283991bef6b23 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 12 Mar 2026 12:28:11 -0400
Subject: [PATCH 03/17] Update etf_diversification_interactive.tsx
---
.../blog/etf_diversification_interactive.tsx | 481 +++++++++---------
1 file changed, 241 insertions(+), 240 deletions(-)
diff --git a/website/blog/etf_diversification_interactive.tsx b/website/blog/etf_diversification_interactive.tsx
index 1ca192656..ffa823719 100644
--- a/website/blog/etf_diversification_interactive.tsx
+++ b/website/blog/etf_diversification_interactive.tsx
@@ -22,15 +22,15 @@ ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, T
// ============================================================
const DATA = {
clusters: [
- { name: "EU Bonds", etfs: ["IBCS", "EUNH"], color: "#4e79a7" },
+ { name: "Bonds", etfs: ["IBCS", "EUNH", "3SUD"], color: "#4e79a7" },
{
- name: "US & Friends",
+ name: "Stocks: US & Canada",
etfs: ["SXR8", "CSCA"],
color: "#e15759",
},
{
- name: "Regional Diversifiers",
- etfs: ["SXRT", "3SUD", "NDIA", "SAUS", "CSJP"],
+ name: "Stocks: EU, Asia & Australia",
+ etfs: ["SXRT", "NDIA", "SAUS", "CSJP"],
color: "#f28e2b",
},
],
@@ -65,7 +65,7 @@ const DATA = {
// 9×9 Ledoit-Wolf annualized covariance matrix (9-ETF subset, removing EXXY, IQQ6, 4BRZ)
// Order: IBCS, EUNH, SXR8, SXRT, 3SUD, NDIA, SAUS, CSJP, CSCA
etf_names: ["IBCS", "EUNH", "SXR8", "SXRT", "3SUD", "NDIA", "SAUS", "CSJP", "CSCA"],
- etf_cluster: [0, 0, 1, 2, 2, 2, 2, 2, 1], // index into clusters[]
+ etf_cluster: [0, 0, 1, 2, 0, 2, 2, 2, 1], // index into clusters[]
etf_labels: [
"EU Corporate Bonds",
"EU Government Bonds",
@@ -666,17 +666,36 @@ function matVec(cov: number[][], w: number[]): number[] {
return result;
}
-/** Solve long-only minimum variance: min w'Σw s.t. Σw=1, w≥0.
- * Projected gradient descent with simplex projection. */
-function solveMinVariance(cov: number[][]): number[] {
+/** Solve risk-budget allocation: find weights w≥0, Σw=1 such that
+ * each cluster's risk contribution matches the target budget fractions.
+ * Ported from NB06 (Roncalli 2013). Uses projected gradient descent
+ * with numerical gradient and simplex projection. */
+function solveRiskBudget(cov: number[][], clusterOf: number[], budgets: number[]): number[] {
const n = cov.length;
let w = new Array(n).fill(1 / n);
- const lr = 0.5;
- for (let iter = 0; iter < 5000; iter++) {
- const grad = matVec(cov, w).map((g) => 2 * g);
- const raw = w.map((wi, i) => wi - lr * grad[i]);
- // Project onto simplex (w≥0, Σw=1)
- const sorted = [...raw].sort((a, b) => b - a);
+ const EPS = 1e-6;
+ const lr = 0.3;
+ const nClusters = budgets.length;
+
+ /** Objective: Σ_c (RC_c/totalRC - b_c)² */
+ function objective(ww: number[]): number {
+ const sw = matVec(cov, ww);
+ const rc = ww.map((wi, i) => wi * sw[i]);
+ const totalRc = rc.reduce((a, b) => a + b, 0);
+ if (totalRc < 1e-15) return 1e10;
+ const clusterRc = new Array(nClusters).fill(0);
+ for (let i = 0; i < n; i++) clusterRc[clusterOf[i]] += rc[i];
+ let loss = 0;
+ for (let c = 0; c < nClusters; c++) {
+ const diff = clusterRc[c] / totalRc - budgets[c];
+ loss += diff * diff;
+ }
+ return loss;
+ }
+
+ /** Project onto simplex (w≥0, Σw=1) — Duchi et al. */
+ function projectSimplex(v: number[]): number[] {
+ const sorted = [...v].sort((a, b) => b - a);
let cumSum = 0;
let rho = 0;
for (let j = 0; j < n; j++) {
@@ -684,41 +703,30 @@ function solveMinVariance(cov: number[][]): number[] {
if (sorted[j] - (cumSum - 1) / (j + 1) > 0) rho = j;
}
const theta = (sorted.slice(0, rho + 1).reduce((a, b) => a + b, 0) - 1) / (rho + 1);
- w = raw.map((r) => Math.max(0, r - theta));
+ return v.map((r) => Math.max(0, r - theta));
+ }
+
+ for (let iter = 0; iter < 10000; iter++) {
+ const f0 = objective(w);
+ // Numerical gradient
+ const grad = new Array(n).fill(0);
+ for (let i = 0; i < n; i++) {
+ const wp = [...w];
+ wp[i] += EPS;
+ grad[i] = (objective(wp) - f0) / EPS;
+ }
+ const raw = w.map((wi, i) => wi - lr * grad[i]);
+ w = projectSimplex(raw);
+ // Early stopping
+ if (f0 < 1e-12) break;
}
return w;
}
-const MIN_VAR_W = solveMinVariance(DATA.cov_etf_ann);
-const MIN_VAR_PCT = MIN_VAR_W.map((v) => Math.round(v * 100));
-// Adjust rounding to sum to exactly 100
-MIN_VAR_PCT[MIN_VAR_W.indexOf(Math.max(...MIN_VAR_W))] += 100 - MIN_VAR_PCT.reduce((a, b) => a + b, 0);
-
-const PRESETS: { label: string; description: string; weights: number[] }[] = [
- {
- label: "Just two",
- description: "50 % S&P 500 + 50 % EU Gov Bonds — the simplest diversified portfolio",
- // IBCS EUNH SXR8 SXRT 3SUD NDIA SAUS CSJP CSCA
- weights: [0, 50, 50, 0, 0, 0, 0, 0, 0],
- },
- {
- label: "Equal weight",
- description: "~11 % in each ETF — simple, no agonizing over the right mix",
- weights: [11, 11, 11, 11, 12, 11, 11, 11, 11],
- },
- {
- label: "Minimum risk",
- description: "The mathematically lowest-risk mix — computed from the covariance matrix",
- weights: MIN_VAR_PCT,
- },
-];
-
-/** Compute portfolio risk metrics from percentage weights */
-function computeRisk(pctWeights: number[]) {
- const sum = pctWeights.reduce((a, b) => a + b, 0);
- // Normalize to fractions
- const w = sum > 0 ? pctWeights.map((p) => p / sum) : pctWeights.map(() => 1 / N_ETF);
+const N_CLUSTERS = DATA.clusters.length;
+/** Compute portfolio risk metrics from fractional weights */
+function computeRisk(w: number[]) {
const sigmaW = matVec(DATA.cov_etf_ann, w);
const portVar = w.reduce((s, wi, i) => s + wi * sigmaW[i], 0);
const portVol = Math.sqrt(Math.max(0, portVar)) * 100; // annualized %
@@ -727,15 +735,15 @@ function computeRisk(pctWeights: number[]) {
const rc = w.map((wi, i) => (portVar > 0 ? ((wi * sigmaW[i]) / portVar) * 100 : 0));
// Cluster-level aggregation
- const clusterWeight = [0, 0, 0];
- const clusterRc = [0, 0, 0];
+ const clusterWeight = new Array(N_CLUSTERS).fill(0);
+ const clusterRc = new Array(N_CLUSTERS).fill(0);
for (let i = 0; i < N_ETF; i++) {
const ci = DATA.etf_cluster[i];
- clusterWeight[ci] += sum > 0 ? (pctWeights[i] / sum) * 100 : 100 / N_ETF;
+ clusterWeight[ci] += w[i] * 100;
clusterRc[ci] += rc[i];
}
- return { w, portVol, rc, clusterWeight, clusterRc, sum };
+ return { w, portVol, rc, clusterWeight, clusterRc };
}
/** Color for the risk gauge */
@@ -745,28 +753,53 @@ function riskColor(vol: number): string {
return "#ef4444"; // red
}
+const PRESETS: { label: string; description: string; budgets: number[] }[] = [
+ {
+ label: "Just two",
+ description: "Only bonds and US stocks contribute risk — the simplest diversified portfolio",
+ budgets: [50, 50, 0],
+ },
+ {
+ label: "Equal risk",
+ description: "Each group contributes equally to risk — a balanced starting point",
+ budgets: [33, 33, 34],
+ },
+ {
+ label: "Growth",
+ description: "Most risk from stocks, small bond cushion — for those with a long time horizon",
+ budgets: [10, 45, 45],
+ },
+];
+
function PortfolioRiskAllocator() {
- const [weights, setWeights] = useState(PRESETS[0].weights);
+ const [budgets, setBudgets] = useState(PRESETS[0].budgets);
const [activePreset, setActivePreset] = useState(0);
const [expandedCluster, setExpandedCluster] = useState(null);
- const risk = useMemo(() => computeRisk(weights), [weights]);
+ // Normalize budgets and solve for weights
+ const { weights, risk } = useMemo(() => {
+ const sum = budgets.reduce((a, b) => a + b, 0);
+ const norm = sum > 0 ? budgets.map((b) => b / sum) : budgets.map(() => 1 / N_CLUSTERS);
+ const w = solveRiskBudget(DATA.cov_etf_ann, DATA.etf_cluster, norm);
+ return { weights: w, risk: computeRisk(w) };
+ }, [budgets]);
- const handleWeight = (idx: number, val: string) => {
- const num = Math.max(0, Math.min(100, parseInt(val, 10) || 0));
- setWeights((prev) => {
+ const handleBudget = (ci: number, val: number) => {
+ setBudgets((prev) => {
const next = [...prev];
- next[idx] = num;
+ next[ci] = Math.max(0, Math.min(100, val));
return next;
});
setActivePreset(-1);
};
const applyPreset = (i: number) => {
- setWeights(PRESETS[i].weights);
+ setBudgets(PRESETS[i].budgets);
setActivePreset(i);
};
+ const budgetSum = budgets.reduce((a, b) => a + b, 0);
+
return (
- Build your portfolio
+ Build your portfolio by risk budget
- Adjust the weights below to see how different allocations change the overall risk of your portfolio.
+ Decide how much of the total portfolio risk each group should contribute. The math then finds the capital
+ allocation that matches your risk budget.
);
@@ -1241,12 +1241,12 @@ And then we can see if this is in line what we are confortable with. Let's make
I actually went through a number of different ETFs that are available in Europe. They allow you to invest
in a number of really large asset classes within the the following clusters:
-- 🟦 EU Bonds: There you might invest into corporate or government bonds.
-- 🟥 US & Friends: US stocks and closely correlated markets like Canada.
-- 🟧 Regional Diversifiers: This includes ETFs that invest into regional markets like Europe, emerging market bonds, India, Australia, and Japan.
+- 🟦 Bonds: Corporate bonds, government bonds, and emerging market bonds.
+- 🟥 Stocks: US & Canada — US stocks and the closely correlated Canadian market.
+- 🟧 Stocks: EU, Asia & Australia — Euro Stoxx, India, Australia, and Japan.
-I then went through the historic data of the last ten years and looked into the fluctuations of the ETFs. Based on
-this data, we can see how the risk contribution of each cluster looks like depending on your investment choice.
+I then went through the historic data of the last ten years and looked into the fluctuations of the ETFs. In the tool
+below, you set how much risk each group should contribute — and the math finds the capital allocation that matches.
`}
@@ -1257,10 +1257,11 @@ this data, we can see how the risk contribution of each cluster looks like depen
{`
### What to notice
-- **Bonds are your shock absorber.** Even 20–30% bonds dramatically reduce
- portfolio swings — the gauge drops fast.
-- **Weight ≠ Risk.** A 30% allocation to US stocks can easily contribute 50%
- of total portfolio risk. The bars above show this mismatch.
+- **Bonds are your shock absorber.** Even a small risk budget for bonds translates into
+ a large capital allocation — that's because bonds fluctuate much less than stocks.
+- **Capital ≠ Risk.** Notice how the capital allocation bars look very different from
+ the risk contribution bars. Bonds need a lot of capital to "earn" their share of risk,
+ while a small stock allocation can dominate portfolio risk.
`}
@@ -1291,7 +1292,7 @@ most of the diversification benefit. Complexity adds fees, not returns.
---
-*The analysis behind this post is based on 12 European-listed ETFs tracked
+*The analysis behind this post is based on 9 European-listed ETFs tracked
from 2018 to 2026. Cluster assignments were determined by Ward-linkage
hierarchical clustering on bootstrapped correlation matrices. Risk metrics
use 252-day annualization. For the full methodology see the
From aa446eeab443858be6e57ef63a6aaae7940675d9 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 12 Mar 2026 12:57:26 -0400
Subject: [PATCH 04/17] Update etf_diversification_interactive.tsx
---
.../blog/etf_diversification_interactive.tsx | 391 ++++++++++--------
1 file changed, 227 insertions(+), 164 deletions(-)
diff --git a/website/blog/etf_diversification_interactive.tsx b/website/blog/etf_diversification_interactive.tsx
index ffa823719..06f82d17f 100644
--- a/website/blog/etf_diversification_interactive.tsx
+++ b/website/blog/etf_diversification_interactive.tsx
@@ -666,36 +666,52 @@ function matVec(cov: number[][], w: number[]): number[] {
return result;
}
-/** Solve risk-budget allocation: find weights w≥0, Σw=1 such that
- * each cluster's risk contribution matches the target budget fractions.
- * Ported from NB06 (Roncalli 2013). Uses projected gradient descent
- * with numerical gradient and simplex projection. */
+/** Solve risk-budget allocation via Roncalli (2013) multiplicative update. */
function solveRiskBudget(cov: number[][], clusterOf: number[], budgets: number[]): number[] {
const n = cov.length;
- let w = new Array(n).fill(1 / n);
- const EPS = 1e-6;
- const lr = 0.3;
- const nClusters = budgets.length;
-
- /** Objective: Σ_c (RC_c/totalRC - b_c)² */
- function objective(ww: number[]): number {
- const sw = matVec(cov, ww);
- const rc = ww.map((wi, i) => wi * sw[i]);
+ const nC = budgets.length;
+ const active = new Array(n).fill(true);
+ for (let i = 0; i < n; i++) if (budgets[clusterOf[i]] < 1e-10) active[i] = false;
+ const activeCount = active.filter(Boolean).length;
+ if (activeCount === 0) return new Array(n).fill(1 / n);
+
+ let w = new Array(n).fill(0);
+ for (let i = 0; i < n; i++) w[i] = active[i] ? 1 / activeCount : 0;
+
+ for (let iter = 0; iter < 5000; iter++) {
+ const sw = matVec(cov, w);
+ const rc = w.map((wi, i) => wi * sw[i]);
const totalRc = rc.reduce((a, b) => a + b, 0);
- if (totalRc < 1e-15) return 1e10;
- const clusterRc = new Array(nClusters).fill(0);
+ if (totalRc < 1e-15) break;
+ const clusterRc = new Array(nC).fill(0);
for (let i = 0; i < n; i++) clusterRc[clusterOf[i]] += rc[i];
- let loss = 0;
- for (let c = 0; c < nClusters; c++) {
- const diff = clusterRc[c] / totalRc - budgets[c];
- loss += diff * diff;
+
+ let maxErr = 0;
+ for (let c = 0; c < nC; c++)
+ if (budgets[c] > 1e-10) maxErr = Math.max(maxErr, Math.abs(clusterRc[c] / totalRc - budgets[c]));
+ if (maxErr < 1e-8) break;
+
+ for (let i = 0; i < n; i++) {
+ if (!active[i]) continue;
+ const c = clusterOf[i];
+ const frac = clusterRc[c] / totalRc;
+ if (frac < 1e-15) continue;
+ w[i] *= Math.pow(budgets[c] / frac, 0.5);
}
- return loss;
+ const wSum = w.reduce((a, b) => a + b, 0);
+ if (wSum > 0) w = w.map((wi) => wi / wSum);
}
+ return w;
+}
- /** Project onto simplex (w≥0, Σw=1) — Duchi et al. */
- function projectSimplex(v: number[]): number[] {
- const sorted = [...v].sort((a, b) => b - a);
+/** Solve long-only minimum variance: min w'Σw s.t. Σw=1, w≥0. */
+function solveMinVariance(cov: number[][]): number[] {
+ const n = cov.length;
+ let w = new Array(n).fill(1 / n);
+ for (let iter = 0; iter < 5000; iter++) {
+ const grad = matVec(cov, w).map((g) => 2 * g);
+ const raw = w.map((wi, i) => wi - 0.5 * grad[i]);
+ const sorted = [...raw].sort((a, b) => b - a);
let cumSum = 0;
let rho = 0;
for (let j = 0; j < n; j++) {
@@ -703,22 +719,7 @@ function solveRiskBudget(cov: number[][], clusterOf: number[], budgets: number[]
if (sorted[j] - (cumSum - 1) / (j + 1) > 0) rho = j;
}
const theta = (sorted.slice(0, rho + 1).reduce((a, b) => a + b, 0) - 1) / (rho + 1);
- return v.map((r) => Math.max(0, r - theta));
- }
-
- for (let iter = 0; iter < 10000; iter++) {
- const f0 = objective(w);
- // Numerical gradient
- const grad = new Array(n).fill(0);
- for (let i = 0; i < n; i++) {
- const wp = [...w];
- wp[i] += EPS;
- grad[i] = (objective(wp) - f0) / EPS;
- }
- const raw = w.map((wi, i) => wi - lr * grad[i]);
- w = projectSimplex(raw);
- // Early stopping
- if (f0 < 1e-12) break;
+ w = raw.map((r) => Math.max(0, r - theta));
}
return w;
}
@@ -748,57 +749,96 @@ function computeRisk(w: number[]) {
/** Color for the risk gauge */
function riskColor(vol: number): string {
- if (vol < 8) return "#22c55e"; // green
- if (vol < 15) return "#eab308"; // yellow
- return "#ef4444"; // red
+ if (vol < 8) return "#22c55e";
+ if (vol < 15) return "#eab308";
+ return "#ef4444";
}
+// Precompute minimum-variance risk budgets for preset
+const _mvW = solveMinVariance(DATA.cov_etf_ann);
+const _mvRisk = (() => {
+ const sw = matVec(DATA.cov_etf_ann, _mvW);
+ const rc = _mvW.map((wi, i) => wi * sw[i]);
+ const total = rc.reduce((a, b) => a + b, 0);
+ const clRc = new Array(N_CLUSTERS).fill(0);
+ for (let i = 0; i < N_ETF; i++) clRc[DATA.etf_cluster[i]] += rc[i];
+ return clRc.map((v) => Math.round((v / total) * 100));
+})();
+_mvRisk[_mvRisk.indexOf(Math.max(..._mvRisk))] += 100 - _mvRisk.reduce((a, b) => a + b, 0);
+
const PRESETS: { label: string; description: string; budgets: number[] }[] = [
{
label: "Just two",
- description: "Only bonds and US stocks contribute risk — the simplest diversified portfolio",
+ description: "Only bonds and US stocks contribute risk",
budgets: [50, 50, 0],
},
{
label: "Equal risk",
- description: "Each group contributes equally to risk — a balanced starting point",
+ description: "Each group contributes equally — a balanced starting point",
budgets: [33, 33, 34],
},
+ {
+ label: "Minimum risk",
+ description: "The lowest-risk portfolio — heavily bond-dominated",
+ budgets: _mvRisk,
+ },
{
label: "Growth",
- description: "Most risk from stocks, small bond cushion — for those with a long time horizon",
+ description: "Most risk from stocks, small bond cushion",
budgets: [10, 45, 45],
},
];
function PortfolioRiskAllocator() {
- const [budgets, setBudgets] = useState(PRESETS[0].budgets);
- const [activePreset, setActivePreset] = useState(0);
+ const [budgets, setBudgets] = useState([...PRESETS[1].budgets]);
+ const [activePreset, setActivePreset] = useState(1);
const [expandedCluster, setExpandedCluster] = useState(null);
+ const [dragging, setDragging] = useState(null);
+ const barRef = useRef(null);
- // Normalize budgets and solve for weights
const { weights, risk } = useMemo(() => {
- const sum = budgets.reduce((a, b) => a + b, 0);
- const norm = sum > 0 ? budgets.map((b) => b / sum) : budgets.map(() => 1 / N_CLUSTERS);
- const w = solveRiskBudget(DATA.cov_etf_ann, DATA.etf_cluster, norm);
+ const w = solveRiskBudget(
+ DATA.cov_etf_ann,
+ DATA.etf_cluster,
+ budgets.map((b) => b / 100),
+ );
return { weights: w, risk: computeRisk(w) };
}, [budgets]);
- const handleBudget = (ci: number, val: number) => {
- setBudgets((prev) => {
- const next = [...prev];
- next[ci] = Math.max(0, Math.min(100, val));
- return next;
- });
- setActivePreset(-1);
- };
-
const applyPreset = (i: number) => {
- setBudgets(PRESETS[i].budgets);
+ setBudgets([...PRESETS[i].budgets]);
setActivePreset(i);
};
- const budgetSum = budgets.reduce((a, b) => a + b, 0);
+ useEffect(() => {
+ if (dragging === null) return;
+ const onMove = (e: PointerEvent) => {
+ if (!barRef.current) return;
+ const rect = barRef.current.getBoundingClientRect();
+ const pos = Math.round(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)) * 100);
+ setBudgets((prev) => {
+ const next = [...prev];
+ if (dragging === 0) {
+ const b2 = next[0] + next[1];
+ next[0] = Math.max(0, Math.min(b2, pos));
+ next[1] = b2 - next[0];
+ } else {
+ const clamped = Math.max(next[0], Math.min(100, pos));
+ next[1] = clamped - next[0];
+ next[2] = 100 - clamped;
+ }
+ return next;
+ });
+ setActivePreset(-1);
+ };
+ const onUp = () => setDragging(null);
+ document.addEventListener("pointermove", onMove);
+ document.addEventListener("pointerup", onUp);
+ return () => {
+ document.removeEventListener("pointermove", onMove);
+ document.removeEventListener("pointerup", onUp);
+ };
+ }, [dragging]);
return (
- Decide how much of the total portfolio risk each group should contribute. The math then finds the capital
- allocation that matches your risk budget.
+ Set how much risk each group should contribute — the math finds the capital allocation.
{/* ── Preset buttons ── */}
-
From 084ac00f62dac98e3c2a9a187564d530ad36bc9e Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 12 Mar 2026 14:52:56 -0400
Subject: [PATCH 06/17] Update etf_diversification_interactive.tsx
---
.../blog/etf_diversification_interactive.tsx | 82 +++++--------------
1 file changed, 22 insertions(+), 60 deletions(-)
diff --git a/website/blog/etf_diversification_interactive.tsx b/website/blog/etf_diversification_interactive.tsx
index 1f9eecab8..78a03e2d4 100644
--- a/website/blog/etf_diversification_interactive.tsx
+++ b/website/blog/etf_diversification_interactive.tsx
@@ -1222,31 +1222,18 @@ export default function ETFDiversification() {
{`
-## Motivation for this post
+## A Systematic Approach to Saving
-In numerous discussions we get to the question on how to invest savings. And quite
-often the people in the group fall into some of the following categories:
-
-- The "Housing Only" crowd: "I put all my money into my house, that's the safest bet!"
-- The "Stock Market" fans: "I invest into stocks. Look at the long-term returns, it's the best way to grow wealth!"
-- The "I do nothing" group: "I just keep my money in the bank, it's safe and I don't have to worry about it."
-- The "desperate quant" who tries to explain his friends about the "magic of diversification" but fails to make it intuitive.
-
-I likely fall into the last category, and I certainly fail all the time to get my point across more than once in a blue moon. So I want to
-try to make it more intuitive here and maybe it helps some of the "I do nothing" people or the "Housing Only" crowd to diversify a bit.
+Most people keep their savings in a bank account, invest in a single asset class, or avoid the topic altogether. This post outlines a systematic approach to diversifying savings — one that doesn't require predicting markets or paying expensive advisors. It's close to what I actually do.
## The Diversification Paradox
-So let's start with the common question. You have some job which allows you to put away some savings every month. But it is not enough to make the
-down payment for a house in the next 5 years. You want to grow your savings, but you also don't want to take too much risk. How do you invest it?
-The savings account is really not giving much of a return, but it feels riskless. So what are other options?
+You have savings you'd like to grow, but you don't want to gamble. The savings account barely keeps up with inflation. So what are your options?
- Bonds feel safe: steady, boring, predictable.
- Stocks feel risky: volatile, unpredictable, scary.
-So maybe put all the money into the safe option — bonds? That way you won't lose money, right?
-
-Try the simulator below: drag the slider to mix bonds and stocks, then press **▶ Start** to watch what happens. Can you find the mix that fluctuates least?
+The safe choice seems obvious — put everything into bonds. But try the simulator below first: drag the slider to mix bonds and stocks. Can you find the mix that fluctuates least?
`}
@@ -1255,20 +1242,13 @@ Try the simulator below: drag the slider to mix bonds and stocks, then press **
{`
-If you played with the slider above, you probably noticed something surprising: a 50/50 mix (purple) fluctuates much less than the 100% stock portfolio (red) — but also less than the 100% bond portfolio (blue).
-In other words: when you combine assets that move independently, their random ups and downs partially cancel. The result: the mix fluctuates less than any single part.
+If you played with the slider, you probably noticed something surprising: a 50/50 mix (purple) fluctuates much less than 100% stocks (red) — but also less than 100% bonds (blue). When you combine assets that move independently, their random ups and downs partially cancel. The mix fluctuates less than any single part.
-So, what should we do ? Think of it like packing for unpredictable weather. If you bring only an
-umbrella, you're covered for rain but stuck in sunshine. If you add sunscreen,
-you haven't made your bag heavier in any meaningful way — you've made it
-*more useful for more situations*. This effect is called **diversification**, and it's the only genuine free lunch
-in investing.
+Think of it like packing for unpredictable weather: an umbrella alone covers rain but not sunshine. Adding sunscreen doesn't make the bag heavier — it makes it useful for more situations. This principle is called **diversification**, and it's the closest thing to a free lunch in investing.
-## So Let's Find the Perfect Mix?
+## Why Chasing Returns Fails
-Once someone accepts that it is helpful to diversify, they typically try to understand what would be an optimal mix.
-And optimal often means "maximizing return for a given level of risk". You only started to look into the whole investment
-thing to get a good return, right?
+Once you accept that diversification helps, the next instinct is to find the *optimal* mix — the one that maximizes return for a given level of risk.
This kind of optimization earned [Harry Markowitz a Nobel Prize in 1990](https://www.nobelprize.org/prizes/economic-sciences/1990/summary/). Banks and fund
managers have tried his "mean-variance optimization" ever since. And if you tried it too, you would unfortunately discover
@@ -1289,21 +1269,17 @@ The good news? We *can* measure risk (volatility, correlations) much more
reliably. So instead of chasing the "best return", we focus on
the thing we *can* control: **risk**.
-## Allocating the risks
+## Allocating Risk Across Asset Classes
-So now you might wonder what we can do with this kind of information ? Actually,
-we can look into the risk contribution of each asset to the overall portfolio risk.
-And then we can see if this is in line what we are confortable with. Let's make it more concrete.
+Since we can measure risk reliably, we can use it as a building block: decide how much risk each asset group should contribute, and let the math determine the capital allocation.
-I actually went through a number of different ETFs that are available in Europe. They allow you to invest
-in a number of really large asset classes within the the following clusters:
+The tool below uses 9 European-listed ETFs grouped into three clusters based on how they move together:
- 🟦 Bonds: Corporate bonds, government bonds, and emerging market bonds.
- 🟥 Stocks: US & Canada — US stocks and the closely correlated Canadian market.
- 🟧 Stocks: EU, Asia & Australia — Euro Stoxx, India, Australia, and Japan.
-I then went through the historic data of the last ten years and looked into the fluctuations of the ETFs. In the tool
-below, you set how much risk each group should contribute — and the math finds the capital allocation that matches.
+Set how much risk each group should bear — the tool computes the matching capital split.
`}
@@ -1314,11 +1290,8 @@ below, you set how much risk each group should contribute — and the math finds
{`
### What to notice
-- **Bonds are your shock absorber.** Even a small risk budget for bonds translates into
- a large capital allocation — that's because bonds fluctuate much less than stocks.
-- **Capital ≠ Risk.** Notice how the capital allocation bars look very different from
- the risk contribution bars. Bonds need a lot of capital to "earn" their share of risk,
- while a small stock allocation can dominate portfolio risk.
+- **Bonds are your shock absorber.** Even a small risk budget for bonds translates into a large capital allocation — bonds fluctuate much less than stocks, so they need more capital to "earn" their share of risk.
+- **A little stock goes a long way.** Even a small stock allocation can dominate portfolio risk. That's why the risk budget view matters more than the capital split.
`}
@@ -1329,31 +1302,20 @@ below, you set how much risk each group should contribute — and the math finds
{`
-## What Does This Mean for You?
-
-Three takeaways from our data:
+## Turning This Into a Monthly Savings Plan
-**1. Diversify broadly — don't put all eggs in one basket.**
-Mixing assets that don't move in lockstep reduces risk more than you'd expect.
-Bonds + stocks is the classic pair, but adding different geographies
-(US, Europe, Emerging Markets) helps too.
+The tool above gives you a capital allocation — but this isn't about a single purchase. The idea is to set up an automatic monthly savings plan:
-**2. Don't chase "optimal" allocations.**
-The math shows that expected returns are too noisy to optimize.
-No model, no AI, no guru can reliably predict which asset class will
-outperform next year. Focus on what's measurable: risk.
+1. **Pick a risk budget** that lets you sleep at night. "Equal risk" is a reasonable starting point.
+2. **Split your monthly savings** according to the capital allocation the tool computes. Most European brokers offer free ETF savings plans that execute automatically.
+3. **Rebalance once a year.** Check whether your actual allocation still matches the target. If one group has drifted more than 5 percentage points, sell some of the overweight and buy more of the underweight.
+4. **Don't tinker.** The biggest risk is yourself — panic-selling during a crash or chasing last year's winner. A systematic plan removes that temptation.
-**3. Keep it simple and cheap.**
-A broad mix of low-cost index ETFs — rebalanced once a year — captures
-most of the diversification benefit. Complexity adds fees, not returns.
+This is close to what I do with my own savings. It doesn't require predicting markets, paying for advice, or watching charts. It just requires patience.
---
-*The analysis behind this post is based on 9 European-listed ETFs tracked
-from 2018 to 2026. Cluster assignments were determined by Ward-linkage
-hierarchical clustering on bootstrapped correlation matrices. Risk metrics
-use 252-day annualization. For the full methodology see the
-[analysis notebooks](https://github.com/fretchen/EuropeSmallCapAnalysis).*
+*The analysis is based on 9 European-listed ETFs tracked from 2018 to 2026. Cluster assignments use Ward-linkage hierarchical clustering on bootstrapped correlation matrices. Risk metrics use 252-day annualization. Full methodology: [analysis notebooks](https://github.com/fretchen/EuropeSmallCapAnalysis).*
`}
From 4434ab3a11926b278c223320f1398e06131c6d87 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 12 Mar 2026 17:42:06 -0400
Subject: [PATCH 07/17] Separate into mdx
---
.../blog/etf_diversification_interactive.mdx | 101 ++
.../blog/etf_diversification_interactive.tsx | 1324 -----------------
.../blog/DiversificationRandomWalk.tsx | 573 +++++++
.../blog/PortfolioRiskAllocator.tsx | 549 +++++++
website/components/blog/etfData.ts | 74 +
5 files changed, 1297 insertions(+), 1324 deletions(-)
create mode 100644 website/blog/etf_diversification_interactive.mdx
delete mode 100644 website/blog/etf_diversification_interactive.tsx
create mode 100644 website/components/blog/DiversificationRandomWalk.tsx
create mode 100644 website/components/blog/PortfolioRiskAllocator.tsx
create mode 100644 website/components/blog/etfData.ts
diff --git a/website/blog/etf_diversification_interactive.mdx b/website/blog/etf_diversification_interactive.mdx
new file mode 100644
index 000000000..2f7654442
--- /dev/null
+++ b/website/blog/etf_diversification_interactive.mdx
@@ -0,0 +1,101 @@
+---
+title: "Why is diversification of savings important?"
+publishing_date: "2026-03-08"
+tokenID: 200
+category: "others"
+description: "A blog post that makes the case for diversification of savings instead of putting all your money into a single asset class."
+---
+
+import DiversificationRandomWalk from "../components/blog/DiversificationRandomWalk";
+import PortfolioRiskAllocator from "../components/blog/PortfolioRiskAllocator";
+
+In this blog post, I present a simple, systematic approach to invest money across multiple asset classes. It is flexible enough to fit different risk tolerances and simple enough that it requires minimal time once set up. So it is pretty close to the approach that I use for my own savings.
+
+I will walk you through the advantages of diversification to minimize risk, present you a way to allocate risks across asset classes according to your preference, and then give a few hints how to turn this into a savings plan.
+
+## The Diversification Paradox
+
+Once, you start to put money in the savings account, you might be tempted to look for better returns. And then you will typically have the choice between two
+major investment classes.
+
+- [Bonds](https://en.wikipedia.org/wiki/Bond_(finance)): You lend money to a government or company. In return, you receive regular interest payments (called _coupons_) and get your principal back at a fixed date. They feel safe — steady, boring, predictable — but the returns are modest.
+- [Stocks](https://en.wikipedia.org/wiki/Stock): You buy a small piece of a company. If the company grows, your share grows too; if it stumbles, so does your investment. They feel risky — volatile, unpredictable, scary — but historically they offer higher returns.
+
+If you are a risk averse first-time investor, the safe choice seems obvious — put everything into bonds. But try the simulator below first: drag the slider to mix bonds and stocks. Which one is the mix that fluctuates least?
+
+
+
+If you played with the slider, you probably noticed something counterintuitive. A mix (purple) fluctuates much less than 100% stocks (red) — but also less than 100% bonds (blue). When you combine assets that move independently, their random ups and downs partially cancel. The mix fluctuates less than any single part.
+
+Think of it like packing for unpredictable weather: an umbrella alone covers rain but not sunshine. Adding sunscreen doesn't make the bag heavier — it makes it useful for more situations. This principle is called **diversification**, and it's the closest thing to a free lunch in investing.
+
+## Why Chasing Returns Fails
+
+The simulator above already hinted at a fundamental trade-off: assets that offer higher returns tend to fluctuate more. Stocks historically earn more than bonds — but they also swing harder. This much is common sense.
+
+The trouble starts when you try to be precise about it. _How much_ extra return do stocks actually deliver? And is that enough to justify the extra risk? Once you try to answer these questions with numbers, you run into a wall.
+
+This kind of optimization earned [Harry Markowitz a Nobel Prize in 1990](https://www.nobelprize.org/prizes/economic-sciences/1990/summary/). Banks and fund
+managers have tried his "mean-variance optimization" ever since. And if you tried it too, you would unfortunately discover
+that this optimization **doesn't work in practice.**
+
+The reason is simple. To find the optimal mix, you need two ingredients:
+
+1. How much each asset _fluctuates_ (risk) — this we can measure reasonably well.
+2. How much each asset will _earn in the future_ (expected return) — this we **cannot**.
+
+Estimating future returns from past data is like predicting next year's
+weather from last year's diary. The mathematician Robert Merton showed in 1980 that you'd need
+**80 to 100 years** of data to estimate stock returns with any confidence.
+
+The good news? We _can_ measure risk (volatility, correlations) much more
+reliably. So instead of chasing the "best return", we focus on
+the thing we _can_ control: **risk**.
+
+## How to actually buy "bonds" and "stocks"
+
+So far we've talked about bonds and stocks as abstract categories. But you can't walk into a shop and buy "some bonds". You need a concrete product — and this is where **ETFs** (Exchange-Traded Funds) come in.
+
+An [ETF](https://en.wikipedia.org/wiki/Exchange-traded_fund) is a fund that holds a basket of assets — hundreds or even thousands of individual bonds or stocks — and trades on a stock exchange like a single share. When you buy one share of an S&P 500 ETF, you instantly own a tiny slice of the 500 largest US companies.
+
+Why are ETFs so popular?
+
+- **Diversification in one click.** Instead of picking individual companies, you get the whole market (or a large chunk of it).
+- **Low fees.** Because ETFs simply track an index rather than paying a manager to pick winners, annual costs are often below 0.2%.
+- **Easy to trade.** You buy and sell them through any regular broker, just like a stock. Most European brokers even offer free monthly savings plans for popular ETFs.
+
+In the next section, we'll use 9 specific ETFs that cover bonds and stocks across different regions. You don't need to memorize them — the tool does the math. But it helps to know that each colored bar in the tool below represents a _group_ of ETFs, not a single company or government.
+
+## Allocating Risk Across Asset Classes
+
+Since we can measure risk reliably, we can use it as a building block: decide how much risk each asset group should contribute, and let the math determine the capital allocation.
+
+The tool below uses 9 European-listed ETFs grouped into three clusters based on how they move together:
+
+- 🟦 Bonds from European corporations and governments as well as governments in emerging markets.
+- 🟥 Stocks for the US and the closely correlated Canadian market.
+- 🟧 Stocks from Europe, Asia & Australia.
+
+Below you can set how much risk each group should bear — the tool computes the matching capital split.
+
+
+
+If you tried to use the tool, you might have noticed a few patterns:
+
+- **Bonds dominate capital, not risk.** Most presets allocate the majority of capital to bonds. That's not a mistake — bonds fluctuate so little that they need a lot of capital to "earn" their share of risk.
+- **Even the safest portfolio holds stocks.** Try "Minimum risk": it's mostly bonds, but it still includes a small slice of equities. That small slice actually _lowers_ total risk — the same diversification effect you saw in the simulator above.
+- **Your risk choice matters — a lot.** Compare "Minimum risk" to "Growth": annual volatility roughly quadruples. This isn't fine-tuning — it's the single most important decision in your savings plan.
+
+## Turning This Into a Monthly Savings Plan
+
+The tool above gives you a capital allocation — but this isn't about a single purchase. The idea is to set up an automatic monthly savings plan:
+
+1. **Pick a risk budget** that lets you sleep at night. "Equal risk" is a reasonable starting point for people that have a long time to sit out some swings.
+2. **Split your monthly savings** according to the capital allocation the tool computes. Most European brokers offer free ETF savings plans that execute automatically.
+3. **Don't tinker.** The biggest risk is yourself — panic-selling during a crash or chasing last year's winner. A systematic plan removes that temptation.
+
+This is close to what I do with my own savings. It doesn't require predicting markets, paying for advice, or watching charts. It just requires patience.
+
+---
+
+_The analysis is based on 9 European-listed ETFs tracked from 2018 to 2026. Cluster assignments use Ward-linkage hierarchical clustering on bootstrapped correlation matrices. Risk metrics use 252-day annualization. Full methodology: [analysis notebooks](https://github.com/fretchen/EuropeSmallCapAnalysis)._
diff --git a/website/blog/etf_diversification_interactive.tsx b/website/blog/etf_diversification_interactive.tsx
deleted file mode 100644
index 78a03e2d4..000000000
--- a/website/blog/etf_diversification_interactive.tsx
+++ /dev/null
@@ -1,1324 +0,0 @@
-import React, { useState, useMemo, useRef, useCallback, useEffect } from "react";
-import { Line } from "react-chartjs-2";
-import {
- Chart as ChartJS,
- CategoryScale,
- LinearScale,
- PointElement,
- LineElement,
- Title,
- Tooltip,
- Legend,
-} from "chart.js";
-import { css } from "../styled-system/css";
-import { MarkdownWithLatex } from "../components/MarkdownWithLatex";
-
-ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
-
-// ============================================================
-// DATA PAYLOAD — extracted from NB02 & NB07
-// 12 ETFs across 3 clusters (Ward-Linkage, k=3)
-// All prices converted to EUR, annualized (252 trading days)
-// ============================================================
-const DATA = {
- clusters: [
- { name: "Bonds", etfs: ["IBCS", "EUNH", "3SUD"], color: "#4e79a7" },
- {
- name: "Stocks: US & Canada",
- etfs: ["SXR8", "CSCA"],
- color: "#e15759",
- },
- {
- name: "Stocks: EU, Asia & Australia",
- etfs: ["SXRT", "NDIA", "SAUS", "CSJP"],
- color: "#f28e2b",
- },
- ],
- // 3×3 annualized covariance matrix (cluster-level equal-weighted returns)
- cov_cluster_ann: [
- [0.00223955, 0.00109166, 0.00127042],
- [0.00109166, 0.03516473, 0.01496456],
- [0.00127042, 0.01496456, 0.01637447],
- ],
- vol_cluster_ann: [0.047324, 0.187523, 0.127963],
- corr_cluster: [
- [1.0, 0.123, 0.2098],
- [0.123, 1.0, 0.6236],
- [0.2098, 0.6236, 1.0],
- ],
- crises: {
- "Corona 2020": { drawdowns: [-0.0663, -0.3836, -0.3355] },
- "Ukraine 2022": { drawdowns: [-0.1681, -0.1192, -0.1299] },
- },
- return_uncertainty: {
- premium_ann: [-0.017068, 0.082034, 0.054618],
- se_ann: [0.018455, 0.07313, 0.049903],
- },
- part1_example: {
- vol_100_bonds: 0.0473,
- vol_80_20: 0.0565,
- vol_50_50: 0.0995,
- vol_100_stocks: 0.1875,
- corr_bonds_stocks: 0.123,
- },
-
- // 9×9 Ledoit-Wolf annualized covariance matrix (9-ETF subset, removing EXXY, IQQ6, 4BRZ)
- // Order: IBCS, EUNH, SXR8, SXRT, 3SUD, NDIA, SAUS, CSJP, CSCA
- etf_names: ["IBCS", "EUNH", "SXR8", "SXRT", "3SUD", "NDIA", "SAUS", "CSJP", "CSCA"],
- etf_cluster: [0, 0, 1, 2, 0, 2, 2, 2, 1], // index into clusters[]
- etf_labels: [
- "EU Corporate Bonds",
- "EU Government Bonds",
- "S&P 500",
- "Euro Stoxx 50",
- "EM Bonds",
- "India",
- "Australia",
- "Japan",
- "Canada",
- ],
- cov_etf_ann: [
- [0.00235705, 0.00188179, 0.00101898, 0.00116085, 0.00153483, 0.0012564, 0.00201358, 0.00177889, 0.00117574],
- [0.00188179, 0.00414109, 0.00102919, 0.00027942, 0.00172953, 0.0003092, 0.00087098, 0.00145604, 0.00091051],
- [0.00101898, 0.00102919, 0.05870897, 0.01795636, 0.01371741, 0.01500817, 0.0169256, 0.00912919, 0.04577796],
- [0.00116085, 0.00027942, 0.01795636, 0.03857897, 0.00564821, 0.01384506, 0.0194126, 0.00793152, 0.02163545],
- [0.00153483, 0.00172953, 0.01371741, 0.00564821, 0.00944084, 0.0080297, 0.00815381, 0.00644341, 0.01361257],
- [0.0012564, 0.0003092, 0.01500817, 0.01384506, 0.0080297, 0.03787152, 0.01661183, 0.01099622, 0.01826119],
- [0.00201358, 0.00087098, 0.0169256, 0.0194126, 0.00815381, 0.01661183, 0.04185897, 0.02076469, 0.02349577],
- [0.00177889, 0.00145604, 0.00912919, 0.00793152, 0.00644341, 0.01099622, 0.02076469, 0.0396158, 0.01126801],
- [0.00117574, 0.00091051, 0.04577796, 0.02163545, 0.01361257, 0.01826119, 0.02349577, 0.01126801, 0.05142616],
- ],
-};
-
-// (Placeholder component removed — replaced by PortfolioRiskAllocator)
-
-// ============================================================
-// Random Walk helpers
-// ============================================================
-const gaussianRandom = (): number => {
- let u = 0;
- let v = 0;
- while (u === 0) u = Math.random();
- while (v === 0) v = Math.random();
- return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
-};
-
-const STEPS = 504; // 2 trading years — more points for smoother histogram
-const ANIM_BATCH = 8; // steps drawn per frame
-const ANIM_INTERVAL = 25; // ms between frames
-
-interface Paths {
- bondReturns: number[];
- stockReturns: number[];
-}
-
-/** Generate correlated daily returns for bonds and stocks.
- * Uses Cholesky decomposition for the 2×2 case. */
-function generatePaths(sigBond: number, sigStock: number, rho: number): Paths {
- const dailySigBond = sigBond / Math.sqrt(STEPS);
- const dailySigStock = sigStock / Math.sqrt(STEPS);
- const bondReturns: number[] = [];
- const stockReturns: number[] = [];
- for (let i = 0; i < STEPS; i++) {
- const z1 = gaussianRandom();
- const z2 = gaussianRandom();
- bondReturns.push(dailySigBond * z1);
- stockReturns.push(dailySigStock * (rho * z1 + Math.sqrt(1 - rho * rho) * z2));
- }
- return { bondReturns, stockReturns };
-}
-
-/** Compute cumulative price path starting at 100. */
-function cumulativePath(dailyReturns: number[], steps: number): number[] {
- const path = [100];
- for (let i = 0; i < steps; i++) {
- path.push(path[i] * (1 + dailyReturns[i]));
- }
- return path;
-}
-
-/** Annualized volatility measured from daily returns. */
-function measuredVol(dailyReturns: number[], steps: number): number {
- const slice = dailyReturns.slice(0, steps);
- const mean = slice.reduce((a, b) => a + b, 0) / slice.length;
- const variance = slice.reduce((s, r) => s + (r - mean) ** 2, 0) / slice.length;
- return Math.sqrt(variance * STEPS) * 100; // percent, annualized
-}
-
-// ============================================================
-// Live-filling SVG Histogram (vertical / rotated — sits beside the line chart)
-// ============================================================
-const HIST_BINS = 25;
-const HIST_RANGE = 0.025; // ±2.5% daily return range
-
-interface HistogramProps {
- bondReturns: number[];
- stockReturns: number[];
- mixReturns: number[];
- visibleSteps: number;
-}
-
-function ReturnHistogram({ bondReturns, stockReturns, mixReturns, visibleSteps }: HistogramProps) {
- const width = 120;
- const height = 300; // matches the line-chart height
- const margin = { top: 6, right: 4, bottom: 6, left: 4 };
- const plotW = width - margin.left - margin.right;
- const plotH = height - margin.top - margin.bottom;
- const binHeight = plotH / HIST_BINS;
- const binSize = (2 * HIST_RANGE) / HIST_BINS;
-
- const toBins = (returns: number[], steps: number): number[] => {
- const bins = new Array(HIST_BINS).fill(0);
- for (let i = 0; i < steps; i++) {
- const idx = Math.floor((returns[i] + HIST_RANGE) / binSize);
- if (idx >= 0 && idx < HIST_BINS) bins[idx]++;
- }
- return bins;
- };
-
- const bondBins = toBins(bondReturns, visibleSteps);
- const stockBins = toBins(stockReturns, visibleSteps);
- const mixBins = toBins(mixReturns, visibleSteps);
- const maxCount = Math.max(1, ...bondBins, ...stockBins, ...mixBins);
-
- // Render order: stocks (back), bonds, mix (front)
- const series = [
- { bins: stockBins, color: DATA.clusters[1].color, opacity: 0.35, label: "Stocks" },
- { bins: bondBins, color: DATA.clusters[0].color, opacity: 0.45, label: "Bonds" },
- { bins: mixBins, color: "#7b3fa0", opacity: 0.6, label: "Mix" },
- ];
-
- // Y-axis ticks (return percentages — bottom=negative, top=positive to match price chart)
- const ticks = [-2, -1, 0, 1, 2].map((pct) => ({
- pct,
- // bin 0 = most negative, bin N-1 = most positive
- // invert so positive is at top
- y: margin.top + plotH - ((pct / 100 + HIST_RANGE) / (2 * HIST_RANGE)) * plotH,
- }));
-
- return (
-
- );
-}
-
-// ============================================================
-// DiversificationRandomWalk component
-// ============================================================
-// Pedagogical parameters — strongly exaggerated for visual clarity
-// (real data: σ_bond=4.6%, σ_stock=20.1%, ρ=+0.17)
-// With these values, σ_mix ≈ 6% at 50/50 — clearly less than either alone
-const PEDAGOGY = {
- sigBond: 0.15, // 15% — inflated so bonds are clearly visible
- sigStock: 0.2, // 20%
- rho: -0.8, // strongly anti-correlated — mix cancels most fluctuation
-};
-
-function DiversificationRandomWalk() {
- const [stockPct, setStockPct] = useState(50);
- const [hasMovedSlider, setHasMovedSlider] = useState(false);
- const [paths, setPaths] = useState(null);
- const [visibleSteps, setVisibleSteps] = useState(0);
- const [isAnimating, setIsAnimating] = useState(false);
- const animRef = useRef | null>(null);
-
- const sigBond = PEDAGOGY.sigBond;
- const sigStock = PEDAGOGY.sigStock;
- const rho = PEDAGOGY.rho;
-
- const stopAnimation = useCallback(() => {
- if (animRef.current) {
- clearInterval(animRef.current);
- animRef.current = null;
- }
- setIsAnimating(false);
- }, []);
-
- // Cleanup on unmount
- useEffect(() => stopAnimation, [stopAnimation]);
-
- // Auto-start animation on mount
- useEffect(() => {
- const fresh = generatePaths(sigBond, sigStock, rho);
- setPaths(fresh);
- setVisibleSteps(0);
- setIsAnimating(true);
- let step = 0;
- animRef.current = setInterval(() => {
- step = Math.min(step + ANIM_BATCH, STEPS);
- setVisibleSteps(step);
- if (step >= STEPS) {
- clearInterval(animRef.current!);
- animRef.current = null;
- setIsAnimating(false);
- }
- }, ANIM_INTERVAL);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const startAnimation = useCallback(
- (newPaths?: Paths) => {
- stopAnimation();
- const p = newPaths ?? paths;
- if (!p) {
- const fresh = generatePaths(sigBond, sigStock, rho);
- setPaths(fresh);
- setVisibleSteps(0);
- setIsAnimating(true);
- // defer interval to next tick so state is set
- setTimeout(() => {
- let step = 0;
- animRef.current = setInterval(() => {
- step = Math.min(step + ANIM_BATCH, STEPS);
- setVisibleSteps(step);
- if (step >= STEPS) {
- clearInterval(animRef.current!);
- animRef.current = null;
- setIsAnimating(false);
- }
- }, ANIM_INTERVAL);
- }, 0);
- return;
- }
- setVisibleSteps(0);
- setIsAnimating(true);
- let step = 0;
- animRef.current = setInterval(() => {
- step = Math.min(step + ANIM_BATCH, STEPS);
- setVisibleSteps(step);
- if (step >= STEPS) {
- clearInterval(animRef.current!);
- animRef.current = null;
- setIsAnimating(false);
- }
- }, ANIM_INTERVAL);
- },
- [paths, sigBond, sigStock, rho, stopAnimation],
- );
-
- const handleNewPaths = useCallback(() => {
- const fresh = generatePaths(sigBond, sigStock, rho);
- setPaths(fresh);
- startAnimation(fresh);
- }, [sigBond, sigStock, rho, startAnimation]);
-
- // Derived data for chart
- const w = stockPct / 100;
- const stepsToShow = paths ? visibleSteps : 0;
- const labels = Array.from({ length: stepsToShow + 1 }, (_, i) => {
- if (i === 0) return "Start";
- if (i === STEPS) return "24 months";
- const month = Math.floor((i / STEPS) * 24);
- return month > 0 && i % Math.round(STEPS / 24) === 0 ? `${month} mo` : "";
- });
-
- const bondPath = paths ? cumulativePath(paths.bondReturns, stepsToShow) : [];
- const stockPath = paths ? cumulativePath(paths.stockReturns, stepsToShow) : [];
- const mixReturns = paths ? paths.bondReturns.map((br, i) => (1 - w) * br + w * paths.stockReturns[i]) : [];
- const mixPath = paths ? cumulativePath(mixReturns, stepsToShow) : [];
-
- const chartData = {
- labels,
- datasets: [
- {
- label: "Bonds only",
- data: bondPath,
- borderColor: DATA.clusters[0].color,
- backgroundColor: "transparent",
- borderWidth: 2,
- pointRadius: 0,
- pointStyle: "line" as const,
- tension: 0.1,
- },
- {
- label: "Stocks only",
- data: stockPath,
- borderColor: DATA.clusters[1].color,
- backgroundColor: "transparent",
- borderWidth: 2,
- pointRadius: 0,
- pointStyle: "line" as const,
- tension: 0.1,
- },
- {
- label: `Mix (${100 - stockPct}/${stockPct})`,
- data: mixPath,
- borderColor: "#7b3fa0",
- backgroundColor: "transparent",
- borderWidth: 3,
- pointRadius: 0,
- pointStyle: "line" as const,
- tension: 0.1,
- },
- ],
- };
-
- const chartOptions = {
- responsive: true,
- maintainAspectRatio: false,
- animation: { duration: 0 },
- plugins: {
- legend: { position: "top" as const, labels: { font: { size: 12 }, usePointStyle: true, pointStyleWidth: 20 } },
- title: { display: false },
- },
- scales: {
- x: {
- title: { display: true, text: "Months", font: { size: 11 } },
- ticks: {
- maxTicksLimit: 13,
- font: { size: 10 },
- callback: function (_: unknown, index: number) {
- return labels[index] || null;
- },
- },
- },
- y: {
- title: {
- display: true,
- text: "Portfolio value (€)",
- font: { size: 11 },
- },
- ticks: { font: { size: 10 } },
- suggestedMin: 70,
- suggestedMax: 130,
- },
- },
- };
-
- // Measured volatilities (only meaningful when animation finished)
- const showVol = stepsToShow >= STEPS && paths;
- const bondVol = showVol ? measuredVol(paths.bondReturns, STEPS) : null;
- const stockVol = showVol ? measuredVol(paths.stockReturns, STEPS) : null;
- const mixVol = showVol ? measuredVol(mixReturns, STEPS) : null;
-
- return (
-
-
- How does mixing bonds and stocks look in practice?
-
-
- Each line shows a possible two-year journey of €100. Parameters are exaggerated for clarity (bonds and stocks
- move in opposite directions here). Watch how the mix (purple) is smoother than either alone.
-
- Left: cumulative portfolio value over time. Right: distribution of daily returns — narrower means less
- fluctuation. Notice how the purple mix is narrower than both bonds and stocks individually.
-
- );
-}
-
-// ============================================================
-// Metadata
-// ============================================================
-export const meta = {
- title: "Why is diversification of savings important?",
- publishing_date: "2026-03-08",
- tokenID: 200,
- category: "others",
- description:
- "A blog post that makes the case for diversification of savings instead of putting all your money into a single asset class.",
-};
-
-// ============================================================
-// Blog Post
-// ============================================================
-export default function ETFDiversification() {
- return (
-
- {/* ============================== */}
- {/* THE PARADOX */}
- {/* ============================== */}
-
-
- {`
-## A Systematic Approach to Saving
-
-Most people keep their savings in a bank account, invest in a single asset class, or avoid the topic altogether. This post outlines a systematic approach to diversifying savings — one that doesn't require predicting markets or paying expensive advisors. It's close to what I actually do.
-
-## The Diversification Paradox
-
-You have savings you'd like to grow, but you don't want to gamble. The savings account barely keeps up with inflation. So what are your options?
-
-- Bonds feel safe: steady, boring, predictable.
-- Stocks feel risky: volatile, unpredictable, scary.
-
-The safe choice seems obvious — put everything into bonds. But try the simulator below first: drag the slider to mix bonds and stocks. Can you find the mix that fluctuates least?
-`}
-
-
-
-
-
- {`
-
-If you played with the slider, you probably noticed something surprising: a 50/50 mix (purple) fluctuates much less than 100% stocks (red) — but also less than 100% bonds (blue). When you combine assets that move independently, their random ups and downs partially cancel. The mix fluctuates less than any single part.
-
-Think of it like packing for unpredictable weather: an umbrella alone covers rain but not sunshine. Adding sunscreen doesn't make the bag heavier — it makes it useful for more situations. This principle is called **diversification**, and it's the closest thing to a free lunch in investing.
-
-## Why Chasing Returns Fails
-
-Once you accept that diversification helps, the next instinct is to find the *optimal* mix — the one that maximizes return for a given level of risk.
-
-This kind of optimization earned [Harry Markowitz a Nobel Prize in 1990](https://www.nobelprize.org/prizes/economic-sciences/1990/summary/). Banks and fund
-managers have tried his "mean-variance optimization" ever since. And if you tried it too, you would unfortunately discover
-that this optimization **doesn't work in practice.**
-
-The reason is simple. To find the optimal mix, you need two ingredients:
-1. How much each asset *fluctuates* (risk) — this we can measure reasonably well.
-2. How much each asset will *earn in the future* (expected return) — this we **cannot**.
-
-Estimating future returns from past data is like predicting next year's
-weather from last year's diary. The mathematician Robert Merton showed in 1980 that you'd need
-**80 to 100 years** of data to estimate stock returns with any confidence.
-
-> "Optimizing" with unreliable inputs doesn't give you the best portfolio —
-> it gives you the portfolio most sensitive to estimation errors.
-
-The good news? We *can* measure risk (volatility, correlations) much more
-reliably. So instead of chasing the "best return", we focus on
-the thing we *can* control: **risk**.
-
-## Allocating Risk Across Asset Classes
-
-Since we can measure risk reliably, we can use it as a building block: decide how much risk each asset group should contribute, and let the math determine the capital allocation.
-
-The tool below uses 9 European-listed ETFs grouped into three clusters based on how they move together:
-
-- 🟦 Bonds: Corporate bonds, government bonds, and emerging market bonds.
-- 🟥 Stocks: US & Canada — US stocks and the closely correlated Canadian market.
-- 🟧 Stocks: EU, Asia & Australia — Euro Stoxx, India, Australia, and Japan.
-
-Set how much risk each group should bear — the tool computes the matching capital split.
-
-`}
-
-
-
-
-
- {`
-### What to notice
-
-- **Bonds are your shock absorber.** Even a small risk budget for bonds translates into a large capital allocation — bonds fluctuate much less than stocks, so they need more capital to "earn" their share of risk.
-- **A little stock goes a long way.** Even a small stock allocation can dominate portfolio risk. That's why the risk budget view matters more than the capital split.
-`}
-
-
-
- {/* ============================== */}
- {/* TAKEAWAYS */}
- {/* ============================== */}
-
-
- {`
-## Turning This Into a Monthly Savings Plan
-
-The tool above gives you a capital allocation — but this isn't about a single purchase. The idea is to set up an automatic monthly savings plan:
-
-1. **Pick a risk budget** that lets you sleep at night. "Equal risk" is a reasonable starting point.
-2. **Split your monthly savings** according to the capital allocation the tool computes. Most European brokers offer free ETF savings plans that execute automatically.
-3. **Rebalance once a year.** Check whether your actual allocation still matches the target. If one group has drifted more than 5 percentage points, sell some of the overweight and buy more of the underweight.
-4. **Don't tinker.** The biggest risk is yourself — panic-selling during a crash or chasing last year's winner. A systematic plan removes that temptation.
-
-This is close to what I do with my own savings. It doesn't require predicting markets, paying for advice, or watching charts. It just requires patience.
-
----
-
-*The analysis is based on 9 European-listed ETFs tracked from 2018 to 2026. Cluster assignments use Ward-linkage hierarchical clustering on bootstrapped correlation matrices. Risk metrics use 252-day annualization. Full methodology: [analysis notebooks](https://github.com/fretchen/EuropeSmallCapAnalysis).*
-`}
-
-
-
- );
-}
diff --git a/website/components/blog/DiversificationRandomWalk.tsx b/website/components/blog/DiversificationRandomWalk.tsx
new file mode 100644
index 000000000..b5648b812
--- /dev/null
+++ b/website/components/blog/DiversificationRandomWalk.tsx
@@ -0,0 +1,573 @@
+import React, { useState, useRef, useCallback, useEffect } from "react";
+import { Line } from "react-chartjs-2";
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend,
+} from "chart.js";
+import { css } from "../../styled-system/css";
+import { DATA } from "./etfData";
+
+ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
+
+// ============================================================
+// Random Walk helpers
+// ============================================================
+const gaussianRandom = (): number => {
+ let u = 0;
+ let v = 0;
+ while (u === 0) u = Math.random();
+ while (v === 0) v = Math.random();
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
+};
+
+const STEPS = 504; // 2 trading years — more points for smoother histogram
+const ANIM_BATCH = 8; // steps drawn per frame
+const ANIM_INTERVAL = 25; // ms between frames
+
+interface Paths {
+ bondReturns: number[];
+ stockReturns: number[];
+}
+
+/** Generate correlated daily returns for bonds and stocks.
+ * Uses Cholesky decomposition for the 2×2 case. */
+function generatePaths(sigBond: number, sigStock: number, rho: number): Paths {
+ const dailySigBond = sigBond / Math.sqrt(STEPS);
+ const dailySigStock = sigStock / Math.sqrt(STEPS);
+ const bondReturns: number[] = [];
+ const stockReturns: number[] = [];
+ for (let i = 0; i < STEPS; i++) {
+ const z1 = gaussianRandom();
+ const z2 = gaussianRandom();
+ bondReturns.push(dailySigBond * z1);
+ stockReturns.push(dailySigStock * (rho * z1 + Math.sqrt(1 - rho * rho) * z2));
+ }
+ return { bondReturns, stockReturns };
+}
+
+/** Compute cumulative price path starting at 100. */
+function cumulativePath(dailyReturns: number[], steps: number): number[] {
+ const path = [100];
+ for (let i = 0; i < steps; i++) {
+ path.push(path[i] * (1 + dailyReturns[i]));
+ }
+ return path;
+}
+
+/** Annualized volatility measured from daily returns. */
+function measuredVol(dailyReturns: number[], steps: number): number {
+ const slice = dailyReturns.slice(0, steps);
+ const mean = slice.reduce((a, b) => a + b, 0) / slice.length;
+ const variance = slice.reduce((s, r) => s + (r - mean) ** 2, 0) / slice.length;
+ return Math.sqrt(variance * STEPS) * 100; // percent, annualized
+}
+
+// ============================================================
+// Live-filling SVG Histogram (vertical / rotated — sits beside the line chart)
+// ============================================================
+const HIST_BINS = 25;
+const HIST_RANGE = 0.025; // ±2.5% daily return range
+
+interface HistogramProps {
+ bondReturns: number[];
+ stockReturns: number[];
+ mixReturns: number[];
+ visibleSteps: number;
+}
+
+function ReturnHistogram({ bondReturns, stockReturns, mixReturns, visibleSteps }: HistogramProps) {
+ const width = 120;
+ const height = 300; // matches the line-chart height
+ const margin = { top: 6, right: 4, bottom: 6, left: 4 };
+ const plotW = width - margin.left - margin.right;
+ const plotH = height - margin.top - margin.bottom;
+ const binHeight = plotH / HIST_BINS;
+ const binSize = (2 * HIST_RANGE) / HIST_BINS;
+
+ const toBins = (returns: number[], steps: number): number[] => {
+ const bins = new Array(HIST_BINS).fill(0);
+ for (let i = 0; i < steps; i++) {
+ const idx = Math.floor((returns[i] + HIST_RANGE) / binSize);
+ if (idx >= 0 && idx < HIST_BINS) bins[idx]++;
+ }
+ return bins;
+ };
+
+ const bondBins = toBins(bondReturns, visibleSteps);
+ const stockBins = toBins(stockReturns, visibleSteps);
+ const mixBins = toBins(mixReturns, visibleSteps);
+ const maxCount = Math.max(1, ...bondBins, ...stockBins, ...mixBins);
+
+ // Render order: stocks (back), bonds, mix (front)
+ const series = [
+ { bins: stockBins, color: DATA.clusters[1].color, opacity: 0.35, label: "Stocks" },
+ { bins: bondBins, color: DATA.clusters[0].color, opacity: 0.45, label: "Bonds" },
+ { bins: mixBins, color: "#7b3fa0", opacity: 0.6, label: "Mix" },
+ ];
+
+ // Y-axis ticks (return percentages — bottom=negative, top=positive to match price chart)
+ const ticks = [-2, -1, 0, 1, 2].map((pct) => ({
+ pct,
+ // bin 0 = most negative, bin N-1 = most positive
+ // invert so positive is at top
+ y: margin.top + plotH - ((pct / 100 + HIST_RANGE) / (2 * HIST_RANGE)) * plotH,
+ }));
+
+ return (
+
+ );
+}
+
+// ============================================================
+// DiversificationRandomWalk component
+// ============================================================
+// Pedagogical parameters — strongly exaggerated for visual clarity
+// (real data: σ_bond=4.6%, σ_stock=20.1%, ρ=+0.17)
+// With these values, σ_mix ≈ 6% at 50/50 — clearly less than either alone
+const PEDAGOGY = {
+ sigBond: 0.15, // 15% — inflated so bonds are clearly visible
+ sigStock: 0.2, // 20%
+ rho: -0.8, // strongly anti-correlated — mix cancels most fluctuation
+};
+
+export default function DiversificationRandomWalk() {
+ const [stockPct, setStockPct] = useState(50);
+ const [hasMovedSlider, setHasMovedSlider] = useState(false);
+ const [paths, setPaths] = useState(null);
+ const [visibleSteps, setVisibleSteps] = useState(0);
+ const [isAnimating, setIsAnimating] = useState(false);
+ const animRef = useRef | null>(null);
+
+ const sigBond = PEDAGOGY.sigBond;
+ const sigStock = PEDAGOGY.sigStock;
+ const rho = PEDAGOGY.rho;
+
+ const stopAnimation = useCallback(() => {
+ if (animRef.current) {
+ clearInterval(animRef.current);
+ animRef.current = null;
+ }
+ setIsAnimating(false);
+ }, []);
+
+ // Cleanup on unmount
+ useEffect(() => stopAnimation, [stopAnimation]);
+
+ // Auto-start animation on mount
+ useEffect(() => {
+ const fresh = generatePaths(sigBond, sigStock, rho);
+ setPaths(fresh);
+ setVisibleSteps(0);
+ setIsAnimating(true);
+ let step = 0;
+ animRef.current = setInterval(() => {
+ step = Math.min(step + ANIM_BATCH, STEPS);
+ setVisibleSteps(step);
+ if (step >= STEPS) {
+ clearInterval(animRef.current!);
+ animRef.current = null;
+ setIsAnimating(false);
+ }
+ }, ANIM_INTERVAL);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const startAnimation = useCallback(
+ (newPaths?: Paths) => {
+ stopAnimation();
+ const p = newPaths ?? paths;
+ if (!p) {
+ const fresh = generatePaths(sigBond, sigStock, rho);
+ setPaths(fresh);
+ setVisibleSteps(0);
+ setIsAnimating(true);
+ // defer interval to next tick so state is set
+ setTimeout(() => {
+ let step = 0;
+ animRef.current = setInterval(() => {
+ step = Math.min(step + ANIM_BATCH, STEPS);
+ setVisibleSteps(step);
+ if (step >= STEPS) {
+ clearInterval(animRef.current!);
+ animRef.current = null;
+ setIsAnimating(false);
+ }
+ }, ANIM_INTERVAL);
+ }, 0);
+ return;
+ }
+ setVisibleSteps(0);
+ setIsAnimating(true);
+ let step = 0;
+ animRef.current = setInterval(() => {
+ step = Math.min(step + ANIM_BATCH, STEPS);
+ setVisibleSteps(step);
+ if (step >= STEPS) {
+ clearInterval(animRef.current!);
+ animRef.current = null;
+ setIsAnimating(false);
+ }
+ }, ANIM_INTERVAL);
+ },
+ [paths, sigBond, sigStock, rho, stopAnimation],
+ );
+
+ const handleNewPaths = useCallback(() => {
+ const fresh = generatePaths(sigBond, sigStock, rho);
+ setPaths(fresh);
+ startAnimation(fresh);
+ }, [sigBond, sigStock, rho, startAnimation]);
+
+ // Derived data for chart
+ const w = stockPct / 100;
+ const stepsToShow = paths ? visibleSteps : 0;
+ const labels = Array.from({ length: stepsToShow + 1 }, (_, i) => {
+ if (i === 0) return "Start";
+ if (i === STEPS) return "24 months";
+ const month = Math.floor((i / STEPS) * 24);
+ return month > 0 && i % Math.round(STEPS / 24) === 0 ? `${month} mo` : "";
+ });
+
+ const bondPath = paths ? cumulativePath(paths.bondReturns, stepsToShow) : [];
+ const stockPath = paths ? cumulativePath(paths.stockReturns, stepsToShow) : [];
+ const mixReturns = paths ? paths.bondReturns.map((br, i) => (1 - w) * br + w * paths.stockReturns[i]) : [];
+ const mixPath = paths ? cumulativePath(mixReturns, stepsToShow) : [];
+
+ const chartData = {
+ labels,
+ datasets: [
+ {
+ label: "Bonds only",
+ data: bondPath,
+ borderColor: DATA.clusters[0].color,
+ backgroundColor: "transparent",
+ borderWidth: 2,
+ pointRadius: 0,
+ pointStyle: "line" as const,
+ tension: 0.1,
+ },
+ {
+ label: "Stocks only",
+ data: stockPath,
+ borderColor: DATA.clusters[1].color,
+ backgroundColor: "transparent",
+ borderWidth: 2,
+ pointRadius: 0,
+ pointStyle: "line" as const,
+ tension: 0.1,
+ },
+ {
+ label: `Mix (${100 - stockPct}/${stockPct})`,
+ data: mixPath,
+ borderColor: "#7b3fa0",
+ backgroundColor: "transparent",
+ borderWidth: 3,
+ pointRadius: 0,
+ pointStyle: "line" as const,
+ tension: 0.1,
+ },
+ ],
+ };
+
+ const chartOptions = {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: { duration: 0 },
+ plugins: {
+ legend: { position: "top" as const, labels: { font: { size: 12 }, usePointStyle: true, pointStyleWidth: 20 } },
+ title: { display: false },
+ },
+ scales: {
+ x: {
+ title: { display: true, text: "Months", font: { size: 11 } },
+ ticks: {
+ maxTicksLimit: 13,
+ font: { size: 10 },
+ callback: function (_: unknown, index: number) {
+ return labels[index] || null;
+ },
+ },
+ },
+ y: {
+ title: {
+ display: true,
+ text: "Portfolio value (€)",
+ font: { size: 11 },
+ },
+ ticks: { font: { size: 10 } },
+ suggestedMin: 70,
+ suggestedMax: 130,
+ },
+ },
+ };
+
+ // Measured volatilities (only meaningful when animation finished)
+ const showVol = stepsToShow >= STEPS && paths;
+ const bondVol = showVol ? measuredVol(paths.bondReturns, STEPS) : null;
+ const stockVol = showVol ? measuredVol(paths.stockReturns, STEPS) : null;
+ const mixVol = showVol ? measuredVol(mixReturns, STEPS) : null;
+
+ return (
+
+
+ How does mixing bonds and stocks look in practice?
+
+
+ Each line shows a possible two-year journey of €100. Parameters are exaggerated for clarity (bonds and stocks
+ move in opposite directions here). Watch how the mix (purple) is smoother than either alone.
+
+ Left: cumulative portfolio value over time. Right: distribution of daily returns — narrower means less
+ fluctuation. Notice how the purple mix is narrower than both bonds and stocks individually.
+
{(weights[idx] * 100).toFixed(1)}%
diff --git a/website/components/blog/etfData.ts b/website/components/blog/etfData.ts
index dad2e7640..b0e0e52b4 100644
--- a/website/components/blog/etfData.ts
+++ b/website/components/blog/etfData.ts
@@ -60,6 +60,17 @@ export const DATA = {
"Japan",
"Canada",
],
+ etf_urls: [
+ "https://www.ishares.com/uk/individual/en/products/251565/ishares-euro-corporate-bond-large-cap-ucits-etf",
+ "https://www.ishares.com/ch/professionals/en/products/251740/ishares-core-euro-government-bond-ucits-etf",
+ "https://www.ishares.com/ch/professionals/en/products/253743/ishares-core-sp-500-ucits-etf",
+ "https://www.ishares.com/ch/professionals/en/products/251781/ishares-core-euro-stoxx-50-ucits-etf-de",
+ "https://www.ishares.com/uk/individual/en/products/308633/ishares-j-p-morgan-em-bond-ucits-etf",
+ "https://www.ishares.com/ch/professionals/en/products/297617/",
+ "https://www.ishares.com/ch/professionals/en/products/251851/ishares-msci-australia-ucits-etf",
+ "https://www.ishares.com/ch/professionals/en/products/253732/ishares-msci-japan-b-ucits-etf-acc-fund",
+ "https://www.ishares.com/ch/professionals/en/products/253721/ishares-msci-canada-b-ucits-etf",
+ ],
cov_etf_ann: [
[0.00235705, 0.00188179, 0.00101898, 0.00116085, 0.00153483, 0.0012564, 0.00201358, 0.00177889, 0.00117574],
[0.00188179, 0.00414109, 0.00102919, 0.00027942, 0.00172953, 0.0003092, 0.00087098, 0.00145604, 0.00091051],
From 4bfdd1902302c8a285e21ea69ac453724f6dfe85 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 12 Mar 2026 18:28:22 -0400
Subject: [PATCH 09/17] Update PortfolioRiskAllocator.tsx
---
.../blog/PortfolioRiskAllocator.tsx | 29 ++++++++++++-------
1 file changed, 19 insertions(+), 10 deletions(-)
diff --git a/website/components/blog/PortfolioRiskAllocator.tsx b/website/components/blog/PortfolioRiskAllocator.tsx
index 9974f59fc..dd55e854a 100644
--- a/website/components/blog/PortfolioRiskAllocator.tsx
+++ b/website/components/blog/PortfolioRiskAllocator.tsx
@@ -16,7 +16,8 @@ function matVec(cov: number[][], w: number[]): number[] {
return result;
}
-/** Solve risk-budget allocation via Roncalli (2013) multiplicative update. */
+/** Solve risk-budget allocation via Roncalli (2013) multiplicative update.
+ * Per-asset ERC within each cluster: b_i = B_c / n_c (matching Python NB07). */
function solveRiskBudget(cov: number[][], clusterOf: number[], budgets: number[]): number[] {
const n = cov.length;
const nC = budgets.length;
@@ -25,28 +26,36 @@ function solveRiskBudget(cov: number[][], clusterOf: number[], budgets: number[]
const activeCount = active.filter(Boolean).length;
if (activeCount === 0) return new Array(n).fill(1 / n);
+ // Per-asset risk budgets: b_i = B_c / clusterSize_c
+ const clusterSize = new Array(nC).fill(0);
+ for (let i = 0; i < n; i++) if (active[i]) clusterSize[clusterOf[i]]++;
+ const targetRc = new Array(n).fill(0);
+ for (let i = 0; i < n; i++) if (active[i]) targetRc[i] = budgets[clusterOf[i]] / clusterSize[clusterOf[i]];
+
+ // Initialize: w_i ∝ b_i / σ_i (inverse-vol scaled by budget)
let w = new Array(n).fill(0);
- for (let i = 0; i < n; i++) w[i] = active[i] ? 1 / activeCount : 0;
+ for (let i = 0; i < n; i++) if (active[i]) w[i] = targetRc[i] / Math.sqrt(cov[i][i]);
+ const wInit = w.reduce((a, b) => a + b, 0);
+ if (wInit > 0) w = w.map((wi) => wi / wInit);
for (let iter = 0; iter < 5000; iter++) {
const sw = matVec(cov, w);
const rc = w.map((wi, i) => wi * sw[i]);
const totalRc = rc.reduce((a, b) => a + b, 0);
if (totalRc < 1e-15) break;
- const clusterRc = new Array(nC).fill(0);
- for (let i = 0; i < n; i++) clusterRc[clusterOf[i]] += rc[i];
+ // Convergence: per-asset risk contribution vs target
let maxErr = 0;
- for (let c = 0; c < nC; c++)
- if (budgets[c] > 1e-10) maxErr = Math.max(maxErr, Math.abs(clusterRc[c] / totalRc - budgets[c]));
+ for (let i = 0; i < n; i++)
+ if (active[i]) maxErr = Math.max(maxErr, Math.abs(rc[i] / totalRc - targetRc[i]));
if (maxErr < 1e-8) break;
+ // Multiplicative update per asset
for (let i = 0; i < n; i++) {
if (!active[i]) continue;
- const c = clusterOf[i];
- const frac = clusterRc[c] / totalRc;
- if (frac < 1e-15) continue;
- w[i] *= Math.pow(budgets[c] / frac, 0.5);
+ const rcFrac = rc[i] / totalRc;
+ if (rcFrac < 1e-15) continue;
+ w[i] *= Math.pow(targetRc[i] / rcFrac, 0.5);
}
const wSum = w.reduce((a, b) => a + b, 0);
if (wSum > 0) w = w.map((wi) => wi / wSum);
From e16148096aa431e9927c287eb6bdae2c87011561 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 12 Mar 2026 18:29:46 -0400
Subject: [PATCH 10/17] Update PortfolioRiskAllocator.tsx
---
website/components/blog/PortfolioRiskAllocator.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/website/components/blog/PortfolioRiskAllocator.tsx b/website/components/blog/PortfolioRiskAllocator.tsx
index dd55e854a..7aab6d61c 100644
--- a/website/components/blog/PortfolioRiskAllocator.tsx
+++ b/website/components/blog/PortfolioRiskAllocator.tsx
@@ -209,7 +209,7 @@ export default function PortfolioRiskAllocator() {
})}
>
{/* ── Header ── */}
-
Build your portfolio by risk budget
-
+
Date: Fri, 13 Mar 2026 08:25:59 -0400
Subject: [PATCH 11/17] Update etf_diversification_interactive.mdx
---
website/blog/etf_diversification_interactive.mdx | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/website/blog/etf_diversification_interactive.mdx b/website/blog/etf_diversification_interactive.mdx
index 2f7654442..d8aede124 100644
--- a/website/blog/etf_diversification_interactive.mdx
+++ b/website/blog/etf_diversification_interactive.mdx
@@ -96,6 +96,13 @@ The tool above gives you a capital allocation — but this isn't about a single
This is close to what I do with my own savings. It doesn't require predicting markets, paying for advice, or watching charts. It just requires patience.
----
+## Methodology (for the curious)
+
+This analysis uses daily EUR-denominated prices of the 9 iShares ETFs listed above, from 2018 to 2026. A few notes on methodology:
+
+- **Covariance estimation.** The covariance matrix is estimated using [Ledoit-Wolf shrinkage](https://en.wikipedia.org/wiki/Estimation_of_covariance_matrices#Shrinkage_estimation), which stabilizes the estimate when the number of assets is large relative to the number of observations.
+- **Risk as volatility.** Risk is measured as annualized portfolio volatility (standard deviation of returns, scaled by √252 trading days). This is a simplification — it treats upside and downside swings equally — but it is the standard starting point for portfolio construction.
+- **Cluster assignment.** The three asset groups (Bonds, US & Canada, EU/Asia/Australia) were derived using Ward-linkage hierarchical clustering on bootstrapped correlation matrices. One manual adjustment: 3SUD (EM Bonds) was moved from the "Regional Diversifiers" cluster to the Bonds cluster, since it is easier to explain a bond in the bond group.
+- **Risk budgeting.** Capital allocation follows the risk budgeting framework of [Roncalli (2013)](https://doi.org/10.2139/ssrn.2272862). Each asset within a cluster receives an equal share of that cluster's risk budget, and the solver finds weights such that actual risk contributions match the targets.
-_The analysis is based on 9 European-listed ETFs tracked from 2018 to 2026. Cluster assignments use Ward-linkage hierarchical clustering on bootstrapped correlation matrices. Risk metrics use 252-day annualization. Full methodology: [analysis notebooks](https://github.com/fretchen/EuropeSmallCapAnalysis)._
+_The full analysis notebooks are not published yet, but can be made available on request._
From 32fc5f15f81928f57dc7e5a533bdb52878191359 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 13 Mar 2026 08:29:57 -0400
Subject: [PATCH 12/17] Update etf_diversification_interactive.mdx
---
website/blog/etf_diversification_interactive.mdx | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/website/blog/etf_diversification_interactive.mdx b/website/blog/etf_diversification_interactive.mdx
index d8aede124..dc02742a8 100644
--- a/website/blog/etf_diversification_interactive.mdx
+++ b/website/blog/etf_diversification_interactive.mdx
@@ -11,8 +11,6 @@ import PortfolioRiskAllocator from "../components/blog/PortfolioRiskAllocator";
In this blog post, I present a simple, systematic approach to invest money across multiple asset classes. It is flexible enough to fit different risk tolerances and simple enough that it requires minimal time once set up. So it is pretty close to the approach that I use for my own savings.
-I will walk you through the advantages of diversification to minimize risk, present you a way to allocate risks across asset classes according to your preference, and then give a few hints how to turn this into a savings plan.
-
## The Diversification Paradox
Once, you start to put money in the savings account, you might be tempted to look for better returns. And then you will typically have the choice between two
@@ -91,12 +89,15 @@ If you tried to use the tool, you might have noticed a few patterns:
The tool above gives you a capital allocation — but this isn't about a single purchase. The idea is to set up an automatic monthly savings plan:
1. **Pick a risk budget** that lets you sleep at night. "Equal risk" is a reasonable starting point for people that have a long time to sit out some swings.
-2. **Split your monthly savings** according to the capital allocation the tool computes. Most European brokers offer free ETF savings plans that execute automatically.
-3. **Don't tinker.** The biggest risk is yourself — panic-selling during a crash or chasing last year's winner. A systematic plan removes that temptation.
+2. **Split your monthly savings** according to the capital allocation the tool computes. For example, if you save €300/month and the tool says 60% bonds / 25% US stocks / 15% other stocks, that's €180 / €75 / €45. Most European brokers offer free ETF savings plans that execute automatically.
+
+The hardest part isn't choosing — it's sticking with it. The biggest risk is yourself: panic-selling during a crash or chasing last year's winner. A systematic monthly plan removes that temptation.
This is close to what I do with my own savings. It doesn't require predicting markets, paying for advice, or watching charts. It just requires patience.
-## Methodology (for the curious)
+---
+
+## Technical notes
This analysis uses daily EUR-denominated prices of the 9 iShares ETFs listed above, from 2018 to 2026. A few notes on methodology:
From ac8b4f8ffb43a401ec7e2db58a60168177941a2d Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 13 Mar 2026 08:35:14 -0400
Subject: [PATCH 13/17] Update etf_diversification_interactive.mdx
---
website/blog/etf_diversification_interactive.mdx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/website/blog/etf_diversification_interactive.mdx b/website/blog/etf_diversification_interactive.mdx
index dc02742a8..a8a63831b 100644
--- a/website/blog/etf_diversification_interactive.mdx
+++ b/website/blog/etf_diversification_interactive.mdx
@@ -1,9 +1,9 @@
---
-title: "Why is diversification of savings important?"
+title: "How to invest your money without predicting markets"
publishing_date: "2026-03-08"
tokenID: 200
category: "others"
-description: "A blog post that makes the case for diversification of savings instead of putting all your money into a single asset class."
+description: "A hands-on guide to ETF diversification using risk budgets, with interactive tools to find your own allocation."
---
import DiversificationRandomWalk from "../components/blog/DiversificationRandomWalk";
From c2b5983810d5219f57154fcaa15b8a4ba72979df Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 13 Mar 2026 08:49:38 -0400
Subject: [PATCH 14/17] Update etf_diversification_interactive.mdx
---
website/blog/etf_diversification_interactive.mdx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/website/blog/etf_diversification_interactive.mdx b/website/blog/etf_diversification_interactive.mdx
index a8a63831b..50d375e8d 100644
--- a/website/blog/etf_diversification_interactive.mdx
+++ b/website/blog/etf_diversification_interactive.mdx
@@ -1,7 +1,7 @@
---
title: "How to invest your money without predicting markets"
-publishing_date: "2026-03-08"
-tokenID: 200
+publishing_date: "2026-03-13"
+tokenID: 192
category: "others"
description: "A hands-on guide to ETF diversification using risk budgets, with interactive tools to find your own allocation."
---
From 69241018d90c6cbdc7c02dcd53310e0b23e13b92 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 13 Mar 2026 09:45:42 -0400
Subject: [PATCH 15/17] Update PortfolioRiskAllocator.tsx
---
website/components/blog/PortfolioRiskAllocator.tsx | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/website/components/blog/PortfolioRiskAllocator.tsx b/website/components/blog/PortfolioRiskAllocator.tsx
index 7aab6d61c..f54d91ca3 100644
--- a/website/components/blog/PortfolioRiskAllocator.tsx
+++ b/website/components/blog/PortfolioRiskAllocator.tsx
@@ -46,8 +46,7 @@ function solveRiskBudget(cov: number[][], clusterOf: number[], budgets: number[]
// Convergence: per-asset risk contribution vs target
let maxErr = 0;
- for (let i = 0; i < n; i++)
- if (active[i]) maxErr = Math.max(maxErr, Math.abs(rc[i] / totalRc - targetRc[i]));
+ for (let i = 0; i < n; i++) if (active[i]) maxErr = Math.max(maxErr, Math.abs(rc[i] / totalRc - targetRc[i]));
if (maxErr < 1e-8) break;
// Multiplicative update per asset
@@ -529,7 +528,11 @@ export default function PortfolioRiskAllocator() {
href={DATA.etf_urls[idx]}
target="_blank"
rel="noopener noreferrer"
- className={css({ color: "#6b7280", textDecoration: "underline", _hover: { color: "#374151" } })}
+ className={css({
+ color: "#6b7280",
+ textDecoration: "underline",
+ _hover: { color: "#374151" },
+ })}
>
{etf}
From 965df2ddd280236f7bd74e905be8ac1cba4d3bf7 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 13 Mar 2026 10:02:38 -0400
Subject: [PATCH 16/17] Lint and fix
---
.../blog/DiversificationRandomWalk.tsx | 29 +++--------
.../blog/PortfolioRiskAllocator.tsx | 52 +++++++++++++++----
2 files changed, 48 insertions(+), 33 deletions(-)
diff --git a/website/components/blog/DiversificationRandomWalk.tsx b/website/components/blog/DiversificationRandomWalk.tsx
index b5648b812..0c2aba974 100644
--- a/website/components/blog/DiversificationRandomWalk.tsx
+++ b/website/components/blog/DiversificationRandomWalk.tsx
@@ -26,6 +26,7 @@ const gaussianRandom = (): number => {
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
};
+const TRADING_DAYS_PER_YEAR = 252;
const STEPS = 504; // 2 trading years — more points for smoother histogram
const ANIM_BATCH = 8; // steps drawn per frame
const ANIM_INTERVAL = 25; // ms between frames
@@ -38,8 +39,8 @@ interface Paths {
/** Generate correlated daily returns for bonds and stocks.
* Uses Cholesky decomposition for the 2×2 case. */
function generatePaths(sigBond: number, sigStock: number, rho: number): Paths {
- const dailySigBond = sigBond / Math.sqrt(STEPS);
- const dailySigStock = sigStock / Math.sqrt(STEPS);
+ const dailySigBond = sigBond / Math.sqrt(TRADING_DAYS_PER_YEAR);
+ const dailySigStock = sigStock / Math.sqrt(TRADING_DAYS_PER_YEAR);
const bondReturns: number[] = [];
const stockReturns: number[] = [];
for (let i = 0; i < STEPS; i++) {
@@ -65,7 +66,7 @@ function measuredVol(dailyReturns: number[], steps: number): number {
const slice = dailyReturns.slice(0, steps);
const mean = slice.reduce((a, b) => a + b, 0) / slice.length;
const variance = slice.reduce((s, r) => s + (r - mean) ** 2, 0) / slice.length;
- return Math.sqrt(variance * STEPS) * 100; // percent, annualized
+ return Math.sqrt(variance * TRADING_DAYS_PER_YEAR) * 100; // percent, annualized
}
// ============================================================
@@ -225,26 +226,10 @@ export default function DiversificationRandomWalk() {
const startAnimation = useCallback(
(newPaths?: Paths) => {
stopAnimation();
- const p = newPaths ?? paths;
+ let p = newPaths ?? paths;
if (!p) {
- const fresh = generatePaths(sigBond, sigStock, rho);
- setPaths(fresh);
- setVisibleSteps(0);
- setIsAnimating(true);
- // defer interval to next tick so state is set
- setTimeout(() => {
- let step = 0;
- animRef.current = setInterval(() => {
- step = Math.min(step + ANIM_BATCH, STEPS);
- setVisibleSteps(step);
- if (step >= STEPS) {
- clearInterval(animRef.current!);
- animRef.current = null;
- setIsAnimating(false);
- }
- }, ANIM_INTERVAL);
- }, 0);
- return;
+ p = generatePaths(sigBond, sigStock, rho);
+ setPaths(p);
}
setVisibleSteps(0);
setIsAnimating(true);
diff --git a/website/components/blog/PortfolioRiskAllocator.tsx b/website/components/blog/PortfolioRiskAllocator.tsx
index f54d91ca3..7df446e4b 100644
--- a/website/components/blog/PortfolioRiskAllocator.tsx
+++ b/website/components/blog/PortfolioRiskAllocator.tsx
@@ -3,6 +3,7 @@ import { css } from "../../styled-system/css";
import { DATA } from "./etfData";
const N_ETF = DATA.etf_names.length;
+const ETF_INDEX = new Map(DATA.etf_names.map((name, i) => [name, i]));
/** Matrix-vector product: Σw */
function matVec(cov: number[][], w: number[]): number[] {
@@ -151,6 +152,7 @@ export default function PortfolioRiskAllocator() {
const [activePreset, setActivePreset] = useState(1);
const [expandedCluster, setExpandedCluster] = useState(null);
const [dragging, setDragging] = useState(null);
+ const [hasEverDragged, setHasEverDragged] = useState(false);
const barRef = useRef(null);
const { weights, risk } = useMemo(() => {
@@ -207,6 +209,12 @@ export default function PortfolioRiskAllocator() {
border: "1px solid rgba(78, 121, 167, 0.15)",
})}
>
+
{/* ── Header ── */}
-
- Risk budget — drag the handles or pick a preset
+