Skip to main content

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:

  • instruments: SPX options, TLT bonds
  • capital: 40% allocation
  • trades/month: 15-20

momentum:

  • instruments: SPX, NQ futures
  • capital: 30% allocation
  • trades/month: 8-12

crypto:

  • instruments: BTC, ETH
  • capital: 30% allocation
  • trades/month: 12-18

the correlation problem
#

if all strategies move together = no diversification.

correlation = how much assets move in same direction.

+1.0 = perfect positive correlation (always move together)

0.0 = no correlation (independent moves)

-1.0 = perfect negative correlation (opposite moves)

ideal portfolio: strategies with <0.3 correlation

calculating strategy correlation
#

import pandas as pd
import numpy as np
import plotly.graph_objects as go
import plotly.express as px

def load_strategy_returns():
    """
    load daily returns for each strategy
    """
    # mean reversion returns (march - sept 2023)
    mean_rev_trades = [
        ('2023-03-15', 285),
        ('2023-03-18', -220),
        ('2023-03-22', 340),
        # ... 180 days of trades
    ]

    # momentum returns
    momentum_trades = [
        ('2023-08-01', 540),
        ('2023-08-05', 425),
        # ... trades since august
    ]

    # crypto returns
    crypto_trades = [
        ('2023-03-10', 380),
        ('2023-03-14', -190),
        # ... 190 days of trades
    ]

    # convert to daily returns dataframe
    df = pd.DataFrame({
        'date': pd.date_range('2023-03-01', '2023-09-18', freq='D'),
        'mean_reversion': generate_daily_returns(mean_rev_trades),
        'momentum': generate_daily_returns(momentum_trades),
        'crypto': generate_daily_returns(crypto_trades)
    })

    return df

def generate_daily_returns(trades):
    """
    simulate daily return series from trade list
    """
    # actual implementation more complex
    # this is simplified for illustration
    returns = []
    for i in range(200):
        if np.random.random() < 0.15:  # 15% trade days
            returns.append(np.random.randn() * 200)
        else:
            returns.append(0)
    return returns[:200]

def calculate_correlation_matrix(df):
    """
    calculate pairwise correlation between strategies
    """
    corr_matrix = df[['mean_reversion', 'momentum', 'crypto']].corr()
    return corr_matrix

actual correlation results
#

ran analysis on 200 days (march - september 2023).

                  mean_reversion  momentum   crypto
mean_reversion          1.00       0.68      0.42
momentum                0.68       1.00      0.51
crypto                  0.42       0.51      1.00

mean reversion ↔ momentum: 0.68 (concerning)

mean reversion ↔ crypto: 0.42 (acceptable)

momentum ↔ crypto: 0.51 (borderline)

correlation visualization
#

generated heatmap using plotly.

Strategy Correlation Matrix

Figure 1: Strategy correlation matrix showing mean reversion and momentum are too correlated (0.68). Target is <0.3 for true diversification.

why mean reversion + momentum correlated?
#

both trade equity indices (SPX, NQ).

when market trends = momentum profits + mean reversion loses.

when market ranges = mean reversion profits + momentum loses.

sounds diversified but correlation still 0.68.

reason: both react to same volatility regime.

high vol = both strategies struggle.

low vol = both strategies perform.

risks of high correlation
#

1. simultaneous drawdowns

if strategies move together, losses compound.

august had 2 days where both strategies lost.

combined loss: -$1,240 (vs -$620 if uncorrelated).

2. false diversification

think i’m diversified across 3 strategies.

reality: 70% capital (mean rev + momentum) moves together.

only crypto provides real diversification.

3. risk management failure

sizing positions assuming independence.

correlation = more risk than calculated.

solutions
#

1. add true uncorrelated strategy

options premium selling = different risk profile.

theta decay vs directional moves.

low correlation to momentum/mean reversion.

2. increase crypto allocation

currently 30% crypto.

crypto is least correlated (0.42 mean rev, 0.51 momentum).

consider increasing to 40%.

3. momentum on different timeframes

currently momentum on 15min.

add daily timeframe momentum = different signals.

reduces intraday correlation.

4. geographic diversification

all strategies trade US markets.

add european hours trading = time diversification.

