Who wins in size and momentum?
If I sort stocks by size and momentum, who wins when I compare returns in excess of the market? The reason I am using size and momentum is that momentum has this famous reputation as an anomaly that performs quite well, but it comes with high turnover, and some of these stocks may be tiny stocks that can come with high trading costs. That is why I am using size to get a broader picture of performance across sizes, because that is also a limitation: all these returns are before trading costs. Looking at size can show whether, even among big companies, you still earn a premium or excess return by focusing on the winners (stocks with the highest past return), since big companies are less likely to incur high trading costs than small companies.
The data
I use Kenneth French’s data: 25 groups sorted by size and momentum, with returns from 12 months to 2 months before, using cutoffs from New York Stock Exchange (NYSE) stocks for both. The groups are re-sorted every month, and for each group I take the value-weighted return in the month after sorting, then compute an annual return for each group. The following animation shows how stocks are assigned to each group.
\[ \begin{aligned} \text{MOM}_{i,t} &= (1 + r_{i,t-12})(1 + r_{i,t-11}) \cdots (1 + r_{i,t-2}) - 1\\[4pt] \text{winners: } & \text{MOM}_{i,t} \text{ above the 80th percentile of NYSE stocks}\\ \text{losers: } & \text{MOM}_{i,t} \text{ below the 20th percentile of NYSE stocks}\\ \text{big: } & \text{size}_{i,t-1} \text{ above the 80th percentile of NYSE stocks}\\ \text{small: } & \text{size}_{i,t-1} \text{ below the 20th percentile of NYSE stocks}\\ & \text{the winners among the big stocks}\\ & \quad \text{are big and winners}\\[4pt] \text{with: } & \text{MOM}_{i,t} = \text{the momentum of stock } i \text{ for month } t\\ & r_{i,t} = \text{the return of stock } i \text{ in month } t\\ & \text{size}_{i,t-1} = \text{the market cap of stock } i\\ & \quad \text{at the end of month } t-1 \end{aligned} \]
- An NYSE stock
- An AMEX or NASDAQ stock
- A larger dot is a larger stock
- Winners among the big stocks
Results
For each year from 1927 to 2025, the animation shows how each group performs against the market; at the end, it shows each group’s average yearly excess return and the proportion of years it beat the market.
- Each group: its return in the year minus the market's, in percentage points
- Beat the market by more than 2 points
- Within 2 points of the market
- Trailed the market by more than 2 points
- No data that year
Winners are indeed winners. Small-cap winners have the highest excess return, but you should not take that as evidence to trade only small caps, because they can also come with high trading costs. But there is still a winner effect for the big ones, too, as the winners among the big stocks beat the market in 60 of the 99 years, by 3.8 points a year on average.
And the losers are indeed losers, as they trail the market on average in every size group except the smallest.
Conclusion
So the conclusion here is that yes, momentum does beat the market. It is not surprising, since it is a factor that the market factor cannot explain, so there is an anomaly in that sense, or an alpha. And it is interesting to see that regardless of the size of the groups, the winners beat the market on average, also among the big companies.
Appendix: the code
import io
import urllib.request
import zipfile
from itertools import takewhile
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
LIBRARY = "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/"
YEARS = list(range(1927, 2026))
def download(name):
with urllib.request.urlopen(LIBRARY + name) as response:
archive = zipfile.ZipFile(io.BytesIO(response.read()))
return archive.read(archive.namelist()[0]).decode("latin-1")
def monthly_table(text):
lines = text.splitlines()
start = next(number for number, line in enumerate(lines) if line.startswith(","))
rows = list(takewhile(str.strip, lines[start:]))
table = pd.read_csv(io.StringIO("\n".join(rows)), index_col=0)
table.index = pd.PeriodIndex(table.index.astype(str).str.strip(), freq="M")
table.columns = table.columns.str.strip()
return table.replace([-99.99, -999], np.nan) / 100
def one_year(monthly):
complete = monthly.groupby(monthly.index.year).count() == 12
compounded = (1 + monthly).groupby(monthly.index.year).prod() - 1
return compounded.where(complete).loc[YEARS]
groups = monthly_table(download("25_Portfolios_ME_Prior_12_2_CSV.zip"))
factors = monthly_table(download("F-F_Research_Data_Factors_CSV.zip"))
market = factors["Mkt-RF"] + factors["RF"]
excess = one_year(groups).sub(one_year(market), axis=0) * 100
average = excess.mean().to_numpy().reshape(5, 5)
share = ((excess > 0).sum() / excess.notna().sum()).to_numpy().reshape(5, 5)
big_winners = excess["BIG HiPRIOR"]
print(f"Winners among the big stocks beat the market in {(big_winners > 0).sum()} "
f"of {big_winners.notna().sum()} years, "
f"by {big_winners.mean():.1f} points a year on average")
print("Average a year minus the market, points (rows small to big, columns losers to winners):")
print(pd.DataFrame(average, index=["Small", "2", "3", "4", "Big"],
columns=["Losers", "2", "3", "4", "Winners"]).round(1).to_string())
BG, INK = "#FCEFE3", "#1f1f1f"
GREEN, ORANGE, RED = "#2f8f4e", "#d2822b", "#c0392b"
fig, axis = plt.subplots(figsize=(6.4, 6.0))
for size in range(5):
for momentum in range(5):
value = average[size, momentum]
color = GREEN if value > 2 else RED if value < -2 else ORANGE
axis.add_patch(plt.Rectangle((momentum, size), 0.96, 0.96, color=color))
axis.text(momentum + 0.48, size + 0.56, f"{value:+.1f}".replace("-", "−"),
ha="center", va="center", color="white", fontsize=13, fontweight="bold")
axis.text(momentum + 0.48, size + 0.3, f"{share[size, momentum]:.0%} of years",
ha="center", va="center", color="white", fontsize=8.5)
axis.set_xlim(-0.04, 5)
axis.set_ylim(-0.04, 5)
axis.set_xticks(np.arange(5) + 0.48, ["Losers", "2", "3", "4", "Winners"])
axis.set_yticks(np.arange(5) + 0.48, ["Small", "2", "3", "4", "Big"])
axis.set_xlabel("Momentum: return from 12 to 2 months before", color=INK)
axis.set_ylabel("Size: market cap", color=INK)
axis.set_title("Return a year minus the market, in percentage points, 1927 to 2025",
color=INK, fontsize=11)
axis.tick_params(length=0, colors=INK)
for side in axis.spines.values():
side.set_visible(False)
fig.patch.set_facecolor(BG)
axis.set_facecolor(BG)
fig.savefig("size_momentum.png", dpi=200, bbox_inches="tight")Winners among the big stocks beat the market in 60 of 99 years, by 3.8 points a year on average
Average a year minus the market, points (rows small to big, columns losers to winners):
Losers 2 3 4 Winners
Small 0.2 6.0 8.9 10.8 11.2
2 -3.4 2.3 4.1 6.3 9.3
3 -4.6 0.9 2.0 3.5 7.6
4 -4.5 -0.6 1.3 3.0 7.5
Big -3.5 -2.2 -0.8 1.0 3.8
Read next
Disclaimer: historical returns before trading costs. Not investment advice.
Data and sources: Kenneth R. French, Data Library, 25 Portfolios Formed on Size and Momentum (5 x 5) and the Fama/French 3 Factors, downloaded on 29 October 2026 (built from the Center for Research in Security Prices (CRSP) database of September 2026). Narasimhan Jegadeesh and Sheridan Titman, “Returns to buying winners and selling losers: Implications for stock market efficiency”, Journal of Finance (1993).