
Every AI stock picker promises you the next NVIDIA.
None of them shows you why.
You get a score from 1 to 10, a green badge and a "Buy" label. What you don't get is the data behind it, the weights, or a way to check if the model is just chasing whatever went up last quarter.
If you're:
This matters.
The best-known AI stock picker apps work the same way. Danelfin, Kavout and Prospero.ai all take thousands of stocks, run them through a proprietary model and hand you a ranking.
That's useful. But there are limits:
The alternative most people try next is worse. They open ChatGPT and ask "what are the best stocks to buy right now?"
The model answers with confidence. Sometimes the P/E ratios it quotes are from 2023. Sometimes they're made up.
Neither option gives you a system you can audit.
Strip away the marketing and every AI stock picker does three things:
The model is the part everyone talks about. The data is the part that breaks.
Stock picking doesn't need more intelligence. It needs better inputs.
Before writing code, it's worth being honest about the trade-off.
| Option | Data you see | Can you change the model? | Cost |
|---|---|---|---|
| Danelfin | Score + top factors | No | Free tier + paid plans |
| Kavout | Score on 9,000+ US stocks | No | Free tier + paid plans |
| Prospero.ai | Signals in an app | No | Free app |
| ChatGPT alone | Whatever it remembers | Prompt only | Free / Plus |
| DIY with EODHD + LLM | Every number | Yes, fully | API plan + a few cents of LLM |
Best for: if you want a ready-made app, use one of the first three. If you want to know exactly why a stock ranks #1, and change it, build your own.
That's what we'll do now.
I use EODHD APIs for this kind of project because one API key covers every input the AI stock picker needs:
Same JSON format, same authentication, same rate limits.
๐ You can get your EODHD API key here and follow along.
Install the dependencies:
pip install requests pandas python-dotenv openai
Create a .env file with your keys:
EODHD_API_KEY=your_eodhd_key
OPENAI_API_KEY=your_openai_key
And a small helper to call any EODHD endpoint:
import os, json
from datetime import date, timedelta
import requests
import pandas as pd
from dotenv import load_dotenv
load_dotenv(override=True)
EODHD_KEY = os.environ["EODHD_API_KEY"]
BASE = "https://eodhd.com/api"
def get(path, **params):
params.update(api_token=EODHD_KEY, fmt="json")
r = requests.get(f"{BASE}/{path}", params=params, timeout=30)
r.raise_for_status()
return r.json()
We start with the 200 largest US stocks above $10B market cap. The Screener API returns up to 100 rows per call, so we paginate.
def build_universe(n=200):
rows = []
for offset in range(0, n, 100):
rows += get(
"screener",
filters=json.dumps([
["market_capitalization", ">", 10_000_000_000],
["exchange", "=", "us"],
]),
sort="market_capitalization.desc",
limit=100,
offset=offset,
)["data"]
df = pd.DataFrame(rows)[["code", "name", "sector", "market_capitalization", "adjusted_close"]]
return df.drop_duplicates("code").reset_index(drop=True)
Five features per stock. Each one maps to a classic factor:
| Feature | Factor | Source |
|---|---|---|
| ROE (TTM) | Quality | Fundamentals API |
| Revenue growth YoY | Growth | Fundamentals API |
| P/E vs sector median | Value | Fundamentals API |
| 6-1 month return | Momentum | EOD API |
| 30-day news sentiment | Sentiment | Sentiment API |
def fundamentals(ticker):
h = get(f"fundamentals/{ticker}.US", filter="Highlights")
return {
"roe": h.get("ReturnOnEquityTTM"),
"rev_growth": h.get("QuarterlyRevenueGrowthYOY"),
"margin": h.get("ProfitMargin"),
"pe": h.get("PERatio"),
}
def momentum(ticker):
"""6-month return, skipping the most recent month (classic 6-1 momentum)."""
start = (date.today() - timedelta(days=200)).isoformat()
px = pd.DataFrame(get(f"eod/{ticker}.US", **{"from": start}))["adjusted_close"]
if len(px) < 130:
return None
return px.iloc[-22] / px.iloc[-130] - 1
def sentiment(tickers):
"""Average news sentiment over the last 30 days."""
start = (date.today() - timedelta(days=30)).isoformat()
out = {}
for i in range(0, len(tickers), 20):
batch = [f"{t}.US" for t in tickers[i:i + 20]]
data = get("sentiments", s=",".join(batch), **{"from": start})
for sym, days in data.items():
if days:
out[sym.split(".")[0]] = sum(d["normalized"] for d in days) / len(days)
return out
Why skip the last month in momentum? Short-term returns tend to mean-revert. The 6-1 window is the standard way to capture trend without that noise.
Here's the real output of steps 1 and 2:

