Betting Against Volatility¶
One of the most stubborn anomalies: low-volatility stocks earn as much or more than high-volatility ones, on a risk-adjusted basis — the opposite of what the textbook risk-return tradeoff predicts. Leverage-constrained investors overpay for exciting, high-vol names. We lean the other way.
try:
import convexpi.lab # noqa
except ImportError:
import subprocess, sys
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "convexpi-lab"])
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
print("ready")
The idea¶
vol_1m is each stock's recent volatility (high = risky). We want to be long low vol / short high
vol, so we sort on the negative of the signal — high score = calm stock.
import numpy as np
from convexpi.lab import Strategy
def _long_short(signal, frac=0.2):
"""Dollar-neutral long/short: long the top `frac`, short the bottom `frac`, equal-weighted."""
s = np.nan_to_num(np.asarray(signal, dtype=float))
n = len(s); k = max(1, int(n * frac))
order = np.argsort(s)
w = np.zeros(n); w[order[-k:]] = 1.0 / k; w[order[:k]] = -1.0 / k
return w
class MyStrategy(Strategy):
"""Long low-volatility names, short high-volatility names."""
def on_day(self, day, features, prices, portfolio):
vol = features.get("vol_1m", np.zeros(len(prices)))
return _long_short(-np.nan_to_num(vol), frac=0.2) # negate: long the calmest
Out-of-sample evaluation¶
Train on the first half of a synthetic market, evaluate on the held-out second half — the same discipline as the leaderboard.
from convexpi.lab import SyntheticMarket, Grader
market = SyntheticMarket(n_stocks=80, n_days=1800, seed=1)
report = Grader(market).evaluate(MyStrategy())
print(f"in-sample Sharpe : {report.is_sharpe:+.2f}")
print(f"out-of-sample Sharpe: {report.oos_sharpe:+.2f}")
print(f"overfitting ratio : {report.overfitting_ratio:+.2f}")
oos = report.oos_result.daily_returns
fig, ax = plt.subplots(figsize=(8, 3))
ax.plot(np.cumprod(1 + oos)); ax.set_title("Out-of-sample equity curve"); ax.set_ylabel("growth of $1")
plt.tight_layout(); plt.show()
print("The low-vol leg is defensive — it tends to shine in drawdowns.")
What I'd try next¶
- True betting-against-beta rescales each leg to equal beta (see the library's
frazzini_pedersen_babreplication). - Combine low-vol with quality — calm and profitable.
- Check whether the edge is just a sector tilt (utilities/staples) in disguise.

