Hide code cell content

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

import pandas_datareader.data as web

def get_factors(factors='CAPM',freq='daily'):   
    
    if freq=='monthly':
        freq_label=''
    else:
        freq_label='_'+freq


    if factors=='CAPM':
        fama_french = web.DataReader("F-F_Research_Data_Factors"+freq_label, "famafrench",start="1921-01-01")
        daily_data = fama_french[0]
    
     
        df_factor = daily_data[['RF','Mkt-RF']] 
    elif factors=='FF3':
        fama_french = web.DataReader("F-F_Research_Data_Factors"+freq_label, "famafrench",start="1921-01-01")
        daily_data = fama_french[0]

        df_factor = daily_data[['RF','Mkt-RF','SMB','HML']]
    elif factors=='FF5':

        fama_french = web.DataReader("F-F_Research_Data_Factors"+freq_label, "famafrench",start="1921-01-01")
        daily_data = fama_french[0]

        df_factor = daily_data[['RF','Mkt-RF','SMB','HML']]
        fama_french2 = web.DataReader("F-F_Research_Data_5_Factors_2x3"+freq_label, "famafrench",start="1921-01-01")
        daily_data2 = fama_french2[0]

        df_factor2 = daily_data2[['RMW','CMA']]
        df_factor=df_factor.merge(df_factor2,on='Date',how='outer')    
        
    else:
        fama_french = web.DataReader("F-F_Research_Data_Factors"+freq_label, "famafrench",start="1921-01-01")
        daily_data = fama_french[0]

        df_factor = daily_data[['RF','Mkt-RF','SMB','HML']]
        fama_french2 = web.DataReader("F-F_Research_Data_5_Factors_2x3"+freq_label, "famafrench",start="1921-01-01")
        daily_data2 = fama_french2[0]

        df_factor2 = daily_data2[['RMW','CMA']]
        df_factor=df_factor.merge(df_factor2,on='Date',how='outer')   
        fama_french = web.DataReader("F-F_Momentum_Factor"+freq_label, "famafrench",start="1921-01-01")
        df_factor=df_factor.merge(fama_french[0],on='Date')
        df_factor.columns=['RF','Mkt-RF','SMB','HML','RMW','CMA','MOM']    
    if freq=='monthly':
        df_factor.index = pd.to_datetime(df_factor.index.to_timestamp())
    else:
        df_factor.index = pd.to_datetime(df_factor.index)
        


    return df_factor/100

10. Capital Allocation II#


🎯 Learning Objectives

By the end of this chapter, you should be able to:

  1. Diagnose estimation risk in mean–variance (MV) optimization and explain how uncertainty in \(\mu\) and \(\Sigma\) affects optimal weights and Sharpe ratio.

  2. Run sensitivity analyses for portfolio weights and performance by perturbing expected returns/covariances within plausible error bands.

  3. Compare bet-sizing heuristics (MV, \(1/N\), volatility targeting, risk parity, shrinkage/regularization) and articulate when each is robust.

  4. Apply factor models to decompose portfolio risk by factor and by asset; compute marginal and component risk contributions.

  5. Translate loss tolerances (VaR-style) into risk/position limits given distributional assumptions, horizon, and Sharpe ratio.

  6. Think as a delegate (fund-of-strategies): set risk budgets, account for correlations and capacity, and monitor out-of-sample performance.


10.1. Capital allocation under uncertainty#

We learned how to maximize Sharpe Ratio if we are sure about the moments

\[E[R^e]Var(R^e)^{-1}\]
  • Investment managers rarely apply the Markowitz framework blindly

  • Why not used more often?

  • It requires specifying expected returns for entire universe of assets

  • Mean-variance optimization weights heavily on assets that have extreme average returns in the sample and with attractive covariance properties, i.e., high correlation with asset yielding slightly different return

