Workshop 2 - Hedge Funds

M6 – Hedge Funds

Author

Alberto Dorantes, Ph.D.

Published

September 21, 2026

Abstract
In Workshop 1 we reviewed the tools: portfolio theory, the CAPM, performance measures, VaR/ES and factor models. In this workshop we use those tools to open the black box of hedge funds. We study what hedge funds actually do (strategies and returns-based style analysis), how they build market-neutral positions, pure plays and portable alpha with index futures, how a global macro manager thinks about the Mexican peso carry trade, why option-like payoffs and tail risk make the usual performance measures misleading, how illiquidity, database biases and fees distort reported returns, and finally how much an investor should allocate to hedge funds using the Treynor-Black and Black-Litterman models. All concepts are illustrated with real data and short Python examples, with a Mexican investor’s perspective in mind.

1 Directions for this workshop

This workshop is the continuation of Workshop 1. In Workshop 1 we put all the foundations in the same language, the same notation and the same dataset. Now we use them to study hedge funds themselves.

As in Workshop 1, you have to work in a new Google Colab notebook (or any other front-end such as VScode or RStudio). Read each section, re-type and run the code yourself (do not only copy-paste; type it so you notice what each line does), take your own notes in text cells, and do the CHALLENGES.

You have to submit the .ipynb file of your Notebook.

2 From Workshop 1 to Workshop 2

2.1 What we already reviewed

In Workshop 1 we built a toolbox. Every tool in it will be used again here, so it is worth listing them:

Workshop 1 What we learned
Introduction Mutual funds vs hedge funds: regulation, investors, fees (“2 and 20”), liquidity, and the idea that a hedge fund sells \alpha while a mutual fund sells \beta
Part I Portfolio return and risk, diversification, GMV, tangency portfolio, efficient frontier, CML, and why optimal weights are fragile
Part II Premium returns, the CAPM regression, beta, systematic vs non-systematic risk, a first hedge with SPY
Part III HPR, time- vs money-weighted returns, Sharpe, Treynor, Jensen’s alpha, Information ratio, M²
Part IV Value at Risk and Expected Shortfall (historical and parametric)
Part V Fama-French factor models, real vs fake alpha, a first market-neutral portfolio, drawdowns

We closed Workshop 1 with three ideas:

  1. A hedge is a negative weight.
  2. Alpha is model-dependent.
  3. Volatility is not risk.

2.2 What is new in Workshop 2

Workshop 1 answered the question “how do we measure a portfolio?”. Workshop 2 answers the questions a professional asks before putting money into a hedge fund:

  1. What does this fund really do? Strategies and returns-based style analysis (Part I).
  2. How is a hedged position built? Dollar-, beta- and factor-neutral positions, hedging with index futures, pure plays, statistical arbitrage, 130/30 funds and portable alpha (Part II).
  3. How does a global macro manager think? The Mexican peso carry trade and currency hedging for a Mexican investor (Part III).
  4. Are the usual performance measures fooling me? Option-like payoffs, market timing and tail risk (Part IV).
  5. Are the reported numbers even right? Smoothed returns, survivorship and backfill bias, and the effect of fees (Part V).
  6. How much should I allocate? The Treynor-Black and Black-Litterman models, inside an investment policy framework (Part VI).

2.3 How the two workshops connect

Every part of this workshop re-uses a tool from Workshop 1:

Tool from Workshop 1 Where it is used in Workshop 2
Negative weights, portfolio variance Market-neutral positions, pure plays, 130/30 (Part II)
CAPM regression and beta Hedge ratios with futures (Part II); up/down betas and market timing (Part IV)
Multiple regression and factor models Style analysis (Part I); factor-neutral positions (Part II)
Jensen’s alpha and Information ratio Portable alpha (Part II); Treynor-Black (Part VI)
Sharpe ratio Why it is inflated by option selling and by smoothed returns (Parts IV and V)
VaR, ES, drawdowns Tail risk of carry trades and put writing (Parts III and IV)
Tangency portfolio and its fragility Black-Litterman as the practical fix (Part VI)
TipWhy this matters for the Reto with Banamex

In the Reto you will design an investment strategy as if you were a family office. A family office does not ask “what is the Sharpe ratio of this fund?”; it asks “should this fund be in my portfolio, and how much of it?”. Parts I to V give you the tools to evaluate a hedge fund honestly; Part VI gives you the tools to decide the allocation, from the point of view of an investor whose wealth is measured in pesos.

3 Workshop structure

The topics and exercises of this workshop will be covered in weeks 7 to 10 (8 sessions of 1 hour 40 minutes, about 13 class hours). Week 11 is for the final exam and feedback.

Session Sections Topics
1 Setup + Part I Hedge fund strategies, access from Mexico, liquid alternatives, first look at the data
2 Part I Returns-based style analysis, style drift, replication — Challenge 1
3 Part II Dollar-, beta- and factor-neutral positions, hedging with index futures, pure plays, statistical arbitrage
4 Part II 130/30 funds, portable alpha, leverage and margin — Challenge 2
5 Part III + Part IV The MXN carry trade and currency hedging — Challenge 3; up- and down-market betas
6 Part IV + Part V Market timing, put writing, tail-risk measures — Challenge 4; smoothed returns and database biases
7 Part V + Part VI Fees, high-water marks, incentive fees as options — Challenge 5; Treynor-Black
8 Part VI Black-Litterman and the family office case — Challenge 6

4 Setup: packages and data

4.1 Libraries

We use the same libraries as in Workshop 1:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
import statsmodels.formula.api as smf
from scipy import optimize, stats

pd.set_option("display.float_format", lambda x: f"{x:,.4f}")
pd.set_option("display.width", 120)
pd.set_option("display.max_columns", 20)
np.set_printoptions(suppress=True, precision=4)

If you are in Google Colab and any library is missing, install it with !pip install <library>.

4.2 The dataset

We work with monthly data from December 2014 to August 2026. In Workshop 1 we had 8 instruments; now we need four groups of series:

1. Asset classes. They are the “building blocks” against which we will explain what hedge funds do:

Ticker Instrument
SPY S&P 500 ETF (US large caps)
IWM Russell 2000 ETF (US small caps)
EFA Developed markets ex-US ETF
EEM Emerging markets ETF
IEF US Treasuries 7–10 years ETF
TLT US Treasuries 20+ years ETF
HYG US high-yield corporate bonds ETF
GLD Gold ETF
UUP US dollar index ETF (long USD against G10 currencies)
DBC Commodities ETF

2. Hedge fund proxies. Real hedge funds do not publish daily prices and their databases are private and expensive. What is public is a growing family of liquid alternatives: mutual funds and ETFs that run hedge-fund strategies inside a regulated wrapper (the “blurring frontier” we mentioned in Workshop 1). They are our window into the hedge fund world:

Ticker Fund Strategy
QAI IQ Hedge Multi-Strategy Tracker ETF Replicates the hedge fund universe with ETFs
HDG ProShares Hedge Replication ETF Replicates a broad hedge fund index with a factor model
MNA IQ Merger Arbitrage ETF Merger (risk) arbitrage
MERFX The Merger Fund Merger arbitrage (the oldest fund of its kind)
BTAL U.S. Market Neutral Anti-Beta ETF Long low-beta stocks, short high-beta stocks
QMNIX AQR Equity Market Neutral Fund Equity market neutral (long/short stock selection)
AQMIX AQR Managed Futures Strategy Fund Managed futures / trend following
QSPIX AQR Style Premia Alternative Fund Multi-asset long/short factor (“alternative risk premia”)

3. Options and volatility. SPX (S&P 500 price index), PUT (the CBOE S&P 500 PutWrite index, a strategy that sells put options every month) and VIX (the implied volatility of the S&P 500, in annual percentage points — it is not a price).

4. Mexico. IPC (S&P/BMV IPC, in pesos), EWW (iShares MSCI Mexico ETF, in dollars), USDMXN (pesos per dollar), GMEXICO (Grupo México, BMV), SCCO (Southern Copper, NYSE, controlled by Grupo México), AMX (América Móvil ADR, NYSE) and AMXB (América Móvil series B, BMV).

Plus two more files: the Fama-French 5 factors + Momentum (monthly, with the US risk-free rate RF), and the 3-month interest rates of the US and Mexico (annual rates, from FRED).

hf2_prices_monthly.csv    # month-end adjusted prices (and VIX level)
hf2_factors_monthly.csv   # Mkt_RF, SMB, HML, RMW, CMA, RF, MOM (monthly, decimals)
hf2_rates_monthly.csv     # US_TB3M, MX_3M (ANNUAL rates, decimals)
ImportantAbout the data files

The three CSV files distributed with this workshop contain real market data downloaded from Yahoo Finance, Kenneth French’s data library and FRED. They are frozen at August 2026 so that everybody in class gets exactly the same numbers.

To update the data, run the script build_workshop2_data.py (included) once. It downloads the same series and overwrites the three CSV files with exactly the same column structure. Everything in this workshop will then run unchanged, but your numbers (and some of the interpretations!) will change slightly.

Alternatively, set USE_LOCAL_DATA = False in the next chunk and the workshop will download the data on the fly.

4.3 Loading the data

USE_LOCAL_DATA = True     # set to False to download live data

if USE_LOCAL_DATA:
    prices = pd.read_csv("hf2_prices_monthly.csv", index_col="Date", parse_dates=True)
    ff     = pd.read_csv("hf2_factors_monthly.csv", index_col="Date", parse_dates=True)
    rates  = pd.read_csv("hf2_rates_monthly.csv", index_col="Date", parse_dates=True)
else:
    import build_workshop2_data as bd    # the script must be in the same folder
    prices, ff, rates = bd.get_prices(), bd.get_factors(), bd.get_rates()

print(prices.shape, ff.shape, rates.shape)
(141, 28) (140, 7) (141, 2)
prices.tail(3)
                SPY      IWM      EFA     EEM     TLT     IEF     HYG      GLD     UUP     DBC  ...        PUT  \
Date                                                                                            ...              
2026-06-30 744.9200 299.6670 103.8800 68.4100 85.4292 93.6002 78.7847 368.3800 28.4100 26.6600  ... 3,517.1200   
2026-07-31 745.1800 290.4410 105.5800 64.0900 81.6073 92.3004 78.6649 371.5400 28.1700 29.4500  ... 3,557.5900   
2026-08-31 765.1500 293.1640 107.4500 67.0200 82.2050 92.4080 79.3750 408.4200 28.1200 31.3000  ... 3,616.8800   

                  SPX     VIX         IPC     EWW  USDMXN  GMEXICO     SCCO     AMX    AMXB  
Date                                                                                         
2026-06-30 7,499.3600 16.4500 66,966.7000 75.2700 17.4720 195.9320 171.2470 25.6825 22.4615  
2026-07-31 7,489.7200 15.9900 66,937.0000 76.8100 17.3446 206.5110 179.5510 25.3500 22.0000  
2026-08-31 7,686.1400 14.9200 65,430.3000 76.7200 17.0306 232.5900 208.8700 23.0400 19.6100  

[3 rows x 28 columns]

Three comments on the data:

  • As in Workshop 1 we use adjusted prices, so returns are total returns (dividends included). The two exceptions are IPC and SPX, which are price indexes: their returns exclude dividends (about 2–3% per year for both). Keep this in mind whenever we compare them with total-return series.

  • VIX is a level of volatility, not a price. We never compute a “return” of the VIX in this workshop; we use it as an input to price options.

  • USDMXN is quoted as pesos per dollar. When it goes up, the peso depreciates (you need more pesos to buy one dollar).

4.4 Returns: the rule we follow

We keep the rule of Workshop 1:

  1. cc (log) returns for means, volatilities, regressions and risk measures;
  2. simple returns whenever we aggregate across assets (portfolios, hedges, style analysis, fees).
r  = np.log(prices.drop(columns="VIX")).diff().dropna()     # cc returns
Rs = prices.drop(columns="VIX").pct_change().dropna()       # simple returns

# the factor file ends one month before the prices; keep common months
r,  ff = r.align(ff, join="inner", axis=0)
Rs = Rs.loc[r.index]                     # same months as r

rf_s = ff["RF"]                          # monthly SIMPLE risk-free rate (US)
rf_m = np.log(1 + ff["RF"])              # monthly cc risk-free rate (US)
rates_m = rates.loc[r.index]             # annual rates, same months

print(r.index.min().date(), "->", r.index.max().date(), "|", len(r), "months")
2015-01-31 -> 2026-07-31 | 139 months

4.5 Helper functions

We re-use two helpers from Workshop 1 (plot_time_series and factor_model) and add a function that summarizes a return series with the statistics that matter for hedge funds. Note the new columns: in Workshop 1 we saw that volatility is not risk, so we add skewness, excess kurtosis, first-order autocorrelation and maximum drawdown.

def plot_time_series(df, title, y_label, legend_title="Legend"):
    """Time plot of every column of a data frame with a datetime index."""
    plt.figure(figsize=(12, 6))
    ax = plt.gca()
    df.plot(ax=ax)
    plt.title(title)
    plt.xlabel("Date")
    plt.ylabel(y_label)
    plt.grid(True, alpha=0.3)
    plt.legend(title=legend_title)
    plt.show()

def factor_model(ret_series, factors=("Mkt_RF",), rf=rf_m, data=ff):
    """OLS of the premium return of a series on a set of factors."""
    d = pd.DataFrame({"y": ret_series - rf})          # premium return
    d = d.join(data[list(factors)], how="inner").dropna()
    formula = "y ~ " + " + ".join(factors)
    return smf.ols(formula, data=d).fit()

def max_drawdown(ret_cc):
    """Maximum drawdown of a monthly cc return series."""
    wealth = np.exp(ret_cc.cumsum())
    return (wealth / wealth.cummax() - 1).min()

def hf_stats(ret_cc, rf=rf_m, mkt=r["SPY"]):
    """Summary statistics of a monthly cc return series."""
    rf, mkt = rf.loc[ret_cc.index], mkt.loc[ret_cc.index]   # same months
    ex = ret_cc - rf
    beta = np.cov(ret_cc, mkt)[0, 1] / mkt.var()
    return pd.Series({
        "Ann. return": np.exp(12 * ret_cc.mean()) - 1,
        "Ann. vol":    ret_cc.std() * np.sqrt(12),
        "Sharpe":      ex.mean() / ex.std() * np.sqrt(12),
        "Beta":        beta,                          # beta with respect to SPY
        "Corr":        ret_cc.corr(mkt),              # correlation with SPY
        "Skew":        stats.skew(ret_cc),
        "Ex. kurt":    stats.kurtosis(ret_cc),        # excess kurtosis
        "AC(1)":       ret_cc.autocorr(1),            # first-order autocorrelation
        "Max DD":      max_drawdown(ret_cc)})         # maximum drawdown

5 PART I — What hedge funds really do: strategies and style analysis

5.1 The hedge fund landscape

In Workshop 1 we defined a hedge fund by what it is allowed to do: short selling, leverage, derivatives, illiquid assets, lock-ups and a “2 and 20” fee. That definition tells us nothing about what a particular fund actually does with that freedom. The industry uses a standard taxonomy of strategies (Bodie, Kane & Marcus, Table 26.1):

Strategy What the manager does
Convertible arbitrage Long convertible bonds, short the underlying stock
Dedicated short bias Net short position, usually in equities
Emerging markets Exploits inefficiencies in emerging markets (often long-only)
Equity market neutral Long/short stocks, hedging market, sector and size exposure
Event driven Mergers, acquisitions, restructurings, bankruptcies
Fixed-income arbitrage Price anomalies among related interest-rate securities
Global macro Long/short positions in any market, based on macroeconomic views
Long/short equity Long and short stocks, not necessarily market neutral
Managed futures Futures on financials, currencies and commodities; often trend following
Multistrategy Opportunistic choice of strategy
Fund of funds Allocates money to other hedge funds

The list is long, but every strategy can be placed on two axes.

Directional vs non-directional. A directional strategy is a bet that some market or sector will go up or down (global macro, long/short equity with a net long bias, dedicated short). A non-directional strategy tries to profit from a temporary misalignment between two related prices, hedging the direction of the market (equity market neutral, fixed-income arbitrage, convertible arbitrage, merger arbitrage). Non-directional strategies are the ones that gave hedge funds their name.

Convergence vs relative value. Within the non-directional group, a convergence trade has a date by which the mispricing must disappear (a futures contract at expiration, a merger that closes on a known date, a bond at maturity). A relative value trade has no such date: the spread between two related prices is “too wide”, but nothing forces it to close by a given day. The distinction matters because a trade that is right but has no convergence date can lose money for a long time before it pays — and a leveraged fund may not survive that long (we will see this with LTCM in Part IV).

NoteHow can a Mexican investor get hedge fund exposure?

Hedge funds, as a legal vehicle, are essentially offshore private funds (Cayman Islands, Delaware, Luxembourg, Ireland). A Mexican investor typically reaches them in one of these ways:

  • Private banking. Wealthy and institutional clients (and family offices) invest in offshore hedge funds or funds of funds through the private banking area of a bank or broker, subject to the minimums and eligibility rules of each fund.
  • Liquid alternatives through the SIC. Many foreign ETFs listed in the Sistema Internacional de Cotizaciones can be bought through a Mexican broker in pesos. This includes some of the liquid alternatives we use in this workshop.
  • Mexican regulated funds. Fondos de inversión (regulated by the CNBV) can follow long/short or multi-asset mandates within their investment regime, and institutional vehicles such as CKDs and CerPIs give access to private alternatives (mostly private equity, infrastructure and real estate, which are the topics of other modules of the concentration).
  • Institutional mandates. Afores and insurance companies invest in foreign alternatives through mandates and structured vehicles, within the limits set by their regulators.

The rules change over time, so always check the current regulation (CNBV, CONSAR, Banxico) before recommending a vehicle to a client. Whatever the vehicle, the analytical questions of this workshop are the same.