rolling correlation analysis
#

def calculate_rolling_correlation(df, window=30):
    """
    calculate 30-day rolling correlation
    """
    rolling_corr = df['mean_reversion'].rolling(window).corr(df['momentum'])
    return rolling_corr

# visualize rolling correlation over time
rolling_corr = calculate_rolling_correlation(df)

fig = go.Figure()
fig.add_trace(go.Scatter(
    x=df['date'],
    y=rolling_corr,
    mode='lines',
    name='30-Day Rolling Correlation',
    line=dict(color='#2E86AB', width=2)
))
fig.add_hline(y=0.3, line_dash="dash", line_color="green",
              annotation_text="Target: <0.3")
fig.add_hline(y=0.7, line_dash="dash", line_color="red",
              annotation_text="Danger: >0.7")
fig.update_layout(
    title='Mean Reversion ↔ Momentum Rolling Correlation',
    xaxis_title='Date',
    yaxis_title='Correlation Coefficient',
    template='plotly_white'
)

Rolling Correlation

Figure 2: 30-day rolling correlation between mean reversion and momentum strategies. Spiked to 0.85 in late August during high volatility period.

correlation not constant.

varies from 0.45 to 0.85.

highest during volatile periods (late august).

impact on portfolio risk
#

calculated risk (assuming independence):

account: $360k

mean rev risk: 0.5% = $1,800

momentum risk: 0.5% = $1,800

combined risk (independent): $2,545 (sqrt(1800² + 1800²))

actual risk (with 0.68 correlation):

combined risk (correlated): $3,024

difference: $479 more risk than calculated (18.8% higher)

fuck. that’s significant.

immediate actions
#

1. reduce momentum position size

drop from 0.5% to 0.4% risk per trade.

compensates for correlation risk.

2. pause mean reversion during momentum trades

if momentum position open, block mean reversion entries.

reduces simultaneous exposure.

3. research premium selling strategy

theta decay strategies = low correlation.

could replace 20% of mean reversion allocation.

4. crypto expansion validation

SOL, ADA, DOGE paper trading results next week.

if validated, increase crypto to 40% (reduce mean rev to 30%).

lessons learned
#

1. asset class ≠ strategy diversification

trading options AND futures doesn’t mean diversified.

correlation depends on strategy logic, not instrument type.

2. volatility regime matters

correlation spikes during high vol.

exactly when you need diversification most.

3. measure everything

assumed diversification without measuring.

correlation analysis = reality check.

next steps
#

monitor correlation daily.

if mean rev ↔ momentum >0.7 = reduce position sizes.

research premium selling (target: <0.3 correlation).

expand crypto (already low correlation).

goal: bring portfolio-wide correlation <0.4.


3:42am tuesday. correlation analysis done. mean reversion and momentum too correlated (0.68). reducing position sizes and researching uncorrelated strategies.

-AK

Related

momentum breakout strategy - how it works
momentum strategy has 5 wins, 0 losses. time to explain how it works. core concept # capture trending moves after consolidation breaks.
analyzed correlation between my strategies - found hidden risk
ran correlation analysis on my strategies. found something i didn’t expect. what i analyzed # all trades from may-july (3 months, 65 trades).
refactored data pipeline to async - 3x faster market data processing
been running synchronous data fetching since january. works but slow during market open. refactored to async this week. 3x speed improvement. the problem with sync code # # Old synchronous approach def fetch_market_data(symbols): results = [] for symbol in symbols: data = fetch_from_api(symbol) # Blocks here results.append(data) return results # With 10 symbols, takes 10 * 180ms = 1,800ms total each API call blocks until complete.
added redis caching - cut market data latency by 60%
been noticing market data latency creeping up. average fetch time: 180ms from polygon API. slowing down entry execution. the problem # every time algo needs current price:
rebuilt backtesting pipeline - 10x faster parameter optimization
spent last 3 days rebuilding backtest optimization pipeline. went from 6 hours to 35 minutes for full parameter sweep. the problem # old approach: sequential parameter testing.
building volatility regime detection
need to stop trading when volatility spikes. building detection system. the problem # this week VIX spiked 18% in 2 days. my strategies got stopped out twice.