Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 38 additions & 23 deletions website/TOC_IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Übersicht

Implementierung eines Docusaurus-ähnlichen Table of Contents für Blog-Posts mit:

- Sticky Sidebar rechts (Desktop >1200px)
- Scroll-Spy (aktive Section hervorgehoben)
- Versteckt auf Mobile/Tablet
Expand All @@ -19,12 +20,14 @@ Implementierung eines Docusaurus-ähnlichen Table of Contents für Blog-Posts mi
**Funktion:** Extrahiert Headings (h2, h3) aus dem DOM nach dem Render.

**Anforderungen:**

- [ ] Akzeptiert `RefObject<HTMLElement>` für Content-Container
- [ ] Generiert IDs für Headings ohne ID (slug-basiert)
- [ ] Gibt Array von `{ id: string, text: string, level: number }` zurück
- [ ] Re-extrahiert bei Content-Änderung

**Abhängigkeiten:**

- Keine externen Pakete nötig

---
Expand All @@ -36,12 +39,14 @@ Implementierung eines Docusaurus-ähnlichen Table of Contents für Blog-Posts mi
**Funktion:** Scroll-Spy mit Intersection Observer API.

**Anforderungen:**

- [ ] Akzeptiert Array von Heading-IDs
- [ ] Verwendet `IntersectionObserver` mit `rootMargin: '-80px 0px -80% 0px'`
- [ ] Gibt aktive Heading-ID zurück
- [ ] Cleanup bei Unmount

**Abhängigkeiten:**

- Keine externen Pakete nötig

---
Expand All @@ -65,6 +70,7 @@ TableOfContents/
### 2.2 `TableOfContents.tsx`

**Anforderungen:**

- [ ] Props: `contentRef: RefObject<HTMLElement>`, `minHeadings?: number` (default: 2)
- [ ] Verwendet `useTableOfContents` und `useActiveHeading`
- [ ] Rendert nichts wenn `headings.length < minHeadings`
Expand All @@ -76,6 +82,7 @@ TableOfContents/
### 2.3 `TocItem.tsx`

**Anforderungen:**

- [ ] Props: `heading: TocItem`, `isActive: boolean`
- [ ] Indent für h3 (level 3)
- [ ] Active-State Styling (linker Border + Farbe)
Expand All @@ -86,6 +93,7 @@ TableOfContents/
### 2.4 `styles.ts` (Panda CSS)

**Anforderungen:**