5.2 A first look at the hedge fund proxies

HF = ["QAI", "HDG", "MNA", "MERFX", "BTAL", "QMNIX", "AQMIX", "QSPIX"]

summary = pd.DataFrame({k: hf_stats(r[k]) for k in ["SPY", "IEF"] + HF}).T
summary.round(3)
       Ann. return  Ann. vol  Sharpe    Beta    Corr    Skew  Ex. kurt   AC(1)  Max DD
SPY         0.1360    0.1500  0.7220  1.0000  1.0000 -0.4880    0.6240 -0.1340 -0.2390
IEF         0.0110    0.0650 -0.1380  0.0470  0.1100 -0.0410    0.1640  0.0200 -0.2320
QAI         0.0320    0.0530  0.2250  0.3100  0.8730 -0.3790    1.5180 -0.0370 -0.1380
HDG         0.0330    0.0580  0.2200  0.3410  0.8830 -0.4410    1.8440 -0.0510 -0.1410
MNA         0.0280    0.0470  0.1560  0.1130  0.3620 -2.0710   14.1810 -0.0790 -0.0950
MERFX       0.0350    0.0280  0.5120  0.0750  0.4060 -0.7130    1.7300 -0.0470 -0.0410
BTAL       -0.0320    0.1570 -0.3350 -0.6300 -0.6020 -0.3600    0.6920  0.0190 -0.5060
QMNIX       0.0690    0.0960  0.4910 -0.1060 -0.1650  0.3920    0.8870  0.2220 -0.3840
AQMIX       0.0420    0.1030  0.2040 -0.1930 -0.2820  0.0890   -0.3230  0.0500 -0.2440
QSPIX       0.0750    0.1260  0.4150 -0.0170 -0.0200  0.6940    1.6000  0.0880 -0.3960
growth = np.exp(r[["SPY"] + HF].cumsum())
plot_time_series(growth, "Value of $1 invested in January 2015",
                 "Investment value", "Fund")

Read the table row by row, with the eyes of Workshop 1:

  • No hedge fund proxy beat SPY in raw return, and only two of them (MERFX and QMNIX) come close in Sharpe ratio. This is not unusual: hedge fund strategies are not designed to beat a bull market in stocks, and 2015–2026 was an exceptional period for US large caps.
  • The betas are small (and several are negative), which is the whole point: a hedge fund sells returns that do not depend on the direction of the market. BTAL, for example, has a beta of about -0.6: it is a hedge against the market.
  • Look at MNA (merger arbitrage): a low volatility (4.7%) but a skewness of -2 and an excess kurtosis of 14. Its volatility is small because most months nothing happens; the risk lives in the few months in which announced deals break. Remember this number: it is the first sign that volatility is not risk (Part IV).
  • QMNIX has a positive autocorrelation of about 0.22: good months tend to be followed by good months. In a liquid market returns should be almost unpredictable. We will come back to this in Part V.
  • QAI and HDG have a correlation of almost 0.9 with SPY. Two funds that are sold as “hedge fund exposure” behave, month by month, very much like a small position in the stock market. The next section shows how to make this statement precise.

5.3 Returns-based style analysis

5.3.1 The idea

In Workshop 1 we ran factor models to decide whether an alpha was real. Style analysis (Sharpe, 1992) uses the same regression machinery with a different purpose: to find the passive portfolio of asset classes that best mimics a fund. The fund’s returns are regressed on the returns of K asset classes (the “styles”):

R_{F,t}=\sum_{k=1}^{K}w_k R_{k,t}+e_t

and the coefficients w_k are interpreted as the fund’s implicit asset allocation. For a mutual fund, which cannot short or borrow, Sharpe imposes two constraints:

w_k\geq 0 \qquad \text{and} \qquad \sum_{k=1}^{K} w_k=1

The weights are found by minimizing the variance of the difference between the fund and its style portfolio (the tracking error), so the problem is a small quadratic optimization rather than an OLS regression. Three numbers come out of it:

  • the style weights w_k: what the fund looks like;
  • the R^2=1-\frac{Var(e)}{Var(R_F)}: the share of the fund’s variance explained by style (asset allocation);
  • the average of e_t (annualized): the return from selection, i.e., what the manager added (or lost) relative to a passive mix of the same styles.

The key idea is that 1-R^2 is the part of the fund that you cannot buy with index funds. That is the only part worth paying active fees for.

5.3.2 Constrained style analysis in Python

We add cash (the T-Bill) as an eleventh style, because many funds keep a large part of their assets in cash (or in collateral, which is the same thing).

STYLES = ["SPY", "IWM", "EFA", "EEM", "IEF", "TLT", "HYG", "GLD", "UUP", "DBC"]
X_style = Rs[STYLES].copy()
X_style["CASH"] = rf_s                    # T-Bill as the eleventh style

def style_analysis(fund, X=X_style):
    """Sharpe (1992) constrained style analysis with SIMPLE returns.
    Minimizes Var(fund - X w) subject to w >= 0 and sum(w) = 1."""
    y, Xv = fund.values * 100, X.values * 100        # in %: better numerics
    K = Xv.shape[1]
    te = lambda w: np.var(y - Xv @ w)                # tracking-error variance
    res = optimize.minimize(te, np.repeat(1/K, K), method="SLSQP",
                            bounds=[(0, 1)] * K,
                            constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}],
                            options={"ftol": 1e-12, "maxiter": 1000})
    w = pd.Series(res.x, index=X.columns)
    e = fund - X @ w                                 # selection return
    return w, 1 - e.var() / fund.var(), e

w_qai, r2_qai, e_qai = style_analysis(Rs["QAI"])
print("Style weights of QAI:")
Style weights of QAI:
print(w_qai[w_qai > 0.005].sort_values(ascending=False).round(3))
CASH   0.5970
EEM    0.0970
SPY    0.0880
IWM    0.0740
EFA    0.0390
HYG    0.0390
TLT    0.0380
GLD    0.0130
DBC    0.0110
dtype: float64
print(f"\nR-squared (style)            : {r2_qai:.3f}")

R-squared (style)            : 0.913
print(f"Selection return (annualized): {12*e_qai.mean():.4f}")
Selection return (annualized): -0.0144
NoteWhy SLSQP and why multiply by 100?

SLSQP is the method of scipy.optimize.minimize that accepts both bounds (each weight between 0 and 1) and an equality constraint (the weights add to one). Monthly returns are small numbers (0.01), so their variance is tiny (0.0001) and the optimizer may stop before moving from the starting point. Working in percentage points is a simple way to give the optimizer numbers it can “see”. If you ever get style weights exactly equal to 1/K, this is the problem.

Now all the funds at once:

styles = {}
for f in HF:
    w, r2, e = style_analysis(Rs[f])
    styles[f] = pd.concat([w, pd.Series({"R2 (style)": r2,
                                         "Selection (ann)": 12 * e.mean()})])
style_table = pd.DataFrame(styles)
style_table.round(3)
                    QAI     HDG     MNA  MERFX    BTAL  QMNIX   AQMIX  QSPIX
SPY              0.0880  0.0130  0.0000 0.0000  0.0000 0.0000  0.0000 0.0000
IWM              0.0740  0.1550  0.0220 0.0300  0.0000 0.0000  0.0000 0.0000
EFA              0.0390  0.1270  0.0000 0.0000  0.0000 0.0000  0.0000 0.0520
EEM              0.0970  0.0670  0.0000 0.0120  0.0000 0.0000  0.0000 0.0000
IEF              0.0040  0.0120  0.0000 0.0000  0.1330 0.0000  0.0000 0.0000
TLT              0.0380  0.0000  0.0000 0.0000  0.0000 0.0000  0.0000 0.0000
HYG              0.0390  0.0000  0.2640 0.1000  0.0000 0.0000  0.0000 0.0000
GLD              0.0130  0.0000  0.0030 0.0000  0.2370 0.0490  0.2080 0.0000
UUP              0.0000  0.0790  0.0000 0.0450  0.6290 0.2130  0.6980 0.4220
DBC              0.0110  0.0000  0.0000 0.0050  0.0000 0.0640  0.0790 0.2310
CASH             0.5970  0.5460  0.7110 0.8080  0.0000 0.6730  0.0150 0.2950
R2 (style)       0.9130  0.9370  0.2330 0.2370  0.1090 0.0360  0.1730 0.1220
Selection (ann) -0.0140 -0.0150 -0.0010 0.0080 -0.0700 0.0410 -0.0060 0.0400

How to read this table:

  • QAI and HDG are about 60% cash and 40% a diversified long-only portfolio (US large and small caps, international and emerging stocks, some bonds). Style explains more than 90% of their variance, and the selection return is negative (about -1.5\% per year, roughly their fees and trading costs). This is no surprise: these two funds are replication products — they try to reproduce the average hedge fund with liquid instruments. What the table tells us is what the average hedge fund looks like from the outside: a diluted balanced portfolio.
  • For the other six funds the R^2 is tiny (between 4% and 24%). With the constraints w_k \geq 0 and \sum w_k=1 the style portfolio cannot reproduce them. Look at the weights: large weights on CASH and UUP (the dollar ETF, the least volatile risky asset). When the optimizer cannot find a mix that moves like the fund, it hides in the lowest-volatility assets. That is a symptom, not a finding.
  • Why does the constrained model fail? Because these funds short and lever. BTAL is short high-beta stocks, so it moves against the market; a portfolio with non-negative weights can never produce that.

5.3.3 Style analysis for hedge funds: remove the constraints

For hedge funds, Bodie, Kane & Marcus (Section 26.4) drop both constraints: loadings can be negative (short positions) and do not need to add up to one (leverage). The model becomes an ordinary multiple regression of premium returns on the premium returns of the styles — exactly the factor models of Workshop 1, with asset classes as factors:

R_{F,t}-R_{f,t}=\alpha_F+\sum_{k=1}^{K}\beta_{F,k}\left(R_{k,t}-R_{f,t}\right)+e_{F,t}

def hf_style(fund, styles=STYLES, rets=Rs, rf=rf_s):
    """Unconstrained style regression for hedge funds (premium returns)."""
    d = rets[[fund] + styles].sub(rf, axis=0)
    return sm.OLS(d[fund], sm.add_constant(d[styles])).fit()

hf_rows = {}
for f in HF:
    m = hf_style(f)
    row = m.params.rename({"const": "alpha (monthly)"})
    row["alpha t-stat"] = m.tvalues["const"]
    row["R2"] = m.rsquared
    hf_rows[f] = row
hf_style_table = pd.DataFrame(hf_rows)
hf_style_table.round(3)
                    QAI     HDG     MNA   MERFX    BTAL   QMNIX   AQMIX   QSPIX
alpha (monthly) -0.0010 -0.0010  0.0000  0.0010 -0.0010  0.0040  0.0000  0.0040
SPY              0.0900  0.0180 -0.1070 -0.0280  0.0520 -0.1330  0.0420 -0.1290
IWM              0.0750  0.1580  0.0560  0.0280 -0.6030 -0.1680 -0.0120 -0.1990
EFA              0.0350  0.1320 -0.0120  0.0080  0.1830  0.4310  0.0250  0.4690
EEM              0.0950  0.0680  0.0070  0.0150 -0.2950 -0.0950 -0.0030 -0.0720
IEF             -0.0060  0.0200 -0.2720 -0.0980  0.1130  0.0690 -0.9690  0.3020
TLT              0.0420 -0.0070  0.0440  0.0070  0.0560 -0.2120  0.3960 -0.2780
HYG              0.0400 -0.0240  0.4290  0.1610  0.2020 -0.0640 -0.6240  0.0360
GLD              0.0110  0.0020  0.0180 -0.0090  0.1550  0.0790  0.2420 -0.0720
UUP             -0.0190  0.0780 -0.0530  0.0190  0.1360  0.2680  0.4310  0.4540
DBC              0.0110 -0.0080 -0.0330 -0.0100  0.0550  0.0360  0.1250  0.2090
alpha t-stat    -2.6290 -3.1340  0.4020  1.3180 -0.2990  1.5450  0.1280  1.3940
R2               0.9100  0.9370  0.2970  0.2600  0.6180  0.1990  0.3440  0.2200

And, as in Bodie, Kane & Marcus (Table 26.2), the t-statistics of the loadings, to see which exposures are statistically reliable (|t|>2):

pd.DataFrame({f: hf_style(f).tvalues for f in HF}).round(2)
          QAI     HDG     MNA   MERFX    BTAL   QMNIX   AQMIX   QSPIX
const -2.6300 -3.1300  0.4000  1.3200 -0.3000  1.5400  0.1300  1.3900
SPY    3.8500  0.8400 -1.8600 -0.7900  0.3700 -1.0400  0.3400 -0.7700
IWM    5.1000 11.7200  1.5600  1.2700 -6.7600 -2.0900 -0.1600 -1.8900
EFA    1.4800  6.1400 -0.2000  0.2400  1.2900  3.3600  0.2100  2.7900
EEM    6.4300  5.0300  0.2000  0.6900 -3.2800 -1.1700 -0.0300 -0.6700
IEF   -0.0900  0.3100 -1.5900 -0.9300  0.2700  0.1800 -2.6400  0.6100
TLT    1.3300 -0.2400  0.5600  0.1600  0.2900 -1.2300  2.3800 -1.2300
HYG    1.0300 -0.6800  4.4900  2.7500  0.8600 -0.3000 -3.0400  0.1300
GLD    0.9600  0.2000  0.6400 -0.5200  2.2100  1.2500  3.9800 -0.8700
UUP   -0.6400  2.8500 -0.7200  0.4300  0.7500  1.6400  2.7400  2.1300
DBC    1.0600 -0.9000 -1.3000 -0.6500  0.8800  0.6400  2.3200  2.8400

Now the picture is much richer, and it makes economic sense:

  • Merger arbitrage (MNA, MERFX): the only reliable loading is on high-yield bonds (HYG), with t above 2.7 in both funds. A merger arbitrageur buys the target after the deal is announced and earns the spread if the deal closes. That is economically equivalent to selling insurance against deal failure, and deals fail precisely when credit conditions deteriorate. Two different funds, same exposure: this is what a style is.
  • BTAL is short small caps (t\approx-6.8) and short emerging markets: exactly its mandate (short high-beta, long low-beta).
  • Managed futures (AQMIX) loads on bonds, credit, gold, the dollar and commodities, with signs that look strange (short IEF but long TLT). A trend follower changes its positions every month, so a regression over 11 years only captures the average position. We will see that its payoff is non-linear (Part IV).
  • Alphas: QAI and HDG have negative and significant alphas: once you account for their styles, they lose about 1.2% per year, which is roughly their cost. QMNIX and QSPIX have positive alphas of about 0.4% per month, but with t\approx1.5 we cannot reject that they are zero (Workshop 1: alpha is estimated with error).

5.3.4 Style drift: rolling style analysis

A regression over the whole sample assumes that the fund’s style is constant. Mutual funds are under pressure to avoid style drift (departing from their stated style); hedge funds are free to change style whenever they want. A rolling style analysis shows whether the exposures are stable. We re-estimate the constrained weights of QAI with a moving window of 36 months:

def rolling_style(fund, window=36, X=X_style):
    out = {}
    for end in range(window, len(fund) + 1):
        idx = fund.index[end - window:end]
        w = style_analysis(fund.loc[idx], X.loc[idx])[0]
        out[idx[-1]] = w
    return pd.DataFrame(out).T

roll_qai = rolling_style(Rs["QAI"])
roll_qai.plot.area(figsize=(12, 6), cmap="tab20", linewidth=0)
plt.title("QAI: rolling 36-month style weights")
plt.ylabel("Style weight")
plt.ylim(0, 1)
(0.0, 1.0)
plt.legend(loc="upper left", bbox_to_anchor=(1, 1))
plt.show()

The cash component of QAI stays around one half during the whole period, but the risky half rotates: US large caps dominate at the beginning, and small caps, emerging markets and even the dollar take their place later. The average weights of the previous table hide this rotation.

For a hedge fund, style drift is better seen in the unconstrained loadings. Here are the rolling 36-month betas of the managed futures fund on four asset classes:

def rolling_betas(fund, styles, window=36, rets=Rs, rf=rf_s):
    d = rets[[fund] + styles].sub(rf, axis=0)
    out = {}
    for end in range(window, len(d) + 1):
        dd = d.iloc[end - window:end]
        m = sm.OLS(dd[fund], sm.add_constant(dd[styles])).fit()
        out[dd.index[-1]] = m.params[styles]
    return pd.DataFrame(out).T

roll_aqmix = rolling_betas("AQMIX", ["SPY", "IEF", "UUP", "DBC"])
plot_time_series(roll_aqmix, "AQMIX: rolling 36-month betas (unconstrained)",
                 "Beta", "Asset class")

The managed futures fund was long bonds (positive IEF beta) during the years of falling rates and turned short bonds when rates started to rise in 2022; its exposure to the dollar and to commodities also switches sign. This is not a defect of the fund: it is the strategy. But it means that any single number we compute for it (a beta, an alpha, a correlation) is an average of very different portfolios.

5.4 Alternative beta and hedge fund replication

Recall the equation we closed Workshop 1 with:

\underbrace{\text{Reported return}}_{\text{what they show you}}=\underbrace{R_f + \sum_k \beta_k F_k}_{\text{exposures you can buy cheaply}}+\underbrace{\alpha}_{\text{what you are actually paying for}}

Style analysis estimates the middle term. If that term explains most of a fund’s return, the investor can buy a clone: a portfolio of index funds with the same exposures. This is the idea behind hedge fund replication (Hasanhodzic and Lo, 2007) and behind products such as QAI and HDG. The exposures that hedge funds sell and that can be bought cheaply have a name in the industry: alternative beta (or alternative risk premia).

