Skip to main content

cross-asset correlation tracking - why diversification is a lie

1:30am and i’m staring at correlation matrices again.

everyone talks about diversification like it’s free lunch.

it’s not.

the diversification myth
#

portfolios are “diversified” until they’re not.

2020 march - everything dropped together.

2022 crypto winter - BTC and ETH moved in lockstep.

late 2025 vol spike - correlations spiked across all my positions.

when you need diversification most, it disappears.

tracking correlations in real time
#

built a system to track rolling correlations across my entire portfolio.

not just at month end.

every day.

Correlation Matrix

current 30-day rolling correlation matrix. ES and NQ at 0.94 - basically same asset. BTC and ETH at 0.87 - also high. gold is the only real diversifier at -0.15 to equities.

what this tells me:

  • my “diversified” portfolio is actually 3 bets: equities, crypto, gold
  • ES and NQ positions are redundant from correlation perspective
  • SPX options are 0.89 correlated to ES - makes sense, same underlying
  • gold is the only true hedge but tiny allocation

correlations aren’t static
#

this is the part most people miss.

correlations change based on market regime.

Rolling Correlations

rolling 30-day correlations over the past 6 months. notice how ES-BTC correlation spiked during the october vol event. when shit hits the fan, everything correlates.

key observation:

during stress, ES-BTC correlation went from 0.35 to 0.55+

that’s a 60% increase in correlation when i needed diversification most.

meanwhile ES-NQ stayed glued at 0.92-0.95.

BTC-ETH stayed glued at 0.85-0.90.

the algo approach
#

from dataclasses import dataclass
from typing import Dict, List, Tuple
import numpy as np
import pandas as pd
from datetime import datetime, timedelta

@dataclass
class AssetReturns:
    symbol: str
    returns: pd.Series

class CorrelationTracker:
    def __init__(self, lookback: int = 30):
        self.lookback = lookback
        self.assets: Dict[str, pd.Series] = {}
        self.correlation_history: List[Dict] = []

    def add_asset(self, symbol: str, returns: pd.Series):
        """Add asset return series"""
        self.assets[symbol] = returns

    def calculate_correlation_matrix(self, as_of: datetime = None) -> pd.DataFrame:
        """Calculate correlation matrix as of specific date"""
        if as_of is None:
            as_of = datetime.now()

        # align all series to same date range
        aligned = {}
        for symbol, returns in self.assets.items():
            mask = returns.index <= as_of
            aligned[symbol] = returns[mask].tail(self.lookback)

        df = pd.DataFrame(aligned)
        return df.corr()

    def get_rolling_correlation(self, asset1: str, asset2: str,
                                 window: int = 30) -> pd.Series:
        """Calculate rolling correlation between two assets"""
        r1 = self.assets[asset1]
        r2 = self.assets[asset2]

        # align dates
        combined = pd.concat([r1, r2], axis=1)
        combined.columns = [asset1, asset2]
        combined = combined.dropna()

        return combined[asset1].rolling(window).corr(combined[asset2])

    def detect_correlation_regime(self) -> str:
        """Classify current correlation environment"""
        matrix = self.calculate_correlation_matrix()

        # average off-diagonal correlation
        n = len(matrix)
        off_diag = []
        for i in range(n):
            for j in range(i+1, n):
                off_diag.append(matrix.iloc[i, j])

        avg_corr = np.mean(off_diag)

        if avg_corr > 0.7:
            return "high_correlation"  # risk-off, everything moving together
        elif avg_corr > 0.4:
            return "normal"
        else:
            return "low_correlation"  # good for diversification

    def calculate_effective_positions(self) -> float:
        """How many truly independent bets do I have?"""
        matrix = self.calculate_correlation_matrix()

        # eigenvalue decomposition
        eigenvalues = np.linalg.eigvals(matrix.values)
        eigenvalues = np.real(eigenvalues)
        eigenvalues = eigenvalues[eigenvalues > 0]

        # effective number of bets (Shannon entropy based)
        eigenvalues = eigenvalues / eigenvalues.sum()
        effective_n = np.exp(-np.sum(eigenvalues * np.log(eigenvalues + 1e-10)))

        return effective_n

    def get_diversification_score(self) -> float:
        """0-100 score of portfolio diversification"""
        n_assets = len(self.assets)
        effective_n = self.calculate_effective_positions()

        # ratio of effective to actual positions
        score = (effective_n / n_assets) * 100
        return min(100, score)

    def daily_snapshot(self) -> Dict:
        """Record daily correlation state"""
        matrix = self.calculate_correlation_matrix()

        snapshot = {
            'timestamp': datetime.now().isoformat(),
            'regime': self.detect_correlation_regime(),
            'effective_positions': self.calculate_effective_positions(),
            'diversification_score': self.get_diversification_score(),
            'correlation_matrix': matrix.to_dict()
        }

        self.correlation_history.append(snapshot)
        return snapshot