In reality knowing the true moments of the return distribution is HARD as we discussed in the last few weeks

  • We will start by using “In-sample” means and variances

  • By construction weight \(W=w E[R^e]\Sigma_{R}^{-1} \) will deliver the Highest In-sample Sharpe Ratio. It is just math!

  • The issue is to what extent these In-sample moments are informative about the forward looking moments

  • What problems that might cause?

  • The key problem is that we are dealing with a limited sample of data

  • We are interested in finding the “ex-ante” tangency portfolio

  • But the basic MV analysis identifies the ex-post tangency portfolio, i.e. a portfolio that did well in the sample, but it can be a statistical fluke

  • The issue is that we do not observe the true population expectation and covariances, but only have a sample estimate

  • Whatever your research process, you are likely to end up quite uncertain about the alphas of your trading strategies

  • We will start by using what we learned and measuring the uncertainty regarding our risk premia estimates for each asset

  • We will then show how the weights change as we pertubate these estimates in a way that is consistent with the amount of uncertainty in our estimates

  • We will then show how sensitive the benefits of optimization are

url="https://raw.githubusercontent.com/amoreira2/Lectures/main/assets/data/GlobalFinMonthly.csv"
Data = pd.read_csv(url,na_values=-99)
Data['Date']=pd.to_datetime(Data['Date'])
Data=Data.set_index(['Date'])
Rf=Data['RF']
# make the risky portfolios excess returns
Data.iloc[:, 1:] = Data.iloc[:, 1:].sub(Data['RF'], axis=0)
Data.head()


# Data=get_factors('ff6',freq='monthly').dropna()
# Data.tail()
RF MKT USA30yearGovBond EmergingMarkets WorldxUSA WorldxUSAGovBond
Date
1963-02-28 0.0023 -0.0238 -0.004178 0.095922 -0.005073 NaN
1963-03-31 0.0023 0.0308 0.001042 0.011849 -0.001929 -0.000387
1963-04-30 0.0025 0.0451 -0.004343 -0.149555 -0.005836 0.005502
1963-05-31 0.0024 0.0176 -0.004207 -0.014572 -0.002586 0.002289
1963-06-30 0.0023 -0.0200 -0.000634 -0.057999 -0.013460 0.000839
# I will drop the risk-free asset and allocate only across excess returns
df=Data.drop(columns=['RF'])

W=df.mean()@ np.linalg.inv(df.cov())
# lets leverage it to have a yearly vol of 16%

W = W / np.sqrt(W @ df.cov() @ W.T) * 0.16
W 
array([ 1.79398726,  1.39130692,  1.5690601 , -1.00329769,  3.28999787])

Now what? What should we look at to gauge the impact of uncertainty?

10.1.1. Weights sensitivity to uncertainty#

📌

Small changes in inputs can flip sigs!

Large uncertainty means your weights can be way off

# lets look at what happens with weights and Sharpes as we manipulate the mean of the market return by one standard deviation

df=Data.drop(columns=['RF'])

Wmve=pd.DataFrame([],index=df.columns)

ERe=df.mean()
CovRe=df.cov()
T=df.shape[0]

for asset in df.columns:
    manipulation=2
    mu=df[asset].mean()
    # the standard deviation of the sample mean is the standard deviation of the asset divided by the square root of the number of observations
    musigma=df[asset].std()/T**0.5
    # I am creating a copy of the average return vector so I can manipulate it below 
    ERemsig=ERe.copy()

    ERemsig[asset]=mu-manipulation*musigma

    # mve for sample mean

    W=np.linalg.inv(CovRe) @ ERe
    Wmve['samplemean'] =W / np.sqrt(W @ CovRe @ W.T) * 0.16/12**0.5
    # mve for perturbed mean
    W =np.linalg.inv(CovRe) @ ERemsig
    # lets leverage it to have a yearly vol of 16%  similar to the market
    # the 12**0.5 is due to the fact that the data is monthly
    Wmve[asset+'-'+str(manipulation)+'std'] = W / np.sqrt(W @ CovRe @ W.T) * 0.16/12**0.5

Wmve
samplemean MKT-2std USA30yearGovBond-2std EmergingMarkets-2std WorldxUSA-2std WorldxUSAGovBond-2std
MKT 0.517880 -0.364918 0.645026 0.767838 0.735558 0.409393
USA30yearGovBond 0.401636 0.584278 -0.398806 0.321217 0.141720 0.799753
EmergingMarkets 0.452949 0.607435 0.387909 -0.176445 0.544975 0.457189
WorldxUSA -0.289627 0.171054 -0.446816 0.084501 -1.063844 0.117512
WorldxUSAGovBond 0.949741 0.722802 1.614553 1.034243 1.390611 -0.777712