A clone is only useful if it works out of sample. We estimate the style weights of HDG with the first 60 months, freeze them, and see how the clone tracks the fund in the following years:

split = 60
train, test = Rs.index[:split], Rs.index[split:]

w_clone, r2_in, e_in = style_analysis(Rs.loc[train, "HDG"], X_style.loc[train])
clone = X_style.loc[test] @ w_clone          # frozen weights, out of sample

te_out = (Rs.loc[test, "HDG"] - clone).std() * np.sqrt(12)
print(f"In-sample R2                   : {r2_in:.3f}")
In-sample R2                   : 0.969
print(f"Out-of-sample correlation      : {Rs.loc[test, 'HDG'].corr(clone):.3f}")
Out-of-sample correlation      : 0.951
print(f"Out-of-sample tracking error   : {te_out:.4f} per year")
Out-of-sample tracking error   : 0.0206 per year
ann_ret = lambda x: (1 + x).prod()**(12/len(x)) - 1         # annualized simple return
print(f"Out-of-sample return, fund     : {ann_ret(Rs.loc[test, 'HDG']):.4f}")
Out-of-sample return, fund     : 0.0406
print(f"Out-of-sample return, clone    : {ann_ret(clone):.4f}")
Out-of-sample return, clone    : 0.0634
plot_time_series(pd.DataFrame({"HDG (fund)": (1 + Rs.loc[test, "HDG"]).cumprod(),
                               "Clone (frozen style weights)": (1 + clone).cumprod()}),
                 "Out-of-sample: a hedge fund replicator and its own clone",
                 "Value of $1", "")

A static mix of index funds, estimated with data that ended years ago, tracks the replicator closely (correlation of 0.95 out of sample) and, in this sample, it even beat the fund by more than 2 percentage points per year. The clone costs a few basis points per year in ETF fees and has daily liquidity and no lock-up. This is the benchmark every hedge fund manager should be forced to beat: not the S&P 500, but their own clone.

5.4.1 Factor-based style analysis

We can also describe a fund with the Fama-French factors of Workshop 1, adding the momentum factor (MOM: long past winners, short past losers). For the long/short equity funds, factors are more natural styles than asset classes:

def model_row(m, name):
    p, t = m.params, m.tvalues
    row = {"alpha (annual)": p["Intercept"] * 12, "alpha t": t["Intercept"]}
    for f in ["Mkt_RF", "SMB", "HML", "RMW", "CMA", "MOM"]:
        row[f] = p.get(f, np.nan)
    row["R2"] = m.rsquared
    return pd.Series(row, name=name)

FF6 = ("Mkt_RF", "SMB", "HML", "RMW", "CMA", "MOM")
pd.DataFrame([model_row(factor_model(r[f], FF6), f)
              for f in ["BTAL", "QMNIX", "QSPIX", "AQMIX"]]).round(3)
       alpha (annual)  alpha t  Mkt_RF     SMB     HML     RMW     CMA    MOM     R2
BTAL          -0.0250  -0.9660 -0.4750 -0.2400 -0.2310  0.7160  0.3480 0.2540 0.7160
QMNIX          0.0410   1.7210 -0.0450 -0.2390  0.4290  0.1750 -0.0240 0.1010 0.3630
QSPIX          0.0310   0.9870  0.0750 -0.2500  0.5320  0.2210  0.1480 0.1370 0.3660
AQMIX          0.0310   1.0350 -0.1300 -0.0410  0.0670 -0.0180  0.0090 0.1870 0.1460

Two lessons from this table:

  • BTAL is a factor portfolio. With six factors the R^2 jumps from 0.62 (asset classes) to about 0.72: the fund is short the market (\beta\approx-0.48) and long profitability (RMW) and investment (CMA) — “quality” stocks tend to be low-beta stocks. Its alpha is negative and not significant.
  • The two AQR equity/multi-style funds (QMNIX, QSPIX) load on value (HML), profitability (RMW) and momentum (MOM) and are short size (SMB); the managed futures fund loads mainly on momentum, as a trend follower should. This is exactly what they advertise: alternative risk premia, i.e., the long/short factors of Workshop 1 packaged as a fund. Once we control for those factors, the alphas of 3% to 4% per year have t-statistics between 1.0 and 1.7: not significant. The factors explain the return; whatever is left is too noisy to call skill.
ImportantOne sentence to remember

Style analysis turns the question “is this hedge fund good?” into “what part of this fund can I buy cheaply, and is the rest worth the fee?”. A hedge fund whose return is 90% style is an expensive index fund; a hedge fund whose return is mostly unexplained is either skilled, or non-linear — and we will need Part IV to tell the difference.

6 CHALLENGE 1

  1. Run the constrained style analysis (with cash) for HDG and for MERFX using only the last 60 months of the sample. Compare the weights and the R^2 with the full-sample results. Did the style of these funds change?

  2. Run the unconstrained style regression for MNA using only three styles: SPY, IEF and HYG. Interpret the three betas and the alpha. Which of the three exposures is the economic signature of merger arbitrage, and why?

  3. Build an out-of-sample clone of QAI as we did for HDG (train with the first 60 months). Report the out-of-sample correlation, tracking error and the difference in annual return between the fund and its clone. Would you recommend a family office to buy QAI or its clone? Mention at least two non-statistical arguments (liquidity, costs, taxes, operational complexity…).

  4. Estimate the FF 5-factor + momentum model for MNA and MERFX. Compare the R^2 with that of the asset-class style regression. Which set of “styles” (asset classes or equity factors) is more appropriate for a merger arbitrage fund? Explain.

  5. In one paragraph: a hedge fund reports an R^2 of 0.15 against every style model you tried. Give two different explanations of why that could happen, one favorable to the manager and one unfavorable.

7 PART II — Market-neutral positions, pure plays and portable alpha

7.1 Three meanings of “neutral”

In Workshop 1 we built a first market-neutral portfolio by shorting SPY against a long position. Hedge funds use the word neutral in three different senses, and it is important not to confuse them. Suppose a fund is long a portfolio P (value V_P) and short a hedge instrument H (value V_H):

Neutrality Condition What it removes
Dollar-neutral V_H=V_P Nothing in particular: the long and short sides have the same size in pesos
Beta-neutral V_H=\beta_P V_P Exposure to the market (the CAPM beta of Workshop 1)
Factor-neutral \beta_{k}=0 for every factor k Exposure to every factor in the model (market, size, value, commodities, currency…)

Dollar-neutral is not market-neutral unless \beta_P=1, and market-neutral is not factor-neutral (we saw this at the end of Workshop 1). Let’s see the three with a Mexican example.

7.1.1 A Mexican example: a pure play on Grupo México

Suppose a Mexican analyst believes that Grupo México (GMEXICO) is undervalued, but she has no view on the direction of the Mexican stock market. If she simply buys the stock, a fall of the IPC can wipe out her profit even if she is right about the company. She wants a pure play: a bet on the stock relative to the market.

She can short the market using IPC futures traded on MexDer. A futures contract requires no investment (only margin), and its fair price follows the cost-of-carry relation of the derivatives course:

F_{t}=S_{t}\left(1+\frac{i-q}{12}\right) \qquad \text{(one-month contract)}

where i is the peso interest rate and q the dividend yield of the index. Over one month, a short futures position gains F_{t-1}-S_{t}, so the return of a short futures position per peso of notional is approximately:

-\left(R_{IPC,t}-\frac{i_{t-1}-q}{12}\right)

We use the Mexican 3-month rate for i and assume a dividend yield of 2.5% for the IPC (remember that our IPC series is a price index).

q_ipc = 0.025                                        # assumed IPC dividend yield
i_mx  = rates_m["MX_3M"].shift(1).bfill()      # rate known at the start of the month
i_us  = rates_m["US_TB3M"].shift(1).bfill()
fut_ipc = Rs["IPC"] - (i_mx - q_ipc) / 12      # LONG IPC futures return, per peso

stock = Rs["GMEXICO"]
beta_gm = np.cov(stock, fut_ipc)[0, 1] / fut_ipc.var()
print(f"Beta of GMEXICO with respect to the IPC futures: {beta_gm:.3f}")
Beta of GMEXICO with respect to the IPC futures: 1.310
hedges = pd.DataFrame({
    "Unhedged (long GMEXICO)": stock,
    "Dollar-neutral":  stock - 1.0 * fut_ipc,       # short 1 peso of futures per peso
    "Beta-neutral":    stock - beta_gm * fut_ipc})  # short beta pesos of futures

def hedge_report(df, market=fut_ipc):
    out = {}
    for c in df:
        x = df[c]
        out[c] = {"Annual return": (1 + x).prod()**(12/len(x)) - 1,
                  "Annual vol": x.std() * np.sqrt(12),
                  "Beta (IPC)": np.cov(x, market)[0, 1] / market.var(),
                  "Corr (IPC)": x.corr(market)}
    return pd.DataFrame(out).T

hedge_report(hedges).round(4)
                         Annual return  Annual vol  Beta (IPC)  Corr (IPC)
Unhedged (long GMEXICO)         0.1952      0.3068      1.3100      0.6471
Dollar-neutral                  0.2158      0.2386      0.3100      0.1969
Beta-neutral                    0.2168      0.2339     -0.0000     -0.0000

The dollar-neutral position still has a beta of about 0.3: Grupo México is more sensitive to the market than the market itself, so one peso of futures per peso of stock is not enough. The beta-neutral position has zero beta and zero correlation with the IPC by construction (in sample). Its return is now the pure play: R_f+\alpha+e in the language of Bodie, Kane & Marcus (eq. 26.1):

R_{P}=R_f+\beta\left(R_M-R_f\right)+\alpha+e \quad\xrightarrow{\ \text{short } \beta \text{ futures}\ }\quad R_f+\alpha+e

Note that the volatility of the hedged position is still large (about three quarters of the stock’s volatility): hedging removes systematic risk, not idiosyncratic risk (Workshop 1, beta targeting is not full risk replication).

7.1.2 Market-neutral is not factor-neutral

Is the beta-neutral position free of every systematic risk? Grupo México is a copper producer that reports in dollars. Let’s regress the hedged position on the IPC futures, commodities (DBC), the peso (USDMXN) and copper itself, through its US subsidiary Southern Copper (SCCO, in dollars):

Xf = pd.DataFrame({"IPC_fut": fut_ipc, "DBC": Rs["DBC"],
                   "USDMXN": Rs["USDMXN"], "SCCO": Rs["SCCO"]})
m_fn = sm.OLS(hedges["Beta-neutral"], sm.add_constant(Xf)).fit()
print(m_fn.summary().tables[1])
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const          0.0056      0.004      1.317      0.190      -0.003       0.014
IPC_fut       -0.3254      0.121     -2.681      0.008      -0.565      -0.085
DBC            0.0071      0.091      0.077      0.939      -0.174       0.188
USDMXN         0.7001      0.141      4.965      0.000       0.421       0.979
SCCO           0.5480      0.050     10.934      0.000       0.449       0.647
==============================================================================
print(f"R-squared: {m_fn.rsquared:.3f}")
R-squared: 0.507

The “market-neutral” position is far from neutral: half of its variance (R^2\approx0.5) is explained by copper (through SCCO) and by the peso. The coefficient on USDMXN is positive: when the peso depreciates, Grupo México (a dollar earner) gains relative to the IPC. Our analyst thought she was betting on the company; in fact she is mostly betting on copper and the exchange rate. To be factor-neutral she would also need to short copper (for example, with COMEX copper futures) and hedge the currency exposure. Every hedge fund that claims to be “market neutral” should be asked: neutral to what?

7.2 Hedging with index futures: how many contracts?

The number of contracts needed to hedge a portfolio of value V_P with beta \beta_P is the hedge ratio:

N^{*}=\beta_P\,\frac{V_P}{F\times m}

where F is the futures price (in index points) and m the contract multiplier (the value of one index point). Two contracts that a Mexican professional must know:

  • IPC futures (MexDer): multiplier of 10 pesos per index point.
  • E-mini S&P 500 futures (CME): multiplier of 50 dollars per index point.

(Contract specifications change; always check them on the exchange’s website.)

def n_contracts(value, beta, F, multiplier):
    return beta * value / (F * multiplier)

ipc_now, spx_now = prices["IPC"].iloc[-1], prices["SPX"].iloc[-1]
i_now, i_us_now = rates["MX_3M"].iloc[-1], rates["US_TB3M"].iloc[-1]

F_ipc = ipc_now * (1 + (i_now - q_ipc) * 3/12)       # 3-month IPC futures (theoretical)
F_spx = spx_now * (1 + (i_us_now - 0.013) * 3/12)    # S&P 500 dividend yield: 1.3%

print(f"IPC = {ipc_now:,.0f}   theoretical 3-month futures = {F_ipc:,.0f}")
IPC = 65,430   theoretical 3-month futures = 66,132
print(f"Hedge a MXN 50 million position in GMEXICO (beta {beta_gm:.2f}): "
      f"{n_contracts(50e6, beta_gm, F_ipc, 10):.1f} IPC contracts")
Hedge a MXN 50 million position in GMEXICO (beta 1.31): 99.0 IPC contracts
print(f"S&P 500 = {spx_now:,.0f}   theoretical 3-month futures = {F_spx:,.0f}")
S&P 500 = 7,686   theoretical 3-month futures = 7,733
print(f"Hedge a USD 10 million US portfolio (beta 1.2): "
      f"{n_contracts(10e6, 1.2, F_spx, 50):.1f} E-mini contracts")
Hedge a USD 10 million US portfolio (beta 1.2): 31.0 E-mini contracts

Contracts come in whole units, so the hedge is always slightly over- or under-sized. That is one source of hedging error. The two most important sources, however, are:

  • Basis risk: the futures price does not move one-for-one with the portfolio, because the portfolio is not the index (and because the futures price also depends on interest rates and dividends).
  • Estimation risk: \beta_P is estimated with error and changes over time (Workshop 1, beta is estimated with error).

The beta-neutral hedge of the previous section used the full-sample beta, which nobody knew in 2015. A realistic test re-estimates beta every month with the previous 36 months and hedges the following month with it:

window = 36
beta_roll = pd.Series(
    {stock.index[t]: np.cov(stock.iloc[t-window:t], fut_ipc.iloc[t-window:t])[0, 1]
                     / fut_ipc.iloc[t-window:t].var()
     for t in range(window, len(stock))})

hedged_oos = stock.loc[beta_roll.index] - beta_roll * fut_ipc.loc[beta_roll.index]
report_oos = hedge_report(pd.DataFrame({
    "Unhedged": stock.loc[beta_roll.index],
    "Beta-neutral, full-sample beta": hedges["Beta-neutral"].loc[beta_roll.index],
    "Beta-neutral, rolling beta (realistic)": hedged_oos}),
    market=fut_ipc.loc[beta_roll.index])
print(report_oos.round(4))
                                        Annual return  Annual vol  Beta (IPC)  Corr (IPC)
Unhedged                                       0.2039      0.3235      1.3813      0.7114
Beta-neutral, full-sample beta                 0.2469      0.2277      0.0713      0.0521
Beta-neutral, rolling beta (realistic)         0.2345      0.2387      0.2077      0.1450
beta_roll.plot(figsize=(12, 4), title="GMEXICO: rolling 36-month beta vs the IPC")
plt.axhline(beta_gm, color="red", ls="--", label="Full-sample beta")
plt.legend()
plt.grid(alpha=0.3)
plt.show()

With a beta that moves between roughly 0.3 and 1.7 (look at the plot), the realistic hedge leaves some market exposure behind: its beta and its correlation with the IPC are no longer zero. This is the daily reality of a market-neutral fund: neutrality has to be re-estimated and re-balanced continuously, and it is never perfect.

7.3 Statistical arbitrage in a nutshell

Statistical arbitrage (Bodie, Kane & Marcus, Section 26.2) is a market-neutral strategy run at scale: instead of one carefully researched pure play, a quantitative system looks for many small, temporary misalignments among related securities and takes small long/short positions in hundreds of them. No single bet is safe, but if each has a positive expected value, the law of large numbers makes the total profit very likely. It requires fast, cheap, automated trading, and holding periods can be as short as minutes.

The classic example is pairs trading: find two stocks whose prices have historically moved together, and when the gap between them opens unusually wide, buy the cheap one and sell the expensive one, betting that the gap will close. The natural Mexican candidate is Grupo México and Southern Copper, since the first owns most of the second. We put both in pesos and look at the spread of their log prices:

lg = np.log(prices["GMEXICO"])
ls = np.log(prices["SCCO"] * prices["USDMXN"])           # SCCO in pesos
b_pair = np.polyfit(ls, lg, 1)                 # hedge ratio: OLS on log prices
spread = lg - b_pair[0] * ls
z = (spread - spread.mean()) / spread.std()

fig, ax = plt.subplots(figsize=(12, 5))
z.plot(ax=ax, label="z-score of the spread")
for lvl, c in [(2, "red"), (-2, "green")]:
    ax.axhline(lvl, color=c, ls="--")
ax.axhline(0, color="black", lw=0.8)
ax.set_title(f"Spread = log GMEXICO - {b_pair[0]:.2f} x log SCCO (both in pesos)")
ax.legend()
ax.grid(alpha=0.3)
plt.show()

from statsmodels.tsa.stattools import coint
coint_result = coint(lg, ls)             # (t statistic, p-value, critical values)
p_val = coint_result[1]
print(f"Engle-Granger cointegration test: p-value = {p_val:.3f}")
Engle-Granger cointegration test: p-value = 0.172

A pairs trader would short the spread when z>2 (sell GMEXICO, buy SCCO) and buy the spread when z<-2. But notice two things:

  1. The spread wanders far from zero and stays there for years: this is a relative value trade with no convergence date.
  2. The cointegration test (which asks whether the spread is mean-reverting) does not reject the null of no cointegration. Two stocks can be economically linked and still not form a tradable pair.

