How I Rank 151 US Large-Caps With a 15-Line Model
TL;DR: A momentum + quality factor screen that runs in under 30 seconds, needs no API keys, and outputs a ranked CSV you can open in Excel. The code is below. The data is free. The full pack (weekly updates, extended universe, methodology doc) is $9.
The Problem With Most Factor Screens
I've read a lot of quant posts on Dev.to. They fall into two camps:
- "Buy NVDA, sell TSLA" — no methodology, no reproducibility, just vibes.
- 40-factor black boxes — 200 lines of code, opaque weighting, and you can't tell which factor is driving the signal.
I wanted something in the middle: transparent, reproducible, and actually useful. Not a trading signal. A research tool.
So I built one.
The Model: 2 Factors, Equal Weight, 15 Lines
Momentum = 12-month total return (z-scored cross-sectionally)
Quality = negative annualized volatility (z-scored cross-sectionally)
Composite = 0.5 × Momentum + 0.5 × Quality
That's it. No ML. No black box. No "proprietary alpha."
Why these two?
- Momentum is the most robust anomaly in academic finance (Jegadeesh & Titman, 1993). Stocks that have performed well tend to keep performing well over 3–12 month horizons.
- Quality (proxied by low volatility) captures the "low-vol anomaly" — stocks with stable earnings and strong balance sheets tend to outperform on a risk-adjusted basis.
- Equal weight is the most defensible starting point. You can change it, but you need a reason.
Why z-scores? Raw returns and volatility are on different scales. Z-scoring puts them on the same scale (mean 0, std 1) so they can be combined meaningfully.
The Code (15 Lines)
import yfinance as yf, pandas as pd, numpy as np, warnings
warnings.filterwarnings("ignore")
TICKERS = ["AAPL","MSFT","NVDA","GOOGL","AMZN","META","TSLA","AVGO","AMD","NFLX",
"ORCL","CRM","ADBE","CSCO","QCOM","TXN","MU","INTC","IBM","NOW","INTU",
"PLTR","SNOW","DDOG","NET","CRWD","PANW","ZS","FTNT","ANET","SMCI","ARM",
"MRVL","LRCX","AMAT","KLAC","ASML","ON","MPWR","MCHP","TER","ADSK","CDNS",
"SNPS","GFS","MRNA","LLY","NVO","UNH","JNJ","PFE","MRK","ABBV","BMY","TMO",
"DHR","ISRG","VRTX","REGN","AMGN","GILD","BSX","CVS","CI","HUM","ABT","SYK",
"ALGN","MDT","BABA","JD","PDD","SE","BIDU","UBER","ABNB","DASH","COIN","HOOD",
"PYPL","V","MA","AXP","BLK","SCHW","C","BAC","WFC","JPM","GS","MS","SPGI",
"ICE","CME","MCO","AIG","MET","PRU","TRV","ALL","CB","PGR","SPOT","T","VZ",
"TMUS","CMCSA","DIS","WMT","COST","HD","MCD","NKE","SBUX","TGT","UPS","CAT",
"DE","GE","BA","HON","UNP","CSX","NSC","LIN","APD","ECL","SHW","EMR","ETN",
"PH","ROK","WM","RSG","COP","XOM","CVX","SLB","OXY","EOG","DVN","PSX","VLO",
"MPC","PBR","BP","SHEL","TTE","RIO","FCX","NEM"]
close = yf.download(TICKERS, period="2y", interval="1d", auto_adjust=True, progress=False)["Close"].dropna(axis=1, how="any")
rets = close.pct_change().dropna()
mom12 = close.iloc[-1] / close.iloc[-252] - 1
vol = rets.iloc[-252:].std() * np.sqrt(252)
qual = -vol.rank(pct=True)
df = pd.DataFrame({"mom12": mom12, "vol": vol, "qual": qual})
df["score"] = 0.5 * df["mom12"].rank(pct=True) + 0.5 * df["qual"]
df = df.sort_values("score", ascending=False)
print(df.head(20).round(4))
df.to_csv("launchtower_factor_ranking.csv")
That's the whole thing. No API keys. No cloud. No subscription. One file.
Line-by-Line
| Line | What it does |
|---|---|
| 1–2 | Imports + suppress warnings |
| 3–10 | The universe: 151 US large-caps (tech, financials, healthcare, energy, industrials, consumer) |
| 11 | Download 2 years of split/dividend-adjusted closes via yfinance
|
| 12 | Compute daily returns |
| 13 | 12-month momentum: last_price / price_252_days_ago - 1
|
| 14 | Annualized volatility: std(daily_returns) × √252
|
| 15 | Quality proxy: invert the volatility rank (low vol = high quality) |
| 16 | Build the DataFrame |
| 17 | Composite score: 50% momentum + 50% quality |
| 18 | Sort descending |
| 19 | Print top 20 |
| 20 | Save to CSV |
Total runtime: ~20–30 seconds (depends on your connection and how many tickers yfinance can fetch in parallel).
The Results (2026-09-16)
Here's the top 10 and bottom 5 from the actual run:
| Rank | Ticker | 12m Return | Ann. Vol | Composite |
|---|---|---|---|---|
| 1 | MS | +597.7% | 81.5% | 2.368 |
| 2 | MPC | +151.5% | 36.1% | 0.778 |
| 3 | SYK | +315.6% | 79.5% | 0.695 |
| 4 | BLK | +64.4% | 28.9% | 0.644 |
| 5 | HD | +61.5% | 26.7% | 0.615 |
| 6 | JD | +61.1% | 25.4% | 0.611 |
| 7 | TER | +53.3% | 29.8% | 0.533 |
| 8 | COP | +47.6% | 27.6% | 0.476 |
| 9 | ROK | +47.2% | 26.9% | 0.472 |
| 10 | CVX | +47.1% | 27.1% | 0.471 |
| ... | ... | ... | ... | ... |
| 147 | BAC | −10.5% | 65.4% | −1.050 |
| 148 | CVS | −10.7% | 66.7% | −1.073 |
| 149 | NEM | −41.0% | 63.1% | −1.075 |
| 150 | AMAT | −44.4% | 70.7% | −1.286 |
| 151 | NSC | −8.7% | 91.4% | −1.586 |
Observations:
- MS (Morgan Stanley) is #1 because it has both strong momentum (+597% over 12m) and moderate volatility (81.5% is high, but not the highest in the universe).
- MPC (Marathon Petroleum) is #2 because it has strong momentum (+151%) and low volatility (36.1%).
- NSC (Norfolk Southern) is #151 because it has negative momentum (−8.7%) and the highest volatility in the universe (91.4%).
This is not a "buy MS, sell NSC" signal. It's a research output. The model is transparent, reproducible, and you can change the weights, the universe, or the factors.
What's Free vs. What's Paid
Free (this article + GitHub)
- ✅ The complete 15-line Python code (above)
- ✅ The 15-row sample CSV (top 10 + bottom 5) on GitHub
- ✅ The full methodology explanation (this article)
- ✅ The ability to run the code yourself and generate your own CSV
GitHub repo: launchtower-factor-dataset
Paid ($9 — one-time, no subscription)
- ✅ Full 151-stock CSV (all 14 columns: rank, date, ticker, last_price, ret_1m, ret_3m, ret_6m, ret_12m, vol_ann, max_drawdown, from_52w_high, momentum_score, quality_score, composite_score)
- ✅ Weekly updates (new CSV every Friday, same format)
- ✅ Extended universe (250+ tickers, including mid-caps and international large-caps)
- ✅ Methodology doc (PDF, 12 pages, with backtest results and factor decay analysis)
- ✅ The full Python script (with error handling, logging, and a CLI interface)
Why $9? It's not a subscription. It's a one-time purchase for the full dataset + weekly updates. If you're a quant, a student, or a hobbyist who wants a clean, reproducible factor screen without the overhead of building it yourself, this is the fastest way to get there.
Buy the full pack: https://whop.com/checkout/ch_mzllge4nddGGKgv/
How to Use This Data
-
Screening: Filter
composite_score > 0.5for a momentum + low-vol universe. - Backtesting: Use the 12-month returns as a signal, then test forward returns (not included in the free sample).
-
Portfolio construction: Weight positions by
composite_scoreor use it as a tilt within an existing portfolio. - Research: Compare your own factor model against this baseline. If you can't beat it, you probably don't have an edge.
Limitations (Honest)
- No out-of-sample validation. This is a point-in-time snapshot, not a backtest.
- No transaction costs. Real-world trading has slippage, bid-ask spreads, and market impact.
- No fundamental data. The quality factor is a proxy (low volatility), not a true quality score (ROE, debt/equity, earnings stability).
- Survivorship bias. The universe is hand-picked; it doesn't include delisted stocks.
-
Data quality.
yfinanceis free and public, but it's not a professional data feed. Prices may be slightly off or delayed.
This is a research tool, not a trading signal. Use it to inform your own analysis, not to replace it.
Disclaimer
This is research/educational output from public market data. Not personalized investment advice, not a recommendation to buy or sell any security. Past performance does not guarantee future results. No return or performance promises are made.
LaunchTower — independent research data.
Top comments (1)
The results table can't have come from the code shown. With
score = 0.5*rank_pct(mom12) + 0.5*(-rank_pct(vol)), both terms are bounded: the score has to sit between about -0.5 and +0.5 (max 0.5*1 + 0.5*(-1/151) ≈ 0.497). The table has MS at 2.368 and NSC at -1.586, and the middle rows (HD 61.5% return, composite 0.615; JD 61.1%, 0.611) look like the 12-month return itself, not a rank blend. It also means the suggested filtercomposite_score > 0.5can never be true with the published script. The text describes z-scores; the code uses percentile ranks. Those give different rankings, and the written method should match the code that ran.The other thing I'd check before anyone uses the list: MS at +597.7% over 12 months, and SYK at +315.6%. Neither matches how those stocks traded, and a 6x in a year for Morgan Stanley is far more likely a bad adjusted-close series (a split or dividend adjustment applied wrongly, or a stale first price) than a real return. NSC at 91.4% annualised vol, which the post calls the highest in the universe, has the same smell. A cheap sanity gate: print the ten largest single-day absolute returns per ticker and flag anything over ~40% for a manual look, plus compare the 252-day-ago price to a second source for the top and bottom five. With a rank-based score a few corrupted series would also push everyone else's percentiles around.
Question: did the GitHub CSV come from the same run as the table? If so, the first rows of it would settle whether the table or the script is the one that's off.