DEV Community

penny penguin
penny penguin

Posted on

How I Rank 151 US Large-Caps With a 15-Line Model

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:

  1. "Buy NVDA, sell TSLA" — no methodology, no reproducibility, just vibes.
  2. 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")
Enter fullscreen mode Exit fullscreen mode

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.5 for 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_score or 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. yfinance is 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)

Collapse
 
arhancanli profile image
Arhan Canli •

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 filter composite_score > 0.5 can 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.