This is the central danger of statistical arbitrage that Bodie, Kane & Marcus call data mining: search through enough pairs and you will always find some that look mean-reverting in the past by pure chance. Real stat-arb funds fight this with out-of-sample testing, economic restrictions on which pairs are allowed, and strict risk limits.

7.4 130/30 funds

Between a long-only mutual fund and a hedge fund there is an intermediate product: the 130/30 fund. For every 100 pesos of capital, the manager shorts 30 pesos of the stocks she likes least and uses the proceeds to increase the long positions to 130 pesos. Two concepts describe the exposure of any long/short portfolio:

\text{Gross exposure}=\sum_i|w_i|=1.30+0.30=160\% \qquad \text{Net exposure}=\sum_i w_i=1.30-0.30=100\%

A 130/30 fund has the same net exposure (and, roughly, the same beta) as a long-only fund, so it can be benchmarked against the same index. The difference is that the manager’s negative views can now be expressed (with a weight below zero) instead of only being ignored (with a weight of zero). In the language of Workshop 1: removing the constraint w_i\geq0 can only improve the optimal portfolio if the forecasts are good.

w_13030 = pd.Series({"A": 0.45, "B": 0.45, "C": 0.40, "D": -0.15, "E": -0.15})
betas   = pd.Series({"A": 1.10, "B": 0.90, "C": 1.00, "D": 1.20, "E": 1.30})
print(f"Gross exposure: {w_13030.abs().sum():.0%}   Net exposure: {w_13030.sum():.0%}")
Gross exposure: 160%   Net exposure: 100%
print(f"Portfolio beta: {w_13030 @ betas:.3f}")
Portfolio beta: 0.925

Note that the beta is below one even if the net exposure is 100%: the manager shorted high-beta stocks. Net exposure and beta are different things, exactly as dollar-neutral and beta-neutral were.

7.5 Portable alpha

7.5.1 The idea

Section 26.3 of Bodie, Kane & Marcus puts the pieces together. An investor has two separate decisions:

  1. Asset allocation (beta): which markets to be exposed to. Beta is a commodity: it can be bought for a few basis points with index futures or ETFs.
  2. Security selection (alpha): where to find managers with skill. Alpha is scarce and expensive.

Nothing forces the two to live in the same market. The investor can find alpha wherever it exists (for example, in a market-neutral hedge fund, whose beta is zero) and then add the market exposure she wants with futures. This is portable alpha (or alpha transfer):

R_{PA,t}=\underbrace{R_{\alpha,t}}_{\text{market-neutral fund}}+\underbrace{\left(R_{M,t}-R_{f,t}\right)}_{\text{long futures on the benchmark}}

The futures position requires no capital (only margin), so the whole capital goes to the alpha source, which earns R_f+\alpha. The result is the benchmark return plus the alpha:

R_{PA,t}=R_{M,t}+\left(R_{\alpha,t}-R_{f,t}\right)=R_{M,t}+\alpha+e_t

7.5.2 Portable alpha with real data (in dollars)

We use the AQR equity market-neutral fund (QMNIX) as the alpha source and S&P 500 futures as the beta overlay (approximated by R_{SPY}-R_f):

alpha_src = Rs["QMNIX"]
pa_us = alpha_src + (Rs["SPY"] - rf_s)

def pa_report(pa, bench):
    act = pa - bench
    return pd.DataFrame({
        "Annual return": [ann_ret(bench), ann_ret(pa)],
        "Annual vol":    [bench.std()*np.sqrt(12), pa.std()*np.sqrt(12)],
        "Tracking error": [0, act.std()*np.sqrt(12)],
        "Info ratio":    [np.nan, act.mean()/act.std()*np.sqrt(12)]},
        index=["Benchmark", "Portable alpha"])

print(pa_report(pa_us, Rs["SPY"]).round(4))
                Annual return  Annual vol  Tracking error  Info ratio
Benchmark              0.1364      0.1502          0.0000         NaN
Portable alpha         0.1935      0.1639          0.0968      0.5352
cum = pd.DataFrame({"S&P 500 (SPY)": (1 + Rs["SPY"]).cumprod(),
                    "Portable alpha: QMNIX + S&P futures": (1 + pa_us).cumprod()})
plot_time_series(cum, "Portable alpha in dollars", "Value of $1", "")

7.5.3 Portable alpha for a Mexican investor

A Mexican pension fund or family office benchmarked to the IPC can do the same: take the alpha of a US market-neutral fund and “port” it onto the Mexican market with IPC futures. There is one extra step: the alpha source is in dollars, so the investor hedges the currency with forwards. Under covered interest parity (Part III), a USD return hedged into pesos earns approximately the USD return plus the interest-rate differential:

R^{hedged}_{MXN}\approx R_{USD}+\frac{i_{MX}-i_{US}}{12}

so the portable alpha return in pesos is:

R_{PA,t}^{MXN}=\underbrace{R_{\alpha,t}+\frac{i_{MX}-i_{US}}{12}}_{\text{alpha source hedged into MXN}}+\underbrace{\left(R_{IPC,t}-\frac{i_{MX}-q}{12}\right)}_{\text{long IPC futures}}

alpha_mxn = alpha_src + (i_mx - i_us) / 12           # FX-hedged alpha source
pa_mx = alpha_mxn + fut_ipc                          # + IPC futures overlay
ipc_tr = Rs["IPC"] + q_ipc / 12                      # IPC with dividends (approx.)

print(pa_report(pa_mx, ipc_tr).round(4))
                Annual return  Annual vol  Tracking error  Info ratio
Benchmark              0.0648      0.1510          0.0000         NaN
Portable alpha         0.1147      0.1821          0.0969      0.5292
act = pa_mx - ipc_tr
roll12 = (1 + act).rolling(12).apply(np.prod, raw=True) - 1
roll12.plot(figsize=(12, 4), title="Portable alpha in pesos: 12-month return over the IPC")
plt.axhline(0, color="black", lw=0.8)
plt.grid(alpha=0.3)
plt.show()

The numbers look spectacular: in both currencies the portable alpha strategy beat its benchmark by around 5 percentage points per year, with an Information ratio above 0.5. But read the rolling plot before getting excited. From 2018 to 2020 the strategy lagged the IPC by 13% to 20% per year, three years in a row: the alpha source (QMNIX) had a drawdown of almost 40% that ended in December 2020. An investment committee that adopted portable alpha in 2017 would have faced three years of explaining losses relative to its benchmark before the strategy recovered in 2021–2025. Most committees do not survive that long; many would have closed the program at the bottom.

7.6 Leverage, margin and the hidden liquidity risk

A portable alpha program is levered even if it does not borrow a single peso: the investor holds 100% of capital in the alpha source and 100% of notional exposure in futures, a gross exposure of 200%. Futures are marked to market every day, so when the market falls the investor must pay variation margin in cash, immediately. The cash has to come from somewhere, but the alpha source is a hedge fund with monthly or quarterly redemptions and possibly a lock-up.

notional = 100e6                                     # MXN 100 million of IPC futures
worst = fut_ipc.min()
print(f"Worst monthly return of the IPC futures: {worst:.2%} ({fut_ipc.idxmin():%b %Y})")
Worst monthly return of the IPC futures: -16.78% (Mar 2020)
print(f"Variation margin on MXN 100 million of long futures: MXN {-worst*notional:,.0f}")
Variation margin on MXN 100 million of long futures: MXN 16,782,129

That cash must be available within a day. This liquidity mismatch (daily margin calls on the beta side, illiquid alpha on the other side) is what hurt many portable alpha programs during the 2008 crisis: the market fell (margin calls) at the same time as hedge funds suspended redemptions (no cash). A prudent program keeps a cash buffer of 10%–20% of the notional in liquid instruments, which dilutes the alpha.

8 CHALLENGE 2

  1. Repeat the analysis of “three meanings of neutral” for América Móvil (AMXB): estimate its beta with respect to the IPC futures, build the dollar-neutral and beta-neutral positions and report the table. Is the dollar-neutral position over-hedged or under-hedged? Why?

  2. Regress the beta-neutral AMXB position on IPC_fut, USDMXN, DBC and SPY. Is it factor-neutral? Compare with Grupo México and explain the difference with economic arguments.

  3. A Mexican fund holds MXN 120 million in a portfolio with beta 0.85 with respect to the IPC. The IPC is at 65,000 points, the peso 3-month rate is 7%, and the dividend yield is 2.5%. (i) Compute the theoretical 3-month futures price; (ii) the number of IPC contracts needed to make the portfolio beta-neutral; (iii) the number of contracts needed to raise the beta to 1.2 instead.

  4. Build a portable alpha strategy in dollars using MERFX (merger arbitrage) as the alpha source instead of QMNIX. Report the annual return, volatility, tracking error and Information ratio, and plot the rolling 12-month active return. Which alpha source would you prefer, and why? (Hint: look at the maximum drawdown and skewness of each source from Part I.)

  5. In one paragraph: explain to an investment committee why portable alpha is a form of leverage even if no money is borrowed, and propose two risk-management rules for the program.

9 PART III — Global macro: the Mexican peso carry trade

9.1 Why the peso matters to hedge funds

Global macro funds take long and short positions in currencies, rates, equity indexes and commodities around the world, based on views about economic trends. One of their most popular trades of the last decades is the carry trade: borrow in a currency with a low interest rate and invest in a currency with a high interest rate. For years the Mexican peso has been one of the favorite “high-yielders” of the market: it is liquid, it trades 24 hours a day, and Banxico has kept rates well above those of the Federal Reserve. Every Mexican finance professional should understand this trade, because it moves the exchange rate that affects every peso-denominated portfolio.

9.2 Covered interest parity

Bodie, Kane & Marcus (Chapter 25) state the interest rate parity relation (covered interest arbitrage). With S quoted in pesos per dollar, the forward rate for a horizon of T years is:

F_0=S_0\,\frac{1+i_{MX}\,T}{1+i_{US}\,T}

If the forward were different, an arbitrageur could borrow in one currency, convert, lend in the other, and lock in a risk-free profit with the forward. Because i_{MX}>i_{US}, the forward price of the dollar is above the spot price: the market “prices in” a depreciation of the peso exactly equal to the interest differential.

S0 = prices["USDMXN"].iloc[-1]
i_mx_now, i_us_now = rates["MX_3M"].iloc[-1], rates["US_TB3M"].iloc[-1]
for T, lbl in [(1/12, "1 month"), (3/12, "3 months"), (1, "1 year")]:
    F = S0 * (1 + i_mx_now*T) / (1 + i_us_now*T)
    print(f"{lbl:>9}: F = {F:.4f}   forward points = {F - S0:+.4f}   "
          f"implied peso depreciation = {F/S0 - 1:.2%}")
  1 month: F = 17.0740   forward points = +0.0434   implied peso depreciation = 0.26%
 3 months: F = 17.1601   forward points = +0.1295   implied peso depreciation = 0.76%
   1 year: F = 17.5347   forward points = +0.5041   implied peso depreciation = 2.96%

9.3 The carry trade: returns

The carry trade does not hedge with the forward. A trader who is long pesos and short dollars for one month:

  1. borrows 1 dollar at i_{US} and converts it to S_{t-1} pesos;
  2. invests the pesos at i_{MX} for one month;
  3. converts back at the new spot rate S_t and repays the dollar loan.

Her excess return (per dollar, with no capital invested) is:

R^{carry}_t=\left(1+\frac{i_{MX,t-1}}{12}\right)\frac{S_{t-1}}{S_t}-\left(1+\frac{i_{US,t-1}}{12}\right)

It has two parts: the interest differential (known in advance) and the currency return S_{t-1}/S_t-1 (unknown, positive when the peso appreciates).

S = prices["USDMXN"]
carry = pd.DataFrame(index=r.index)
carry["Interest differential"] = (i_mx - i_us) / 12
carry["Currency return"] = (S.shift(1) / S).loc[r.index] - 1
carry["Carry trade"] = (1 + i_mx/12) * (S.shift(1) / S).loc[r.index] - (1 + i_us/12)

print(pd.DataFrame({
    "Annualized mean": carry.mean() * 12,
    "Annual vol":      carry.std() * np.sqrt(12)}).round(4))
                       Annualized mean  Annual vol
Interest differential           0.0531      0.0033
Currency return                -0.0069      0.1188
Carry trade                     0.0463      0.1198
c_cc = np.log(1 + carry["Carry trade"])
print("\nCarry trade statistics (excess return, no capital):")

Carry trade statistics (excess return, no capital):
ct = carry["Carry trade"]
print(pd.Series({"Sharpe":       ct.mean() / ct.std() * np.sqrt(12),
                 "Skewness":     stats.skew(c_cc),
                 "Excess kurt.": stats.kurtosis(c_cc),
                 "Corr (SPY)":   carry["Carry trade"].corr(Rs["SPY"]),
                 "Max drawdown": max_drawdown(c_cc)}).round(3))
Sharpe          0.3860
Skewness       -1.2860
Excess kurt.    5.5670
Corr (SPY)      0.4620
Max drawdown   -0.2280
dtype: float64
cum_carry = pd.DataFrame({
    "Carry trade (long MXN / short USD)": (1 + carry["Carry trade"]).cumprod(),
    "Interest differential only":        (1 + carry["Interest differential"]).cumprod()})
plot_time_series(cum_carry, "The MXN carry trade: $1 of notional, excess return",
                 "Cumulative value", "")

print("The five worst months of the carry trade:")
The five worst months of the carry trade:
print(carry["Carry trade"].nsmallest(5).round(4))
Date
2020-03-31   -0.1737
2016-11-30   -0.0790
2024-06-30   -0.0675
2016-05-31   -0.0663
2018-10-31   -0.0632
Name: Carry trade, dtype: float64

9.4 “Up by the stairs, down by the elevator”

The plot shows the famous shape of carry returns: long periods of slow, steady gains (the interest differential accumulates month after month) interrupted by sudden crashes when the peso depreciates sharply. The worst months coincide with global or local shocks: March 2020 (COVID), November 2016 (the US election), June 2024 (the Mexican election and the judicial reform debate) and October 2018 (the cancellation of the new Mexico City airport). The carry trade has a volatility similar to the stock market, a skewness of -1.3 and a correlation of almost 0.5 with the S&P 500: it loses precisely when everything else loses. It is a bet that the world will stay calm; it is, economically, a short position in a crash insurance policy, just like the put-writing strategy we will study in Part IV.

Does the forward rate predict the future spot rate, as covered parity might suggest? If markets expected the peso to depreciate by exactly the interest differential (uncovered interest parity), then in a regression

\ln S_t-\ln S_{t-1}=a+b\,\frac{i_{MX,t-1}-i_{US,t-1}}{12}+e_t

we should find b=1, and the carry trade would earn zero on average.

d_fama = pd.DataFrame({"dlnS": np.log(S).diff().loc[r.index],
                       "diff": (i_mx - i_us) / 12})
m_fama = smf.ols("dlnS ~ diff", data=d_fama).fit()
print(m_fama.summary().tables[1])
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
Intercept      0.0115      0.014      0.815      0.416      -0.016       0.040
diff          -2.3405      3.125     -0.749      0.455      -8.519       3.838
==============================================================================
print(f"\nTest of H0: b = 1   ->   p-value = {m_fama.t_test('diff = 1').pvalue:.3f}")

Test of H0: b = 1   ->   p-value = 0.287

The slope is far below one (its point estimate is not even positive), but the standard error is enormous and we cannot even reject b=1: with 11 years of monthly data, the currency noise dominates everything. Studies with long samples and many currencies find b<1 consistently (the forward premium puzzle): high-yield currencies do not depreciate enough to offset their interest advantage, on average. The average is earned in calm times and partially given back in crises; that is why the carry premium is interpreted as compensation for crash risk.

9.5 Currency hedging for a Mexican investor

Now turn the problem around. A Mexican family office that buys US stocks (SPY) has two exposures: to the S&P 500 and to the dollar. It can keep both (unhedged) or hedge the dollar with forwards (hedged). In pesos:

R^{unhedged}_{MXN}=(1+R_{SPY})(1+R_{USDMXN})-1 \qquad\qquad R^{hedged}_{MXN}\approx R_{SPY}+\frac{i_{MX}-i_{US}}{12}

Hedging the dollar earns the interest differential (the investor sells dollars forward at a premium) — it is the mirror image of the carry trade.

spy_unh = (1 + Rs["SPY"]) * (1 + Rs["USDMXN"]) - 1
spy_hed = Rs["SPY"] + (i_mx - i_us) / 12

fx_tab = pd.DataFrame({
    "Annual return": [(1+x).prod()**(12/len(x))-1 for x in (Rs["SPY"], spy_unh, spy_hed)],
    "Annual vol":    [x.std()*np.sqrt(12) for x in (Rs["SPY"], spy_unh, spy_hed)],
    "Max drawdown":  [max_drawdown(np.log(1+x)) for x in (Rs["SPY"], spy_unh, spy_hed)]},
    index=["SPY in USD", "SPY in MXN, unhedged", "SPY in MXN, hedged"])
print(fx_tab.round(4))
                      Annual return  Annual vol  Max drawdown
SPY in USD                   0.1364      0.1502       -0.2393
SPY in MXN, unhedged         0.1526      0.1437       -0.2612
SPY in MXN, hedged           0.1977      0.1502       -0.2028
print(f"\nCorrelation between SPY and USDMXN: {Rs['SPY'].corr(Rs['USDMXN']):.3f}")

Correlation between SPY and USDMXN: -0.463