10.1.2. Performance sensitivity to uncertainty#

SR=pd.DataFrame([],index=Wmve.columns)
for col in Wmve.columns:
    SR.loc[col,'SR'] =(Wmve[col] @ ERe / np.sqrt(Wmve[col] @ CovRe @ Wmve[col].T)) *12**0.5

    
SR
SR
samplemean 1.639023
MKT-2std 1.279627
USA30yearGovBond-2std 1.347976
EmergingMarkets-2std 1.312629
WorldxUSA-2std 1.546410
WorldxUSAGovBond-2std 1.194083

This is by moving around our estimates of expected returns one at a time

In practice we will also be off on correlations, variances, and several expected returns at the same time

Here we have quite a bit of data and we are assuming means are constant, so our standard errors are giving us too much confidence

The rally true measure of uncertainty can be found by looking at out of sample as we will do in our Performance evaluation lecture

an out of sample test let the data speak in the most pure form and it is closer to capture what real life trading is like

10.1.3. Bet sizing under uncertainty#

The industry developed many Ad-hoc approaches for bet sizing exactly to deal with the fact that Mean-variance optimization is quite sensitive to inputs

Bet sizing is one of the great skills in a portfolio manager because it requires instinct for uncertainty

I show some of the different approaches below.

You can obviously apply these rules to any trading strategy, but in practice this works better if you first purge the strategies from the obvious sources of co-movement. Certainly the market factor, but in practice many additional factors are used

Here I am applying them to the problem of combining “alpha” strategies–i.e it can be thought as applied to hedged factors, so the alphas are there expected excess returns

(w is a scalar that controls the overall size of the portfolio )

  • Mean-variance rule (the one we looked so far):

\[W_i=w \alpha\Sigma_{\epsilon}^{-1}[i]\]

in case the strategies have no co-movement, then $\(W_i=w \alpha_i/\sigma_{\epsilon,i}^2\)$

  • 1/N rule: ignore the magnitude of the alpha and simply bet on the direction of your idea

\[W_i=\frac{1}{N}\left((\alpha_i>0)-(\alpha_i<0)\right)\]

this is good if you have good hunches for mispricing, but you don’t get the magnitudes quite right

  • Proportional rule: Buy/sell proportional to the alpha

\[W_i=w \alpha_i\]

This ignore variance information and might be relevant if cannot take leverage

  • Risky-parity rule: assumes the Appraisal ratio of your different ideas are the same

    \[\frac{\alpha_i}{\sigma_i}=\frac{\alpha_j}{\sigma_j}\]

    If you apply this to the MV rule you get

\[W_i=w \frac{1}{\sigma_{\epsilon,i}}\]

which essentially mean you allocate the same vol for each strategy

\[W_i\sigma_{\epsilon,i}=W_j\sigma_{\epsilon,j}\]
  • Minimum-Variance rule:

\[W_i=w 1\Sigma_{\epsilon}^{-1}[i]\]

Note that instead of a vector of alphas we have a vector of 1’s A constant vector, no matter the particular value will give the same relative weights This assumes alphas are all the same and focus on using the information in the variance matrix to boost the Sharpe ratio If you look at the problem in chapter X, we will see that this is the solution of the variance minimization problem

