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
import requests
import io
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)Workshop 2 - Hedge Funds
M6 – Hedge Funds
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.
In Workshop 1 we estimated the CAPM (and the factor models) with cc (log) returns, and in the first hedge example we built the long leg of the position as a weighted average of cc returns. From now on we will use simple returns for these calculations. The reasons are the following:
- A portfolio return is a weighted average of simple returns, R_{P}=\sum_i w_i R_i. With log returns this is not true: r_P=\ln\left(\sum_i w_i e^{r_i}\right)\neq\sum_i w_i r_i. Every long/short leg, hedge or portfolio must be built with simple returns.
- By convention, the CAPM for Portfolios is estimated with simple returns. In addition, the CAPM is a model of simple returns. If we run it with log returns, the beta barely changes, but the alpha is biased downwards by approximately half the residual variance: \alpha_{log}\approx\alpha-\frac{1}{2}\sigma^2_e+\frac{1}{2}\beta(1-\beta)\sigma^2_M. For a volatile stock with a residual volatility of 40% per year, that is about -8\% of fake alpha per year, even if the true alpha is zero. And alpha is exactly what we evaluate in hedge funds.
- The Sharpe ratio is defined with the arithmetic mean of simple excess returns. With log returns it is lower by roughly \frac{1}{2}\sigma (annual volatility), which unfairly penalizes volatile strategies.
Log returns keep an important role: they add up over time, so we use them to aggregate returns across periods (cumulative value, the compound annual return e^{12\bar r}-1) and for time-series tests on a single log price. In short: across assets and for performance measures, simple returns; across time, cc returns.
What this means for Workshop 1: the betas and hedge ratios you estimated are practically the same with either type of return; the alphas were somewhat understated, more so for volatile stocks. You do not need to redo Workshop 1. Part 0 below shows the difference with real data.
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:
- A hedge is a negative weight.
- Alpha is model-dependent.
- 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:
- What does this fund really do? Strategies and returns-based style analysis (Part I).
- 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).
- How does a global macro manager think? The Mexican peso carry trade and currency hedging for a Mexican investor (Part III).
- What could be going on, other than skill, behind a high Sharpe ratio? A digest of non-linear payoffs, tail risk, smoothed returns, database biases and fees (Part IV).
- How much should I allocate? The Treynor-Black and Black-Litterman models, inside an investment policy framework (Part V).
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 | Warm-up (Part 0); hedge ratios with futures (Part II); up/down betas (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 V) |
| Sharpe ratio | Why it is inflated by option selling and by smoothed returns (Part IV) |
| VaR, ES, drawdowns | Warm-up (Part 0); tail risk of carry trades and put writing (Part IV) |
| Tangency portfolio and its fragility | Black-Litterman as the practical fix (Part V) |
In the Reto you might 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 IV give you the tools to evaluate a hedge fund honestly; Part V 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 workshop is covered in 7 sessions of 1 hour 40 minutes. The first one is a review of the midterm exam plus the warm-up of Part 0; after the workshop there is one session for review and one for the final exam.
| Session | Sections | Topics |
|---|---|---|
| 1 | Midterm review + Part 0 | Exam feedback and the Workshop 1 toolbox in code: cc vs simple returns, CAPM, FF3, performance measures, VaR/ES |
| 2 | Part I | Hedge fund strategies, access from Mexico, liquid alternatives, 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 | The MXN carry trade, covered interest parity and currency hedging — Challenge 3 |
| 6 | Part IV (digest) + Part V | Non-linear payoffs, tail risk, smoothed returns, biases and fees; Treynor-Black |
| 7 | Part V | Black-Litterman and the family office case — Challenge 4 |
4 Setup: packages and data
4.1 Libraries
We use the same libraries as in Workshop 1:
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: funds that run hedge-fund strategies inside a regulated wrapper, with daily liquidity and no lock-up (the “blurring frontier” we mentioned in Workshop 1). They are our window into the hedge fund world. Two things are worth knowing about them before we use them:
- Only four of them are ETFs (
QAI,HDG,MNA,BTAL), which trade on an exchange during the day. The other four (MERFX,QMNIX,AQMIX,QSPIX) are open-end mutual funds registered in the US: they are bought and redeemed once a day at their net asset value (NAV). - They belong to two different groups. The replicators (
QAI,HDG) try to imitate the average hedge fund. The strategy funds (the other six) run one specific hedge fund strategy.
Replicators of the “average hedge fund”
QAI— NYLIM Hedge Multi-Strategy Tracker ETF (formerly IQ Hedge Multi-Strategy Tracker; New York Life Investment Management). It tracks an index that tries to reproduce the risk-return characteristics of the hedge fund universe (long/short equity, global macro, market neutral, event driven, fixed-income arbitrage, emerging markets). It does not buy hedge funds: it invests in other ETFs of stocks, bonds, currencies and commodities, with weights chosen by a replication model. In the issuer’s words, it replicates the “beta” portion of hedge fund returns, the part unrelated to manager skill.HDG— ProShares Hedge Replication ETF. It tracks the Merrill Lynch Factor Model – Exchange Series, which replicates the HFRI Fund Weighted Composite Index (a broad hedge fund index) with a rolling regression on six liquid factors: the S&P 500, the Russell 2000, MSCI EAFE, MSCI Emerging Markets, a short position in the euro against the dollar, and 3-month T-Bills. The weights are updated every month and the exposures are taken with futures and swaps, keeping a large part of the fund in T-Bills. It is literally the “clone” of Part I turned into a product — and our style analysis will recover almost exactly these factors.
Funds that run one specific strategy
MNA— NYLIM Merger Arbitrage ETF (formerly IQ Merger Arbitrage). An index ETF that buys global companies that are targets of announced takeovers, and earns the spread between the market price and the offer price if the deal closes. When the deal is paid in stock, the index takes a short position in the acquirer’s stock. It is a passive, rules-based version of merger arbitrage.MERFX— The Merger Fund (Class A; the institutional class isMERIX). A mutual fund created by Westchester Capital Management in 1989, today part of Virtus: the oldest merger arbitrage fund. UnlikeMNA, it is actively managed: the managers choose which deals to take, buy the targets, short the acquirers in stock deals and use options to hedge.MNAvsMERFXis a nice comparison: the same strategy, one passive and one active.BTAL— AGF U.S. Market Neutral Anti-Beta Fund (an ETF). It tracks the Dow Jones U.S. Thematic Market Neutral Low Beta Index: among large US stocks it goes long the lowest-beta stocks and short the highest-beta stocks (beta estimated with 52 weeks of weekly returns), with the same amount of money on each side and neutral by sector. Because the short leg has more beta than the long leg, the net position has a negative beta: it gains when the market falls, which is why it is used as a portfolio hedge. It also harvests the low-beta anomaly: defensive stocks have historically earned more than the CAPM predicts.QMNIX— AQR Equity Market Neutral Fund (Class I). A quantitative long/short stock fund that targets zero market beta, neutral by sector and country. It selects stocks with hundreds of signals grouped in factors such as value, momentum and quality/defensive. We will see that it loads onHML,RMWandMOMand is shortSMB; its drawdown of almost 40% in 2018–2020 coincides with the worst crisis of the value factor in decades.AQMIX— AQR Managed Futures Strategy Fund (Class I). A trend following (time-series momentum) fund: it trades several hundred futures and forwards on equity indexes, bonds, currencies and commodities, going long what has been rising and short what has been falling, and sizing positions by volatility. Most of its capital sits in cash as collateral. It emulates CTAs / managed futures funds, whose appeal is crisis alpha: they tend to gain in long market declines.QSPIX— AQR Style Premia Alternative Fund (Class I). A market-neutral multi-asset long/short fund that harvests four alternative risk premia — value, momentum, carry and defensive — in individual stocks and industries, equity indexes, bonds, currencies and commodities. It is the “factor hedge fund” in a package: the idea that much of what hedge funds charge for are style premia that can be earned systematically and more cheaply.
| Ticker | Fund | Vehicle | Management | What it emulates | Inception | Expense ratio (net) |
|---|---|---|---|---|---|---|
QAI |
NYLIM Hedge Multi-Strategy Tracker | ETF | Index (replication model) | Multi-strategy hedge fund industry | 2009 | 0.81% |
HDG |
ProShares Hedge Replication | ETF | Index (factor model) | HFRI Fund Weighted Composite Index | 2011 | 0.95% (gross 2.26%) |
MNA |
NYLIM Merger Arbitrage | ETF | Index | Merger arbitrage | 2009 | 0.78% |
MERFX |
The Merger Fund (Class A) | Mutual fund | Active | Merger arbitrage | 1989 | 1.55% (1.46% excl. short-sale costs) + up to 5.5% sales load |
BTAL |
AGF U.S. Market Neutral Anti-Beta | ETF | Index | Equity market neutral (anti-beta) | 2011 | 1.40% (0.45% excl. short-sale costs) |
QMNIX |
AQR Equity Market Neutral (Class I) | Mutual fund | Quantitative | Multi-factor equity market neutral | 2014 | 6.03% (1.34% excl. short-sale and interest costs) |
AQMIX |
AQR Managed Futures Strategy (Class I) | Mutual fund | Quantitative | Managed futures / trend following | 2010 | 3.09% (1.25% excl. interest and short-sale costs) |
QSPIX |
AQR Style Premia Alternative (Class I) | Mutual fund | Quantitative | Multi-asset alternative risk premia | 2013 | 6.13% (1.53% excl. short-sale and interest costs) |
The expense ratios come from the issuers’ websites and fact sheets (and ETFdb for QAI and MNA), as consulted in September 2026. They change every year, so always check the current prospectus. Three remarks:
- The large ratios of the AQR funds (3%–6%) are not management fees. For funds that short, the reported expense ratio includes the dividends paid on stocks sold short and the interest on borrowings: an economic cost of running the long/short strategy that the fund pays out of its assets. The comparable “cost of the manager” is the ratio that excludes them (about 1.3%–1.5%; AQR’s management fee is about 1.1% for
QMNIXand 1.05% forAQMIX). - All the prices we use are already net of these expenses (and of trading costs), so the returns in this workshop are what an investor in the fund received. They do not include the sales load of
MERFXClass A. - Compare these numbers with the “2 and 20” of a private hedge fund (Part IV): a liquid alternative costs roughly 1%–1.5% per year for the manager, with no incentive fee. Even so, a replicator such as
HDGcharges 0.95% for exposures that its own clone gets for a few basis points (Part I).
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)
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.
4.3 Loading the data
I downloaded the datasets of the workshop and uploaded them in my website (apradie.com)
We download them directly from apradie.com:
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}
try:
# Donwloading the price dataset:
url = "http://www.apradie.com/datos/hf2_prices_monthly.csv"
response = requests.get(url, headers=headers)
response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)
# Read the content into a pandas DataFrame using io.StringIO
prices = pd.read_csv(io.StringIO(response.text), index_col="Date", parse_dates=True )
print("File 'hf2_prices_monthly.csv' downloaded successfully.")
# Downloading the factors dataset:
url = "http://www.apradie.com/datos/hf2_factors_monthly.csv"
response = requests.get(url, headers=headers)
response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)
# Read the content into a pandas DataFrame using io.StringIO
ff = pd.read_csv(io.StringIO(response.text), index_col="Date", parse_dates=True )
print("File 'hf2_factors_monthly.csv' downloaded successfully.")
# Downloading the rates dataset:
url = "http://www.apradie.com/datos/hf2_rates_monthly.csv"
response = requests.get(url, headers=headers)
response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)
# Read the content into a pandas DataFrame using io.StringIO
rates = pd.read_csv(io.StringIO(response.text), index_col="Date", parse_dates=True )
print("File 'hf2_rates_monthly.csv' downloaded successfully.")
except requests.exceptions.HTTPError as e:
print(f"HTTP Error occurred: {e}")
except requests.exceptions.RequestException as e:
print(f"An error occurred during the request: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")File 'hf2_prices_monthly.csv' downloaded successfully.
File 'hf2_factors_monthly.csv' downloaded successfully.
File 'hf2_rates_monthly.csv' downloaded successfully.
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
IPCandSPX, 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.VIXis 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.USDMXNis 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
As explained at the beginning of this workshop, we now follow this rule:
- Simple returns for everything that combines assets or measures performance: portfolios, hedges and long/short legs, CAPM and factor regressions (alpha and beta), style analysis, Sharpe, Treynor, Information ratio, VaR/ES and fees. The risk-free rate we subtract is also a simple monthly rate, and the Fama-French factors are simple returns too.
- cc (log) returns only to aggregate over time: cumulative value of an investment and the compound annual return (e^{12\bar r}-1, which is identical to compounding the simple returns).
To annualize monthly numbers we follow the usual convention: means (and alphas) \times 12, volatilities \times\sqrt{12}, so a monthly Sharpe ratio is multiplied by \sqrt{12}. The realized annual return of an investment is the compound annual growth rate, \left[\prod_t(1+R_t)\right]^{12/T}-1.
r = np.log(prices.drop(columns="VIX")).diff().dropna() # cc returns (aggregation over time)
Rs = prices.drop(columns="VIX").pct_change().dropna() # simple returns (everything else)
# 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)
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 functions (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 by itself is not a measure of 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_s, data=ff):
"""OLS of the premium return of a SIMPLE return series on a set of factors
(the Fama-French factors are simple returns too)."""
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):
"""Maximum drawdown of a monthly SIMPLE return series."""
wealth = (1 + ret).cumprod() # value of $1 over time
return (wealth / wealth.cummax() - 1).min()
def hf_stats(ret, rf=rf_s, mkt=Rs["SPY"]):
"""Summary statistics of a monthly SIMPLE return series."""
rf, mkt = rf.loc[ret.index], mkt.loc[ret.index] # same months
ex = ret - rf # simple premium return
beta = np.cov(ret, mkt)[0, 1] / mkt.var()
return pd.Series({
"Ann. return": (1 + ret).prod()**(12/len(ret)) - 1, # compound annual return
"Ann. vol": ret.std() * np.sqrt(12),
"Sharpe": ex.mean() / ex.std() * np.sqrt(12),
"Beta": beta, # beta with respect to SPY
"Corr": ret.corr(mkt), # correlation with SPY
"Skew": stats.skew(ret), # skewness
"Ex. kurt": stats.kurtosis(ret), # excess kurtosis
"AC(1)": ret.autocorr(1), # first-order autocorrelation
"Max DD": max_drawdown(ret)}) # maximum drawdown5 PART 0 — Warm-up: the Workshop 1 toolbox, in code
The midterm exam showed a clear pattern: the concepts are in place, but the calculations are not yet automatic. This part is a guided workout. Everything here comes from Workshop 1; the only new thing is that we now run it on the Workshop 2 dataset. Type the code, run it, and make sure you can explain every number out loud. If you can do this part without help, you are ready for the rest of the workshop.
5.1 Simple returns and cc returns: same information, different uses
f = "QMNIX" # AQR Equity Market Neutral
check = pd.DataFrame({"Simple return": Rs[f], "cc return": r[f]}).tail(3)
print(check.round(4)) Simple return cc return
Date
2026-05-31 0.0060 0.0060
2026-06-30 -0.0248 -0.0251
2026-07-31 0.0316 0.0311
print(f"\nAnnual return from cc returns : {np.exp(12*r[f].mean()) - 1:.4f}")
Annual return from cc returns : 0.0691
print(f"Annual return by compounding : {(1 + Rs[f]).prod()**(12/len(Rs)) - 1:.4f}")Annual return by compounding : 0.0691
print(f"Annual volatility (cc returns) : {r[f].std()*np.sqrt(12):.4f}")Annual volatility (cc returns) : 0.0963
Both routes to the annual return agree, as they must: this is where cc returns are useful, because they add up over time. Remember the rule: cc returns to aggregate over time; simple returns for portfolios, regressions and performance measures.
5.2 The CAPM regression, line by line
We regress the premium return of the fund on the premium return of the market:
R_{i,t}-R_{f,t}=\alpha_i+\beta_i\left(R_{M,t}-R_{f,t}\right)+e_{i,t}
mkt_ex = Rs["SPY"] - rf_s # market premium (simple returns)
capm = smf.ols("y ~ x", data=pd.DataFrame({"y": Rs[f] - rf_s, "x": mkt_ex})).fit()
print(capm.summary().tables[1])==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
Intercept 0.0055 0.002 2.273 0.025 0.001 0.010
x -0.1146 0.054 -2.112 0.037 -0.222 -0.007
==============================================================================
print(f"\nalpha (monthly) = {capm.params.iloc[0]:.4f} -> annual = {12*capm.params.iloc[0]:.4f}")
alpha (monthly) = 0.0055 -> annual = 0.0656
print(f"beta = {capm.params.iloc[1]:.4f} (t = {capm.tvalues.iloc[1]:.2f})")beta = -0.1146 (t = -2.11)
print(f"R-squared = {capm.rsquared:.4f}")R-squared = 0.0315
print(f"Residual (non-systematic) volatility, annual = {capm.resid.std()*np.sqrt(12):.4f}")Residual (non-systematic) volatility, annual = 0.0953
Four numbers you must be able to read:
- alpha: 0.55% per month, or about 6.6% per year. The intercept is monthly, so it must be multiplied by 12 before you call it an annual alpha. Its t-statistic is 2.27: borderline significant.
- beta: -0.11, statistically different from zero but tiny. This is a market-neutral fund: its return barely depends on the direction of the market.
- R-squared: 0.03. The market explains 3% of this fund’s variance. The other 97% is non-systematic risk.
- Residual volatility \sigma(e): this is the denominator of the appraisal ratio (see the next sections) and of the Treynor-Black weights (Part V).
5.3 Why the CAPM uses simple returns: the evidence
In Workshop 1 we ran the CAPM with cc returns. Let’s run the same regression with both types of return for five series, from a market-neutral fund to a very volatile stock (SCCO, Southern Copper), and compare the difference in alphas with the approximation \alpha-\alpha_{log}\approx\frac{1}{2}\sigma^2_e-\frac{1}{2}\beta(1-\beta)\sigma^2_M:
rf_cc = np.log(1 + rf_s) # monthly cc risk-free rate (only for this comparison)
rows = {}
for s in ["QMNIX", "BTAL", "GLD", "AMX", "SCCO"]:
m_s = sm.OLS(Rs[s] - rf_s, sm.add_constant(Rs["SPY"] - rf_s)).fit() # simple returns
m_c = sm.OLS(r[s] - rf_cc, sm.add_constant(r["SPY"] - rf_cc)).fit() # cc returns
b, s2e, s2m = m_s.params.iloc[1], m_s.resid.var(), mkt_ex.var()
rows[s] = {"beta (simple)": b, "beta (cc)": m_c.params.iloc[1],
"alpha ann (simple)": 12*m_s.params.iloc[0],
"alpha ann (cc)": 12*m_c.params.iloc[0],
"difference": 12*(m_s.params.iloc[0] - m_c.params.iloc[0]),
"approximation": 12*(0.5*s2e - 0.5*b*(1 - b)*s2m),
"resid. vol (ann)": np.sqrt(12*s2e)}
pd.DataFrame(rows).T.round(4) beta (simple) beta (cc) alpha ann (simple) alpha ann (cc) difference approximation resid. vol (ann)
QMNIX -0.1146 -0.1101 0.0656 0.0589 0.0066 0.0060 0.0953
BTAL -0.6295 -0.6320 0.0350 0.0157 0.0193 0.0192 0.1237
GLD 0.0690 0.0702 0.0865 0.0749 0.0116 0.0112 0.1542
AMX 0.7216 0.7339 -0.0315 -0.0613 0.0298 0.0300 0.2539
SCCO 1.0874 1.0918 0.1082 0.0614 0.0468 0.0483 0.3072
The betas are practically identical, but the alpha with cc returns is always lower, and the gap grows with the residual volatility: less than 1% per year for QMNIX, but almost 5% per year for SCCO, whose CAPM alpha falls from about 10.8% to 6.1% just by changing the type of return. The approximation explains the gap almost exactly. The cc alpha is not “more conservative”: it is biased, and it penalizes idiosyncratic volatility, which the CAPM says should not affect alpha.
5.4 From one factor to three
ff3 = factor_model(Rs[f], ("Mkt_RF", "SMB", "HML"))
print(ff3.summary().tables[1])==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
Intercept 0.0045 0.002 2.244 0.026 0.001 0.008
Mkt_RF -0.0602 0.046 -1.312 0.192 -0.151 0.031
SMB -0.3324 0.075 -4.408 0.000 -0.482 -0.183
HML 0.4353 0.057 7.611 0.000 0.322 0.548
==============================================================================
print(f"\nalpha (annual) = {12*ff3.params['Intercept']:.4f} R2 = {ff3.rsquared:.4f}")
alpha (annual) = 0.0541 R2 = 0.3404
Compare with the CAPM: the alpha falls from about 6.6% to 5.4% per year and the R^2 jumps from 0.03 to about 0.34. The fund has a negative loading on SMB (-0.33, t\approx-4.4: it is short small caps / long large caps) and a positive loading on HML (+0.44, t\approx7.6: it is long value stocks). Part of what looked like alpha under the CAPM was exposure to value and size, which any investor can buy cheaply. This is the single most important idea of Workshop 1, and the starting point of Workshop 2.
5.5 Performance measures: compute them, then rank
Before computing them, note that there are two versions of the Information ratio, and they answer different questions:
IR=\frac{\overline{R_P-R_B}}{\sigma\left(R_P-R_B\right)} \qquad\qquad AR=\frac{\alpha_P}{\sigma\left(e_P\right)}
- The Information ratio relative to a benchmark (IR) divides the average active return R_P-R_B by its standard deviation, the tracking error TE=\sigma(R_P-R_B). Use it when the fund replaces a benchmark (a long-only fund benchmarked to the S&P 500 or the IPC, a mandate with a tracking-error budget).
- The appraisal ratio (AR) divides Jensen’s alpha by the residual (non-systematic) volatility \sigma(e_P) of the CAPM regression R_P-R_f=\alpha_P+\beta_P(R_M-R_f)+e_P. Bodie, Kane & Marcus call this ratio the information ratio. Use it when the fund is added to a passive portfolio whose market exposure can be adjusted separately: this is the typical case of a hedge fund, and it is the measure behind Treynor-Black (Part V). With a multi-factor model, use the alpha and the residual of that model.
Both are annualized by multiplying the monthly ratio by \sqrt{12}. They coincide only when \beta_P=1. With the market as benchmark, R_P-R_M=\alpha_P+(\beta_P-1)(R_M-R_f)+e_P, so
TE^2=(\beta_P-1)^2\sigma^2_M+\sigma^2(e_P)\;\geq\;\sigma^2(e_P)
The tracking error counts a beta different from one as risk relative to the benchmark; the residual volatility does not.
ex, ex_m = Rs[f] - rf_s, Rs["SPY"] - rf_s # premium returns (simple)
sharpe = ex.mean() / ex.std() * np.sqrt(12) # monthly Sharpe x sqrt(12)
sharpe_m = ex_m.mean() / ex_m.std() * np.sqrt(12)
active = Rs[f] - Rs["SPY"] # active return (simple)
print(pd.Series({
"Sharpe (fund)": sharpe,
"Sharpe (market)": sharpe_m,
"Treynor (fund)": ex.mean()*12 / capm.params.iloc[1],
"Jensen alpha (ann)": 12*capm.params.iloc[0],
"Info ratio (vs SPY)": active.mean() / active.std() * np.sqrt(12),
"Tracking error (ann)": active.std() * np.sqrt(12),
"Appraisal ratio": capm.params.iloc[0] / capm.resid.std() * np.sqrt(12),
"M2": (sharpe - sharpe_m) * ex_m.std()*np.sqrt(12)}).round(4))Sharpe (fund) 0.5352
Sharpe (market) 0.7996
Treynor (fund) -0.4522
Jensen alpha (ann) 0.0656
Info ratio (vs SPY) -0.3540
Tracking error (ann) 0.1924
Appraisal ratio 0.6881
M2 -0.0397
dtype: float64
Look at the Treynor ratio: it is large and negative, because the beta in the denominator is almost zero and negative. For a market-neutral fund the Treynor ratio is meaningless. And the two information ratios have opposite signs. The Information ratio vs SPY is negative because it compares a fund with a beta close to zero against the stock market in a bull market: most of its tracking error (about 19% per year) comes from not having market beta, not from stock selection. The appraisal ratio is positive (about 0.7): per unit of residual risk (about 9.5% per year), the fund earned a Jensen’s alpha of about 6.6% per year. All the numbers are “correct”, and all are useless unless you say what the fund is being compared to. The right measure depends on the role of the fund in the portfolio (Workshop 1, Part III): for a hedge fund that is added to a portfolio, the appraisal ratio is the relevant one.
5.6 VaR and Expected Shortfall, with the convention written down
Half of the mistakes with VaR are not about statistics; they are about conventions. Write yours down every time:
- VaR is computed with simple returns, so it is a loss in percent of the value of the position.
- VaR is reported as a positive loss number (we report -q).
- At 95% we use the 5th percentile of the return distribution.
- The Expected Shortfall is the arithmetic mean of the returns at or below that percentile (not the median).
x = Rs[f] # simple returns: a loss in % of the value
# Calculate the 5 percentile:
q95 = np.quantile(x, 0.05)
# Get those periods where returns is less than the 5 percentile:
tail_obs = x[x <= q95]
print(f"5th percentile (monthly simple return): {q95:.4f}")5th percentile (monthly simple return): -0.0360
print(f"Historical VaR 95% (monthly loss) : {-q95:.4f}")Historical VaR 95% (monthly loss) : 0.0360
print(f"Historical ES 95% (monthly loss) : {-tail_obs.mean():.4f}")Historical ES 95% (monthly loss) : 0.0485
print(f"Observations in the tail : {len(tail_obs)} of {len(x)}")Observations in the tail : 7 of 139
print(f"Parametric (normal) VaR 95% : {-(x.mean() + stats.norm.ppf(0.05)*x.std()):.4f}")Parametric (normal) VaR 95% : 0.0403
The ES is always larger than the VaR: it is the average of the worst losses, and the VaR is only the threshold where they start.
Answer these on paper before we move on. If you can, the rest of the workshop will be much easier.
- A fund has a monthly alpha of 0.004 with a standard error of 0.002. What is the annual alpha, and is it significant?
- A fund has \beta=0.05 and an average premium return of 4% per year. What is its Treynor ratio, and why should you not use it?
- Two funds have the same Sharpe ratio, but one has a skewness of -2 and the other of 0. Which one would you rather own?
- With 139 monthly returns, how many observations are in the 95% tail, and why does that matter for the precision of your ES?
6 PART I — What hedge funds really do: strategies and style analysis
6.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).
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.
6.2 A first look at the hedge fund proxies
HF = ["QAI", "HDG", "MNA", "MERFX", "BTAL", "QMNIX", "AQMIX", "QSPIX"]
summary = pd.DataFrame({k: hf_stats(Rs[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.8000 1.0000 1.0000 -0.3340 0.4780 -0.1400 -0.2390
IEF 0.0110 0.0650 -0.1060 0.0460 0.1080 0.0190 0.1410 0.0180 -0.2320
QAI 0.0320 0.0530 0.2520 0.3090 0.8710 -0.3020 1.3770 -0.0370 -0.1380
HDG 0.0330 0.0580 0.2500 0.3390 0.8800 -0.3500 1.7630 -0.0510 -0.1410
MNA 0.0280 0.0460 0.1810 0.1080 0.3520 -1.8380 12.5180 -0.0790 -0.0950
MERFX 0.0350 0.0280 0.5270 0.0750 0.4060 -0.6740 1.6490 -0.0480 -0.0410
BTAL -0.0320 0.1550 -0.2600 -0.6280 -0.6070 -0.1910 0.4560 0.0150 -0.5060
QMNIX 0.0690 0.0970 0.5350 -0.1110 -0.1710 0.5070 1.0870 0.2240 -0.3840
AQMIX 0.0420 0.1030 0.2540 -0.1930 -0.2810 0.1640 -0.2830 0.0550 -0.2440
QSPIX 0.0750 0.1290 0.4700 -0.0220 -0.0260 0.8640 1.8500 0.0940 -0.3960
# Aggregation over time: adding up cc returns = compounding simple returns
growth = np.exp(r[["SPY"] + HF].cumsum()) # identical to (1 + Rs[["SPY"] + HF]).cumprod()
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
SPYin raw return, and even the best two in Sharpe ratio (MERFXandQMNIX, about 0.53) reach only two thirds of the Sharpe ratio ofSPY(0.80). 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.6%) but a skewness of almost -2 and an excess kurtosis above 12. 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). QMNIXhas 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 the Part IV digest.QAIandHDGhave a correlation of almost 0.9 withSPY. 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.
6.3 Returns-based style analysis
6.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.
6.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
# We optimize by finding the minimum tracking error with constraints
# start with equally weighted, and indicate using the SLSQP optimization method
res = optimize.minimize(te, np.repeat(1/K, K), method="SLSQP",
# indicate that weights > = 0 and less than 1
bounds=[(0, 1)] * K,
# indicate that sum of weights = 1
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
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. We run a for loop and call the style_analysis function for each column return:
styles = {}
# We estimate the style analysis for each return series in HF:
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:
QAIandHDGare 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
CASHandUUP(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.
BTALis short high-beta stocks, so it moves against the market; a portfolio with non-negative weights can never produce that.
6.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}
CASH style in the unconstrained model?
In the constrained model we added CASH because the weights must add up to one: without it, the optimizer would be forced to invest 100% in risky assets even if the fund keeps half of its money in cash or collateral. In the unconstrained model cash is already there, implicitly:
- The regression uses premium returns. The premium return of cash is R_f-R_f=0 every month, so a
CASHcolumn would be a column of zeros: it has no variance and OLS cannot estimate its coefficient. - Moving R_f to the right-hand side of the equation shows where the cash is: R_{F,t}=\alpha_F+\sum_{k=1}^{K}\beta_{F,k}R_{k,t}+\underbrace{\left(1-\sum_{k=1}^{K}\beta_{F,k}\right)}_{\text{implied cash}}R_{f,t}+e_{F,t} The loadings \sum_k\beta_{F,k} are the net exposure to the styles, and 1-\sum_k\beta_{F,k} is the implied cash position. It is positive when the fund keeps cash or collateral (including the proceeds of its short sales), and negative when it borrows (leverage). This is why the loadings do not need to add up to one.
- And if we ran the regression with raw returns and added R_f as one more regressor? The T-Bill rate barely moves from month to month, so it is almost collinear with the intercept: OLS could not separate the alpha from the cash weight, and both would be very imprecise.
We define a function to estimate the style of a hedge fund where there is no constraint, so we run an OLS regression, which minimizes the sum of squared errors:
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)
# the sm.OLS function needs to add a 1 column to the X variables
# the sm.OLS and the sm.ols function run the same regression model, but
# the sm.OLS needs a column of 1's before the X columns
return sm.OLS(d[fund], sm.add_constant(d[styles])).fit()
# Now we run the style regression for all return columns in HF:
# The results will be stored in hf_rows:
hf_rows = {}
for f in HF:
m = hf_style(f)
row = m.params.rename({"const": "alpha (monthly)"})
row["Implied cash (1 - sum of w)"] = 1 - m.params[STYLES].sum()
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
Implied cash (1 - sum of w) 0.6280 0.5650 0.9210 0.9050 0.9460 0.7890 1.3470 0.2800
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. BTALis 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 (shortIEFbut longTLT). 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). - Implied cash (1-\sum_k\beta_{F,k}): for the replicators
QAIandHDGit is about 0.6, almost the same cash weight that the constrained model found: both versions tell the same story. ForBTALit is about 0.95: its long and short legs almost cancel out (it is close to dollar-neutral), so practically all its capital sits as collateral. ForAQMIXit is above one (about 1.35): its net position is short (mainly in bonds), and the proceeds of those short positions are held in cash. A fund with an implied cash below zero would be borrowing, i.e., levered. - Alphas:
QAIandHDGhave negative and significant alphas: once you account for their styles, they lose about 1.2% per year, which is roughly their cost.QMNIXandQSPIXhave 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).
6.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.
6.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.
6.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(Rs[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.0130 -0.4930 -0.4740 -0.2400 -0.2340 0.7070 0.3530 0.2440 0.7170
QMNIX 0.0460 1.9000 -0.0450 -0.2440 0.4360 0.1790 -0.0220 0.1010 0.3680
QSPIX 0.0390 1.2090 0.0760 -0.2590 0.5430 0.2330 0.1550 0.1400 0.3720
AQMIX 0.0360 1.2110 -0.1310 -0.0410 0.0650 -0.0170 0.0120 0.1890 0.1470
Two lessons from this table:
BTALis 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 about 3.5% to 4.5% per year have t-statistics between 1.2 and 1.9: not significant at the 5% level. The factors explain the return; whatever is left is too noisy to call skill.
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 the Part IV digest to tell the difference.
7 CHALLENGE 1
Run the constrained style analysis (with cash) for
HDGand forMERFXusing only the last 60 months of the sample. Compare the weights, the R^2 and the selection return with the full-sample results. Is there style drift?Run the unconstrained style regression for
MNAusing only three styles:SPY,IEFandHYG. Interpret the three betas and the alpha. Which exposure is the economic signature of merger arbitrage, and why?Build an out-of-sample clone of
QAIas we did forHDG(train with the first 60 months). Report the out-of-sample correlation, the tracking error and the difference in annual return between the fund and its clone. Would you recommend a family office to buyQAIor its clone? Give at least one non-statistical argument.
8 PART II — Market-neutral positions, pure plays and portable alpha
8.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.
8.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).
8.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?
8.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.
8.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:
- The spread wanders far from zero and stays there for years: this is a relative value trade with no convergence date.
- 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.
8.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.
8.5 Portable alpha
8.5.1 The idea
Section 26.3 of Bodie, Kane & Marcus puts the pieces together. An investor has two separate decisions:
- 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.
- 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. Note that R_{\alpha,t} above is the total return of the market-neutral fund, so it already includes the risk-free rate:
R_{\alpha,t}=R_{f,t}+\alpha+e_t
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
A long futures position is a zero-investment position, so its return is an excess return. Buying the index in the spot market costs S_0 and earns R_M. A futures contract costs nothing up front (only margin), and its price already embeds the financing cost, F_0\approx S_0\,(1+R_f-d), where d is the dividend yield. At maturity F_T=S_T, so the gain per dollar of notional is \frac{F_T-F_0}{S_0}\approx\frac{S_T-S_0}{S_0}+d-R_f=R_M-R_f which is the same as buying the index with money borrowed at R_f. The investor does not earn R_f on the futures leg, but she does earn it on her capital, which sits in the alpha source (R_f+\alpha+e). The two R_f terms cancel and the total is R_M+\alpha+e.
8.5.2 Porting a single stock’s alpha: Grupo México on the S&P 500
Before using a hedge fund as the alpha source, let’s port the alpha we already built in the Grupo México example (Part II). Suppose now that the investor is benchmarked to the S&P 500 in dollars (a US fund, or a Mexican family office whose equity benchmark is the S&P 500), and she believes that Grupo México has alpha. She needs three steps:
- Isolate the alpha (in pesos). This is the beta-neutral position of the Grupo México example: long
GMEXICOand short \beta pesos of IPC futures. It earns R_{f,MX}+\alpha+e. - Move it into dollars. The position is in pesos, so she hedges the currency with forwards. Under covered interest parity (Part III), a peso return hedged into dollars earns approximately the peso return minus the interest-rate differential, so the position now earns R_{f,US}+\alpha+e.
- Add the US beta. She buys S&P 500 futures, which earn R_{SPY}-R_{f,US} and require no capital (only margin).
R_{PA,t}^{USD}=\underbrace{R_{GM,t}-\beta\,F_{IPC,t}}_{\text{pure play (MXN)}}-\underbrace{\frac{i_{MX}-i_{US}}{12}}_{\text{FX hedge}}+\underbrace{\left(R_{SPY,t}-R_{f,US,t}\right)}_{\text{long S\&P 500 futures}}\approx R_{SPY,t}+\alpha+e_t
Note that the two futures legs (short IPC, long S&P 500) do not use capital: the whole capital stays invested in Grupo México. We also define a small function to compare a portable alpha strategy with its benchmark; we will use it again in the next sections.
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"])
pure_mxn = hedges["Beta-neutral"] # Rf_MX + alpha + e (pesos)
pure_usd = pure_mxn - (i_mx - i_us) / 12 # FX-hedged into USD: Rf_US + alpha + e
pa_gm = pure_usd + (Rs["SPY"] - rf_s) # + long S&P 500 futures
tab = pa_report(pa_gm, Rs["SPY"])
tab["Beta (SPY)"] = [1, np.cov(pa_gm, Rs["SPY"])[0, 1] / Rs["SPY"].var()]
print(tab.round(4)) Annual return Annual vol Tracking error Info ratio Beta (SPY)
Benchmark 0.1364 0.1502 0.0000 NaN 1.0000
Portable alpha 0.2802 0.2925 0.2336 0.6465 1.1865
act_gm = pa_gm - Rs["SPY"]
roll12_gm = (1 + act_gm).rolling(12).apply(np.prod, raw=True) - 1
roll12_gm.plot(figsize=(12, 4),
title="Grupo Mexico alpha ported to the S&P 500: 12-month return over SPY")
plt.axhline(0, color="black", lw=0.8)
plt.grid(alpha=0.3)
plt.show()At first sight the strategy is a success: it earned about 28% per year against 14% for the S&P 500, with an Information ratio of about 0.65. Read the rest of the table and the plot before getting excited:
- The tracking error (about 23% per year) is larger than the volatility of the S&P 500 itself (about 15%). The active return \alpha+e carries all the idiosyncratic risk of one company. The 12-month return over the S&P 500 goes from about -18\% to more than +100\%: in 2018 and again in 2021–2022 the investor lagged her benchmark, and nothing guaranteed that the good years would come.
- Neutral to the IPC is not neutral to the S&P 500. The beta with respect to the S&P 500 is about 1.2, not 1: the pure play still carries copper and dollar exposure (see Market-neutral is not factor-neutral), and that exposure is correlated with the US market. The investor holds more US market risk than she wanted.
- The alpha is measured ex post. The beta was estimated with the same sample, and the roughly 15% per year of alpha is what Grupo México happened to deliver in 2015–2026. It is one realized path of one company, not evidence of skill.
- The FX hedge has a cost. The pure play earned about 22% per year in pesos but about 15% once hedged into dollars: the difference is the interest-rate differential between Mexico and the US.
Portable alpha only works if the investor can live with the tracking error, because the committee judges her relative to the benchmark. With a single stock the residual e is huge. If instead the alpha source holds N pure plays with similar alphas and independent residuals of volatility \sigma_e each (equal weights), the alpha stays the same but the residual risk falls:
\alpha_P\approx\alpha \qquad\qquad \sigma(e_P)=\frac{\sigma_e}{\sqrt{N}} \qquad\Longrightarrow\qquad IR_P=\frac{\alpha_P}{\sigma(e_P)}\approx\sqrt{N}\,\frac{\alpha}{\sigma_e}
This is exactly what an equity market-neutral hedge fund does: it holds hundreds of long/short positions, so the company-specific risk diversifies away and what is left is (hopefully) the manager’s alpha. That is why in practice the alpha source of a portable alpha program is a diversified fund, not a stock. In the next section we replace Grupo México with such a fund.
8.5.3 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)
# pa_report() was defined in the Grupo Mexico example above
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", "")Compare this table with the Grupo México example. With a diversified market-neutral fund as the alpha source, the tracking error falls from about 23% to about 10% per year: diversification removed most of the company-specific risk, and the volatility of the strategy is now close to that of the S&P 500. (In this sample Grupo México even shows a higher Information ratio, but that is the realized path of one company; nobody could count on it ex ante.) Diversification, however, does not remove every risk: as we will see next, the alpha of the fund itself can disappear for several years.
8.5.4 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.
9 CHALLENGE 2
Repeat the analysis of the three neutralities for América Móvil (
AMXB): estimate its beta against the IPC futures, build the dollar-neutral and beta-neutral positions, report the table, and then regress the beta-neutral position onIPC_fut,USDMXN,DBCandSPY. Is the dollar-neutral position over- or under-hedged? Is the beta-neutral position factor-neutral? Compare with Grupo México and explain the difference economically.A Mexican fund holds MXN 120 million in a portfolio with a beta of 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%. Compute (i) 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.
Build a portable alpha strategy in dollars using
MERFX(merger arbitrage) instead ofQMNIXas the alpha source. Report the annual return, volatility, tracking error and Information ratio, and plot the rolling 12-month active return. Which alpha source would you prefer for a family office, and why?
10 PART III — Global macro: the Mexican peso carry trade
10.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.
10.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%
10.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:
- borrows 1 dollar at i_{US} and converts it to S_{t-1} pesos;
- invests the pesos at i_{MX} for one month;
- 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
print("\nCarry trade statistics (excess return, no capital):")
Carry trade statistics (excess return, no capital):
ct = carry["Carry trade"] # simple excess return per dollar of notional
print(pd.Series({"Sharpe": ct.mean() / ct.std() * np.sqrt(12),
"Skewness": stats.skew(ct),
"Excess kurt.": stats.kurtosis(ct),
"Corr (SPY)": ct.corr(Rs["SPY"]),
"Max drawdown": max_drawdown(ct)}).round(3))Sharpe 0.3860
Skewness -0.9960
Excess kurt. 4.0080
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
10.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.0 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.
10.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(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 V.
11 CHALLENGE 3
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.
Split the carry trade sample in two: 2015–2019 and 2020–2026. For each subperiod report the annualized mean, volatility, Sharpe ratio, skewness, the worst month and the average interest differential. Did the carry trade become more attractive after 2020? Was it because of the differential or because of the exchange rate?
Repeat the currency-hedging comparison for international developed stocks (
EFA) and for US Treasuries (IEF). For which asset does hedging reduce the volatility in pesos, and for which does it increase it? Explain with the correlation between each asset and USDMXN, and state a hedging policy for a Mexican family office in two lines.
12 PART IV — Digest: why a high Sharpe ratio is a question, not an answer
This part condenses two topics that deserve a full workshop each: non-linear risk (Chapter 26.5 and Section 24.4 of Bodie, Kane & Marcus) and the biases in hedge fund numbers (Section 26.5 and 26.6). We will cover it in one session, with fewer derivations and more interpretation. There is no Challenge for this part: there are discussion questions at the end, and the material is fair game for the final exam only at the conceptual level.
Everything here answers one question: a manager shows you a Sharpe ratio of 2 — what could be going on, other than skill?
12.1 Answer 1: the fund is selling options
Every measure of Workshop 1 comes from a straight line: the CAPM regression. That line is the wrong model for a manager who times the market (a payoff shaped like a bought call) or who sells options, explicitly or implicitly (a payoff shaped like a sold put: small gains most of the time, rare large losses). Fitting a line to a curve gives a wrong slope and a wrong intercept — and alpha is the intercept.
The simplest diagnostic lets beta differ in up and down markets:
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 with the market more than it rises with it: it behaves as if it had sold puts. The opposite (\beta^{+}>\beta^{-}) is a convex payoff, which investors like.
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)
pd.DataFrame([up_down_beta(f) for f in ["PUT", "MNA", "MERFX", "QMNIX", "AQMIX"]]).round(3) beta up beta down down - up t (down = up)
PUT 0.3650 0.7710 0.4060 5.4210
MNA 0.0350 0.1920 0.1570 2.0210
MERFX 0.0780 0.0700 -0.0080 -0.1720
QMNIX -0.2000 -0.0170 0.1830 1.0660
AQMIX -0.0960 -0.3110 -0.2150 -1.2110
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)
ax.plot(xs, np.polyval(np.polyfit(mkt_ex, y, 2), xs), color="red") # quadratic fit
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 PutWrite index (PUT, a strategy that sells S&P 500 puts every month) has a down-market beta of about 0.77 against an up-market beta of 0.37, and the difference is highly significant (t\approx5.4): textbook short put. Merger arbitrage (MNA) shows the same asymmetry, because buying an announced target is selling insurance against the deal breaking. Managed futures (AQMIX) goes the other way: its beta is more negative when the market falls (a convex payoff, the crisis alpha that makes trend followers popular as diversifiers).
The two formal tests, both with the market premium R_M-R_f on the right-hand side, are Treynor-Mazuy (add a quadratic term) and Henriksson-Merton (let beta jump when the market is up):
R_F-R_f=a+b\left(R_M-R_f\right)+c\left(R_M-R_f\right)^2+e \qquad\qquad R_F-R_f=a+b\left(R_M-R_f\right)+c\left(R_M-R_f\right)D+e
In both, c>0 means timing ability and c<0 is the signature of selling options.
def timing_tests(fund):
d = pd.DataFrame({"y": Rs[fund] - rf_s, "m": mkt_ex})
d["m2"], d["mD"] = d["m"]**2, d["m"] * (d["m"] > 0)
tm, hm = smf.ols("y ~ m + m2", data=d).fit(), smf.ols("y ~ m + mD", data=d).fit()
capm_ = smf.ols("y ~ m", data=d).fit()
return pd.Series({"CAPM alpha (ann)": 12*capm_.params["Intercept"],
"TM alpha (ann)": 12*tm.params["Intercept"],
"TM c": tm.params["m2"], "TM t(c)": tm.tvalues["m2"],
"HM c": hm.params["mD"], "HM t(c)": hm.tvalues["mD"]}, name=fund)
pd.DataFrame([timing_tests(f) for f in ["PUT", "MNA", "AQMIX"]]).round(3) CAPM alpha (ann) TM alpha (ann) TM c TM t(c) HM c HM t(c)
PUT -0.0040 0.0420 -2.0210 -5.7540 -0.4060 -5.4210
MNA -0.0050 0.0180 -0.9920 -2.7280 -0.1570 -2.0210
AQMIX 0.0500 0.0310 0.8010 0.9470 0.2150 1.2110
Read the PUT row from left to right: its CAPM alpha is about zero, but once the regression allows for curvature the timing coefficient is strongly negative and the alpha jumps to 4%–8% per year. That “alpha” is the option premium the strategy collects, and the negative c is the option it gave away. Neither is skill.
12.2 Answer 2: the risk has not shown up yet
Let’s build the fund that Bodie, Kane & Marcus describe: it keeps its capital in T-Bills and every month sells S&P 500 puts 5% out of the money on a notional of 4 times its capital, priced with Black-Scholes using the previous month’s VIX plus 2 volatility points (out-of-the-money puts trade at a higher implied volatility: the volatility skew).
def bs_price(S, K, T, r, sigma, q=0.0, kind="call"):
"""Black-Scholes-Merton price of a European option."""
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_OPT = 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_OPT, 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]
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")
pw = putw["ret"] # monthly simple return on capital
print(f"Months with a loss on the puts: {(putw['payoff'] > putw['premium']).sum()} 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%
pd.DataFrame({"Put writer": hf_stats(pw), "SPY": hf_stats(Rs["SPY"])}).T.round(3) Ann. return Ann. vol Sharpe Beta Corr Skew Ex. kurt AC(1) Max DD
Put writer 0.2260 0.1400 1.3990 0.6350 0.6820 -2.0060 9.3520 -0.0780 -0.2870
SPY 0.1360 0.1500 0.8000 1.0000 1.0000 -0.3340 0.4780 -0.1400 -0.2390
plot_time_series(pd.DataFrame({"Put writer (5% OTM, 4x)": (1 + pw).cumprod(),
"S&P 500 (SPY)": (1 + Rs["SPY"]).cumprod()}),
"Picking up nickels in front of a steamroller", "Value of $1", "")The Sharpe ratio is far higher than the market’s, the line looks smooth — and the skewness is -2.0 with an excess kurtosis above 9. Our sample does not contain the worst crash of the century. What would this fund have done in October 2008, when the VIX ended September at 39.4 and the S&P 500 fell 16.9% during the month?
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%
More than a third of the capital in one month: over 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. Measures that do look at the tail are the Sortino ratio (divides by the downside deviation), the maximum drawdown and the Calmar ratio, the historical VaR and ES of Workshop 1, and the Cornish-Fisher VaR, which 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)
def tail_report(ret, rf=rf_s, c=0.95):
"""Tail-risk measures of a monthly SIMPLE return series."""
rf = rf.loc[ret.index]
ex = ret - rf
downside = np.sqrt((np.minimum(ex, 0)**2).mean())
mu, sd = ret.mean(), ret.std()
S, K = stats.skew(ret), stats.kurtosis(ret)
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, 1 - c)
mdd = max_drawdown(ret)
cagr = (1 + ret).prod()**(12/len(ret)) - 1 # compound annual return
return pd.Series({"Sharpe": ex.mean()/ex.std()*np.sqrt(12),
"Sortino": ex.mean()/downside*np.sqrt(12),
"Max DD": mdd, "Calmar": cagr/abs(mdd),
"VaR normal": -(mu + z*sd), "VaR CF": -(mu + z_cf*sd),
"VaR hist": -q, "ES hist": -ret[ret <= q].mean()})
# the carry trade is an excess return; adding rf_s gives the return of a
# fully collateralized position (capital in T-Bills + carry overlay)
tails = {"SPY": Rs["SPY"], "Put writer": pw, "MNA": Rs["MNA"],
"Carry trade": carry["Carry trade"] + rf_s, "AQMIX": Rs["AQMIX"]}
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.7996 1.2720 -0.2393 0.5703 0.0597 0.0633 0.0643 0.0891
Put writer 1.3990 1.9429 -0.2867 0.7898 0.0484 0.0607 0.0531 0.1202
MNA 0.1812 0.2403 -0.0952 0.2893 0.0196 0.0224 0.0166 0.0290
Carry trade 0.3862 0.5429 -0.2260 0.2673 0.0516 0.0581 0.0485 0.0775
AQMIX 0.2543 0.3931 -0.2443 0.1703 0.0452 0.0440 0.0435 0.0535
For the short-volatility strategies (put writer, merger arbitrage, carry trade) the Cornish-Fisher VaR is above the normal VaR, and the ES is much larger than any VaR: the risk lives beyond the VaR. For AQMIX, whose returns are nearly symmetric, the three VaRs agree. And notice: 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, which is why professionals add stress tests like our October 2008 scenario.
In the late 1990s LTCM was the most admired hedge fund in the world; two of its partners won the Nobel prize. Its models said its capital was about 10 times the monthly standard deviation of its returns — enough to survive anything, if returns were normal. In August 1998 Russia defaulted, investors ran to the safest and most liquid assets, and every convergence spread the fund was betting on widened at the same time. It lost about $550 million in a single day (nine monthly standard deviations) and had to be rescued. The lesson joins the whole workshop: leverage plus liquidity mismatch plus short-volatility payoffs plus correlations that go to one in a crisis.
12.3 Answer 3: the numbers themselves are biased
Even when the strategy is honest, three mechanisms flatter the numbers a hedge fund reports.
12.3.1 Illiquid assets and smoothed returns
A fund holding illiquid assets cannot observe market prices every month; it reports model values that adjust slowly. Getmansky, Lo and Makarov (2004) model the reported return as a weighted average of current and past true returns, R^{o}_t=\theta_0R_t+\theta_1R_{t-1}+\theta_2R_{t-2}. A simulation shows what that does:
rng = np.random.default_rng(2026)
n = 100_000
mkt_true = rng.normal(0.007, 0.045, n)
fund_true = 0.003 + 0.5*mkt_true + rng.normal(0, 0.03, n)
fund_rep = np.convolve(fund_true, np.array([0.5, 0.3, 0.2]), mode="full")[:n]
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(),
"AC(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 AC(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
Same average return, volatility down by almost 40%, Sharpe ratio up in the same proportion, beta cut in half (so the CAPM alpha rises), and a large positive autocorrelation — the tell-tale sign. There is a second, more technical effect (Lo, 2002): annualizing a monthly Sharpe ratio by multiplying it by \sqrt{12} assumes that monthly returns are not autocorrelated. With positive autocorrelation, the \sqrt{12} rule overstates the annual Sharpe ratio even further. In our sample, QMNIX has an autocorrelation of 0.22: not proof of anything, but a light to check before comparing its Sharpe ratio with that of a liquid fund.
12.3.2 Survivorship and backfill bias
Funds report to commercial databases voluntarily. Bad funds close and stop reporting (survivorship bias), and new funds are added with their good incubation history attached (backfill bias). A simulation where nobody has any skill shows how large this is:
rng = np.random.default_rng(7)
n_funds, n_months = 2000, 120
mkt = rng.normal(0.006, 0.045, n_months)
ex_ret = 0.0 + 0.4*mkt + rng.normal(0, 0.035, (n_funds, n_months)) # alpha = 0
wealth = np.cumprod(1 + ex_ret, axis=1)
alive = ~((wealth < 0.75).cumsum(axis=1) > 0)[:, -1] # closes after losing 25%
good_start = wealth[:, 23] > 1.0 # 24-month incubation
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 surviving 10 years : {alive.mean():.1%}")Funds surviving 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]):+.4f}")Average annual alpha, SURVIVORS only : +0.0348
print(f"Average alpha during incubation, all : {avg_alpha(ex_ret[:, :24]):+.4f}")Average alpha during incubation, all : +0.0002
print(f"Average alpha during incubation, backfill: {avg_alpha(ex_ret[good_start, :24]):+.4f}")Average alpha during incubation, backfill: +0.1126
Published estimates of survivorship bias in hedge fund databases run from about 2% to more than 4% per year. When somebody shows you “the average hedge fund return”, ask what happened to the dead funds.
12.3.3 Fees: what the investor actually gets
The classic contract is 2 and 20: 2% of assets plus 20% of the profits above a hurdle rate, subject to a high-water mark (no incentive fee until past losses are recovered). We apply it to the monthly returns of QMNIX, treated as the gross returns of a hypothetical hedge fund:
def net_of_fees(gross, rf=rf_s, mgmt=0.02, perf=0.20, hwm=True):
"""Management fee monthly; incentive fee every December on the profit
above the hurdle (rf) and above the high-water mark."""
nav, peak, nav_start_year, hurdle_acc, out = 1.0, 1.0, 1.0, 1.0, []
for t, g in gross.items():
nav *= (1 + g)
nav -= nav * mgmt / 12
hurdle_acc *= (1 + rf[t])
if t.month == 12 or t == gross.index[-1]:
base = max(nav_start_year * hurdle_acc, peak if hwm else 0)
nav -= perf * max(nav - base, 0)
peak = max(peak, nav) if hwm else nav
nav_start_year, hurdle_acc = nav, 1.0
out.append(nav)
net = pd.Series(out, index=gross.index).pct_change()
net.iloc[0] = out[0] - 1
return net
gross = Rs["QMNIX"]
net_hwm = net_of_fees(gross)
net_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
print(f"Share of the gross profit kept by the investor: "
f"{((1+net_hwm).prod()-1)/((1+gross).prod()-1):.1%}")Share of the gross profit kept by the investor: 47.4%
The investor keeps less than half of the gross profit. And the incentive fee is a call option on the fund’s assets: the manager takes 20% of the upside and loses nothing on the downside. With Black-Scholes (Example 26.4 of Bodie, Kane & Marcus: 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"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
for sig in [0.15, 0.45]:
c = bs_price(100, 105, 1, 0.0488, sig, kind="call")
print(f" with volatility {sig:.0%}: incentive fee = {0.2*c:.2f}% of NAV") with volatility 15%: incentive fee = 1.20% of NAV
with volatility 45%: incentive fee = 3.56% of NAV
The expected cost of “2 and 20” for a fund with 30% volatility is about 4.4% of assets per year, and the fee is worth three times more in a fund with 45% volatility than in one with 15%. The manager’s compensation increases with the risk of the fund while the investor bears the losses. That is why investors insist on hurdle rates, high-water marks and clawbacks.
- A fund has a Sharpe ratio of 2.0, no losing year and a maximum drawdown of 3%. Name three checks you would run before investing, and say what each one would tell you.
- Why does a positive autocorrelation in monthly returns inflate the Sharpe ratio and reduce the estimated beta at the same time?
- Your database says the average hedge fund earned 9% per year. What two questions determine whether you can believe that number?
- Explain to a client, without formulas, why “2 and 20” can cost more than 4% per year, and why the cost grows with the fund’s volatility.
13 PART V — How much to allocate to hedge funds: Treynor-Black and Black-Litterman
13.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.
13.2 The Treynor-Black model
13.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:
- 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)}
- 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).
- 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}
- 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, in its appraisal ratio version \alpha_A/\sigma(e_A): 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 measured as an appraisal ratio, \alpha/\sigma(e), 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 appraisal ratio. (In the code below, IR_A is this ratio.)
13.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 SEcolumn: forQSPIXandAQMIXthe 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.
13.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.
13.3 The Black-Litterman model
13.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.
13.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
13.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.
13.3.3.1 Where does the naive portfolio sit? The CML decomposition
Call w_1=\frac{1}{\delta}\Sigma^{-1}\mu the vector of risky weights and w_2=1-\mathbf 1^{\top}w_1 the weight in CETES. By construction \mathbf 1^{\top}w_1+w_2=1, so this is a complete portfolio. To see where it lies, define a=\mathbf 1^{\top}\Sigma^{-1}\mu and the tangency portfolio w_T=\Sigma^{-1}\mu/a (its weights sum to 1). Then
w_1=\frac{a}{\delta}\,w_T=y\,w_T,\qquad y=\frac{a}{\delta},\qquad w_2=1-y .
The investor puts a fraction y of her wealth in the tangency portfolio and 1-y in CETES. Every such mix lies on the Capital Market Line:
E[R_p]-r_f \;=\; y\,\mu_T \;=\; \frac{\mu_T}{\sigma_T}\;\sigma_p, \qquad \sigma_p=y\,\sigma_T ,
where \mu_T=w_T^{\top}\mu and \sigma_T=\sqrt{w_T^{\top}\Sigma w_T}. The CML is determined by the inputs (\mu and \Sigma): its slope is the Sharpe ratio of the tangency portfolio. The risk aversion \delta does not change the line; it only decides where on the line the investor stands: a higher \delta gives a smaller y (closer to CETES), a lower \delta a larger y. If y>1 the investor borrows at the CETES rate (w_2<0).
ones = np.ones(len(ASSETS_FO))
sol = np.linalg.solve(Sigma, hist_mu) # Sigma^-1 mu
a = ones @ sol
w_T = pd.Series(sol / a, index=ASSETS_FO) # tangency portfolio
mu_T = w_T @ hist_mu
sig_T = np.sqrt(w_T @ Sigma @ w_T)
y = a / DELTA # fraction in the tangency portfolio
print(f"Fraction in the tangency portfolio, y = a/delta = {y:.3f} (CETES: {1-y:.3f})")Fraction in the tangency portfolio, y = a/delta = 0.123 (CETES: 0.877)
print(f"Tangency portfolio: premium = {mu_T:.3f}, vol = {sig_T:.3f}, Sharpe = {mu_T/sig_T:.3f}")Tangency portfolio: premium = 2.222, vol = 1.811, Sharpe = 1.227
print(f"Naive portfolio : premium = {y*mu_T:.3f}, vol = {y*sig_T:.3f}")Naive portfolio : premium = 0.273, vol = 0.222
print("Check y * w_T == w_naive:", np.allclose(y * w_T, w_naive))Check y * w_T == w_naive: True
Note that the “total in risky assets” printed above is exactly y: the risky weights of the naive portfolio sum to y because w_T sums to 1.
vol_i = np.sqrt(np.diag(Sigma)) # asset volatilities
sharpe_T = mu_T / sig_T
x = np.linspace(0, 0.32, 50)
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(100*x, 100*sharpe_T*x, color="tab:blue", lw=2,
label=f"CML (slope = Sharpe of tangency = {sharpe_T:.2f})")
ax.scatter(100*vol_i, 100*hist_mu.values, color="gray", zorder=3)
for nm, v, m in zip(ASSETS_FO, vol_i, hist_mu.values):
ax.annotate(nm, (100*v, 100*m), textcoords="offset points", xytext=(6, -10))
ax.scatter(100*sigma_eq, 100*(w_eq @ hist_mu), color="tab:green", s=80, marker="s",
zorder=4, label="Strategic benchmark (with historical $\\mu$)")
ax.scatter(100*y*sig_T, 100*y*mu_T, color="tab:red", s=110, marker="*",
zorder=5, label=f"Naive portfolio ($\\delta$ = {DELTA:.2f}, y = {y:.2f})")
y2 = a / (2*DELTA) # a more risk-averse investor
ax.scatter(100*y2*sig_T, 100*y2*mu_T, color="tab:orange", s=70, marker="D",
zorder=5, label=f"Naive portfolio with $2\\delta$ (y = {y2:.2f})")
ax.scatter(0, 0, color="black", zorder=5)
ax.annotate("CETES", (0, 0), textcoords="offset points", xytext=(6, 6))
ax.text(0.98, 0.04,
f"The tangency portfolio is off the chart:\nvol = {100*sig_T:.0f}%, premium = {100*mu_T:.0f}%",
transform=ax.transAxes, ha="right", va="bottom", fontsize=9)
ax.set_xlim(0, 32); ax.set_ylim(-8, 32)(0.0, 32.0)
(-8.0, 32.0)
ax.axhline(0, color="black", lw=0.8)
ax.set_xlabel("Volatility (% per year)")
ax.set_ylabel("Expected premium over CETES (% per year)")
ax.set_title("The naive portfolio lies on the CML: delta only picks the point on the line")
ax.legend(loc="upper left"); ax.grid(alpha=0.3)
plt.show()Two observations. First, the naive portfolio is only a fraction y of a tangency portfolio that is itself absurd (its volatility is many times that of any single asset, because it is a huge long-short position). The investor’s risk aversion scales this position down, but it cannot fix its composition. Second, the benchmark lies below the line: with the historical premia it is an inefficient portfolio. The in-sample Sharpe ratio of the naive portfolio is several times that of any asset and of the benchmark, which is a symptom of overfitting rather than a real opportunity: the optimizer has taken advantage of the estimation errors in \mu.
13.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.
13.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.
13.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.
13.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.
13.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.
14 CHALLENGE 4 — the Reto rehearsal
You are the investment team of a Mexican family office.
Treynor-Black. Add
BTALto the list of candidates and rerun the model, first with raw alphas and then with adjusted alphas (\sigma_\alpha=2\%). Which fund gets a negative weight and why? What happens to the Information ratio and to the Sharpe ratio of the optimal portfolio when the alphas are adjusted?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 Black-Litterman with
conf = 1andconf = 4. Report \Pi, \mu_{BL} and the unconstrained and constrained weights, and comment on what changes when confidence rises.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 Part I (style analysis), one from Part II or III (neutrality, portable alpha or currency) and one from Part V (Treynor-Black or Black-Litterman), plus one risk warning from the Part IV digest.
15 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 |
|---|---|
| The Workshop 1 toolbox, fluently (Part 0) | Everything else in this workshop |
| 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:
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.
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.
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.
16 What you have to submit
Each week, depending on how much we covered, you have to submit a version of your Workshop 2 notebook (code, your notes, and the four Challenges):
The .ipynb file of your Notebook. You can easily generate it from Google Colab or any other front-end.
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.
17 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.pyIt 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.
17.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.