Three things to notice:

  • The hedged position earned more, about 4.5 percentage points per year more than the unhedged one. That is the interest differential (about 5.3% per year) that the hedger collects, minus the gain from the peso’s depreciation over the sample, which only the unhedged investor received.
  • The unhedged position is less volatile than the hedged one, even though it carries currency risk on top of stock risk. The reason is the negative correlation (-0.46) between the S&P 500 and USDMXN: in global crises stocks fall and the peso depreciates, so the dollar gains cushion the stock losses. For a Mexican investor, the dollar is a natural hedge — the opposite of the carry trade, which loses in exactly those months.
  • The natural hedge does not always work: the maximum drawdown of the unhedged position is larger. In 2022 US stocks fell because of inflation and rising rates while the peso appreciated (the “super peso”), so the Mexican investor lost on both sides. Correlations are not constants. Whether to hedge is therefore not only a question of expected return (the interest differential); it is a question of what risk the investor wants to keep. We will use this in the family office case of Part VI.

10 CHALLENGE 3

  1. Using the rates of the last month of the sample, compute the theoretical 6-month USDMXN forward. A client asks you: “the forward says the peso will depreciate; should I buy dollars now?”. Answer using covered and uncovered interest parity.

  2. Split the carry trade sample in two: 2015–2019 and 2020–2026. For each subperiod report the annualized mean, volatility, Sharpe ratio, skewness and the worst month. Did the carry trade become more or less attractive after the Banxico hiking cycle of 2022–2023? Why?

  3. Compute the historical VaR and Expected Shortfall at 95% of the monthly carry trade returns (use your functions from Workshop 1). Then compute the parametric (normal) VaR. Which one is larger, and what does that tell you about the distribution of carry returns?

  4. Repeat the currency hedging comparison for international developed stocks (EFA) and for US Treasuries (IEF). For which asset does hedging the currency reduce the volatility in pesos, and for which does it increase it? Explain with the correlation between each asset and USDMXN.

  5. In one paragraph: explain why a Mexican pension fund and a New York global macro fund can both be happy on opposite sides of the USDMXN forward market.

11 PART IV — Non-linear risk: market timing, option-like payoffs and tail risk

11.1 Why a straight line is not enough

Every performance measure of Workshop 1 (beta, Jensen’s alpha, Treynor, Information ratio) comes from a straight line: the CAPM regression of the fund on the market. That line is the right model for a portfolio of stocks held with constant weights. It is the wrong model for a manager who:

  • times the market, raising beta before good months and lowering it before bad months (a perfect timer earns the market return in good months and the risk-free rate in bad months: a payoff with the shape of a call option);
  • sells options, explicitly or implicitly (a merger arbitrageur, a carry trader, a put writer): payoffs with the shape of a short put — small gains most of the time, large losses rarely.

Fitting a straight line to a curved relationship gives a wrong slope and a wrong intercept (Bodie, Kane & Marcus, Figures 26.4 and 26.5). Since alpha is the intercept, non-linear strategies can show a positive alpha that is nothing but the premium of an option the fund is selling.

11.2 Up-market and down-market betas

The simplest way to detect non-linearity is to let beta be different when the market goes up and when it goes down:

R_{F,t}-R_{f,t}=\alpha+\beta^{+}\max\left(R_{M,t}-R_{f,t},0\right)+\beta^{-}\min\left(R_{M,t}-R_{f,t},0\right)+e_t

A fund with \beta^{-}>\beta^{+} falls more with the market than it rises with it: an undesirable asymmetry (it behaves as if it had sold puts). A fund with \beta^{+}>\beta^{-} has a convex, option-like payoff that investors like.

mkt_ex = Rs["SPY"] - rf_s                       # market premium (simple returns)

def up_down_beta(fund):
    d = pd.DataFrame({"y": Rs[fund] - rf_s,
                      "up": np.maximum(mkt_ex, 0),
                      "down": np.minimum(mkt_ex, 0)})
    m = smf.ols("y ~ up + down", data=d).fit()
    return pd.Series({"beta up": m.params["up"], "beta down": m.params["down"],
                      "down - up": m.params["down"] - m.params["up"],
                      "t (down = up)": m.t_test("down - up = 0").tvalue.item()}, name=fund)

ud = pd.DataFrame([up_down_beta(f) for f in ["PUT"] + HF])
ud.round(3)
       beta up  beta down  down - up  t (down = up)
PUT     0.3650     0.7710     0.4060         5.4210
QAI     0.2880     0.3290     0.0420         0.9090
HDG     0.2990     0.3840     0.0850         1.7250
MNA     0.0350     0.1920     0.1570         2.0210
MERFX   0.0780     0.0700    -0.0080        -0.1720
BTAL   -0.7190    -0.5270     0.1920         0.8610
QMNIX  -0.2000    -0.0170     0.1830         1.0660
AQMIX  -0.0960    -0.3110    -0.2150        -1.2110
QSPIX  -0.1210     0.0850     0.2060         0.8900
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for ax, f in zip(axes, ["PUT", "MNA", "AQMIX"]):
    y = Rs[f] - rf_s
    ax.scatter(mkt_ex, y, s=15, alpha=0.6)
    xs = np.linspace(mkt_ex.min(), mkt_ex.max(), 100)
    c2 = np.polyfit(mkt_ex, y, 2)                 # quadratic fit
    ax.plot(xs, np.polyval(c2, xs), color="red")
    ax.axhline(0, color="black", lw=0.5)
    ax.axvline(0, color="black", lw=0.5)
    ax.set_title(f)
    ax.set_xlabel("S&P 500 premium")
    ax.grid(alpha=0.3)
axes[0].set_ylabel("Fund premium")
plt.suptitle("Non-linear characteristic lines (red: quadratic fit)")
plt.tight_layout()
plt.show()

The table and the plots tell a consistent story:

  • The PutWrite index has a down-market beta of about 0.77 and an up-market beta of about 0.37, and the difference is highly significant (t\approx5.4). This is the textbook shape of a short put: it participates in the crashes and gives up most of the rallies.
  • Merger arbitrage (MNA) shows the same asymmetry (down beta 0.19 vs up beta 0.04, t\approx2). When markets fall, deals break; when markets rise, the upside is capped at the deal spread. Economically, merger arbitrage is selling insurance on deals.
  • Managed futures (AQMIX) goes the other way: its beta is more negative when the market falls. The quadratic fit is convex. Trend followers tend to be short the market during long falls, so they profit from crashes that last several months (crisis alpha). With our sample the difference is not statistically significant, but its sign is the one that makes managed futures a popular diversifier.

11.3 Market timing: Treynor-Mazuy and Henriksson-Merton

Bodie, Kane & Marcus (Section 24.4) present two classic regressions to test for timing ability. Treynor and Mazuy add a quadratic term:

R_{F,t}-R_{f,t}=a+b\left(R_{M,t}-R_{f,t}\right)+c\left(R_{M,t}-R_{f,t}\right)^2+e_t

and Henriksson and Merton let beta take two values, with a dummy D_t=1 when R_{M,t}>R_{f,t}:

R_{F,t}-R_{f,t}=a+b\left(R_{M,t}-R_{f,t}\right)+c\left(R_{M,t}-R_{f,t}\right)D_t+e_t

In both models, c>0 is evidence of timing ability (a convex, call-like payoff) and c<0 is the signature of selling options. The Henriksson-Merton regression is just the up/down beta regression written differently: c=\beta^{+}-\beta^{-}.

def timing_tests(fund):
    d = pd.DataFrame({"y": Rs[fund] - rf_s, "m": mkt_ex})
    d["m2"] = d["m"]**2
    d["mD"] = d["m"] * (d["m"] > 0)
    tm = smf.ols("y ~ m + m2", data=d).fit()
    hm = smf.ols("y ~ m + mD", data=d).fit()
    return pd.Series({"TM alpha (ann)": tm.params["Intercept"]*12,
                      "TM c": tm.params["m2"], "TM t(c)": tm.tvalues["m2"],
                      "HM alpha (ann)": hm.params["Intercept"]*12,
                      "HM c": hm.params["mD"], "HM t(c)": hm.tvalues["mD"],
                      "CAPM alpha (ann)": 12 * smf.ols("y ~ m", data=d).fit().params["Intercept"]},
                     name=fund)

pd.DataFrame([timing_tests(f) for f in ["PUT", "MNA", "HDG", "QMNIX", "AQMIX"]]).round(3)
       TM alpha (ann)    TM c  TM t(c)  HM alpha (ann)    HM c  HM t(c)  CAPM alpha (ann)
PUT            0.0420 -2.0210  -5.7540          0.0790 -0.4060  -5.4210           -0.0040
MNA            0.0180 -0.9920  -2.7280          0.0270 -0.1570  -2.0210           -0.0050
HDG           -0.0190 -0.3310  -1.4180         -0.0090 -0.0850  -1.7250           -0.0260
QMNIX          0.0980 -1.4350  -1.7750          0.1030 -0.1830  -1.0660            0.0660
AQMIX          0.0310  0.8010   0.9470          0.0060  0.2150   1.2110            0.0500

Compare the last column with the first and the fourth. The CAPM alpha of the PutWrite index is about zero, but once the regression allows for curvature, the timing coefficient c is strongly negative (t\approx-5.5) and the alpha jumps to between 4% and 8% per year. Is the put writer a stock picker with terrible timing? No: the “alpha” of a put writer is the option premium it collects every month, and the negative c is the option it gave away. The two terms are the two sides of the same trade, and neither of them is skill. The same pattern, weaker, appears in merger arbitrage (MNA). Only AQMIX shows a positive (convex) c, although not significant. A straight-line alpha simply cannot be trusted for a strategy with option-like payoffs.

11.4 Selling insurance: “picking up nickels in front of a steamroller”

Bodie, Kane & Marcus describe a hypothetical fund that holds an index fund and writes out-of-the-money puts. It brings in a small premium every month and loses only in rare crashes, so for years it looks like a skilled manager. Let’s build it. Every month the fund:

  1. keeps its capital in T-Bills (as collateral);
  2. sells one-month S&P 500 put options with a strike 5% below the current index level, on a notional equal to 4 times its capital;
  3. prices the puts with Black-Scholes, using the VIX of the previous month plus 2 volatility points as implied volatility (out-of-the-money puts trade at a higher implied volatility than at-the-money options: the volatility skew).
