Ranking & Scoring: Multi-Factor Models
Once stocks pass the screener, algorithms rank them by combining multiple signals into a single score to find the best candidates.
Ranking & Scoring: Multi-Factor Models
A screener says yes or no. A scoring model says how much. After filtering the universe to 20–80 candidates, the algorithm ranks them so capital goes to the highest-conviction ideas first. This is the core of every systematic equity strategy.
Why a Single Score?
You might like 15 stocks, but you only have capital for 5. Rather than choosing arbitrarily, the algorithm computes a composite score for each candidate and buys the top N. This removes discretion, ensures consistency, and makes the process auditable.
Step 1 — Pick Your Factors
Each factor is a quantifiable signal that has historically predicted future returns. Academic research has validated dozens; practitioners typically use 4–8 to keep the model simple.
| Factor | Formula | Edge |
|---|---|---|
| Price Momentum | 12-month return excluding last month | Winners keep winning (1–12 month horizon) |
| Earnings Momentum | EPS revision % over 90 days | Analyst upgrades predict near-term outperformance |
| Value | Earnings Yield (E/P) | Cheap stocks outperform long-term |
| Quality | Return on Assets | High-ROIC companies compound faster |
| Short Interest | % of float sold short | High short interest predicts underperformance |
| Relative Strength | Price vs. sector over 3 months | Sector leaders tend to stay leaders |
Step 2 — Normalize Each Factor (Z-Score)
Raw factor values are on different scales: momentum might be 0.30 (30%) while EPS yield is 0.05 (5%). Before combining them you must normalize each factor so they are comparable.
A z-score of +2 means the stock is 2 standard deviations better than average on that factor. A z-score of −1 means it is 1 standard deviation below average. Now all factors speak the same language.
import numpy as np
def z_score(values):
arr = np.array(values, dtype=float)
mean = arr.mean()
std = arr.std()
return (arr - mean) / (std + 1e-9) # epsilon avoids /0
# Example: rank 5 stocks on momentum and quality
momentum = [0.32, 0.15, -0.05, 0.44, 0.08]
quality = [0.18, 0.09, 0.22, 0.07, 0.31]
z_mom = z_score(momentum) # [0.38, -0.45, -1.23, 1.04, -0.61]
z_qual = z_score(quality) # [-0.05, -0.98, 0.44, -1.27, 1.05]
# Combine with weights: 60% momentum, 40% quality
composite = 0.6 * z_mom + 0.4 * z_qual
# Rank highest score first → buy top 2
ranked = sorted(zip(composite, ['A','B','C','D','E']), reverse=True)Step 3 — Assign Weights
Weights encode your hypothesis. A pure momentum strategy weights momentum at 100%. A balanced multi-factor model might spread weight across 4–6 factors. There is no universally correct weighting — the right weights depend on your holding period, market regime, and risk tolerance.
Overfitting Risk
If you optimize factor weights on your backtest data, the weights will be perfectly tuned to the past and useless for the future. Always split your data: fit weights on 70% of history and validate on the remaining 30% you never touched.
Step 4 — Rank and Select
Sort all candidates by composite score descending. Buy the top N (determined by your position sizing rules). Rerank on a schedule — weekly or monthly for fundamental factors, daily for technical ones.
Quintile Analysis
A classic validation technique: split your universe into five groups (quintiles) by score. If the model works, Quintile 1 (highest score) should outperform Quintile 5 (lowest score) consistently across time. If it does not, the factors have no predictive power.
- A score converts many signals into a single number you can rank
- Normalizing signals (z-score) before combining prevents one factor from dominating
- Factor weights encode your hypothesis about what drives returns
- Always validate factor weights on out-of-sample data