DEV Community

Cover image for 10 Stocks, One Hidden Risk: Build a Portfolio Correlation Lab in Python
Sarthak Verma for MarketDeck

Posted on Fully Autonomous

10 Stocks, One Hidden Risk: Build a Portfolio Correlation Lab in Python

Ten stocks can still behave like one crowded trade. The number of positions in your portfolio does not tell you how much risk they share.

Picture a portfolio containing a bank, an IT company, an automaker, a consumer brand, and six other businesses. Ten tickers. Several industries. It feels diversified.

Now imagine a broad market selloff in which most of those stocks fall together. You diversified the company names, but did you diversify the forces moving their prices?

That's the problem we'll investigate. We'll start with a simple experiment you can run in MarketDeck's interactive learning lab, rebuild the mathematics in Python, and finish with a more useful way to question portfolio concentration.

What you'll learn: why correlations matter, how to calculate portfolio volatility from a covariance matrix, and what an "effective number of exposures" really means. Everything below uses hypothetical numbers; you won't need a brokerage account or market-data API.


1. Two stocks, one question

Suppose you hold two fictional stocks, A and B. Each has an assumed annual volatility of 20%, and each receives 50% of your portfolio.

Only one thing changes between experiments: correlation.

Correlation describes how two return series tend to move relative to each other, on a scale from -1 to +1. A value near +1 indicates strong positive co-movement; a value near 0 means little linear relationship. It does not mean the assets are risk-free or genuinely independent in every statistical sense.

Before reading the results, make a prediction: if A and B move perfectly together, does holding both reduce your risk?

Assumed correlation Portfolio volatility
-0.50 10.00%
0.00 14.14%
+0.25 15.81%
+0.80 18.97%
+1.00 20.00%

At +1, the portfolio is just as volatile as either constituent. Splitting your money across two names hasn't lowered this particular measure of risk.

At 0, volatility falls to about 14.14%, not 10%. Diversification helps, but risk doesn't shrink in a simple one-for-one proportion to the number of holdings.

Try the experiment on MarketDeck

Open the Expected Returns interactive guide and find the exercise about adding holdings versus changing risks.

  1. Set both assumed volatilities to 20% and the weights to 50% / 50%.
  2. Set correlation to +1. Predict the answer before running the experiment.
  3. Repeat with 0 and -0.5. Check your results against the table.
  4. Try changing only the weights to 70% / 30%. What happens, and why?

This exercise is an assumption-driven calculator, not a backtest or a stock recommendation. That is its strength: you can isolate one idea instead of confusing it with changing market prices.

2. The mathematical reason

For two assets, portfolio variance is:

portfolio_variance = wA² × σA² + wB² × σB² + 2 × wA × wB × σA × σB × correlation

Here, w is the asset weight and σ is the standard deviation of returns. Take the square root of the resulting variance to get portfolio volatility.

Notice the final term. It connects the two assets. Ignoring that term amounts to ignoring how the stocks move together.

For a larger portfolio, linear algebra gives us a cleaner expression:

portfolio_variance = weights.T @ covariance_matrix @ weights

The covariance matrix contains individual asset variances along its diagonal and pairwise covariances elsewhere. The matrix is not just a technical convenience: its off-diagonal entries are part of the portfolio's risk.

3. The ten-stock illusion

Now imagine ten hypothetical stocks, each weighted at 10%, each with assumed annual volatility of 20%.

To make the mechanism easy to see, let's assume every distinct pair has exactly the same correlation, which we'll call rho. Real portfolios don't work this neatly. We're creating a controlled experiment, not modelling ten actual NSE-listed companies.

With n equal-weighted, equally volatile assets, the formula simplifies to:

portfolio_volatility = asset_volatility × sqrt((1 + (n - 1) × rho) / n)

Watch what happens as common correlation rises:

Correlation between every pair Portfolio volatility
0.00 6.32%
0.25 11.40%
0.50 14.83%
0.75 17.61%
1.00 20.00%

The portfolio always contains ten stocks. Its weights and each stock's assumed volatility are unchanged. Only the relationships between them are different.