def bs_price(S, K, T, r, sigma, q=0.0, kind="call"):
    """Black-Scholes-Merton price of a European option (continuous rates)."""
    d1 = (np.log(S/K) + (r - q + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
    d2 = d1 - sigma*np.sqrt(T)
    if kind == "call":
        return S*np.exp(-q*T)*stats.norm.cdf(d1) - K*np.exp(-r*T)*stats.norm.cdf(d2)
    return K*np.exp(-r*T)*stats.norm.cdf(-d2) - S*np.exp(-q*T)*stats.norm.cdf(-d1)

spx, vix = prices["SPX"], prices["VIX"] / 100
MONEYNESS, LEVERAGE, SKEW, T = 0.95, 4.0, 0.02, 1/12

def put_writer_month(S0, S1, vol, r_us, rf_month):
    """Return on capital of one month of put writing."""
    K = MONEYNESS * S0
    prem = bs_price(S0, K, T, np.log(1 + r_us), vol + SKEW, q=0.013, kind="put")
    payoff = max(K - S1, 0)
    return prem / S0, payoff / S0, rf_month + LEVERAGE * (prem - payoff) / S0

rows = []
for t in r.index:
    prev = prices.index[prices.index.get_loc(t) - 1]           # previous month-end
    prem, pay, ret = put_writer_month(spx[prev], spx[t], vix[prev], i_us[t], rf_s[t])
    rows.append({"Date": t, "premium": prem, "payoff": pay, "ret": ret})
putw = pd.DataFrame(rows).set_index("Date")

n_loss = (putw["payoff"] > putw["premium"]).sum()
print(f"Months with a loss on the puts: {n_loss} of {len(putw)}")
Months with a loss on the puts: 11 of 139
print(f"Average premium per month (x{LEVERAGE:.0f}): {LEVERAGE*putw['premium'].mean():.4%}")
Average premium per month (x4): 2.5981%
print("Worst months:")
Worst months:
print(putw["ret"].nsmallest(4).round(4))
Date
2020-03-31   -0.1941
2018-12-31   -0.1426
2022-09-30   -0.1231
2022-04-30   -0.1201
Name: ret, dtype: float64
pw = np.log(1 + putw["ret"])
comp = pd.DataFrame({"Put writer": hf_stats(pw),
                     "PutWrite index (PUT)": hf_stats(r["PUT"]),
                     "SPY": hf_stats(r["SPY"])}).T
comp.round(3)
                      Ann. return  Ann. vol  Sharpe   Beta   Corr    Skew  Ex. kurt   AC(1)  Max DD
Put writer                 0.2260    0.1440  1.2770 0.6630 0.6900 -2.4460   10.9080 -0.0710 -0.2870
PutWrite index (PUT)       0.0800    0.0960  0.5960 0.5650 0.8800 -1.6960    6.3910  0.0040 -0.2070
SPY                        0.1360    0.1500  0.7220 1.0000 1.0000 -0.4880    0.6240 -0.1340 -0.2390
plot_time_series(pd.DataFrame({"Put writer (5% OTM, 4x)": np.exp(pw.cumsum()),
                               "S&P 500 (SPY)": np.exp(r["SPY"].cumsum())}),
                 "Picking up nickels in front of a steamroller", "Value of $1", "")

Look at the Sharpe ratio: it is far higher than that of the S&P 500. A manager who showed you this track record would look like a genius. But the other columns tell the truth: a skewness far below zero, a huge excess kurtosis, and a worst month (March 2020) that wiped out about seven months of premiums at once. The steady line in the plot is not low risk: it is risk that has not shown up yet.

Our sample does not contain the worst crash of the century. What would this fund have done in October 2008? At the end of September 2008 the VIX was 39.4 and the S&P 500 fell 16.9% during October:

prem, pay, ret_oct08 = put_writer_month(S0=100, S1=100*(1 - 0.169), vol=0.394,
                                        r_us=0.01, rf_month=0.01/12)
print(f"Premium collected (x4): {4*prem:.2%}   Payoff paid (x4): {4*pay:.2%}")
Premium collected (x4): 10.29%   Payoff paid (x4): 47.60%
print(f"Return on capital in October 2008: {ret_oct08:.2%}")
Return on capital in October 2008: -37.23%

A single month would have destroyed more than a third of the capital, more than a year of premiums. The Sharpe ratio cannot see this risk because the risk is not in the volatility of normal months; it is in the tail.

11.5 Measuring tail risk

For strategies with option-like payoffs we need measures that look at the downside and at the tail:

  • Sortino ratio: like Sharpe, but divides by the downside deviation (the volatility of returns below a target, here zero premium). It does not penalize upside volatility.
  • Maximum drawdown and the Calmar ratio (annual return divided by the absolute maximum drawdown).
  • Historical VaR and ES (Workshop 1), which use the empirical distribution.
  • Cornish-Fisher VaR: a parametric VaR that corrects the normal quantile for skewness S and excess kurtosis K:

z_{CF}=z+\frac{(z^2-1)S}{6}+\frac{(z^3-3z)K}{24}-\frac{(2z^3-5z)S^2}{36}, \qquad VaR_{CF}=-\left(\bar r+z_{CF}\,\sigma\right)

where z is the normal quantile (for 95%, z=-1.645).

def tail_report(ret_cc, rf=rf_m, c=0.95):
    ex = ret_cc - rf
    downside = np.sqrt((np.minimum(ex, 0)**2).mean())
    mu, sd = ret_cc.mean(), ret_cc.std()
    S, K = stats.skew(ret_cc), stats.kurtosis(ret_cc)
    z = stats.norm.ppf(1 - c)
    z_cf = z + (z**2 - 1)*S/6 + (z**3 - 3*z)*K/24 - (2*z**3 - 5*z)*S**2/36
    q = np.quantile(ret_cc, 1 - c)
    mdd = max_drawdown(ret_cc)
    return pd.Series({
        "Sharpe":     ex.mean() / ex.std() * np.sqrt(12),
        "Sortino":    ex.mean() / downside * np.sqrt(12),
        "Max DD":     mdd,
        "Calmar":     (np.exp(12*mu) - 1) / abs(mdd),
        "VaR normal": -(mu + z*sd),
        "VaR CF":     -(mu + z_cf*sd),                # Cornish-Fisher
        "VaR hist":   -q,
        "ES hist":    -ret_cc[ret_cc <= q].mean()})

tails = {"SPY": r["SPY"], "PUT index": r["PUT"], "Put writer": pw,
         "MNA": r["MNA"], "Carry trade": np.log(1 + carry["Carry trade"] + rf_s),
         "AQMIX": r["AQMIX"], "QMNIX": r["QMNIX"]}
pd.DataFrame({k: tail_report(v) for k, v in tails.items()}).T.round(4)
             Sharpe  Sortino  Max DD  Calmar  VaR normal  VaR CF  VaR hist  ES hist
SPY          0.7218   1.1035 -0.2393  0.5703      0.0605  0.0658    0.0665   0.0935
PUT index    0.5961   0.7883 -0.2068  0.3880      0.0392  0.0475    0.0441   0.0736
Put writer   1.2770   1.6938 -0.2867  0.7898      0.0513  0.0664    0.0545   0.1290
MNA          0.1563   0.2039 -0.0952  0.2893      0.0200  0.0230    0.0167   0.0297
Carry trade  0.3191   0.4335 -0.2260  0.2673      0.0531  0.0611    0.0497   0.0816
AQMIX        0.2040   0.3076 -0.2443  0.1703      0.0454  0.0449    0.0445   0.0550
QMNIX        0.4915   0.8302 -0.3838  0.1801      0.0401  0.0365    0.0366   0.0498

(For the carry trade we added the US risk-free rate, so that it represents a fully collateralized position.) Read the table column by column:

  • For the four short-volatility strategies (the PutWrite index, our put writer, merger arbitrage and the carry trade) the Cornish-Fisher VaR is above the normal VaR: correcting for negative skewness and fat tails raises the estimated loss. The Expected Shortfall is much larger than any VaR; for the put writer it is about 2.5 times the normal VaR. The losses beyond the VaR are where these strategies hide their risk.
  • For AQMIX (almost symmetric) the three VaRs are practically the same, and for QMNIX (positive skewness) the Cornish-Fisher VaR is below the normal one.
  • Even with these measures, the put writer still ranks first by Sharpe, Sortino and Calmar in this sample. Tail measures are only as good as the sample: none of them can see a crash that did not happen between 2015 and 2026. That is why professional risk managers complement them with stress tests like our October 2008 scenario.

11.6 Tail events: Long-Term Capital Management

Bodie, Kane & Marcus (Example 26.3) tell the story that every hedge fund professional knows. In the late 1990s Long-Term Capital Management was considered the most successful hedge fund in history. Its partners included two future Nobel laureates, and its models estimated that its capital was about 10 times the monthly standard deviation of its returns: enough to survive any “possible” shock if returns were normal. Its trades were mostly convergence and relative value positions (like the on-the-run vs off-the-run Treasury bonds of Example 26.1), hedged and highly levered.

In August 1998 Russia defaulted on its debt. Investors fled to the safest and most liquid assets, and all the spreads that LTCM was betting would converge widened at the same time, in markets that normally had nothing to do with each other. The fund lost about $550 million in a single day (around nine monthly standard deviations) and had to be rescued by a consortium of banks organized by the Federal Reserve Bank of New York.

The lessons connect every part of this workshop: convergence trades without a convergence date (Part I), leverage and liquidity mismatch (Part II), short-volatility payoffs whose risk is in the tail (Part IV), and correlations that go to one in a crisis (Workshop 1: volatility is not risk).

12 CHALLENGE 4

  1. Estimate the up/down beta regression and the Treynor-Mazuy and Henriksson-Merton regressions for the carry trade (use its excess return as the dependent variable). Does the carry trade behave like a short put on the stock market? Report and interpret the coefficients.

  2. Modify the put-writing fund: strike 10% below the index and a leverage of 6x. Report the table of hf_stats and the tail_report. Compare with the 5%/4x version: which one has the higher Sharpe ratio, and which one would you consider riskier? Why?

  3. For the modified fund of (b), compute the return in the October 2008 stress scenario. How many months of average premium does that single month destroy?

  4. Rank the funds of the tail-risk table by Sharpe, Sortino and Calmar. Do the rankings agree? Which measure would you use to present a merger arbitrage fund to a family office, and why?

  5. In one paragraph: a manager shows you 5 years of monthly returns with a Sharpe ratio of 2.0, no losing year and a maximum drawdown of 3%. List the questions (and the regressions) you would use before investing.

13 PART V — Why hedge fund numbers can lie: illiquidity, biases and fees

In Part IV the numbers were right, but our measures were wrong. In this part the problem is worse: the numbers themselves can be biased. Bodie, Kane & Marcus (Section 26.5) discuss three sources of bias: illiquid assets, the construction of hedge fund databases, and the fee structure.

13.1 Illiquid assets and smoothed returns

A hedge fund that holds illiquid assets (distressed debt, private loans, thinly traded securities) cannot observe their market price every month. It reports a model value or an old transaction price, and those valuations adjust slowly to the true value. Getmansky, Lo and Makarov (2004) model the reported return as a weighted average of the current and past true returns:

R^{o}_t=\theta_0R_t+\theta_1R_{t-1}+\theta_2R_{t-2}, \qquad \theta_0+\theta_1+\theta_2=1

The average return is unchanged, but the reported returns are smoother. Let’s see what that does to the statistics of Workshop 1 with a simulation:

rng = np.random.default_rng(2026)
n = 100_000
mkt_true = rng.normal(0.007, 0.045, n)                     # true market excess returns
fund_true = 0.003 + 0.5*mkt_true + rng.normal(0, 0.03, n)  # true fund excess returns
theta = np.array([0.5, 0.3, 0.2])
fund_rep = np.convolve(fund_true, theta, mode="full")[:n]  # reported = smoothed
fund_rep[:2] = fund_true[:2]

def sim_stats(x, m=mkt_true):
    return pd.Series({"Mean (ann)": 12*x.mean(), "Vol (ann)": x.std()*np.sqrt(12),
                      "Sharpe": x.mean()/x.std()*np.sqrt(12),
                      "Beta": np.cov(x, m)[0, 1]/m.var(),
                      "Autocorr(1)": pd.Series(x).autocorr(1)})

pd.DataFrame({"True returns": sim_stats(fund_true),
              "Reported (smoothed)": sim_stats(fund_rep)}).T.round(3)
                     Mean (ann)  Vol (ann)  Sharpe   Beta  Autocorr(1)
True returns             0.0790     0.1300  0.6070 0.5000       0.0010
Reported (smoothed)      0.0790     0.0800  0.9850 0.2500       0.5530

Smoothing does three things at once, all of them flattering:

  1. The volatility falls (here by almost 40%), so the Sharpe ratio rises in the same proportion, with exactly the same average return.
  2. The beta falls (here, by half), because only part of the market move shows up in the same month. The fund looks less exposed to the market than it is, and its CAPM alpha rises.
  3. The returns become positively autocorrelated: the tell-tale sign.

Hasanhodzic and Lo find significant positive autocorrelation in many hedge funds, and Bodie, Kane & Marcus show that hedge fund indexes with higher serial correlation report higher alphas and Sharpe ratios (Figure 26.2).

13.1.1 Unsmoothing

If the smoothing follows a first-order pattern R^{o}_t=(1-\rho)R_t+\rho R^{o}_{t-1}, we can recover an estimate of the true returns (Geltner’s method, used for real estate and private equity):

\hat{R}_t=\frac{R^{o}_t-\rho\,R^{o}_{t-1}}{1-\rho}

where \rho is the first-order autocorrelation of the reported series. In Part I we found that QMNIX has an autocorrelation of about 0.22. Its holdings are liquid stocks, so the autocorrelation probably comes from the strategy itself (slow-moving factor exposures) rather than from stale prices; but let’s see how much its statistics would change if we treated it as smoothing:

def unsmooth(x):
    rho = x.autocorr(1)
    return ((x - rho * x.shift(1)) / (1 - rho)).dropna(), rho

for f in ["QMNIX", "QSPIX", "MERFX"]:
    xs, rho = unsmooth(r[f])
    print(f"{f}: rho = {rho:.3f} | vol reported = {r[f].std()*np.sqrt(12):.4f}, "
          f"unsmoothed = {xs.std()*np.sqrt(12):.4f} | Sharpe reported = "
          f"{hf_stats(r[f])['Sharpe']:.3f}, unsmoothed = {hf_stats(xs)['Sharpe']:.3f}")
QMNIX: rho = 0.222 | vol reported = 0.0963, unsmoothed = 0.1208 | Sharpe reported = 0.491, unsmoothed = 0.374
QSPIX: rho = 0.088 | vol reported = 0.1264, unsmoothed = 0.1384 | Sharpe reported = 0.415, unsmoothed = 0.392
MERFX: rho = -0.047 | vol reported = 0.0278, unsmoothed = 0.0266 | Sharpe reported = 0.512, unsmoothed = 0.550

If the autocorrelation of QMNIX came from smoothing, its true volatility would be about 12% instead of 9.6%, and its Sharpe ratio would fall from 0.49 to about 0.37. For MERFX, whose autocorrelation is slightly negative, nothing changes. The point is not that QMNIX smooths its returns (we have no evidence of that); it is that a positive autocorrelation is a warning light that you must check before comparing Sharpe ratios.

13.1.2 Annualizing a Sharpe ratio when returns are autocorrelated

In Workshop 1 we annualized a monthly Sharpe ratio by multiplying by \sqrt{12}. That rule assumes that returns are independent over time. Lo (2002) shows that with autocorrelations \rho_k the correct factor is:

SR_{annual}=\eta\;SR_{monthly},\qquad \eta=\frac{q}{\sqrt{q+2\sum_{k=1}^{q-1}(q-k)\rho_k}},\quad q=12

With \rho_k=0 we recover \eta=\sqrt{12}\approx3.46. Positive autocorrelation makes \eta smaller:

def lo_sharpe(ret_cc, rf=rf_m, q=12):
    ex = ret_cc - rf
    rho = [ex.autocorr(k) for k in range(1, q)]
    eta = q / np.sqrt(q + 2*sum((q - k)*rho[k-1] for k in range(1, q)))
    sr_m = ex.mean() / ex.std()
    return pd.Series({"eta": eta, "Sharpe (x sqrt 12)": sr_m*np.sqrt(12),
                      "Sharpe (Lo 2002)": sr_m*eta})

series_lo = ["SPY", "MERFX", "QMNIX", "QSPIX", "AQMIX"]
pd.DataFrame({f: lo_sharpe(r[f]) for f in series_lo}).T.round(3)
         eta  Sharpe (x sqrt 12)  Sharpe (Lo 2002)
SPY   4.6270              0.7220            0.9640
MERFX 3.8900              0.5120            0.5750
QMNIX 2.4270              0.4910            0.3440
QSPIX 3.0090              0.4150            0.3600
AQMIX 3.5610              0.2040            0.2100

QMNIX has the largest autocorrelations, and its annualized Sharpe ratio falls from 0.49 to 0.34 with the correct factor. Note the opposite case of SPY: its monthly returns are slightly negatively autocorrelated in this sample, so the \sqrt{12} rule underestimates its annual Sharpe ratio. The rule of Workshop 1 is an approximation that works only when returns are independent.

13.2 Survivorship bias and backfill bias

Hedge funds report to commercial databases voluntarily. This creates two biases (Bodie, Kane & Marcus, Section 26.5):

  • Survivorship bias: funds that perform badly close and stop reporting. A database that only contains living funds overstates the average performance. Hedge fund attrition is high, more than double that of mutual funds.
  • Backfill bias: a new fund is started with seed money and only starts reporting (to attract investors) if its first months were good, and when it does, its good early history is added (“backfilled”) to the database.

A simulation shows how large these biases can be, even when no manager has any skill:

rng = np.random.default_rng(7)
n_funds, n_months = 2000, 120
mkt = rng.normal(0.006, 0.045, n_months)                   # common market excess return
alpha_true, beta_true, idio = 0.0, 0.4, 0.035              # NO skill: alpha = 0
ex_ret = alpha_true + beta_true*mkt + rng.normal(0, idio, (n_funds, n_months))

wealth = np.cumprod(1 + ex_ret, axis=1)
dead = (wealth < 0.75).cumsum(axis=1) > 0                  # closes after losing 25%
alive_at_end = ~dead[:, -1]

def avg_alpha(returns):
    X = sm.add_constant(mkt[:returns.shape[1]])
    return np.mean([sm.OLS(y, X).fit().params[0] for y in returns]) * 12

print(f"Funds that survive 10 years: {alive_at_end.mean():.1%}")
Funds that survive 10 years: 32.2%
print(f"Average annual alpha, ALL funds (true): {avg_alpha(ex_ret):+.4f}")
Average annual alpha, ALL funds (true): +0.0004
print(f"Average annual alpha, SURVIVORS only  : {avg_alpha(ex_ret[alive_at_end]):+.4f}")
Average annual alpha, SURVIVORS only  : +0.0348
# backfill: a fund enters the database only if its first 24 months were profitable
incubation = 24
good_start = wealth[:, incubation - 1] > 1.0
print(f"\nFunds with a profitable 24-month incubation: {good_start.mean():.1%}")

Funds with a profitable 24-month incubation: 23.4%
inc_all  = avg_alpha(ex_ret[:, :incubation])
inc_back = avg_alpha(ex_ret[good_start, :incubation])
print(f"Average alpha during incubation, all funds : {inc_all:+.4f}")
Average alpha during incubation, all funds : +0.0002
print(f"Average alpha during incubation, backfilled: {inc_back:+.4f}")
Average alpha during incubation, backfilled: +0.1126

Nobody in this simulated industry has any skill, yet a database of survivors shows a positive average alpha, and the backfilled histories look even better. Published estimates of survivorship bias in hedge fund databases range from about 2% to more than 4% per year. When a salesperson shows you “the average hedge fund return” from a database, ask how dead funds and backfilled returns were treated.

13.3 Fees: what the investor actually gets

13.3.1 The “2 and 20” contract with a high-water mark

In Workshop 1 we described the typical fee: a management fee of about 2% of assets plus an incentive fee of 20% of the profits above a hurdle rate, subject to a high-water mark (no incentive fee until past losses are recovered). Let’s apply it to a real return series. We treat the monthly returns of QMNIX as the gross returns of a hypothetical hedge fund, and compute what an investor would receive net of a 2/20 contract with the T-Bill as hurdle and an annual high-water mark:

def net_of_fees(gross, rf=rf_s, mgmt=0.02, perf=0.20, hwm=True):
    """Monthly 'm and p' fee: management fee monthly, incentive fee
    crystallized every December on the profit above the hurdle (rf)
    and above the high-water mark."""
    nav, peak = 1.0, 1.0
    nav_start_year, hurdle_acc = 1.0, 1.0
    out, fees = [], []
    for t, g in gross.items():
        nav *= (1 + g)
        m_fee = nav * mgmt / 12
        nav -= m_fee
        hurdle_acc *= (1 + rf[t])
        p_fee = 0.0
        if t.month == 12 or t == gross.index[-1]:
            base = max(nav_start_year * hurdle_acc, peak if hwm else 0)
            p_fee = perf * max(nav - base, 0)
            nav -= p_fee
            peak = max(peak, nav) if hwm else nav
            nav_start_year, hurdle_acc = nav, 1.0
        out.append(nav)
        fees.append(m_fee + p_fee)
    net = pd.Series(out, index=gross.index).pct_change()
    net.iloc[0] = out[0] - 1
    return net, pd.Series(fees, index=gross.index)

gross = Rs["QMNIX"]
net_hwm, fees_hwm = net_of_fees(gross)
net_no,  fees_no  = net_of_fees(gross, hwm=False)

ann = lambda x: (1 + x).prod()**(12/len(x)) - 1
print(f"Gross annual return             : {ann(gross):.4f}")
Gross annual return             : 0.0691
print(f"Net annual return, with HWM     : {ann(net_hwm):.4f}")
Net annual return, with HWM     : 0.0388
print(f"Net annual return, without HWM  : {ann(net_no):.4f}")
Net annual return, without HWM  : 0.0315
g_profit = (1 + gross).prod() - 1
n_profit = (1 + net_hwm).prod() - 1
print(f"\nShare of the gross profit kept by the investor: {n_profit/g_profit:.1%}")

Share of the gross profit kept by the investor: 47.4%
plot_time_series(pd.DataFrame({"Gross": (1+gross).cumprod(),
                               "Net of 2/20 with HWM": (1+net_hwm).cumprod(),
                               "Net of 2/20 without HWM": (1+net_no).cumprod()}),
                 "What 2 and 20 does to a hedge fund return", "Value of $1", "")

The investor keeps less than half of the gross profit: the fund earned about 6.9% per year before fees and the investor about 3.9% after fees. Notice also the effect of the high-water mark: without it the manager would have charged incentive fees in the good years after the 2018–2020 losses, before the investor had recovered them, and the net return would have been even lower. The high-water mark protects the investor from paying twice for the same gains.

13.3.2 The incentive fee is a call option

Bodie, Kane & Marcus (Section 26.6) point out that the incentive fee is a call option on the fund’s assets: the manager receives 20% of the value above the hurdle and loses nothing if the fund falls below it. With a net asset value S_0 and a hurdle rate r_f, the incentive fee is worth 0.20 calls with strike X=S_0(1+r_f). Let’s reproduce their Example 26.4 (S_0=100, X=105, \sigma=30\%, r_{cc}=4.88\%, T=1):

call = bs_price(S=100, K=105, T=1, r=0.0488, sigma=0.30, kind="call")
print(f"Black-Scholes value of the call : {call:.2f}")
Black-Scholes value of the call : 11.92
print(f"Value of the 20% incentive fee  : {0.20*call:.2f}% of NAV per year")
Value of the 20% incentive fee  : 2.38% of NAV per year
print(f"Total expected fee (+ 2% mgmt)  : {0.20*call + 2:.2f}% of NAV per year")
Total expected fee (+ 2% mgmt)  : 4.38% of NAV per year
vols = np.linspace(0.05, 0.60, 56)
fee_value = [0.20 * bs_price(100, 105, 1, 0.0488, v, kind="call") for v in vols]
plt.figure(figsize=(10, 5))
plt.plot(vols, fee_value)
plt.xlabel("Volatility of the fund")
plt.ylabel("Value of the incentive fee (% of NAV)")
plt.title("The manager's incentive fee is worth more when the fund is riskier")
plt.grid(alpha=0.3)
plt.show()

The expected cost of “2 and 20” for a fund with 30% volatility is about 4.4% of assets per year, not 2%. And the plot shows the incentive problem we anticipated in Workshop 1: because an option is worth more when volatility is higher, the manager’s fee increases with the risk of the fund, while the investor bears the losses. The high-water mark adds a second, darker incentive: a manager far below the high-water mark holds a deep out-of-the-money option that is almost worthless, which explains why many funds close after large losses instead of trying to recover them (another source of survivorship bias).

13.3.3 Fees on fees: funds of funds

A fund of funds invests in several hedge funds, offering diversification and due diligence (a service whose value was badly questioned after the Madoff fraud of 2008, in which several large funds of funds had invested). Because each underlying fund charges its incentive fee separately, the fund of funds pays incentive fees to the winners even when the total portfolio loses. Bodie, Kane & Marcus (Example 26.5):

fof = pd.DataFrame({"Start": [1.0, 1.0, 1.0], "End (gross)": [1.20, 1.40, 0.25]},
                   index=["Fund 1", "Fund 2", "Fund 3"])
fof["Gross return"] = fof["End (gross)"] / fof["Start"] - 1
fof["Incentive fee"] = 0.20 * np.maximum(fof["End (gross)"] - fof["Start"], 0)
fof["End (net)"] = fof["End (gross)"] - fof["Incentive fee"]
fof.loc["Fund of funds"] = fof.sum()
fof.loc["Fund of funds", "Gross return"] = fof.loc["Fund of funds", "End (gross)"] / 3 - 1
fof["Net return"] = fof["End (net)"] / fof["Start"] - 1
fof.round(3)
               Start  End (gross)  Gross return  Incentive fee  End (net)  Net return
Fund 1        1.0000       1.2000        0.2000         0.0400     1.1600      0.1600
Fund 2        1.0000       1.4000        0.4000         0.0800     1.3200      0.3200
Fund 3        1.0000       0.2500       -0.7500         0.0000     0.2500     -0.7500
Fund of funds 3.0000       2.8500       -0.0500         0.1200     2.7300     -0.0900

The fund of funds lost 5% gross, but paid 4% of its assets in incentive fees, so its investors lost 9% — and that is before the fund of funds charges its own layer of fees. Diversification across managers increases the expected incentive fees paid, because the investor pays for the winners and is not compensated for the losers.

14 CHALLENGE 5

  1. Simulate a fund with true monthly excess returns 0.004+0.6\,R_M+e (use the simulated mkt_true and an idiosyncratic volatility of 2.5%) and smooth it with weights \theta=(0.4, 0.35, 0.25). Report the true and reported volatility, Sharpe ratio, beta and autocorrelation. By what factor is the Sharpe ratio inflated?

  2. Compute the Lo (2002) annualized Sharpe ratio for MNA, BTAL, QAI and the carry trade. For which series does the correction matter most? Relate your answer to the autocorrelations.

  3. In the survivorship simulation, change the closing rule to “a fund closes after losing 15%” and then “after losing 40%”. How does the survivorship bias change? Explain the intuition.

  4. Apply the fee function to the gross returns of QSPIX with three contracts: (i) 2/20 with high-water mark, (ii) 1.5/15 with high-water mark, (iii) 1% management fee only. Report the net annual return and the share of the gross profit kept by the investor in each case. Which contract would you negotiate for a family office, and why?

  5. Using Black-Scholes, compute the value of a 20% incentive fee for a fund with \sigma=15\% and for a fund with \sigma=45\% (hurdle =r_f=5\%). Explain, in terms of the manager’s incentives, why investors often ask for a hurdle rate, a high-water mark and a clawback clause.

15 PART VI — How much to allocate to hedge funds: Treynor-Black and Black-Litterman

15.1 The allocation problem

We can now evaluate a hedge fund honestly. The last question is the one a family office actually has to answer: how much of its portfolio should go to hedge funds, and to which ones?

In Workshop 1 we saw why the obvious answer (compute the tangency portfolio with historical means and covariances) fails: the optimal weights are extremely sensitive to expected returns, which are estimated with huge error. Bodie, Kane & Marcus (Chapter 27) present two models that give that optimization the structure it needs:

  • Treynor-Black (TB): start from the passive market portfolio and add an active portfolio built from the securities (or managers) for which we have an alpha forecast. It is a model for security and manager selection.
  • Black-Litterman (BL): start from the expected returns implied by a neutral portfolio and blend them with the investor’s own views, weighted by the confidence in each view. It is a model for asset allocation.

The two are complements, not substitutes (Section 27.4): BL decides how much goes to each asset class (including the hedge fund sleeve); TB decides how to build the active part inside a class.

15.2 The Treynor-Black model

15.2.1 The logic

Treynor and Black assume that markets are nearly efficient: the passive index M is the starting point, and only a few assets (or managers) have a nonzero alpha. Each active asset i follows the index model of Workshop 1:

R_i-R_f=\alpha_i+\beta_i\left(R_M-R_f\right)+e_i

The model proceeds in four steps:

  1. Active portfolio weights. Each asset gets a weight proportional to its alpha divided by its residual variance (the risk you take to earn that alpha): w_i=\frac{\alpha_i/\sigma^2(e_i)}{\sum_j \alpha_j/\sigma^2(e_j)}
  2. Active portfolio characteristics. \alpha_A=\sum_i w_i\alpha_i, \beta_A=\sum_i w_i\beta_i, \sigma^2(e_A)=\sum_i w_i^2\sigma^2(e_i).
  3. How much goes to the active portfolio. The initial weight compares the active portfolio’s “reward to diversifiable risk” with the market’s reward to market risk, and is then adjusted for the beta of the active portfolio: w_A^0=\frac{\alpha_A/\sigma^2(e_A)}{E\left[R_M-R_f\right]/\sigma^2_M} \qquad\qquad w_A^{*}=\frac{w_A^0}{1+(1-\beta_A)\,w_A^0}
  4. The gain. The squared Sharpe ratio of the optimal risky portfolio is the squared Sharpe ratio of the market plus the squared Information ratio of the active portfolio: S_P^2=S_M^2+\left(\frac{\alpha_A}{\sigma(e_A)}\right)^2,\qquad \left(\frac{\alpha_A}{\sigma(e_A)}\right)^2=\sum_i\left(\frac{\alpha_i}{\sigma(e_i)}\right)^2

This last equation is the formal reason why, in Workshop 1 and in Bodie, Kane & Marcus (Chapter 24), the Information ratio is the right measure for a fund that will be added to a passive portfolio: every manager contributes to the portfolio’s Sharpe ratio exactly through the square of their Information ratio.

15.2.2 Treynor-Black with our hedge fund proxies

We use SPY as the passive portfolio and five hedge fund proxies as the candidates for the active portfolio. The alphas, betas and residual variances come from CAPM regressions (monthly, simple premium returns); for the market we use a forecast of the annual risk premium (5.5%) rather than the historical average, which in our sample is unusually high.

CANDIDATES = ["QMNIX", "QSPIX", "MERFX", "AQMIX", "MNA"]
MKT_PREMIUM = 0.055                                   # forecast, annual
sigma_M = mkt_ex.std() * np.sqrt(12)

def index_model(f):
    m = sm.OLS(Rs[f] - rf_s, sm.add_constant(mkt_ex)).fit()
    return pd.Series({"alpha": m.params.iloc[0]*12,             # annual
                      "alpha SE": m.bse.iloc[0]*12,
                      "beta": m.params.iloc[1],
                      "resid var": m.resid.var()*12})           # annual

im = pd.DataFrame({f: index_model(f) for f in CANDIDATES}).T

def treynor_black(alpha, beta, resid_var, mkt_prem=MKT_PREMIUM, sig_m=sigma_M):
    w = (alpha / resid_var) / (alpha / resid_var).sum()
    a_A, b_A = w @ alpha, w @ beta
    s2e_A = (w**2) @ resid_var
    wA0 = (a_A / s2e_A) / (mkt_prem / sig_m**2)
    wA = wA0 / (1 + (1 - b_A) * wA0)
    S_M = mkt_prem / sig_m
    IR_A = a_A / np.sqrt(s2e_A)
    summary = pd.Series({"alpha_A": a_A, "beta_A": b_A, "sigma(e_A)": np.sqrt(s2e_A),
                         "IR_A": IR_A, "w_A (initial)": wA0, "w_A* (adjusted)": wA,
                         "Sharpe market": S_M, "Sharpe optimal": np.sqrt(S_M**2 + IR_A**2)})
    return w, summary

w_tb, tb_sum = treynor_black(im["alpha"], im["beta"], im["resid var"])
print(im.round(4))
        alpha  alpha SE    beta  resid var
QMNIX  0.0656    0.0288 -0.1146     0.0091
QSPIX  0.0635    0.0389 -0.0254     0.0165
MERFX  0.0057    0.0077  0.0742     0.0006
AQMIX  0.0497    0.0299 -0.1956     0.0098
MNA   -0.0045    0.0132  0.1078     0.0019
print("\nWeights inside the active portfolio:")

Weights inside the active portfolio:
print(w_tb.round(3))
QMNIX    0.3200
QSPIX    0.1700
MERFX    0.3910
AQMIX    0.2250
MNA     -0.1060
dtype: float64
print()
print(tb_sum.round(4))
alpha_A            0.0456
beta_A            -0.0673
sigma(e_A)         0.0449
IR_A               1.0155
w_A (initial)      9.2444
w_A* (adjusted)    0.8507
Sharpe market      0.3667
Sharpe optimal     1.0797
dtype: float64

Two problems jump out immediately:

  • The active portfolio takes long and short positions in hedge funds driven by alphas estimated with large standard errors (look at the alpha SE column: for QSPIX and AQMIX the standard error is more than half of the alpha).
  • The model recommends putting about 85% of the risky portfolio in the active portfolio and promises a Sharpe ratio of about 1.1, three times that of the market.

Bodie, Kane & Marcus (Section 27.2) insist that this is not a flaw of the model but of its inputs: alpha forecasts must be adjusted for their precision.

15.2.3 Shrinking the alphas

The solution is to shrink each alpha towards zero in proportion to how imprecise it is. Bodie, Kane & Marcus multiply each forecast by the R^2 of a regression of realized alphas on past forecasts. Without a forecasting record, a simple and very common version is the Bayesian (empirical Bayes) adjustment: if we believe that true alphas across managers are distributed around zero with a standard deviation \sigma_\alpha, and our estimate has standard error SE(\hat\alpha), the best estimate of the true alpha is:

\alpha^{adj}=\underbrace{\frac{\sigma^2_\alpha}{\sigma^2_\alpha+SE(\hat\alpha)^2}}_{\text{shrinkage factor, between 0 and 1}}\;\hat\alpha

We assume \sigma_\alpha=2\% per year (skilled managers exist, but true alphas above 4% are rare):

SIGMA_ALPHA = 0.02
shrink = SIGMA_ALPHA**2 / (SIGMA_ALPHA**2 + im["alpha SE"]**2)
alpha_adj = shrink * im["alpha"]
print(pd.DataFrame({"alpha (raw)": im["alpha"], "shrinkage factor": shrink,
                    "alpha (adjusted)": alpha_adj}).round(4))
       alpha (raw)  shrinkage factor  alpha (adjusted)
QMNIX       0.0656            0.3247            0.0213
QSPIX       0.0635            0.2091            0.0133
MERFX       0.0057            0.8715            0.0050
AQMIX       0.0497            0.3084            0.0153
MNA        -0.0045            0.6975           -0.0032
w_tb2, tb_sum2 = treynor_black(alpha_adj, im["beta"], im["resid var"])
print("\nWeights inside the active portfolio (adjusted alphas):")

Weights inside the active portfolio (adjusted alphas):
print(w_tb2.round(3))
QMNIX    0.2180
QSPIX    0.0750
MERFX    0.7160
AQMIX    0.1460
MNA     -0.1550
dtype: float64
print()
print(pd.DataFrame({"Raw alphas": tb_sum, "Adjusted alphas": tb_sum2}).round(4))
                 Raw alphas  Adjusted alphas
alpha_A              0.0456           0.0119
beta_A              -0.0673          -0.0189
sigma(e_A)           0.0449           0.0333
IR_A                 1.0155           0.3578
w_A (initial)        9.2444           4.3986
w_A* (adjusted)      0.8507           0.8024
Sharpe market        0.3667           0.3667
Sharpe optimal       1.0797           0.5123

Look at the last table. With adjusted alphas the Information ratio of the active portfolio falls from about 1.0 to 0.36 and the promised Sharpe ratio is cut in half, to a much more plausible number. The weights move towards the fund whose alpha is measured most precisely (MERFX), and the model no longer “believes” the noisy alphas. This is the professional way to use an alpha estimate: never at face value.

But notice that the recommended weight of the active portfolio is still about 80%. Why? Because Treynor-Black, like every mean-variance model, measures risk only by variance, and our active portfolio has a very low residual volatility (about 3% a year), dominated by merger arbitrage. Part IV taught us that merger arbitrage has a low volatility and a very negative skewness. The model cannot see tail risk, illiquidity, lock-ups, fees or the operational risk of real hedge funds. That is why, in practice, the investment policy of the investor imposes explicit limits on the allocation to hedge funds — which is exactly what we do next with Black-Litterman.

15.3 The Black-Litterman model

15.3.1 Why we need it

Suppose a Mexican family office wants to allocate its wealth among six asset classes. The textbook recipe of Workshop 1 is: estimate the expected returns and the covariance matrix with historical data and compute the tangency portfolio. We will see in a moment how badly this works. Black and Litterman (1992), from Goldman Sachs, proposed a different starting point: instead of historical averages, use the expected returns that make a neutral, reasonable portfolio optimal, and then move away from them only where the investor has a view, and only as much as her confidence in that view justifies.

15.3.2 The family office universe (in pesos)

All returns are measured in pesos, because that is the currency in which the family measures its wealth. Foreign assets are held unhedged (Part III showed that the dollar is a partial natural hedge for a Mexican investor), and the risk-free asset is the peso 3-month rate (our proxy for CETES):

Asset class Proxy In pesos
Mexican equities EWW (1+R_{EWW})(1+R_{USDMXN})-1
US equities SPY (1+R_{SPY})(1+R_{USDMXN})-1
International equities EFA (1+R_{EFA})(1+R_{USDMXN})-1
US Treasuries IEF (1+R_{IEF})(1+R_{USDMXN})-1
Gold GLD (1+R_{GLD})(1+R_{USDMXN})-1
Hedge funds equal-weighted sleeve of QMNIX, QSPIX, AQMIX, MERFX idem

(A real family office would also hold Mexican government bonds, M Bonos and Udibonos. We leave them out only because they are not in our dataset; adding them would not change the method.)

fx = Rs["USDMXN"]
to_mxn = lambda x: (1 + x) * (1 + fx) - 1

hf_sleeve = Rs[["QMNIX", "QSPIX", "AQMIX", "MERFX"]].mean(axis=1)   # monthly rebalanced
fo = pd.DataFrame({"MX_EQ":   to_mxn(Rs["EWW"]),
                   "US_EQ":   to_mxn(Rs["SPY"]),
                   "INTL_EQ": to_mxn(Rs["EFA"]),
                   "UST":     to_mxn(Rs["IEF"]),
                   "GOLD":    to_mxn(Rs["GLD"]),
                   "HF":      to_mxn(hf_sleeve)})
rf_mx = rates_m["MX_3M"].shift(1).bfill() / 12                    # monthly peso risk-free
fo_ex = fo.sub(rf_mx, axis=0)                                     # excess returns in MXN

ASSETS_FO = list(fo.columns)
Sigma = fo_ex.cov() * 12                                          # annual covariance
hist_mu = fo_ex.mean() * 12                                       # annual historical premia

print(pd.DataFrame({"Hist. premium": hist_mu, "Vol": np.sqrt(np.diag(Sigma))},
                   index=ASSETS_FO).round(4))
         Hist. premium    Vol
MX_EQ          -0.0006 0.1479
US_EQ           0.0794 0.1440
INTL_EQ         0.0257 0.1259
UST            -0.0387 0.1444
GOLD            0.0589 0.1782
HF              0.0058 0.1372
print("\nCorrelations:")

Correlations:
print(fo_ex.corr().round(2))
          MX_EQ  US_EQ  INTL_EQ     UST    GOLD      HF
MX_EQ    1.0000 0.1000   0.2900 -0.4100 -0.1800 -0.4800
US_EQ    0.1000 1.0000   0.8100  0.3800  0.1600  0.2400
INTL_EQ  0.2900 0.8100   1.0000  0.3600  0.2400  0.2000
UST     -0.4100 0.3800   0.3600  1.0000  0.6400  0.7300
GOLD    -0.1800 0.1600   0.2400  0.6400  1.0000  0.4500
HF      -0.4800 0.2400   0.2000  0.7300  0.4500  1.0000

15.3.3 Step 1: the naive approach (and why it fails)

With historical premia, the unconstrained mean-variance weights are w=\frac{1}{\delta}\Sigma^{-1}\mu, where \delta is the investor’s risk aversion; the rest of the wealth goes to CETES. Instead of guessing \delta, we calibrate it with the family office’s strategic benchmark w_{eq} (defined in Step 2): if the family expects its benchmark to earn a premium of 4% per year over CETES, then, since the benchmark’s premium is w_{eq}^{\top}\Pi=\delta\,\sigma^2_{eq},

\delta=\frac{0.04}{\sigma^2_{eq}}

w_eq = pd.Series({"MX_EQ": 0.25, "US_EQ": 0.30, "INTL_EQ": 0.10,
                  "UST": 0.20, "GOLD": 0.05, "HF": 0.10})     # strategic benchmark
BENCH_PREMIUM = 0.04
sigma_eq = np.sqrt(w_eq @ Sigma @ w_eq)
DELTA = BENCH_PREMIUM / sigma_eq**2
print(f"Volatility of the benchmark: {sigma_eq:.4f}   ->   delta = {DELTA:.2f}")
Volatility of the benchmark: 0.0851   ->   delta = 5.52
w_naive = pd.Series(np.linalg.solve(DELTA * Sigma, hist_mu), index=ASSETS_FO)
print(w_naive.round(3))
MX_EQ     -0.3500
US_EQ      1.7900
INTL_EQ   -0.8350
UST       -2.1620
GOLD       1.0980
HF         0.5810
dtype: float64
print(f"Total in risky assets: {w_naive.sum():.2f}  (rest in CETES: {1-w_naive.sum():.2f})")
Total in risky assets: 0.12  (rest in CETES: 0.88)

Nobody would sign this: 179% in US stocks, 110% in gold, and a short position of more than 200% in US Treasuries. The weights are extreme and driven by the accidents of one sample (the assets with the best past premia get large long positions, and the assets that are highly correlated with them but performed worse get large short positions). Change the sample by a few years and the portfolio changes completely. This is the fragility we saw in Workshop 1, now with real consequences.

15.3.4 Step 2: implied equilibrium returns (reverse optimization)

Black and Litterman run the optimization backwards. Start from a reasonable, diversified portfolio w_{eq} — for a global investor it is the market-capitalization portfolio; for our family office it is its strategic (policy) benchmark. If that portfolio is optimal for an investor with risk aversion \delta, the first-order condition of Workshop 1 implies that the expected excess returns must be:

\Pi=\delta\,\Sigma\,w_{eq}

These implied returns \Pi are the “neutral” expectations: they are the returns that justify holding the benchmark.

Pi = DELTA * Sigma @ w_eq

print(pd.DataFrame({"Benchmark weight": w_eq, "Implied premium (Pi)": Pi,
                    "Historical premium": hist_mu}).round(4))
         Benchmark weight  Implied premium (Pi)  Historical premium
MX_EQ              0.2500                0.0204             -0.0006
US_EQ              0.3000                0.0580              0.0794
INTL_EQ            0.1000                0.0510              0.0257
UST                0.2000                0.0401             -0.0387
GOLD               0.0500                0.0361              0.0589
HF                 0.1000                0.0256              0.0058
print("\nCheck - optimal weights with Pi:",
      np.linalg.solve(DELTA * Sigma, Pi).round(3))

Check - optimal weights with Pi: [0.25 0.3  0.1  0.2  0.05 0.1 ]

The implied premia are sensible and, unlike the historical ones, they all have the right sign. An asset gets a high implied premium when it contributes a lot of risk to the benchmark (US and international equities). Mexican equities get the lowest premium: measured in pesos, they have a low or negative correlation with the dollar assets of the portfolio (when the peso depreciates, Mexican stocks tend to fall while every dollar asset gains in pesos), so for a Mexican investor they are a diversifier. And by construction, if we optimize with \Pi we get back exactly the benchmark. That is the neutral starting point.

15.3.5 Step 3: the investor’s views

Now the family office adds its own views. Each view is a linear combination of asset returns, written as P\,\mu=Q+\varepsilon, with uncertainty \varepsilon\sim N(0,\Omega):

  • View 1 (relative): thanks to nearshoring, Mexican equities will outperform US equities by 1% per year.
  • View 2 (relative): the hedge fund sleeve will outperform US Treasuries by 1% per year.
  • View 3 (absolute): gold will have an excess return (over CETES) of 0% per year.

Each row of P is one view:

P = pd.DataFrame(0.0, index=["V1: MX > US", "V2: HF > UST", "V3: Gold"], columns=ASSETS_FO)
P.loc["V1: MX > US", ["MX_EQ", "US_EQ"]] = [1, -1]
P.loc["V2: HF > UST", ["HF", "UST"]] = [1, -1]
P.loc["V3: Gold", "GOLD"] = 1
Q = pd.Series([0.01, 0.01, 0.00], index=P.index)
print(P)
              MX_EQ   US_EQ  INTL_EQ     UST   GOLD     HF
V1: MX > US  1.0000 -1.0000   0.0000  0.0000 0.0000 0.0000
V2: HF > UST 0.0000  0.0000   0.0000 -1.0000 0.0000 1.0000
V3: Gold     0.0000  0.0000   0.0000  0.0000 1.0000 0.0000
print("\nWhat the equilibrium implies for each view (P x Pi):")

What the equilibrium implies for each view (P x Pi):
print((P @ Pi).round(4))
V1: MX > US    -0.0376
V2: HF > UST   -0.0144
V3: Gold        0.0361
dtype: float64

Compare P\Pi with Q: a view only matters when it differs from what the equilibrium already implies.

15.3.6 Step 4: blending equilibrium and views

The posterior (blended) expected returns are a precision-weighted average of \Pi and the views:

\mu_{BL}=\left[(\tau\Sigma)^{-1}+P^{\top}\Omega^{-1}P\right]^{-1} \left[(\tau\Sigma)^{-1}\Pi+P^{\top}\Omega^{-1}Q\right]

Two parameters express confidence:

  • \tau (a small number, typically 0.025–0.05) measures the uncertainty of the equilibrium returns;
  • \Omega is the covariance matrix of the view errors. A common choice (He and Litterman) is \Omega=\text{diag}\left(P\,\tau\Sigma\,P^{\top}\right): each view is as uncertain as the equilibrium estimate of the same portfolio. Dividing \Omega by a number greater than one expresses more confidence in the views.
TAU = 0.05

def black_litterman(Sigma, Pi, P, Q, tau=TAU, conf=1.0):
    """Posterior mean and covariance. conf > 1 means more confidence in the views."""
    S, Pm = Sigma.values, P.values
    Omega = np.diag(np.diag(Pm @ (tau * S) @ Pm.T)) / conf
    A = np.linalg.inv(tau * S) + Pm.T @ np.linalg.inv(Omega) @ Pm
    b = np.linalg.inv(tau * S) @ Pi.values + Pm.T @ np.linalg.inv(Omega) @ Q.values
    mu_bl = np.linalg.solve(A, b)
    Sigma_bl = S + np.linalg.inv(A)                  # posterior covariance of returns
    return (pd.Series(mu_bl, index=Sigma.index),
            pd.DataFrame(Sigma_bl, index=Sigma.index, columns=Sigma.index))

mu_bl, Sigma_bl = black_litterman(Sigma, Pi, P, Q)
print(pd.DataFrame({"Equilibrium (Pi)": Pi, "Black-Litterman": mu_bl,
                    "Historical": hist_mu}).round(4))
         Equilibrium (Pi)  Black-Litterman  Historical
MX_EQ              0.0204           0.0322     -0.0006
US_EQ              0.0580           0.0431      0.0794
INTL_EQ            0.0510           0.0407      0.0257
UST                0.0401           0.0180     -0.0387
GOLD               0.0361           0.0130      0.0589
HF                 0.0256           0.0174      0.0058
print("\nThe views, after blending (P x mu_BL):")

The views, after blending (P x mu_BL):
print((P @ mu_bl).round(4))
V1: MX > US    -0.0109
V2: HF > UST   -0.0006
V3: Gold        0.0130
dtype: float64

The blended returns move towards the views, but not all the way: each view is only partially believed. For example, the equilibrium implied that Mexican equities would underperform US equities by 3.8%; the family’s view is +1\%; the blended expectation is about -1.1\%, a compromise weighted by the confidence in each source. Notice also that a view about two assets moves the expected returns of other assets (international equities, for example, have no view but their expected return falls, because they are highly correlated with US equities): the model spreads the information through the covariance matrix.

15.3.7 Step 5: the optimal portfolio

w_bl = pd.Series(np.linalg.solve(DELTA * Sigma_bl.values, mu_bl.values), index=ASSETS_FO)
mu_bl4, Sigma_bl4 = black_litterman(Sigma, Pi, P, Q, conf=4)     # more confidence
w_bl_conf = pd.Series(np.linalg.solve(DELTA * Sigma_bl4.values, mu_bl4.values),
                      index=ASSETS_FO)

alloc = pd.DataFrame({"Benchmark": w_eq, "BL (conf = 1)": w_bl,
                      "BL (conf = 4)": w_bl_conf, "Naive (historical)": w_naive})
alloc.loc["CETES (rest)"] = 1 - alloc.sum()
alloc.round(3)
              Benchmark  BL (conf = 1)  BL (conf = 4)  Naive (historical)
MX_EQ            0.2500         0.3320         0.3820             -0.3500
US_EQ            0.3000         0.1920         0.1420              1.7900
INTL_EQ          0.1000         0.0950         0.0950             -0.8350
UST              0.2000         0.0230        -0.0660             -2.1620
GOLD             0.0500        -0.0220        -0.0420              1.0980
HF               0.1000         0.2630         0.3510              0.5810
CETES (rest)    -0.0000         0.1170         0.1370              0.8770

The unconstrained Black-Litterman portfolio is the benchmark tilted in the direction of the views: more Mexican equities and less US equities (view 1), more hedge funds and fewer Treasuries (view 2), and no gold (view 3). International equities, which have no view, stay practically at their benchmark weight. With more confidence (conf = 4) the tilts are larger. Compare the first three columns with the naive portfolio: that is the whole point of the model. (The small position in CETES appears because the posterior covariance \Sigma_{BL} adds the uncertainty about the expected returns to the risk of each asset, so the investor takes slightly less risk.)

Finally, the family office’s investment policy imposes constraints that the formula does not know: no short positions and leverage, and a maximum of 15% in hedge funds (because of lock-ups and redemption notices). We solve the constrained problem numerically, maximizing the mean-variance utility of Workshop 1:

\max_w\; w^{\top}\mu_{BL}-\frac{\delta}{2}\,w^{\top}\Sigma_{BL}\,w \qquad \text{s.t.}\quad w_i\geq0,\;\sum_i w_i\leq 1,\; w_{HF}\leq 0.15

def constrained_mv(mu, S, delta=DELTA, max_hf=0.15):
    n = len(mu)
    obj = lambda w: -(w @ mu.values - 0.5 * delta * w @ S.values @ w)
    cons = [{"type": "ineq", "fun": lambda w: 1 - w.sum()}]          # sum <= 1
    bnds = [(0, max_hf) if a == "HF" else (0, 1) for a in mu.index]
    res = optimize.minimize(obj, np.repeat(1/n, n), method="SLSQP",
                            bounds=bnds, constraints=cons)
    return pd.Series(res.x, index=mu.index)

w_bl_c = constrained_mv(mu_bl, Sigma_bl)
w_naive_c = constrained_mv(hist_mu, Sigma)

alloc_c = pd.DataFrame({"Benchmark": w_eq, "BL constrained": w_bl_c,
                        "Naive constrained": w_naive_c})
alloc_c.loc["CETES (rest)"] = 1 - alloc_c.sum()
print(alloc_c.round(3))
              Benchmark  BL constrained  Naive constrained
MX_EQ            0.2500          0.3070             0.0000
US_EQ            0.3000          0.1910             0.6420
INTL_EQ          0.1000          0.1010             0.0000
UST              0.2000          0.0750             0.0000
GOLD             0.0500          0.0000             0.2510
HF               0.1000          0.1500             0.0000
CETES (rest)    -0.0000          0.1770             0.1070
alloc_c.drop("CETES (rest)").plot.bar(figsize=(12, 5), rot=0)
plt.title("Family office: benchmark vs Black-Litterman vs naive (long-only, HF <= 15%)")
plt.ylabel("Weight")
plt.grid(alpha=0.3, axis="y")
plt.show()

With the policy constraints the hedge fund allocation stops at its 15% limit, gold disappears, the Treasuries position shrinks, and Mexican equities get the largest overweight; the remaining 18% stays in CETES. The naive constrained portfolio, by contrast, puts almost two thirds in US equities and a quarter in gold — the two best performers of the last decade — and nothing in Mexico or in hedge funds. The Black-Litterman portfolio is explainable: each deviation from the benchmark can be traced to a view that the family office wrote down and can defend in front of its investment committee.

15.4 From models to an investment policy

Bodie, Kane & Marcus (Chapter 28) describe the framework of the CFA Institute for the investment management process: first the investor’s objectives (return and risk) and constraints, written down in an Investment Policy Statement (IPS); then the capital market expectations; and only then the asset allocation. Black-Litterman fits naturally in that framework: w_{eq} is the strategic allocation of the IPS, \Pi and the views are the capital market expectations, and the constraints of the optimizer come from the IPS. Here is how an IPS for a Mexican family office translates into model inputs:

IPS element Example for a Mexican family office Where it enters the model
Return objective CETES + 4% per year in pesos, over rolling 5-year periods \delta and the benchmark premium
Risk tolerance Maximum drawdown of 15%; annual volatility around 9% \delta; stress tests (Part IV)
Liquidity 10% of assets available within one month; lock-ups limited Cap on hedge funds (w_{HF}\leq15\%)
Time horizon Multi-generational, with periodic distributions to family members Strategic benchmark w_{eq}
Currency Wealth measured in pesos; spending partly in dollars Returns in MXN; hedging policy (Part III)
Taxes and regulation Mexican tax treatment of foreign funds (SIC, offshore) Choice of vehicle (Part I)
Unique circumstances Concentrated position in the family business; ESG preferences Additional constraints or views

Note in particular the last row: a family whose wealth comes from a Mexican industrial business is already heavily exposed to the Mexican economy and the peso. That is an argument against a large overweight in Mexican equities, whatever the nearshoring view says — the kind of trade-off an IPS forces you to make explicit.

16 CHALLENGE 6

This Challenge is a rehearsal of the Reto: you are the investment team of a Mexican family office.

  1. Treynor-Black. Add BTAL to the list of candidates and rerun the Treynor-Black model, first with raw alphas and then with adjusted alphas (\sigma_\alpha=2\%). Which funds get a negative weight, and why? How much does BTAL receive, given its beta of about -0.6? What happens to the Sharpe ratio of the optimal portfolio with raw and with adjusted alphas?

  2. Sensitivity to the prior. Repeat the adjusted Treynor-Black model with \sigma_\alpha=1\% and \sigma_\alpha=4\%. Explain, in words, what \sigma_\alpha represents and why a skeptical investor should choose a small value.

  3. Your own views. Write three views of your own for the six asset classes (at least one relative and one absolute), justify each one in two or three lines with an economic argument, and run the Black-Litterman model. Report \Pi, \mu_{BL} and the unconstrained and constrained weights. Use conf = 1 and conf = 4 and comment on the difference.

  4. Your IPS. Change the strategic benchmark w_{eq} and the constraints so that they reflect an IPS that you write for a specific (hypothetical) family: for example, a family that owns an exporting company, or a family that will need 20% of its wealth in dollars in three years. Rerun the constrained optimization and explain how the IPS changed the allocation.

  5. Memo. Write a one-page memo to the family’s investment committee recommending the allocation to hedge funds (how much and in which strategies). The memo must use at least one result from each Part of this workshop (style analysis, neutrality or portable alpha, currency, tail risk, fees or biases, and TB/BL).

17 Wrap-up: from evaluating hedge funds to allocating to them

Workshop 1 gave us the tools; Workshop 2 used them to open the black box. Here is the map of what you can now do, and where it goes next:

What you learned in Workshop 2 What it is for
Returns-based style analysis, clones Deciding what part of a fund you can buy cheaply
Dollar-, beta- and factor-neutral positions; futures hedge ratios Building and auditing “market-neutral” strategies
Pure plays, stat arb, 130/30, portable alpha Separating alpha from beta, and moving alpha across markets
CIP, the carry trade, currency hedging Understanding the peso and deciding the currency policy of a Mexican portfolio
Up/down betas, TM and HM regressions, put writing, CF VaR Detecting option-like payoffs and tail risk behind a high Sharpe ratio
Smoothing, survivorship/backfill bias, fees, incentive options Correcting the numbers before believing them
Treynor-Black, Black-Litterman, IPS Deciding how much to allocate, and defending it in front of a committee

Three ideas to carry into the Reto:

  1. Buy beta cheaply, pay only for alpha. Every hedge fund is part style, part option, part skill. Style analysis and non-linear regressions tell you how much of each you are buying.

  2. A high Sharpe ratio is a question, not an answer. Before believing it, check the skewness, the autocorrelation, the drawdowns, the fees and the database it came from.

  3. Allocation is a policy decision, not an optimization output. Models like Black-Litterman are useful because they make every deviation from the benchmark explicit and defensible, inside the constraints of the investor’s IPS.

18 What you have to submit

Each week (from week 7 to week 10), depending on how much we covered, you have to submit a version of your Workshop 2 (code, your notes, and Challenges):

  1. The .ipynb file of your Notebook. You can easily generate it from Google Colab or any other front-end.

  2. Your notebook must contain your notes for each topic (in text cells), replicate and run the Python code of the Workshop, and for each Challenge: the code, the output, and a written interpretation in your own words. Code without interpretation gets no credit; the interpretation is the part that will be on the exam.

19 Appendix: rebuilding the dataset with real market data

The file build_workshop2_data.py (posted in Canvas) regenerates the three CSV files from live sources. Run it once from the same folder as this document:

pip install yfinance pandas-datareader
python build_workshop2_data.py

It downloads:

  • monthly adjusted prices of the 28 series from Yahoo Finance (yfinance);
  • the Fama-French 5 factors and the Momentum factor from Kenneth French’s data library (pandas-datareader);
  • the US 3-month T-Bill rate (TB3MS) and the Mexican 3-month interbank rate (IR3TIB01MXM156N) from FRED (pandas-datareader);

and writes hf2_prices_monthly.csv, hf2_factors_monthly.csv and hf2_rates_monthly.csv with exactly the same column names used above, so every chunk in this workshop runs unchanged. To extend the sample, change END in the script to the first day of the month after the last complete month.

19.0.1 References

  • Bodie, Z., Kane, A. and Marcus, A. Investments, 11th edition, McGraw-Hill. Chapters 24 (performance evaluation, style analysis, market timing), 25 (international diversification and interest rate parity), 26 (hedge funds), 27 (Treynor-Black and Black-Litterman) and 28 (investment policy).
  • Black, F. and Litterman, R. (1992). Global Portfolio Optimization. Financial Analysts Journal, 48(5), 28–43.
  • Getmansky, M., Lo, A. and Makarov, I. (2004). An Econometric Model of Serial Correlation and Illiquidity in Hedge Fund Returns. Journal of Financial Economics, 74, 529–609.
  • Hasanhodzic, J. and Lo, A. (2007). Can Hedge-Fund Returns Be Replicated?: The Linear Case. Journal of Investment Management, 5(2), 5–45.
  • Lo, A. (2002). The Statistics of Sharpe Ratios. Financial Analysts Journal, 58(4), 36–52.
  • Sharpe, W. (1992). Asset Allocation: Management Style and Performance Measurement. Journal of Portfolio Management, 18(2), 7–19.