\[ \min_W Var(W'R^e_T)~~~subject~~to~~~1'W= 1 \]

When applying this you want to make flip the strategy to make sure they are all have positive alphas. For example, if the idea is that AI is overvalued jsut make the trading strategy to be short AI stocks in case the strategies have no co-movement, then
$\(W_i=w \frac{1}{\sigma_{\epsilon,i}^2}\)$

  • Variance shrinkage rule: you shrink the variance-covariance matrix towards a particular value

\[W_i=w \alpha((1-\tau)\Sigma+\tau \overline{\sigma^2})^{-1}\]
  • where \(\tau\in[0,1]\) is the shrinkage factor, and \(\overline{\sigma^2}\) is the average indio variance

This makes the variance-covariance matrix behaved and less likely to have very specific correlation patterns You are shrinking all the variances to the average and all correlations to zero It is good idea to do that in general–so to some extent this is really the MV approach with a better variance estimator

In case the strategies have no co-movement, then $\(W_i=w \frac{\alpha_i}{\sigma_{\epsilon,i}^2(1-\tau)+\tau\overline{\sigma^2}}\)$

To illustrate these different approaches We will use 6 classic academic strategies:

  1. Market

  2. Size

  3. Value

  4. Profitability

  5. Investment

  6. Momentum.

These six factors are particularly popular both in the industry and in the academic community

Our portfolio problem will be to find the maximum SR combination out of these strategies

In a couple weeks we will learn how to construct these strategies from scratch

A few of them have ETFS that aim to replicate them, which potentially allow retail investors to get exposure to them cheaply and also industry people to easily hedge their factor exposures. (We will investigate carefully to what extent these ETFs do a good job..coming soon in a theater near you!)

For now we will use this data to take the perspective of an investor that does not differentiate across the factors and simply cares for the final SR

I.e for simplicity we will apply these rule directly to these factors and not the market hedged versions

towards the end we will discuss how to do this for the hedged ones

df_ff6=get_factors('ff6',freq='monthly').dropna().drop(columns=['RF'])
df_ff6.tail()
c:\Users\alan.moreira\Anaconda3\lib\site-packages\pandas_datareader\famafrench.py:114: FutureWarning: The argument 'date_parser' is deprecated and will be removed in a future version. Please use 'date_format' instead, or read your data in as 'object' dtype and then call 'to_datetime'.
  df = read_csv(StringIO("Date" + src[start:]), **params)
c:\Users\alan.moreira\Anaconda3\lib\site-packages\pandas_datareader\famafrench.py:114: FutureWarning: The argument 'date_parser' is deprecated and will be removed in a future version. Please use 'date_format' instead, or read your data in as 'object' dtype and then call 'to_datetime'.
  df = read_csv(StringIO("Date" + src[start:]), **params)
c:\Users\alan.moreira\Anaconda3\lib\site-packages\pandas_datareader\famafrench.py:114: FutureWarning: The argument 'date_parser' is deprecated and will be removed in a future version. Please use 'date_format' instead, or read your data in as 'object' dtype and then call 'to_datetime'.
  df = read_csv(StringIO("Date" + src[start:]), **params)
c:\Users\alan.moreira\Anaconda3\lib\site-packages\pandas_datareader\famafrench.py:114: FutureWarning: The argument 'date_parser' is deprecated and will be removed in a future version. Please use 'date_format' instead, or read your data in as 'object' dtype and then call 'to_datetime'.
  df = read_csv(StringIO("Date" + src[start:]), **params)
c:\Users\alan.moreira\Anaconda3\lib\site-packages\pandas_datareader\famafrench.py:114: FutureWarning: The argument 'date_parser' is deprecated and will be removed in a future version. Please use 'date_format' instead, or read your data in as 'object' dtype and then call 'to_datetime'.
  df = read_csv(StringIO("Date" + src[start:]), **params)
c:\Users\alan.moreira\Anaconda3\lib\site-packages\pandas_datareader\famafrench.py:114: FutureWarning: The argument 'date_parser' is deprecated and will be removed in a future version. Please use 'date_format' instead, or read your data in as 'object' dtype and then call 'to_datetime'.
  df = read_csv(StringIO("Date" + src[start:]), **params)
Mkt-RF SMB HML RMW CMA MOM
Date
2025-02-01 -0.0244 -0.0579 0.0491 0.0110 0.0306 -0.0081
2025-03-01 -0.0639 -0.0276 0.0290 0.0211 -0.0047 -0.0284
2025-04-01 -0.0084 -0.0059 -0.0340 -0.0285 -0.0267 0.0497
2025-05-01 0.0606 0.0070 -0.0288 0.0126 0.0251 0.0221
2025-06-01 0.0486 0.0083 -0.0160 -0.0319 0.0145 -0.0265
  1. We will target Total portfolio with yearly volatility 16%

  2. We will get the factors and focus on the sample that we have all the factors available

  3. We will apply the different rules

Mean Variance

# WE START WITH OUR ESTIMATED MOMENTS
# WE TAKE THOSE AS GIVEN
Sigma=df_ff6.cov().values
MU=df_ff6.mean().values
# THE DATA IS MONTHLY SO i WILL ADJUST  THE VOL TARGET TO HIT 16% ANNUALIZED
VolTarget=0.16/12**0

# optimal RELATIVE weights, need to calibrate the volatility
W=MU@np.linalg.inv(Sigma)

# compute the variance of the W portoflio
VarW=0 # to be replaced by the actual formula
print(VarW)
#adjusting the weights to meet the volatility target
w= 0 # to be replaced by the actual formula
print(w)
# Ww is our final weights with the volatility target
Ww=w*W
# final weights
print(Ww)

# check vol 

vol=0 # to be replaced by the actual formula


ER=0 # to be replaced by the actual formula
SR=0 # to be replaced by the actual formula

print(f"your volatility is {vol}")
print(f"Your Sharpe  Ratio is {SR}")
print(f"So your optimal portfolio e is \n {Ww[0]} in MKT, {Ww[1]} in SMB, {Ww[2]} in HML, {Ww[3]} in RMW, {Ww[4]} in CMA, {Ww[5]} in MOM")
[0.55890596 4.14714181 7.16424937 9.49356702 4.1166404 ]
0
0
[0. 0. 0. 0. 0.]
your volatility is 0
Your Appraisal Ratio is 0
So your optimal portfolio with market neutral exposure is 
 0.0 in SMB, 0.0 in HML, 0.0 in RMW, 0.0 in CMA, 0.0 in MOM, and 0 in Mkt-RF
# lets wrap it up in a function

def sizing(W,MU,Sigma,VolTarget=0.16/12**0.5,name='?'):
    VarW=0 # to be replaced by the actual formula
    w= 0 # to be replaced by the actual formula
    # Ww is our final weights with the volatility target
    Ww=w*W
    vol=0 # to be replaced by the actual formula
    ER=0 # to be replaced by the actual formula
    SR=0 # to be replaced by the actual formula

    print(f"your volatility is {vol}")
    print(f"Your Sharpe  Ratio is {SR}")
    print(f"The {name} portfolio is \n {Ww[0]} in MKT, {Ww[1]} in SMB, {Ww[2]} in HML, {Ww[3]} in RMW, {Ww[4]} in CMA, {Ww[5]} in MOM")
    return SR, Ww

W=MU@np.linalg.inv(Sigma)
sr,W=sizing(W,MU,Sigma,name='Mean-Variance')
your volatility is 0
So your optimal portfolio with market neutral exposure is 
 0.0 in SMB, 0.0 in HML, 0.0 in RMW, 0.0 in CMA, 0.0 in MOM, and 0 in Mkt-RF
Your Appraisal Ratio is 0

1/N rule

W=np.ones(5)/5
sr,W=sizing(W,MU,Sigma,name='1/N')
your volatility is 0
So your optimal portfolio with market neutral exposure is 
 0.0 in SMB, 0.0 in HML, 0.0 in RMW, 0.0 in CMA, 0.0 in MOM, and 0 in Mkt-RF
Your Appraisal Ratio is 0

Proportional rule

W=MU
sr,W=sizing(W,MU,Sigma,name='Proportional')
your volatility is 0
So your optimal portfolio with market neutral exposure is 
 0.0 in SMB, 0.0 in HML, 0.0 in RMW, 0.0 in CMA, 0.0 in MOM, and 0 in Mkt-RF
Your Appraisal Ratio is 0

Risky Parity rule

W=np.ones(5)@np.linalg.inv(np.diag(np.diag(Sigma)**0.5))
sr,W=sizing(W,MU,Sigma,name='Risk Parity')
your volatility is 0
So your optimal portfolio with market neutral exposure is 
 0.0 in SMB, 0.0 in HML, 0.0 in RMW, 0.0 in CMA, 0.0 in MOM, and 0 in Mkt-RF
Your Appraisal Ratio is 0

Minimum Variance rule

W=np.ones(5)@np.linalg.inv(Sigma)
sr,W=sizing(W,MU,Sigma,name='Minimum Variance')
your volatility is 0
So your optimal portfolio with market neutral exposure is 
 0.0 in SMB, 0.0 in HML, 0.0 in RMW, 0.0 in CMA, 0.0 in MOM, and 0 in Mkt-RF
Your Appraisal Ratio is 0

Variance Shrinkage rule

  • I will shrink them to the the average vol

  • I will shrink by 20%

  • The idea makes sense when you think the different assets are kind of similar so you think a good chunk of the sample variation is noise

  • I will do using mean-variance and minimum variances approaches

tau=0.2
sigma_bar=np.mean(np.diag(Sigma))
Sigma_hat=Sigma*(1-tau)+tau*np.eye(5)*sigma_bar
W=MU@np.linalg.inv(Sigma_hat)
sr,W=sizing(W,MU,Sigma,name='Mean-variance Shrinkage')
your volatility is 0
So your optimal portfolio with market neutral exposure is 
 0.0 in SMB, 0.0 in HML, 0.0 in RMW, 0.0 in CMA, 0.0 in MOM, and 0 in Mkt-RF
Your Appraisal Ratio is 0
c:\Users\alan.moreira\Anaconda3\lib\site-packages\numpy\core\fromnumeric.py:3504: RuntimeWarning: Mean of empty slice.
  return _methods._mean(a, axis=axis, dtype=dtype,
c:\Users\alan.moreira\Anaconda3\lib\site-packages\numpy\core\_methods.py:129: RuntimeWarning: invalid value encountered in scalar divide
  ret = ret.dtype.type(ret / rcount)
W=np.ones(5)@np.linalg.inv(Sigma_hat)
sr,W=sizing(W,MU,Sigma,name='Minimum-variance Shrinkage')

Question? How would implement this from the perspective of a CAPM capital allocator that wants their alpha portfolio to be market neutral?

10.2. Portfolios and Factor models#

Here we show how portfolio returns can be decomposed into factor exposures, alphas, and residuals using a linear factor model.

We derive the expected return and variance formulas for a portfolio under this model, highlighting the role of factor risk and idiosyncratic risk.

The objective is to set up the framework for estimating and analyzing portfolio risk and return using factor model outputs.

Say you have

  • a vector of asset excess returns R (N by 1),

  • weights W (N by 1),

  • factor loadings B (N by 1),

  • alphas A (N by 1),

  • residuals U (N by 1)

  • f is a scalar excess return factor

then I can write my portfolio return as

\[r_p=W.T @R=W.T @(A+Bf+U)\]

Because \(E[U]=0\), Thus it follows that

\[E[r_p]=W.T @(A+BE[f])\]

and

\[Var[r_p]=Var(W.T @(A+Bf+U))=(W.T@B)@Var(f)@(W.T@B).T+W.T@Var(U)@W\]

If we are working with a single factor then Var(f) is a scalar and it simplifies this way

\[Var[r_p]=Var(W.T @(A+Bf+U))=(W.T@B)^2*Var(f)+W.T@Var(U)@W\]
  • How do you compute the share of variance that is systematic/idiosyncratic?

  • Why not simply use the in-sample variance of your portfolio?

  • What is the important assumption that likely fails in reality? how to deal with it?

To implement this lets starts start by estimating our factor model for these trading strategies

import statsmodels.api as sm

# Define the factors and the market factor
factors = ['SMB', 'HML', 'RMW', 'CMA', 'MOM']
market_factor = 'Mkt-RF'

# Initialize lists to store the results
Alpha = []
Beta = []
residuals = []
Alpha_se = []
# Run univariate regressions
# here we are running a regression of each factor on the market factor
for factor in factors:
    X = sm.add_constant(df_ff6[market_factor])
    y = df_ff6[factor]
    model = sm.OLS(y, X).fit()
    Alpha.append(model.params['const'])
    Beta.append(model.params[market_factor])
    # I am storying the entire time-series of residuals
    residuals.append(model.resid)

# Convert Alpha and Beta to numpy arrays
Alpha = np.array(Alpha)
Beta = np.array(Beta)

# Calculate the variance-covariance matrix of the residuals
residuals_matrix = np.vstack(residuals)
Sigma_e = np.cov(residuals_matrix)
#now under the assumption that the residuals are uncorrelated ( they are not!)


Sigma_e_uncorr = np.diag(np.diag(np.cov(residuals_matrix.T)))

# Display the results
print("Alpha:", Alpha*12)
print("Beta:", Beta)
print("Sigma_e:")
display(Sigma_e*12)
print("Sigma_e_uncorr:")
display(Sigma_e_uncorr*12)
Alpha: [0.00324195 0.04349594 0.03955175 0.04143328 0.08341451]
Beta: [ 0.19954208 -0.13719646 -0.09352601 -0.16246303 -0.16205943]
Sigma_e:
array([[ 1.00563313e-02, -9.24337485e-04, -2.82466739e-03,
        -5.14453377e-04,  1.36587317e-06],
       [-9.24337485e-04,  1.01484903e-02,  3.74978561e-04,
         4.47136746e-03, -3.41595108e-03],
       [-2.82466739e-03,  3.74978561e-04,  5.70013428e-03,
        -3.92044656e-04,  5.51842057e-04],
       [-5.14453377e-04,  4.47136746e-03, -3.92044656e-04,
         4.46193077e-03, -7.83303223e-04],
       [ 1.36587317e-06, -3.41595108e-03,  5.51842057e-04,
        -7.83303223e-04,  2.03162979e-02]])
Sigma_e_uncorr:
array([[0.00083899, 0.        , 0.        , ..., 0.        , 0.        ,
        0.        ],
       [0.        , 0.00270649, 0.        , ..., 0.        , 0.        ,
        0.        ],
       [0.        , 0.        , 0.00027303, ..., 0.        , 0.        ,
        0.        ],
       ...,
       [0.        , 0.        , 0.        , ..., 0.01326286, 0.        ,
        0.        ],
       [0.        , 0.        , 0.        , ..., 0.        , 0.00629202,
        0.        ],
       [0.        , 0.        , 0.        , ..., 0.        , 0.        ,
        0.00475249]])

10.3. The rise of “pod shops”#

Hedge funds used to be associated with Principal traders

  • George Soros

  • Julian Robertson

  • David Tepper

  • Paul Tudor Jones

  • Steve Cohen

  • John Paulson

Now we have the rise of Citadel and Millenium among a few others

Pod shops are organized totally different

Idea generation are done at the pod level in groups of 5-10 people

Capital is allocated to the pod by the hedge fund principal, who monitors exposures and hedges residual factor risk

Why pod traders work for Citadel and not choose to manage their own fund?

A calculation

Consider a pod shop with N pods with SR sr and total vol \(\sigma\), what is their alpha? What is their SR?

\[E[\sum x^*_ir_i]=E[\sum \frac{\alpha_i}{\sigma^2_i}\alpha_i]=N*sr^2\]
\[Var[\sum x^*_ir_i]=Var[\sum \frac{\alpha_i}{\sigma^2_i}\epsilon_i]=\sum (\frac{\alpha_i}{\sigma^2_i})^2Var[\epsilon_i]\]
\[Var[\sum x^*_ir_i]=\sum (\frac{\alpha_i}{\sigma^2_i})^2\sigma^2_i=N sr^2\]

so the Sharpe Ratio of the pool of pods is

\[sr_{pool}=\frac{Nsr_{pod}^2}{\sqrt{N}sr_{pod}}=\sqrt{N}sr_{pod}\]

The Sharpe ratio of the pool of pods grows with the number of pods

This means that if managed individually, the average pod will accumulate wealth at rate \(\sigma*sr\)

But if managed in a pool structure, the average pod will accumulate as \(\sqrt{N}\sigma*sr\)

Finding a new uncorrelated idea is super valuable!

The marginal change in cash flows are

\[\frac{\sigma*sr_{pool}}{N}\]
  • decreases in the number of pods

  • But always positive–if indeed similar SR and uncorrelated!

Note that this calculation under-estimates the value of the pod structure

  • The higher your Sharpe ratio, the more volatility you can take without bearing significant risk of loss (wealth just grows too quickly)

But it also not realistic

  • Your marginal pod is likely to be worse or correlated with the others

10.4. đź§  Key Takeaways#

  • Estimation risk matters. MV weights are highly sensitive when signals are weak or assets are collinear; always stress test inputs.

  • Robust bet sizing. Simple baselines (e.g., \(1/N\), vol targeting, risk parity) often outperform noisy MV in real time without aggressive views.

  • Shrinkage helps. Regularizing \(\Sigma\) (and sometimes \(\mu\)) stabilizes weights and improves out-of-sample SR.

  • Factor lens. Decomposing risk by factor clarifies whether diversification is true (low common exposures) or illusory (highly correlated bets).