Tail Risk and Short-Term Capital Management#
A Puzzle#
Suppose we are examining a fund, Short-Term Capital Management, (STCM). We see the following:
stcm_returns.loc[['mean','std','sharpe_ratio'],['Conditional']].rename(index={'sharpe_ratio': 'Sharpe ratio'}).style.format('{:,.1%}')
| Conditional | |
|---|---|
| mean | 26.8% |
| std | 14.3% |
| Sharpe ratio | 187.2% |
Seems like a great strategy right? Well, let’s look at the higher moments.
stcm_returns[['Conditional']].rename(index={'sharpe_ratio': 'Sharpe ratio', 'excess_kurtosis': 'Excess kurtosis'}).style.format('{:,.1%}')
| Conditional | |
|---|---|
| mean | 26.8% |
| std | 14.3% |
| Sharpe ratio | 187.2% |
| skewness | -872.4% |
| Excess kurtosis | 8,617.0% |
What is going on here? Is this still a good strategy?
Notwithstanding the substantial negative skewness, the Sharpe (mean-versus-vol) is very good.
Does VaR or CVaR reveal anything?
rstcm_cond = STCM_Performance[STCM_Performance['Fund Bust'] == 0]['Gross Returns'].astype('float')
tab = pd.DataFrame(index = ['VaR (.01)','CVaR (.01)','VaR-to-vol'], columns=['Conditional'],dtype=float)
tab.loc['VaR (.01)'] = rstcm_cond.quantile(.01)
tab.loc['CVaR (.01)'] = rstcm_cond[rstcm_cond<rstcm_cond.quantile(.01)].mean()
tab.loc['VaR-to-vol'] = rstcm_cond.quantile(.01) / rstcm_cond.std()
tab.style.format('{:.1%}')
| Conditional | |
|---|---|
| VaR (.01) | -16.2% |
| CVaR (.01) | -31.6% |
| VaR-to-vol | -391.5% |
The Strategy#
Their strategy is simple: each month they sell out-of-the-money put options.
They invest the fund’s assets along with the proceeds from selling the puts into riskless treasuries. One month later, the assets have grown by the riskless rate, and they buy back the put options to cover last month’s position. They then repeat the process by selling fresh put options and investing everything back in the riskless rate.
Assets: Boom and Bust#
spy
| SPY | Price | |
|---|---|---|
| date | ||
| 1994-01-31 | 3.77e-02 | 1.04 |
| 1994-02-28 | -2.92e-02 | 1.01 |
| 1994-03-31 | -4.19e-02 | 0.97 |
| 1994-04-30 | 1.12e-02 | 0.98 |
| 1994-05-31 | 1.59e-02 | 0.99 |
| ... | ... | ... |
| 2026-02-28 | -8.64e-03 | 26.10 |
| 2026-03-31 | -4.94e-02 | 24.81 |
| 2026-04-30 | 1.05e-01 | 27.42 |
| 2026-05-31 | 5.26e-02 | 28.86 |
| 2026-06-30 | -1.95e-02 | 28.30 |
390 rows × 2 columns
display(STCM_Performance.head(5))
display(STCM_Performance.tail(5))
| Net Assets | Gross Assets | Gross Returns | Management Fee | Fund Bust | |
|---|---|---|---|---|---|
| date | |||||
| 1994-01-31 | 50000000 | 50000000 | 0 | 0 | 0 |
| 1994-02-28 | 51169088.86 | 51540527.74 | 0.03 | 371438.88 | 0 |
| 1994-03-31 | 52261318.41 | 52615393.52 | 0.03 | 354075.11 | 0 |
| 1994-04-30 | 53534386.66 | 53935400.81 | 0.03 | 401014.15 | 0 |
| 1994-05-31 | 54838907.02 | 55249799.89 | 0.03 | 410892.87 | 0 |
| Net Assets | Gross Assets | Gross Returns | Management Fee | Fund Bust | |
|---|---|---|---|---|---|
| date | |||||
| 2026-02-28 | 172895887.77 | 174185718.44 | 0.03 | 1289830.67 | 0 |
| 2026-03-31 | 176126668.95 | 177208116.07 | 0.02 | 1081447.12 | 0 |
| 2026-04-30 | 180420871.87 | 181773289.83 | 0.03 | 1352417.96 | 0 |
| 2026-05-31 | 184819724.36 | 186205103.86 | 0.03 | 1385379.5 | 0 |
| 2026-06-30 | 189249058.01 | 190649022.65 | 0.03 | 1399964.64 | 0 |
ax = STCM_Performance['Net Assets'].plot()
if KEY != 'SPY':
ax.set_yscale('log') # Set y-axis to log scale
for i in fund_bust_dates.index:
plt.axvline(i, linestyle='dotted', color='r', label='Fund Death - ' + str(i.date()))
plt.title('Fund Net Assets')
plt.ylabel('Net Assets ($100 mil)')
plt.legend()
plt.show()
display(fund_bust_dates)
| Fund Bust | |
|---|---|
| date | |
| 1998-08-31 | 1 |
| 2008-10-31 | 1 |
| 2020-03-31 | 1 |
# Dynamically create fund DataFrames according to number of fund busts
funds = []
bust_dates = list(fund_bust_dates.index)
for i in range(len(bust_dates) + 1):
if i == 0:
fund_period = STCM_Performance.loc[:str(bust_dates[0])]
elif i < len(bust_dates):
fund_period = STCM_Performance.loc[str(bust_dates[i-1]):str(bust_dates[i])].iloc[1:]
else:
fund_period = STCM_Performance.loc[str(bust_dates[-1]):].iloc[1:]
funds.append(fund_period)
# Now funds[0], funds[1], ..., funds[n] are the DataFrames for each era between busts
# (corresponds to fund_1, fund_2, etc.)
# Build funds DataFrame for each era between busts (plus one for final epoch)
funds_list = []
start_idx = 0
bust_dates = list(fund_bust_dates.index) # List of bust dates (sorted as index)
# Iterate through bust periods
for i in range(len(bust_dates) + 1):
if i == 0:
fund_period = STCM_Performance.loc[:str(bust_dates[0])]
elif i < len(bust_dates):
fund_period = STCM_Performance.loc[str(bust_dates[i-1]):str(bust_dates[i])].iloc[1:] # skip repeated date
else:
fund_period = STCM_Performance.loc[str(bust_dates[-1]):].iloc[1:]
if len(fund_period) > 0:
lifespan = f"{fund_period.index.date[0]} - {fund_period.index.date[-1]}"
max_net_assets = fund_period['Net Assets'].max() / 1_000_000
funds_list.append({'Lifespan': lifespan, 'Max Net Assets ($ mil)': max_net_assets})
funds = pd.DataFrame(funds_list, index=[f"Fund {i+1}" for i in range(len(funds_list))])
display(funds)
| Lifespan | Max Net Assets ($ mil) | |
|---|---|---|
| Fund 1 | 1994-01-31 - 1998-08-31 | 179.21 |
| Fund 2 | 1998-09-30 - 2008-10-31 | 275.03 |
| Fund 3 | 2008-11-30 - 2020-03-31 | 412.15 |
| Fund 4 | 2020-04-30 - 2026-06-30 | 189.25 |
print('Maximum value of net assets is ${:,.2f} million. This high water mark is observed on {}'.
format(max(STCM_Performance['Net Assets'])/1000000,
STCM_Performance[STCM_Performance['Net Assets'] == max(STCM_Performance['Net Assets'])].index.date[0]))
Maximum value of net assets is $412.15 million. This high water mark is observed on 2020-01-31
Performance Metrics#
STCM_Performance['Gross Returns'].plot()
plt.title('Gross Monthly Returns')
plt.ylabel('Gross Returns')
plt.ylim(-.15, 0.05)
plt.show()
Good-Times Stats#
For the conditional stats, the mean is quite high and the Sharpe ratio is fantastic. However, the returns are very negatively skewed and massively kurtotic. Thus, even conditional on surviving it is clear the fund has substantial nonlinear risk.
Full-Sample Stats#
As seen in Table above, the return stats change enormously. The mean return is now negative and the volatility is massive. Furthermore, the skewness and kurtosis were already extreme but now even more so.
display(stcm_returns.rename(index={'sharpe_ratio': 'Sharpe ratio', 'excess_kurtosis': 'Excess kurtosis'}).style.format('{:,.1%}'))
| Conditional | Unconditional | |
|---|---|---|
| mean | 26.8% | -23.1% |
| std | 14.3% | 207.5% |
| Sharpe ratio | 187.2% | -11.1% |
| skewness | -872.4% | -1,712.4% |
| Excess kurtosis | 8,617.0% | 30,800.5% |
ax = STCM_Performance[STCM_Performance['Fund Bust'] == 0]['Gross Returns'].astype('float').hist(bins=50)
ax.set_ylim([0,15]);
ax.set_xlim([-.5,.05]);
ax = STCM_Performance['Gross Returns'].astype('float').hist(bins=200)
ax.set_ylim([0,15]);
ax.set_xlim([-12,0.05]);
Investors in nonlinear strategies need to be very careful that they evaluate the fund performance on a large enough data sample so as to see the true distribution of the returns.
Appendix#
Use the following data to test the strategy:
Use the market index data in the “S&P500” tab.
Assume a constant monthly riskless rate of \(r_f\) = 0.0020.
Assume a constant implied volatility of \(\sigma\) = 0.05.
Test the strategy assuming the fund begins with assets of \(W_0\) = $50, 000, 000.
The details of the strategy are as follows:
At the end of the first month, we sell puts with maturity \(\tau = 2\) and strike price of \(K = 0.80S\), where \(S\) is the current market price, \(P^m_t\)
Calculate the price you receive on the puts using the Black-Scholes formulas:
where, \(r_f\) is the constant riskless rate given above, \(\sigma\) is the constant implied volatility given above, and \(\mathcal{N}(·)\) is the standard normal cumulative distribution.
We sell \(n_t\) of these put options, where \(n_t\) is calculated such that the premium we receive equals 3% of the fund’s total assets, \(W_t\),
Thus the dollars received, \(G_t\), from selling this number of puts will immediately increase the fund’s assets by 3%.
At the end of the following month, t + 1, we must use Lt+1 in assets to cover the puts from last month buy purchasing puts with \(\tau = 1\) and \(K = .80P^m_t\). We use the Black-Scholes formula above to calculate this amount:
Calculate the fund’s gross (before-fees) assets, \(\tilde{W}\), at time \(t + 1\) after covering the puts:
where the first term is the past month’s assets plus the 3% jump from issuing puts at the last month, all invested at the riskless rate until the end of this month, at which point we cover the position
Calculate the fund’s gross (before fees) excess return from \(t\) to \(t + 1\):
Calculate the management compensation at time \(t\) as follows:
Get the fund’s net capital by subtracting the managerial fee:
Let \(B_t\) be a true/false variable indicating whether the fund has gone bust. If \(W_t < 0\), then assume the fund immediately re-opens with the initial asset level of \(W_0\) = $50 million
Finally, still at the end of month \(t + 1\), we repeat the whole process by selling \(n_{t+1}\) put options, so as to make the fund’s net assets jump 3% immediately
Thus, equations above give a recursion for building a timeseries of the funds net assets, \(W_{t}\), the managerial compensation, \(\pi_{t}\), and the fund’s gross excess returns, \(R^{\#stcm}_{t}\)