Originally published on medium.com

Most trading content sells you a signal. Buy the dip. Watch the RSI. Follow the 200-day average.
Almost none of it shows you the actual number behind the claim.
So I pulled ten years of daily S&P 500 data through EODHD, built nine of the most common "predictive" signals people talk about, and measured each one against what actually happened to prices a month later. No cherry-picked date range. No backtest that starts conveniently after a crash. Just 2,262 trading days of real numbers.
The result wasn't what most finance content implies.
Type "what predicts stock returns" into YouTube and you'll get a wall of confident answers. Momentum. RSI. Volume spikes. The 200-day moving average. Each one presented like a rule.
The tell is that none of these videos show a correlation coefficient. None show an R². They show a chart with an arrow pointing up, and a strategy that "would have" made money if you'd started at exactly the right week.
That's not analysis. That's a story fitted backward onto data that already happened.
I wanted to know something simpler: across a full decade, how much of next month's return can you actually explain using information you had today?
I picked nine candidate predictors, all calculable from ordinary price and volume data, no proprietary indicators:
For each day in the dataset, I recorded the predictor's value and the S&P 500's actual return over the following 21 trading days, then measured the correlation and R² across the full sample.
Here's the setup, using EODHD's historical prices endpoint for SPY:
import requests
import pandas as pd
import numpy as np
API_TOKEN = "YOUR_API_TOKEN"
url = f"https://eodhd.com/api/eod/SPY.US"
params = {
"api_token": API_TOKEN,
"from": "2015-08-01",
"to": "2025-08-30",
"fmt": "json"
}
data = requests.get(url, params=params).json()
df = pd.DataFrame(data)
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date").reset_index(drop=True)
df["adj_close"] = df["adjusted_close"]
horizon = 21
df["fwd_ret_1m"] = df["adj_close"].shift(-horizon) / df["adj_close"] - 1
df["mom_3m"] = df["adj_close"] / df["adj_close"].shift(63) - 1
df["drawdown_63d"] = df["adj_close"] / df["adj_close"].rolling(63).max() - 1
Ten years of daily bars, no gaps, adjusted for splits and dividends, in one call. Building the momentum, volatility, and RSI features on top of that took maybe 30 lines of pandas.
Here's the full ranking, sorted by R² (how much of the variance in next-month returns each signal explains on its own):
| Predictor | Correlation | R² |
|---|---|---|
| Drawdown from 63-day high | -0.26 | 6.6% |
| 21-day realized volatility | +0.23 | 5.4% |
| 6-month momentum | -0.19 | 3.5% |
| 3-month momentum | -0.19 | 3.5% |
| Distance from 200-day MA | -0.18 | 3.4% |
| 1-month momentum | -0.15 | 2.3% |
| 12-month momentum | -0.10 | 0.9% |
| RSI(14) | -0.09 | 0.8% |
| Recent volume surge | -0.01 | 0.0% |
A few things jump out.
First, nothing here comes close to explaining most of next month's return. The best single predictor accounts for roughly 6.6% of the variance. The other 93% is noise, news, and everything else that moves markets.
Second, the signs are the opposite of what "momentum trading" content usually implies. Stocks that had fallen hard over the past 3 to 6 months tended to bounce, not continue falling. Stretched-above-the-200-day-average stocks tended to underperform, not keep running.
Breaking distance-from-200-day-MA into quintiles makes the pattern concrete:
| Quintile (low to high) | Avg next-month return |
|---|---|
| Deeply below the average | 1.89% |
| Below the average | 1.81% |
| Near the average | 1.22% |
| Above the average | 0.83% |
| Deeply above the average | 0.53% |
The cheaper a stock looked relative to its own recent trend, the better it tended to do over the next month. That's mean reversion, not momentum.
Third, and this is the part most content skips entirely: volume spikes told me nothing. Recent volume relative to its own average had a correlation of -0.01. If you've seen a strategy built around "unusual volume precedes a move," this decade of S&P 500 data doesn't support it as a standalone signal.
I checked how correlated the nine predictors were with each other, and this is where the real story sits.
Drawdown, distance from the 200-day average, and 3 to 6 month momentum were all correlated with each other at 0.6 to 0.88. They're not nine independent signals. They're one underlying idea, measured nine slightly different ways: has the price recently fallen relative to its own history.
Combining the two strongest signals, drawdown and volatility, into a single model only pushed R² from 6.6% to 6.8%. Adding a second measurement of the same underlying phenomenon barely moved the number.
There's also a regime problem. I split the sample at 2020 and re-ran the drawdown correlation. Pre-2020, it was -0.14. Post-2020, it jumped to -0.28. The signal roughly doubled in strength, which lines up with two of the sharpest crash-and-recover episodes in modern market history: March 2020 and the 2022 bear market bottom.
That's not necessarily a stable law of markets. It might be three or four specific events doing most of the work in a ten-year sample.
I ran this whole test with one data provider. EODHD's historical prices endpoint gave me adjusted closes going back a full decade for a single ticker, with splits and dividends already handled, so I didn't have to reconstruct adjusted prices myself.
The same setup extends without much extra work:
If you're testing signals across a decade of history, get 10% off EODHD here: eodhd.com
You don't need the Python setup above to run this test yourself. EODHD ships an MCP server, which means Claude can call its endpoints directly, no API glue code required on your end.
Once it's connected, Claude sees EODHD's tools the same way it sees any other tool: historical prices, technical indicators, fundamentals, and more, callable directly from a plain-language request.
This is the prompt that reproduces the analysis in this article:
Pull 10 years of daily adjusted close and volume data for SPY.US from EODHD. Build these features for each day: 1, 3, 6, and 12-month momentum, 21-day realized volatility, distance from the 200-day moving average, RSI(14), 63-day drawdown from the trailing high, and 5-day average volume relative to the 21-day average. Calculate the forward 21-trading-day return for each date. Report the Pearson correlation and R² of each feature against that forward return, sorted by R², then break the strongest feature into quintiles and show the average forward return per quintile.
I ran that exact prompt against the connector to confirm it reproduces. Claude pulled the RSI series directly, no code, one tool call:
date rsi
2025-08-29 59.20
2025-08-28 65.11
2025-08-27 63.08
2025-08-26 61.76
2025-08-25 59.28
Same numbers you'd get from the pandas version above, just without maintaining the script yourself. For a one-off test, the connector is faster. For something you'll run weekly or want full control over, the Python version in the earlier section is worth keeping.
Want the reusable Claude skill I use for this kind of backtest? It's on my YouTube channel, where I walk through the full setup in Spanish.
The predictors above are about next-month noise, not what to hold for years. Different question, different tool: EODHD's screener endpoint instead of historical prices.
I ran one version of a long-term quality-income filter: market cap above $50B, positive EPS, dividend yield above 1.5%, US-listed common stock (excluding thin-volume OTC ADRs and preferred shares, which show up in the raw results but aren't useful here).
| Ticker | Company | Sector | Market cap | EPS | Dividend yield |
|---|---|---|---|---|---|
| JPM | JPMorgan Chase | Financials | $951B | 23.57 | 1.69% |
| JNJ | Johnson & Johnson | Healthcare | $646B | 8.68 | 1.97% |
| XOM | Exxon Mobil | Energy | $644B | 7.79 | 2.61% |
| ABBV | AbbVie | Healthcare | $451B | 3.54 | 2.65% |
| BAC | Bank of America | Financials | $436B | 4.41 | 1.83% |
| CVX | Chevron | Energy | $396B | 10.50 | 3.49% |
| KO | Coca-Cola | Consumer Defensive | $386B | 3.35 | 2.34% |
| MRK | Merck | Healthcare | $366B | 1.25 | 2.22% |
| UNH | UnitedHealth | Healthcare | $353B | 15.48 | 2.27% |
| MS | Morgan Stanley | Financials | $337B | 12.37 | 1.93% |
| PG | Procter & Gamble | Consumer Defensive | $335B | 6.64 | 2.98% |
Notice what's missing. NVDA, AAPL, and MSFT all fail this filter, their dividend yields sit under 1%. That's not an oversight, it's what happens when you screen for cash-flow-plus-dividend instead of growth. Change the filter and the list flips entirely.
This screen also says nothing about balance sheet quality, margin trends, or how long each company has actually grown its dividend. Getting that requires pulling fundamentals per ticker and checking debt-to-EBITDA and margin history individually, a heavier task than a single screener call.
Treat a list like this as a starting shortlist to research further, not a buy list.
Mean reversion after a real drawdown is the closest thing to a repeatable pattern in this data. Stocks that fell hard relative to their own recent history tended to recover some of that ground over the following month.
Momentum, RSI, and volume spikes, used alone, explained almost nothing. If a strategy leans entirely on one of these as its edge, this decade of S&P 500 data doesn't back it up.
Combining redundant signals doesn't multiply your edge. Nine indicators sounds more rigorous than one. Here, most of them were measuring the same thing from a different angle.
None of this is investment advice, and an R² of 6.6% is not a trading system. It's a starting point for asking better questions about your own data, instead of trusting a chart with an arrow on it.
Does momentum actually predict stock returns? In this 10-year S&P 500 sample, 3, 6, and 12-month momentum were all negatively correlated with next-month returns. Stocks that had run up tended to slow down, not keep accelerating. That's the opposite of what "momentum investing" content usually claims for a single-month horizon.
Is RSI a good predictor on its own? Not by this measure. RSI(14) explained under 1% of the variance in next-month returns. It moved in the same direction as the other mean-reversion signals, just much more weakly.
What's the strongest signal in this test? Drawdown from the trailing 63-day high, at an R² of 6.6%. It's still a weak signal on its own, and it's highly correlated with several of the other predictors, so it's not really nine independent ideas confirming each other.
Can I run this on a different stock or index? Yes. Swap the ticker in the code or the MCP prompt above. The same feature set works for any symbol EODHD covers, including non-US markets and crypto.
Is a 6.6% R² good enough to trade on? No. It means the signal explains a small slice of next-month variance, with the other 93%+ coming from everything else. Treat it as one input, not a system.
Looking for technical content for your company? I can help — LinkedIn · kevinmenesesgonzalez@gmail.com
More breakdowns like this one: kevinmeneses.com