That one change takes the estimated portfolio volatility from 6.32% to 17.61% when correlation goes from 0 to 0.75.

Key insight: Diversification is not only about owning different businesses. It's also about whether the risks driving those businesses are sufficiently different.

An important constraint on negative correlations: When every pair of assets shares the same correlation, the correlation matrix is only valid when −1/(n − 1) ≤ ρ ≤ 1.

For ten assets, the minimum common correlation is approximately −0.111. So although −0.50 is a valid correlation between two assets, it cannot be the common correlation across all ten assets.

The Python example checks the matrix's eigenvalues to reject invalid assumptions.

4. Build the correlation lab in Python

Let's reproduce these numbers with NumPy. This is standalone code: it does not use a MarketDeck API or download real stock prices.

Install NumPy if needed with pip install numpy, save the following as portfolio_correlation.py, and run it with Python 3.

import numpy as np

N = 10
ASSET_VOL = 0.20  # assumed annualised volatility
weights = np.full(N, 1.0 / N)


def portfolio_lab(rho: float):
    # Correlation matrix: 1 on the diagonal, rho elsewhere
    corr = (1 - rho) * np.eye(N) + rho * np.ones((N, N))

    # Reject invalid correlation matrices
    assert np.linalg.eigvalsh(corr).min() > -1e-10

    covariance = (ASSET_VOL ** 2) * corr
    variance = float(weights @ covariance @ weights)
    volatility = np.sqrt(variance)

    # Variance-equivalent count; equal individual volatilities
    effective_count = (
        ASSET_VOL ** 2 / variance
        if variance > 0
        else float("inf")
    )

    return volatility, effective_count


print("rho     volatility     effective_count")
for rho in (0.00, 0.25, 0.50, 0.75, 1.00):
    vol, count = portfolio_lab(rho)
    print(f"{rho:4.2f}       {vol:6.2%}          {count:5.2f}")

# Two quick sanity checks
assert np.isclose(portfolio_lab(0)[0], ASSET_VOL / np.sqrt(N))
assert np.isclose(portfolio_lab(1)[0], ASSET_VOL)
print("Sanity checks passed.")
Enter fullscreen mode Exit fullscreen mode

The expected output is:

rho     volatility     effective_count
0.00        6.32%          10.00
0.25       11.40%           3.08
0.50       14.83%           1.82
0.75       17.61%           1.29
1.00       20.00%           1.00
Sanity checks passed.
Enter fullscreen mode Exit fullscreen mode

Try modifying N, the common correlation, or the weights. The volatility calculation works with unequal weights, and the revised effective-count calculation also accommodates unequal weights as long as the individual asset volatilities remain equal. It remains a variance-equivalent comparison, not a literal count of independent economic risks.

5. What does “1.29 effective stocks” actually mean?

At a common correlation of 0.75, the code prints 1.29. It sounds dramatic. It also needs a careful definition.

Under our specific assumptions, we can ask: How many equally weighted, equally volatile, mutually uncorrelated assets would produce the same portfolio variance?

That variance-equivalent number is:

effective_count = n / (1 + (n - 1) × rho)

For ten assets with rho = 0.75, it is about 1.29.

It does not mean the portfolio contains literally 1.29 companies, that we have identified its true economic factors, or that its future losses can be predicted with precision. It is a comparison within an unusually simple model.

Other measures, including eigenvalue-based estimates of the dimensionality of a correlation matrix, answer different questions and can produce different results. Don't combine them under the label "independent bets" without explaining which definition you used.

6. Where real portfolio research gets harder

In a classroom example, we pick correlations. In a real portfolio, we must estimate them from returns—and those estimates may be unstable.

A few practical questions become essential:

  • Which period? Correlations based on the past three months may disagree with those based on three years.
  • Which returns? Closing prices, total returns, currency conversions, and corporate actions must be handled consistently.
  • Which observations? Holidays, missing prices, and securities with short histories can distort the matrix.
  • Which risks are missing? Liquidity shocks, options payoffs, concentrated factors, and extreme market events may not be described well by ordinary correlation.
  • What changed? A historically diversified portfolio can become more correlated precisely when diversification matters most.