One ticker failed: VTI. It's an ETF, so it has no company fundamentals. The try/except skipped it and the run continued with 199 stocks.
No neural network here. Each feature becomes a percentile rank (0 to 1), multiplied by a weight. The sum is a score from 0 to 100.
WEIGHTS = {"momentum": 0.25, "quality": 0.25, "growth": 0.20, "value": 0.15, "sentiment": 0.15}
def score(df):
df = df[df["pe"].fillna(0) > 0].copy() # drop loss-makers and missing P/E
df["rel_pe"] = df["pe"] / df.groupby("sector")["pe"].transform("median")
ranks = pd.DataFrame({
"momentum": df["momentum"].rank(pct=True),
"quality": df["roe"].rank(pct=True),
"growth": df["rev_growth"].rank(pct=True),
"value": (-df["rel_pe"]).rank(pct=True),
"sentiment": df["sentiment"].rank(pct=True),
}).fillna(0.5)
for f, w in WEIGHTS.items():
df[f"s_{f}"] = ranks[f] * w * 100
df["score"] = df[[f"s_{f}" for f in WEIGHTS]].sum(axis=1).round(1)
return df.sort_values("score", ascending=False).reset_index(drop=True)
Two design choices matter here:
This is the result. 194 stocks survived the P/E filter:

And the same ranking as a chart, so you can see which factor drives each score:

SanDisk and Micron get there with strength on every factor. Recruit Holdings (RCRUY) is the opposite case: top-tier momentum and sentiment, but almost nothing from value. Its P/E is 3.16x its sector median.
That's the kind of insight you never get from a single score.
This is where most "AI stock picker" tutorials go wrong. They ask the LLM to choose stocks.
We do the opposite. The model has already picked. ChatGPT only gets the numbers for one stock and writes the thesis.
from openai import OpenAI
def explain(row):
facts = {
"ticker": row["code"], "name": row["name"], "sector": row["sector"],
"score": row["score"],
"momentum_6_1": round(row["momentum"], 3),
"roe_ttm": row["roe"], "revenue_growth_yoy": row["rev_growth"],
"profit_margin": row["margin"], "pe": row["pe"],
"pe_vs_sector_median": round(row["rel_pe"], 2),
"news_sentiment_30d": round(row["sentiment"], 3) if pd.notna(row["sentiment"]) else None,
}
prompt = (
"You are an equity analyst. Using ONLY the data below, write a 4-sentence "
"investment thesis: why the model ranks this stock highly, and the main risk. "
"Do not use any number that is not in the data. Ratios are decimals "
"(0.25 = 25%): write them as percentages.\n\n" + json.dumps(facts, indent=2)
)
resp = OpenAI().responses.create(model="gpt-5.4-mini", input=prompt)
return resp.output_text
The real output for the top 3:

Read it carefully, because there's a lesson in it.
The theses are grounded. Every number comes from our data. But the LLM still made two small mistakes:
No invented P/E ratios. No hallucinated earnings. Just formatting slips that a reviewer catches in seconds.
Constrain the LLM to your data and its errors become cosmetic instead of dangerous.
The most useful output wasn't the #1 pick. It was this chart:

Technology is 20.6% of the universe. It's 85% of the top 20 picks (17 out of 20).
The model isn't broken. Memory, storage and semiconductor equipment stocks lead on momentum, growth and sentiment at the same time right now. A factor model rewards exactly that.
But if you bought the top 20 blindly, you wouldn't own a diversified portfolio. You'd own one big bet on the AI hardware cycle.
There's a second, smaller catch. ASML appears twice in the top 20: ASML (the US listing) and ASMLF (the OTC line). Same company, two tickers. Deduplicating by company is a one-line fix once you see it.
A black-box AI stock picker would have shown you neither problem.
From here you can build:
โ Is there a free AI stock picker? โ Yes. Prospero.ai offers a free app, and Danelfin and Kavout have free tiers with limited rankings. If you want full control, the Python script in this article is free to run. You only pay for the data API and a few cents of LLM usage.
โ Can ChatGPT pick stocks? โ Not reliably on its own. Without live data, ChatGPT can quote outdated or invented numbers. It works well as an explanation layer when you give it real, current data from an API, like we did in Step 4.
โ Are AI stock pickers accurate? โ No AI stock picker can guarantee returns. Factor models like this one rank stocks on characteristics that have historically mattered (momentum, quality, value), but the only way to judge accuracy is a proper backtest with point-in-time data.
โ What data does an AI stock picker need? โ At minimum: price history for momentum, fundamentals (ROE, growth, valuation) and ideally news sentiment. EODHD covers all of them through one API key.
Most investors don't need a smarter AI stock picker.
They need one they can see inside.
In the next article of this series, we go from ranking hundreds of stocks to analyzing one in depth: AI stock analysis with an LLM and real financial statements.
๐ Start building with EODHD APIs: Screener, Fundamentals, EOD prices and News Sentiment with one key. Use the code KEVIN10 at checkout for 10% off.
Disclaimer: This article is for educational purposes only and is not investment advice. The rankings shown are the output of an example model on November 1, 2026.
Looking for technical content for your company? I can help โ LinkedIn ยท kevinmenesesgonzalez@gmail.com