TA Solution - LTCM#
Thanks to Tobias
Case: Long-Term Capital Management, L.P. (A) [9-200-007].
1. READING#
1.#
Describe LTCM’s investment strategy with regard to the following aspects:
Securities traded
Trading frequency
Skewness (Do they seek many small wins or a few big hits?)
Forecasting (What is behind their selection of trades?)
Securities traded:
LTCM tried to trade on market mispricing and arbitrage, Relative Value and Convergence trades. They go long-short on these arbitrages. Use leverage to trade bigger principal on these small mispricings and try to hedge out their positions via their long-short trades. They primarily used derivatives, in the form of swaps to achieve these positions.
LTCM was also heavily involved income and credit, and they also have sizeable positions in equities. In all these asset classes, they trade a large number of securities, across global markets.
Trading frequency:
LTCM’s trading frequency varied according to their strategies. Their largest investment in the form of convergence trades had a long term trading horizon and frequency (weeks or months). They are not trying to arbitrage intraday movements and nor do they make long-term directional bets.
Skewness:
They are picking up pennies in front of a bulldozer. So, many small wins. They seek small positive returns using leverage and do not bet significantly on any specific events. Have lower skewness than SPY. However, they are susceptible to extreme market events (it was the Russian currency crisis that brought them down).
Forecasting:
Build models to find mispricing and the reason behind the mispricing. Then forecast their P&L on these trades. Their forecast is not better because of better mathematical model (the convergence trade/ relative value theory is not the edge), it is their knowledge of the market.
2.#
What are LTCM’s biggest advantages over its competitors?
Efficient financing: Their edge was on financing and funding, along with their proprietary trading and modelling capabilities.
Fund Size: They had a larger AUM, meaning they could lever at favorable rates
Collatralization: Better collateralize these positions. (pay lower haircuts)
Long-term Horizon: Long term commitment of capital from investors as well as availability of credit line
Liquidity and Hedging: LTCM has in place many mechanisms to ensure liquidity. They also avoid taking too much default risk or explicit directional bets.
3.#
The case discusses four types of funding risk facing LTCM:
collateral haircuts
repo maturity
equity redemption
loan access
The case discusses specific ways in which LTCM manages each of these risks. Briefly discuss them.
Collateral haircuts:
The haircuts go up in a market disruption event leading to unfavorable collateral terms for LTCM in terms of funding a spread trade. For most trades, LTCM obtains 100% financing on a fully collateralized basis. Furthermore, LTCM stress tests the haircuts across its asset classes.
Repo maturity:
In an adverse situation, where their credit risk goes up, they wont be able to secure these longer term repos which were favorable to their trades. LTCM goes against the norm by entering into relatively long-maturity repo. While much of it is overnight, LTCM uses contracts that typically have maturity of 6-12 months. Furthermore, LTCM manages their aggregate repo maturity.
Equity redemption:
If in a convergence trade, the two securities, before converging, diverge a lot, LTCM are facing redemption risk from their investors at a time where the Margin calls need them to furhter finance their strategies. Equity Redemption at a unfavorable time also leads LTCM to unwind their positions at unfavorable rates leading to further losses of capital. The firm is highly levered, so equity funding risk is especially important. LTCM restricts redemptions of equity year by year. The restriction is particularly strong in that unredeemed money is re-locked.
Loan access:
Loan access can be tough to come by in times of a crisis, leading to a further decline in the fund’s performance. For debt funding, LTCM negotiated a revolving loan that has no Material Adverse Change clause. Thus, the availability of debt funding is not so highly correlated with fund performance.
4.#
LTCM is largely in the business of selling liquidity and volatility. Describe how LTCM accounts for liquidity risk in their quantitative measurements.
LTCM required counterparties to maitain the collateral balance via a ‘two-way mark to market process on a daily basis. Thus the cash flow coming in from the counterparties mark to market would fund LTCM’s outflow for the mark to market call on their offsetting position.
LTCM als also estimated theoretical worst case haircuts it would face in adverse market situations. Forecasting these worst case liquidity LTCM was able to better structure its financing so as not to liquidate its positions solely due to these adverse market events.
LTCM attempts to account for liquidity risk quantitatively by adjusting security correlations. For short-term horizons, LTCM assumes positive correlation between all trade cat- egories. Even if their net exposure to a strategy flips sides, they still assume positive correlation to the new net position
5.#
Is leverage risk currently a concern for LTCM?
Currently since there were no extreme market events, leverage risk is not a concern, but still a potential threat for LTCM. Given the size of their commited capital and fewer opportunites for the excess capital to enhance LTCM's return, they are considering returning some of the investments made, which would reduce the leverage.Note: the amount of “true” leverage is also frequently misreported. The reason being that SEC filings require the reporting of the gross notional exposure, not the net exposure! As an example, consider this article. It claims that Michael Burry “bet” \(1.6 billion on a market crash. In reality, his exposure is \)1.6 billion; he achieved this by buying put options for much, much, cheaper (potentially as low as ~\(10m in premiums; capping his losses at \)10m).
Updated for 2025; media reporting indicates that Michael Burry (once again!) has hundreds of millions of exposure against AI stocks, when in reality he just bought put options.
6.#
Many strategies of LTCM rely on converging spreads. LTCM feels that these are almost win/win situations because of the fact that if the spread converges, they make money. If it diverges, the trade becomes even more attractive, as convergence is still expected at a future date.
What is the risk in these convergence trades?
About a year after the time of the case, the fund loses most of its value due to non-converging trades. So clearly there is some risk!
Positions are subject to liquidity risk. If market liquidity dries up or the markets become segmented, the divergent spreads can persist for a long time. This indeed happens later to LTCM. The trades that get them in trouble ultimately pay off, but not before LTCM blew up. LTCM believed it can exit these convergence trades if they become too unprofitable. However, a stop-loss order is not the same as a put option. If the price jumps discontinuously through the stop-loss, then it is ineffective.
Or a market may be paralyzed/illiquid when trying to execute the stop-loss. A put option does not need to worry about price impact, whereas a stop-loss does. Finally, a stop-loss ensures that an investor sells as soon as a security price hits a worst-case scenario, ensuring unfavorable market timing.
2. Fund Performance and Attribution#
Data#
ltcm exhibits data.xlsx,Exhibit 2: Gross and net (total) returns of LTCMspy_data.xlsx: SPY returns and risk-free rate (scaled tbill index)
import pandas as pd
import numpy as np
import statsmodels.api as sm
def calc_return_metrics(data, as_df=False, adj=12):
"""
Calculate return metrics for a DataFrame of assets.
Args:
data (pd.DataFrame): DataFrame of asset returns.
as_df (bool, optional): Return a DF or a dict. Defaults to False (return a dict).
adj (int, optional): Annualization. Defaults to 12.
Returns:
Union[dict, DataFrame]: Dict or DataFrame of return metrics.
"""
summary = dict()
summary["Annualized Return"] = data.mean() * adj
summary["Annualized Volatility"] = data.std() * np.sqrt(adj)
summary["Annualized Sharpe Ratio"] = (
summary["Annualized Return"] / summary["Annualized Volatility"]
)
summary["Annualized Sortino Ratio"] = summary["Annualized Return"] / (
data[data < 0].std() * np.sqrt(adj)
)
return pd.DataFrame(summary, index=data.columns) if as_df else summary
def calc_risk_metrics(data, as_df=False, var=0.05):
"""
Calculate risk metrics for a DataFrame of assets.
Args:
data (pd.DataFrame): DataFrame of asset returns.
as_df (bool, optional): Return a DF or a dict. Defaults to False.
adj (int, optional): Annualizatin. Defaults to 12.
var (float, optional): VaR level. Defaults to 0.05.
Returns:
Union[dict, DataFrame]: Dict or DataFrame of risk metrics.
"""
summary = dict()
summary["Skewness"] = data.skew()
summary["Excess Kurtosis"] = data.kurtosis()
summary[f"VaR ({var})"] = data.quantile(var, axis=0)
summary[f"CVaR ({var})"] = data[data <= data.quantile(var, axis=0)].mean()
summary["Min"] = data.min()
summary["Max"] = data.max()
wealth_index = 1000 * (1 + data).cumprod()
previous_peaks = wealth_index.cummax()
drawdowns = (wealth_index - previous_peaks) / previous_peaks
summary["Max Drawdown"] = drawdowns.min()
summary["Bottom"] = drawdowns.idxmin()
summary["Peak"] = previous_peaks.idxmax()
recovery_date = []
for col in wealth_index.columns:
prev_max = previous_peaks[col][: drawdowns[col].idxmin()].max()
recovery_wealth = pd.DataFrame([wealth_index[col][drawdowns[col].idxmin() :]]).T
recovery_date.append(
recovery_wealth[recovery_wealth[col] >= prev_max].index.min()
)
summary["Recovery"] = ["-" if pd.isnull(i) else i for i in recovery_date]
summary["Duration (days)"] = [
(i - j).days if i != "-" else "-"
for i, j in zip(summary["Recovery"], summary["Bottom"])
]
return pd.DataFrame(summary, index=data.columns) if as_df else summary
def calc_performance_metrics(data, adj=12, var=0.05):
"""
Aggregating function for calculating performance metrics. Returns both
risk and performance metrics.
Args:
data (pd.DataFrame): DataFrame of asset returns.
adj (int, optional): Annualization. Defaults to 12.
var (float, optional): VaR level. Defaults to 0.05.
Returns:
DataFrame: DataFrame of performance metrics.
"""
summary = {
**calc_return_metrics(data=data, adj=adj),
**calc_risk_metrics(data=data, var=var),
}
summary["Calmar Ratio"] = summary["Annualized Return"] / abs(
summary["Max Drawdown"]
)
return pd.DataFrame(summary, index=data.columns)
def calc_univariate_regression(y, X, intercept=True, adj=12):
"""
Calculate a univariate regression of y on X. Note that both X and y
need to be one-dimensional.
Args:
y : target variable
X : independent variable
intercept (bool, optional): Fit the regression with an intercept or not. Defaults to True.
adj (int, optional): What to adjust the returns by. Defaults to 12.
Returns:
DataFrame: Summary of regression results
"""
X_down = X[y < 0]
y_down = y[y < 0]
if intercept:
X = sm.add_constant(X)
X_down = sm.add_constant(X_down)
model = sm.OLS(y, X, missing="drop")
results = model.fit()
inter = results.params.iloc[0] if intercept else 0
beta = results.params.iloc[1] if intercept else results.params.iloc[0]
summary = dict()
summary["Alpha"] = inter * adj
summary["Beta"] = beta
down_mod = sm.OLS(y_down, X_down, missing="drop").fit()
summary["Downside Beta"] = (
down_mod.params.iloc[1] if intercept else down_mod.params.iloc[0]
)
summary["R-Squared"] = results.rsquared
summary["Treynor Ratio"] = (y.mean() / beta) * adj
summary["Information Ratio"] = (inter / results.resid.std()) * np.sqrt(adj)
summary["Tracking Error"] = (
inter / summary["Information Ratio"]
if intercept
else results.resid.std() * np.sqrt(adj)
)
if isinstance(y, pd.Series):
return pd.DataFrame(summary, index=[y.name])
else:
return pd.DataFrame(summary, index=y.columns)
def calc_multivariate_regression(y, X, intercept=True, adj=12):
"""
Calculate a multivariate regression of y on X. Adds useful metrics such
as the Information Ratio and Tracking Error. Note that we can't calculate
Treynor Ratio or Downside Beta here.
Args:
y : target variable
X : independent variables
intercept (bool, optional): Defaults to True.
adj (int, optional): Annualization factor. Defaults to 12.
Returns:
DataFrame: Summary of regression results
"""
if intercept:
X = sm.add_constant(X)
model = sm.OLS(y, X, missing="drop")
results = model.fit()
summary = dict()
inter = results.params.iloc[0] if intercept else 0
betas = results.params.iloc[1:] if intercept else results.params
summary["Alpha"] = inter * adj
summary["R-Squared"] = results.rsquared
X_cols = X.columns[1:] if intercept else X.columns
for i, col in enumerate(X_cols):
summary[f"{col} Beta"] = betas[col]
summary["Information Ratio"] = (inter / results.resid.std()) * np.sqrt(adj)
summary["Tracking Error"] = results.resid.std() * np.sqrt(adj)
if isinstance(y, pd.Series):
return pd.DataFrame(summary, index=[y.name])
else:
return pd.DataFrame(summary, index=y.columns)
def calc_iterative_regression(y, X, intercept=True, one_to_many=False, adj=12):
"""
Iterative regression for checking one X column against many different y columns,
or vice versa. "one_to_many=True" means that we are checking one X column against many
y columns, and "one_to_many=False" means that we are checking many X columns against a
single y column.
To enforce dynamic behavior in terms of regressors and regressands, we
check that BOTH X and y are DataFrames
Args:
y : Target variable(s)
X : Independent variable(s)
intercept (bool, optional): Defaults to True.
one_to_many (bool, optional): Which way to run the regression. Defaults to False.
adj (int, optional): Annualization. Defaults to 12.
Returns:
DataFrame : Summary of regression results.
"""
if not isinstance(X, pd.DataFrame) or not isinstance(y, pd.DataFrame):
raise TypeError("X and y must both be DataFrames.")
if one_to_many:
if isinstance(X, pd.Series) or X.shape[1] > 1:
summary = pd.concat(
[
calc_multivariate_regression(y[col], X, intercept, adj)
for col in y.columns
],
axis=0,
)
else:
summary = pd.concat(
[
calc_univariate_regression(y[col], X, intercept, adj)
for col in y.columns
],
axis=0,
)
summary.index = y.columns
return summary
else:
summary = pd.concat(
[
calc_univariate_regression(y, X[col], intercept, adj)
for col in X.columns
],
axis=0,
)
summary.index = X.columns
return summary
ADJ = 12
ltcm = (
pd.read_excel("ltcm_exhibits_data.xlsx", sheet_name="Exhibit 2", skiprows=2)
.dropna(how="any")
.rename(
columns={
"Unnamed: 0": "date",
"Fund Capital ($billions)": "Fund Capital",
"Gross Monthly Performancea": "Gross",
"Net Monthly Performanceb": "Net",
"Index of Net Performance": "Index",
}
)
)
ltcm["date"] = pd.to_datetime(ltcm["date"]) + pd.tseries.offsets.MonthEnd(0)
ltcm = ltcm.set_index("date")
spy = pd.read_excel(
"spy_data.xlsx", sheet_name="excess returns", index_col=0, parse_dates=[0]
).dropna()
ltcm = ltcm.merge(spy[["SPY"]], left_index=True, right_index=True, how="inner")
# Get rfr
rfr = (
pd.read_excel(
"spy_data.xlsx", sheet_name="total returns", index_col=0, parse_dates=[0]
)
.dropna()
.rename(columns={"^IRX": "RFR"})
)
ltcm = ltcm.merge(rfr[["RFR"]], left_index=True, right_index=True, how="inner")
ltcm = ltcm.drop(columns=["Fund Capital", "Index"])
# Convert to EXCESS returns
ltcm.loc[:, ["Gross", "Net"]] = ltcm.loc[:, ["Gross", "Net"]].subtract(
ltcm["RFR"], axis=0
)
1. Summary stats.#
For both the gross and net series of LTCM excess returns, report the annualized
mean
volatility
Sharpe ratios
Also report the
skewness
kurtosis
5th quantile
calc_performance_metrics(ltcm, adj=ADJ).T
| Gross | Net | SPY | RFR | |
|---|---|---|---|---|
| Annualized Return | 0.2436 | 0.156883 | 0.154775 | 0.050287 |
| Annualized Volatility | 0.136238 | 0.111773 | 0.114073 | 0.001271 |
| Annualized Sharpe Ratio | 1.78805 | 1.40359 | 1.356806 | 39.579951 |
| Annualized Sortino Ratio | 2.356764 | 1.628167 | 2.318467 | NaN |
| Skewness | -0.288328 | -0.81087 | -0.406867 | -1.202728 |
| Excess Kurtosis | 1.586681 | 2.927724 | -0.388002 | 3.015576 |
| VaR (0.05) | -0.030305 | -0.0263 | -0.049667 | 0.003465 |
| CVaR (0.05) | -0.072889 | -0.068556 | -0.052804 | 0.003186 |
| Min | -0.105142 | -0.105142 | -0.056045 | 0.002892 |
| Max | 0.112442 | 0.080442 | 0.075014 | 0.004867 |
| Max Drawdown | -0.168744 | -0.175402 | -0.060568 | 0.0 |
| Bottom | 1998-06-30 00:00:00 | 1998-07-31 00:00:00 | 1994-12-31 00:00:00 | 1994-03-31 00:00:00 |
| Peak | 1998-04-30 00:00:00 | 1997-12-31 00:00:00 | 1998-06-30 00:00:00 | 1998-07-31 00:00:00 |
| Recovery | - | - | 1995-02-28 00:00:00 | 1994-03-31 00:00:00 |
| Duration (days) | - | - | 59 | 0 |
| Calmar Ratio | 1.443609 | 0.894421 | 2.555371 | inf |
2. Compare to SPY#
Comment on how these stats compare to SPY and other assets we have seen.
How much do they differ between gross and net?
Comparing the Net Monthly Performance of LTCM and Excess returns of SPY, LTCM displays a higher return, with a very similar volatility to SPY. Thus the sharpe ratio of LTCM net of fee and other charges is slightly higher than SPY’s.
The excess net returns of LTCM however, underperform SPY with similar volatility levels and thus have a slightly lower sharpe ratio.
However, looking at other moments, LTCM Net returns are more negatively skewed and have a significantly fatter tail compared to SPY, indicating the presence of heavy negative monthly returns over the sample period. Although, since the VaR of LTCM is lower compared to SPY, the indication is that these negative returns are fewer in frequency.
3. LFD#
Estimate a linear factor decomposition of net LTCM excess returns on SPY excess returns.
Report
annualized alpha
beta
r-squared
Does LTCM deliver performance beyond SPY?
calc_multivariate_regression(ltcm[["Net"]], ltcm[["SPY"]], intercept=True, adj=ADJ).T
| Net | |
|---|---|
| Alpha | 0.135076 |
| R-Squared | 0.020676 |
| SPY Beta | 0.140892 |
| Information Ratio | 1.221183 |
| Tracking Error | 0.110611 |
Yes, certainly. It has a 13.5% annualized \(\alpha\), an \(r^2\) that is close to 0, and a \(\beta\) that is also close to 0. This tells us that LTCM’s returns cannot be explained by the market, and it delivers significant performance in a way that is uncorrelated to the market. Note that from above, LTCM’s net returns are 15%, and they have an \(\alpha\) of 13.5%. This tells us virtually all of their returns are \(\alpha\).
4. Nonlinear Exposure#
Let’s check for non-linear market exposure. Run the following regression on LTCM’s net excess returns:
Report
annualized alpha
the linear and quadratic betas
r-squared
ltcm["SPY_Squared"] = ltcm["SPY"] ** 2
calc_multivariate_regression(
y=ltcm[["Net"]], X=ltcm[["SPY", "SPY_Squared"]], intercept=True, adj=ADJ
).T
| Net | |
|---|---|
| Alpha | 0.162629 |
| R-Squared | 0.027801 |
| SPY Beta | 0.168741 |
| SPY_Squared Beta | -2.158255 |
| Information Ratio | 1.475657 |
| Tracking Error | 0.110208 |
5.#
Does the quadratic market factor do much to increase the overall LTCM variation explained by the market?
From the regression evidence, does LTCM’s market exposure behave as if it is long market options or short market options?
Should we describe LTCM as being positively or negatively exposed to market volatility?
Q1. No, not at all. The \(r^2\) goes up slightly, but of course we would expect this simply by adding more regressors. The \(\alpha\) actually goes up to 16.2%!
Q2 and 3. Short market options. Note that \(\text{SPY}^2\) is a proxy for realized volatility (in particular, if SPY has 0 mean return, it would exactly equal realized volatility). The \(\beta\) on \(\text{SPY}^2\) is -2.16, which is quite large, indicating that we are short realized volatility. If we’re short volatility that therefore means that we are short options.
6.#
Let’s try to pinpoint the nature of LTCM’s nonlinear exposure. Does it come more from exposure to up-markets or down-markets? Run the following regression on LTCM’s net excess returns:
where \(k_1= .03\) and \(k_2= -.03\).
Report
annualized alpha
market beta, the up and down betas
r-squared
ltcm["SPY_Put"] = np.maximum(-0.03 - ltcm["SPY"], 0)
ltcm["SPY_Call"] = np.maximum(ltcm["SPY"] - 0.03, 0)
calc_multivariate_regression(
ltcm[["Net"]], ltcm[["SPY", "SPY_Put", "SPY_Call"]], intercept=True, adj=ADJ
).T
| Net | |
|---|---|
| Alpha | 0.109438 |
| R-Squared | 0.048607 |
| SPY Beta | 0.434499 |
| SPY_Put Beta | 1.042253 |
| SPY_Call Beta | -0.721876 |
| Information Ratio | 1.003814 |
| Tracking Error | 0.109022 |
7.#
Is LTCM long or short the call-like factor? And the put-like factor?
Which factor moves LTCM more, the call-like factor, or the put-like factor?
In the previous problem, you commented on whether LTCM is positively or negatively exposed to market volatility. Using this current regression, does this volatility exposure come more from being long the market’s upside? Short the market’s downside? Something else?
Q1. They are long the put-like factor, and short the call-like factor (you can tell by the sign of the \(\beta\)’s).
Q2. The put-like factor, given that it has a larger \(\beta\).
Q3. This volatility exposure likely comes from being short the market upside. Note that we found the \(\text{SPY}^2\) \(\beta\) to be negative, indicating a short realized volatility position. We are long the put-like factor, so it can’t be coming from that. Since we’re short the call-like factor it tells us our negative exposure comes from being short calls. You’ll note that we talked about this in TA Review 8, where many structured products behave like a bond + an upside call, and LTCM was in the business of selling volatility to these structured products – and so this exposure aligns somewhat with the kinds of trades they would put on.