- [ ] Container: `position: sticky`, `top: 80px`, `max-height: calc(100vh - 100px)`
- [ ] Versteckt unter 1200px Breakpoint
- [ ] Titel-Styling (klein, uppercase, muted)
Expand Down Expand Up @@ -121,10 +129,11 @@ export interface TocItem {
**Datei:** `website/layouts/styles.ts` (erweitern)

**Neue Styles:**

- [ ] `articleLayout`: 3-Spalten Grid (`1fr minmax(0, 720px) 250px`)
- [ ] `articleContent`: Mittlere Spalte
- [ ] `articleSidebar`: Rechte Spalte (ToC)
- [ ] Responsive:
- [ ] Responsive:
- `>1200px`: 3 Spalten
- `768-1200px`: Content zentriert, kein ToC
- `<768px`: Full-width
Expand All @@ -136,12 +145,14 @@ export interface TocItem {
**Datei:** `website/components/Post.tsx`

**Änderungen:**

- [ ] `useRef` für Content-Container hinzufügen
- [ ] Ref an `.e-content` Container übergeben
- [ ] `<TableOfContents contentRef={contentRef} />` in Sidebar rendern
- [ ] Layout-Wrapper mit neuem Grid

**Struktur nach Änderung:**

```tsx
<article className="h-entry">
<div className={articleLayout}>
Expand Down Expand Up @@ -171,11 +182,13 @@ export interface TocItem {

**Problem:** Markdown-Headings brauchen IDs für Scroll-Navigation.

**Lösung A (empfohlen):**
**Lösung A (empfohlen):**

- [ ] In `useTableOfContents`: IDs zur Laufzeit generieren falls nicht vorhanden
- [ ] Slug-Funktion: `text.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '')`

**Alternative B:**

- [ ] rehype-slug Plugin in react-markdown integrieren (komplexer)

---
Expand Down Expand Up @@ -219,38 +232,39 @@ export interface TocItem {

## Datei-Checkliste

| Datei | Aktion | Status |
|-------|--------|--------|
| `hooks/useTableOfContents.ts` | Neu erstellen | ✅ |
| `hooks/useActiveHeading.ts` | Neu erstellen | ✅ |
| `components/TableOfContents/index.ts` | Neu erstellen | ✅ |
| `components/TableOfContents/TableOfContents.tsx` | Neu erstellen | ✅ |
| `components/TableOfContents/TocItem.tsx` | Neu erstellen | ✅ |
| `components/TableOfContents/styles.ts` | Neu erstellen | ✅ |
| `types/components.ts` | Erweitern (TocItem) | ✅ (in useTableOfContents.ts) |
| `layouts/styles.ts` | Erweitern (articleLayout) | ✅ |
| `components/Post.tsx` | Refactoring (Grid + ToC) | ✅ |
| Datei | Aktion | Status |
| ------------------------------------------------ | ------------------------- | ----------------------------- |
| `hooks/useTableOfContents.ts` | Neu erstellen | ✅ |
| `hooks/useActiveHeading.ts` | Neu erstellen | ✅ |
| `components/TableOfContents/index.ts` | Neu erstellen | ✅ |
| `components/TableOfContents/TableOfContents.tsx` | Neu erstellen | ✅ |
| `components/TableOfContents/TocItem.tsx` | Neu erstellen | ✅ |
| `components/TableOfContents/styles.ts` | Neu erstellen | ✅ |
| `types/components.ts` | Erweitern (TocItem) | ✅ (in useTableOfContents.ts) |
| `layouts/styles.ts` | Erweitern (articleLayout) | ✅ |
| `components/Post.tsx` | Refactoring (Grid + ToC) | ✅ |

---

## Geschätzter Zeitaufwand

| Phase | Zeit |
|-------|------|
| Phase 1: Hooks | 45 min |
| Phase 2: Komponente | 60 min |
| Phase 3: Types | 10 min |
| Phase 4: Layout | 45 min |
| Phase 5: Heading-IDs | 20 min |
| Phase 6: Testing | 30 min |
| Phase 7: Feinschliff | 30 min |
| **Gesamt** | **~4 Stunden** |
| Phase | Zeit |
| -------------------- | -------------- |
| Phase 1: Hooks | 45 min |
| Phase 2: Komponente | 60 min |
| Phase 3: Types | 10 min |
| Phase 4: Layout | 45 min |
| Phase 5: Heading-IDs | 20 min |
| Phase 6: Testing | 30 min |
| Phase 7: Feinschliff | 30 min |
| **Gesamt** | **~4 Stunden** |

---

## Abhängigkeiten

Keine neuen npm-Pakete erforderlich. Verwendet:

- React Hooks (useRef, useState, useEffect)
- Intersection Observer API (nativ)
- Panda CSS (bereits installiert)
Expand All @@ -260,6 +274,7 @@ Keine neuen npm-Pakete erforderlich. Verwendet:
## Rollback-Plan

Falls Probleme auftreten:

1. `Post.tsx` auf vorherige Version zurücksetzen
2. ToC-Komponenten können ohne Side-Effects entfernt werden
3. Layout-Änderungen sind isoliert in neuen CSS-Klassen
Expand Down
109 changes: 109 additions & 0 deletions website/blog/etf_diversification_interactive.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
title: "How to invest your money without predicting markets"
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."
---

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.

## 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?

<DiversificationRandomWalk />

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.

<PortfolioRiskAllocator />

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. 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.

---

## 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:

- **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 full analysis notebooks are not published yet, but can be made available on request._
26 changes: 17 additions & 9 deletions website/blog/multichain_technical_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
<ChainBadge network="eip155:8453" /> // Renders "Base" badge
```
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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;
```
Expand All @@ -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)
- [Implementation Proposal](/website/MULTICHAIN_EXPANSION_PROPOSAL.md)
Loading
Loading