This commit is contained in:
2026-07-03 08:41:22 -04:00
parent ee85b04b00
commit 572fd0e47d
6 changed files with 31 additions and 31 deletions
@@ -21,11 +21,11 @@
"\n",
"In linear-algebraic terms, this is a question about the **angle** between two vectors. If $f_t$ and $r_{t+1}$ point in similar directions (small angle, high cosine similarity), the factor has predictive power. If they're nearly orthogonal, it doesn't. \n",
"\n",
"We test four standard cross-sectional factors, most of which we compute as priced-based proxies since we are using pretty basic data. They are momentum, value, quality, and low volatility.\n",
"We test four standard cross-sectional factors, most of which we compute as price-based proxies since we are using pretty basic data. They are momentum, value, quality, and low volatility.\n",
"\n",
"> **Spoiler**: Momentum wins, and the rest have negative or insignificant information coefficients over this period. The methodology for diagnosing a factor is the same whether it works or not, and showing *why* the proxies fail is more instructive than silently dropping them.\n",
"\n",
"The main goals of this notebook are.\n",
"The main goals of this notebook are:\n",
"1. To define and compute four cross-sectional factors (momentum, value, quality, low-vol) as vectors $f_t$.\n",
"2. To measure each factor's information coefficient (**IC**) — the cosine similarity between $f_t$ and $r_{t+1}$.\n",
"3. To examine **IC stability** across subperiods (walk-forward: is the factor consistent, or just lucky in one period?).\n",
@@ -141,9 +141,9 @@
"metadata": {},
"source": [
"## Factor definitions\n",
"We'll now compute are four standard factors. All are cross-sectional ranks (permutations) at each month-end, so they live on the same scale and can be combined later without further normalization.\n",
"We'll now compute our four standard factors. All are cross-sectional ranks (permutations) at each month-end, so they live on the same scale and can be combined later without further normalization.\n",
"\n",
"- **Momemtum**: Trailing 12-month return skipping the most recent month ($t - 12$ to $t - 2$). The intuition behind is that stocks that went up over the past year tend to keep going up for another month or two\n",
"- **Momentum**: Trailing 12-month return skipping the most recent month ($t - 12$ to $t - 2$). The intuition behind it is that stocks that went up over the past year tend to keep going up for another month or two\n",
"- **Values**: we use price-based inverse momentum as a value proxy: 60-month trailing return, inverted. The intuition is that stocks that went down over 5 years are \"cheap\" and may mean-revert.\n",
"- **Quality**: 12-month Sharpe-like ratio of monthly returns (mean / std). The intuition is that stocks with smooth positive returns are \"higher quality\".\n",
"- **Low-volatility**: Inverse of 60-month trailing volatility, ranked. Intuition is that low-risk stocks tend to outperform on a risk-adjusted basis.\n",
@@ -227,7 +227,7 @@
"\n",
"The **information ratio (IR)** is the mean IC divided by the standard deviation of IC across dates: \n",
"$$ \\text{IR} = \\frac{\\text{Mean IC}}{\\text{Std IC}} \\times \\sqrt{12}. $$\n",
"This is a ration of signal and noise, which tells us whether the factor reliably predicts returns or is just noise."
"This is a ratio of signal and noise, which tells us whether the factor reliably predicts returns or is just noise."
]
},
{
@@ -372,7 +372,7 @@
"id": "5659b941",
"metadata": {},
"source": [
"Note that added a sample size filter with `mask.sum() < 20`. The `mask` identifies stocks that have both a valid factor score and a valid forward return in a given month, and `mask.sum()` counts how many usable pairs you actually have. The `< 20` threshold prevents the code from computing a correlation on a tiny sample, which is statistically meaningless and numerically unstable (e.g., Spearman correlation on 2 stocks is always exactly ±1). If you don't gate this, early-history months, mass delistings, or data gaps inject garbage ±1.0 values into your IC time series, which then contaminate every downstream statistic like the mean IC, Information Ratio, and decay curves. Setting a floor of 20 filters out those degenerate months while retaining enough valid data to produce a reliable signal.\n",
"Note that we added a sample size filter with `mask.sum() < 20`. The `mask` identifies stocks that have both a valid factor score and a valid forward return in a given month, and `mask.sum()` counts how many usable pairs you actually have. The `< 20` threshold prevents the code from computing a correlation on a tiny sample, which is statistically meaningless and numerically unstable (e.g., Spearman correlation on 2 stocks is always exactly ±1). If you don't gate this, early-history months, mass delistings, or data gaps inject garbage ±1.0 values into your IC time series, which then contaminate every downstream statistic like the mean IC, Information Ratio, and decay curves. Setting a floor of 20 filters out those degenerate months while retaining enough valid data to produce a reliable signal.\n",
"\n",
"We do this sort of masking throughout."
]