not rocket science.

tracks rolling correlations.

calculates “effective positions” - how many truly independent bets i have.

gives me a diversification score.

what the data says
#

ran this across all of 2025.

findings:

  • average effective positions: 2.4 (out of 6 assets)
  • average diversification score: 40/100
  • correlation regime breakdown:
    • high correlation (>0.7 avg): 18% of days
    • normal (0.4-0.7): 62% of days
    • low correlation (<0.4): 20% of days

translation:

my “6 asset” portfolio is really 2.4 independent bets on average.

and during stress, that drops to like 1.5.

how i use this
#

rule 1: reduce size when correlations spike

when regime = “high_correlation”, i cut position sizes by 30%.

everything is moving together. doubling down on any position doubles total portfolio risk.

rule 2: track effective positions, not actual positions

my dashboard shows effective positions, not asset count.

if effective positions < 2, i’m basically making one big bet.

rule 3: rebalance based on correlation changes

when ES-BTC correlation dropped back to 0.35 in december, i increased crypto allocation.

diversification value had returned.

current state
#

as of today (jan 22):

  • correlation regime: normal
  • effective positions: 2.6
  • diversification score: 43/100
  • ES-NQ correlation: 0.94 (very high - expected)
  • ES-BTC correlation: 0.42 (moderate)
  • gold correlation to equities: -0.15 (negative - good hedge)

sitting in a reasonable spot.

not over-concentrated.

not perfectly diversified either.

been tracking correlation regimes with some traders on NexusFi who run similar multi-asset portfolios. the consensus is that correlation-based position sizing is one of the few edges that actually scales.

the takeaway
#

diversification isn’t a set-it-and-forget-it thing.

correlations change.

your “diversified” portfolio can become a single bet overnight.

track it. adjust for it. don’t assume yesterday’s correlation matrix applies today.


1:30am wednesday. tracking cross-asset correlations. my “6 asset” portfolio is really 2.4 independent bets. ES-NQ at 0.94 is basically one position. BTC-ETH at 0.87 same thing. gold at -0.15 to equities is the only real diversifier. diversification score: 43/100. not great but not terrible. regime is normal. will reduce size if correlations spike again.

-AK

Related

regime detection filter - why it failed march, python implementation fix
march disaster taught lesson. regime detection lagged. cost $6,690 before pausing. fixing implementation. what went wrong # my current filter:
correlation risk - portfolio diversification check
running 3 strategies across different asset classes. but are they actually diversified? checking portfolio correlation to find out. current strategy allocation # mean reversion:
theta decay tracking - why i obsess over time
3am on a monday and i’m watching theta tick down across my options book. most people don’t realize how much money they’re leaving on the table by not tracking theta properly.
saturday slippage deep dive - where your edge goes to die
woke up at 2am couldn’t sleep. decided to run a full slippage analysis on last quarter’s trades. what i found is annoying but fixable. the invisible tax # every algo trader knows slippage exists.
volatility regime detection - when to switch strategies
the market doesn’t care what strategy you’re running. it runs whatever regime it wants. your job is to detect the regime and adapt. why regime matters # every strategy has conditions where it crushes and conditions where it bleeds.
first week 2026 - january momentum algo kicking off
new year. new momentum. first real trading week of 2026 in the books. january effect algo activated. the january effect # some people think it’s BS.