MarketDeck's Portfolio Analysis is designed for this broader research process. Its public overview describes connected lenses covering performance, core risk, tail risk, return distributions, factor exposures, diversification, risk–return analysis, and implied returns. Analysing your own holdings requires signing in.

The two-asset learning lab is the place to understand the mechanics first. The portfolio workspace is where more of the real research questions can be considered together.

Our goal isn't to turn covariance into a “buy” or “sell” score. It's to make the assumptions behind a result easier to inspect.

Try a rolling-correlation sensitivity check.

Instead of estimating correlations once, repeat the calculation using rolling windows of 60 daily returns.

With ten assets, each window contains 45 distinct pairwise correlation estimates. Short-window estimates can be noisy, so one effective-count figure may give a misleading impression of precision.

Under the equal-weight, equal-volatility assumptions, an average pairwise correlation of 0.65 corresponds to approximately 1.46 variance-equivalent positions. At 0.85, the count drops to approximately 1.16.

These are illustrative sensitivity calculations, not observed market results. They show why diversification estimates should be examined across different windows and assumptions rather than treated as permanent characteristics of a portfolio.

7. Three challenges before you close the notebook

Challenge 1 — Unequal risk: Return to MarketDeck's two-asset experiment. Keep 50/50 weights and zero correlation, but give the second asset 30% volatility instead of 20%. Predict the new combined volatility. (Check: approximately 18.03%.)

Challenge 2 — Concentrated weights: In Python, change the ten weights so the first stock receives 50% and the remaining nine share the other 50%. Keep the common correlation at 0.75. Compare the result with the original 17.61%. (Check: approximately 18.10%.)

Challenge 3 — Explain it simply: A friend tells you, “I own ten stocks, so I'm diversified.” Ask two follow-up questions that would help establish whether that's actually true.

You don't need to memorise every equation. If you remember one habit, make it this:

Don't stop at “How many stocks do I own?” Ask “What makes these stocks move together, and how reliable is my estimate?”


Explore and verify

About MarketDeck: We're building an India-focused financial research workspace spanning screening, charts, portfolio analysis, and quantitative learning. I am part of the team behind MarketDeck. All numerical cases in this article are hypothetical educational examples, not historical backtests or personalised investment advice.

Top comments (2)

Collapse
 
arhancanli profile image
Arhan Canli • • Edited

I ran the code and the table matches (6.32, 11.40, 14.83, 17.61, 20.00 and the 3.08/1.82/1.29 effective counts). One check value is off: Challenge 2 (first stock 50%, other nine 5.56% each, rho 0.75, vol 20%) gives 18.10%, not 18.11%. With w @ C @ w in NumPy I get 0.181046, so it rounds down.

One thing worth adding next to the equal-correlation table: a common rho below zero isn't available at n = 10. A valid equicorrelation matrix needs rho >= -1/(n-1), which is -0.111 here, so the "-0.5" row of the two-asset table doesn't have a ten-stock equivalent. Your eigvalsh assert would catch it if someone tried rho = -0.5.

Also worth trying in Challenge 3: ask for the rolling estimate of the average pairwise correlation, not one number. With 10 names and 60 daily returns the sample matrix has 45 correlations from 60 observations each, so the estimate is noisy, and the effective count of 1.29 is very sensitive to it (rho 0.65 gives 1.46, rho 0.85 gives 1.16).

Collapse
 
sarthak_verma_7f63f8ef445 profile image
Sarthak Verma MarketDeck •

Thanks for running the code and checking the calculations so carefully, Arhan.

You're absolutely right about Challenge 2. The unrounded result is 18.104634%, so it should read 18.10%, not 18.11%. That was my mistake.

Your point about positive semidefiniteness is also important. The −0.50 correlation is valid for the two-asset example, but not as a common pairwise correlation across ten assets. The eigenvalue check catches it, but the article should explain why.

The rolling-correlation suggestion is excellent too. The sensitivity of the effective count to different correlation estimates is worth making explicit.

I'm correcting the rounding, clarifying the valid correlation range, and adding the estimation-sensitivity discussion.

Appreciate the careful and constructive review!