How it works
Everything you need to understand the picks, scores, and stats.
How a pick is made
Each weekday morning at 9:30 AM ET, the scheduler runs through a two-pass process:
Pass 1 — bulk pre-screen. All ~500 S&P 500 tickers plus sector
ETFs are downloaded in a single batched request. Each stock is given a quick
pre-score based on 5-day momentum and volume. The top 60 advance to the next pass.
This keeps the detailed API calls to a manageable number while covering the
full index.
Pass 2 — full scoring. For each of the 60 candidates, 4 months
of price history plus fundamentals and news are fetched. Fourteen factors are computed
and combined into a single composite score (0.000–1.000).
All scores are then multiplied by a market regime discount (varies
by regime and VIX level) so candidates aren't over-scored when the broad market is
working against them. The highest scorer becomes the day's recommendation.
No AI or external service is involved. You can read and modify every piece
of logic in picker.py — especially WEIGHTS at the top
of the file, which you can edit freely to experiment.
The fourteen scoring factors
Each factor is normalised to 0–1 before being multiplied by its weight.
The weights shown are the baseline defaults in WEIGHTS —
the actual weight applied to any given stock may differ slightly because of the
two-mode blending described in the next section. Run python backtest.py
to see how weights perform historically.
12%
% price change over the last 5 trading sessions. Near-term price direction
is one of the strongest predictors of the next day's move.
100%+5% or more
50%flat (0%)
0%−5% or worse
10%
The stock's 5-day momentum
minus its sector ETF's 5-day momentum.
A stock up 3% in a sector that's up 5% is underperforming; one up 3% in a
sector down 1% is clearly leading. This filters out pure beta and rewards
genuine outperformance.
100%+5% above sector
50%in line with sector
0%−5% below sector
11%
14-day Relative Strength Index. Uses a
smooth asymmetric curve
(not discrete buckets) peaked at RSI 38 — the oversold-recovery zone where
bounce setups are most reliable. The curve falls gradually on both sides so
RSI 37 and RSI 39 score nearly the same.
100%RSI 38 (sweet spot)
76%RSI 25 — deeply oversold
81%RSI 50 — neutral
56%RSI 65 — trending but extended
32%RSI 80 — overbought
16%RSI 90 — extremely overbought
10%
Year-over-year earnings-per-share growth. Growing earnings give the market
a fundamental reason to push prices higher, even in the near term around
analyst updates and sentiment shifts.
100%+40% YoY or more
50%+10% YoY
0%−20% YoY or worse
8%
Where the current price sits relative to its 52-week high.
Academic research (George & Hwang 2004) shows stocks near their
52-week high systematically outperform — investors anchor on that level
and a breakout above it tends to attract momentum buying.
100%At or above 52-wk high (breakout)
88%Within 3% below the high
65%3–10% below the high
40%10–25% below the high
15%>25% below (deep in range)
7%
A smarter volume signal than raw volume ratio. Compares total volume
traded on up-days to total volume on down-days over the past 20 sessions
(an On-Balance Volume proxy). A ratio above 1.0 means buyers are
committing more size than sellers — a stronger confirmation of bullish intent
than simply "volume was high today."
95%Up-volume 3× down-volume or more
85%Up-volume 2× down-volume
72%Up-volume 1.5× down-volume
60%Roughly balanced (ratio ≈ 1.0)
20%Down-volume 2.5× up-volume
7%
Where the current price sits within the 20-day Bollinger Bands (2 std dev).
%B = 0 means at the lower band; %B = 1 means at the upper band.
Scored with a peak near the lower band (%B ≈ 0.15) — the oversold-bounce
zone — falling off toward the upper band.
100%%B ≈ 0.15 (near lower band) — sweet spot
70%%B = 0.00 (at lower band)
59%%B = 0.50 (middle)
30%%B = 0.75 (upper area)
0%%B = 1.00 (at upper band)
9%
Standard MACD (12, 26, 9). Scored on the histogram's direction and whether
a bullish crossover just occurred. A fresh crossover (histogram flipping from
negative to positive) is the strongest signal.
100%Fresh bullish crossover (just turned positive)
85%Histogram positive and growing
65%Histogram positive but slowing
50%Histogram negative but recovering
15%Histogram negative and falling
7%
Year-over-year revenue growth. Supports the earnings story — a company
growing its top line is harder to dismiss as a one-off beat.
100%+30% YoY or more
50%+10% YoY
0%−10% YoY or worse
5%
Today's volume ÷ 20-day average volume. A quick check for whether
today saw unusual activity. Given a lower weight than before because
OBV Ratio captures volume quality more precisely — this factor mainly
catches big single-day spikes that OBV might dilute.
100%2.5× average or more
38%1.0× (average day)
0%0.5× or less
4%
Today's open vs yesterday's close, expressed as a percentage. An
overnight gap-up on a stock already showing positive momentum
confirms that the thesis is playing out in real time. A gap-down
is a warning that the setup may be fading.
90%Gap-up >+1% (strong confirmation)
72%Gap-up +0.3% to +1%
55%Roughly flat (within ±0.3%)
35%Gap-down −0.3% to −1%
15%Gap-down >−1% (bearish signal)
4%
How far (%) the price sits from its 50-day simple moving average.
Being near the SMA scores best — the stock is either setting up a
breakout above it, or a mean-reversion bounce off it. Being far extended
above it is a pullback risk.
100%Within ±5% — breakout/bounce zone
65%5–15% above or below
25%>15% extended either direction
3%
How many days until the next earnings announcement. Earnings create
near-term catalysts: stocks often move sharply in the days before and
after a report. This rewards stocks with an imminent catalyst while
remaining neutral when no earnings are nearby.
90%Earnings in 1–3 days — imminent catalyst
70%Earnings in 4–7 days
65%Earnings just reported (1–3 days ago)
40%No near-term earnings
3%
Short interest as a fraction of float. High short interest combined with
upward price momentum elevates short-squeeze risk, which can produce
outsized 1-day returns as shorts are forced to cover. Without price
confirmation, high short interest on a falling stock is not a positive signal.
88%>20% short float and price moving up
72%10–20% short float and price moving up
52%5–10% short float (direction neutral)
45%<5% short float — minimal squeeze risk
55%>20% short float but price falling
Two-mode weight blending
The same stock can be a great buy for two completely different reasons: it might
be a trending breakout (strong momentum, near 52-week high, high RSI)
or an oversold bounce (deeply oversold RSI, near lower Bollinger band,
price below SMA). The "right" weights for each case are different — RSI matters much
more for a bounce candidate, while 52-week high proximity matters much more for a
trend candidate.
To handle this, the picker computes a trend-mode score (0–1) for
each stock based on four signals: whether it's above its 50-day SMA, its 5-day
momentum direction, its RSI level (>60 = trending, <45 = oversold), and its
proximity to the 52-week high. The final weights used to score that stock are then
linearly blended between two pre-set weight tables:
Trend weights (mode = 1.0)
Emphasise 52-week high proximity (13%), momentum (16%), MACD (11%), and OBV ratio (9%).
RSI and Bollinger carry very low weight because a trending stock is expected to
have an elevated RSI and price near the upper band.
Mean-reversion weights (mode = 0.0)
Emphasise RSI setup (17%), Bollinger %B (12%), and sector-relative momentum (9%).
52-week high proximity is de-emphasised because a bounce candidate is expected to
be well below its high.
Most stocks fall somewhere in between and receive a blend. The baseline
WEIGHTS shown in the factor table above are the starting point for
the interpolation, not the final weights — but they're a reasonable representation
of the average effective weight across all candidates on a typical day.
Market regime & VIX filter
Before scoring any individual stock, the picker checks the broad market using
SPY vs its 50-day SMA and 5-day momentum. This produces a regime classification
and a base score multiplier applied to every candidate equally.
The VIX fear gauge then adds a further overlay on top.
Step 1 — SPY regime (base multiplier)
Bullish ×1.00
SPY above SMA-50 and 5d momentum ≥ −0.5%. No discount — full signal strength.
Neutral ×0.95
Mixed signals. Mild 5% discount reflects reduced conviction.
Bearish ×0.85
SPY >3% below SMA-50 and falling. 15% discount — market headwind overrides individual signals.
Step 2 — VIX overlay (additional multiplier, applied on top of Step 1)
Normal — no change
VIX ≤ 25. Market volatility is within a typical range. No additional discount.
Elevated ×0.90
VIX 25–30. Heightened uncertainty. Extra 10% discount stacked on top of the regime multiplier.
High ×0.80
VIX 30–40. Significant fear in the market. Extra 20% discount applied.
Extreme ×0.70 🚨
VIX > 40. Panic mode. Extra 30% discount applied and a "skip today" warning is shown — consider waiting for volatility to subside.
The two discounts stack multiplicatively. For example, a Neutral regime (×0.95) combined
with Elevated VIX (×0.90) gives an effective multiplier of 0.95 × 0.90 = ×0.855.
The multiplier compresses scores downward so the relative ranking of stocks
doesn't change, but the absolute confidence level will be lower on high-volatility days.
Market Pulse gauge
The Market Pulse gauge on the home page shows a composite sentiment score
from 0 to 100, where 0 is Extreme Fear and 100 is Extreme Greed. It is
a quick read on how the broad market "feels" right now, independent of any
individual stock pick.
The score is computed from three components each time the page loads:
VIX level
35%
Inverted: low VIX = greed, high VIX = fear.
Scaled so VIX ≤ 10 → score 100 and VIX ≥ 40 → score 0.
If VIX data is unavailable, this component is omitted and its weight redistributed.
SPY vs 50-day SMA
30%
How far SPY sits above or below its 50-day moving average.
SPY 5% above SMA → score 100 (greed); 5% below → score 0 (fear).
Values within that range are interpolated linearly.
SPY 5-day momentum
35%
SPY's % price change over the last 5 trading sessions.
Positive momentum contributes toward greed; negative toward fear.
Carries the highest weight because it's the most responsive to recent sentiment shifts.
Why are there two VIX numbers? You may notice the VIX value shown
in the Market Pulse gauge differs from the VIX recorded on a pick card. That's
expected: the pick-card VIX was captured at the moment the scheduler
ran that morning and is stored permanently with the pick. The Market Pulse gauge
fetches VIX live each time the page loads, so it reflects the current level —
which will differ as the trading day progresses. Older picks may also show no
VIX at all because VIX tracking was added after those picks were made.
Confidence levels
Confidence depends on two things: the absolute composite score
and the gap between the #1 pick and the #2 pick. A high score alone
isn't enough — if another stock was nearly as good, the pick wasn't decisive.
High
score ≥ 0.650
AND gap to #2 ≥ 0.040
Strong absolute score and a clear margin over the runner-up.
Both conditions must hold — a stock that scores 0.72 but barely edged
out a 0.71 competitor is Medium, not High.
Medium
score ≥ 0.500
OR score ≥ 0.650 but gap < 0.040
Either a solid score that fell just short of High, or a strong score
but a close race against the runner-up. The "close race" case means the
pick is correct but the conviction isn't as high.
Low
score < 0.500
(regardless of gap)
Today's best candidate, but no stock in the sample scored strongly —
often a sign of a bearish market regime, elevated VIX, or a broadly
mixed session. Treat with extra caution.
The score gap is shown on the pick card as "Score gap to #2." Confidence
reflects signal strength and decisiveness, not probability. A Low pick can
win and a High pick can lose — but over time, High picks should outperform Low ones.
Stats explained
Win / Loss / Flat
A pick is a Win if the stock closes
more than +0.1% above entry the next trading day.
A Loss is a close more than −0.1% below entry.
Flat is anything within ±0.1% — essentially unchanged.
The ±0.1% band excludes noise from bid/ask spread and minor rounding.
Win Rate
Wins ÷ resolved picks (Flat counts as resolved but not as a win).
A random baseline for any single stock on any day is roughly 50%, so
consistently above that suggests the scoring is adding value. Use
python backtest.py to check historical win rate before
trusting live numbers built from a small sample.
Avg Return / Pick
Arithmetic mean of each pick's next-day % change. Matters more than win
rate alone — a strategy that wins 60% of the time but loses 3% on each loss
and gains 0.5% on each win is still net negative. You want both a high win
rate and a positive average return.
Total Return
Sum of every pick's next-day % change. This is a simple cumulative tally,
not a compound return, so it slightly understates what you'd get if you
actually reinvested gains. Still a quick way to see if the system is net
positive over its lifetime.
Current Streak
Consecutive wins or losses counting backward from the most recent resolved
pick. Flat results break a streak. Streaks are psychologically compelling
but largely noise over small sample sizes — don't over-index on them.
Pending
A pick is pending while the next trading day hasn't closed yet, or on
market holidays. Results update automatically the next time any page loads —
no manual action needed.
Backtesting
backtest.py simulates running the daily picker over the past year
using a fixed set of stocks, then reports what the win rate and average return
would have been historically. This lets you validate changes to WEIGHTS
before relying on them with live picks.
Run it with: python backtest.py (defaults: 60 stocks, 1 year).
Or: python backtest.py --stocks 100 --days 504 for a wider, longer test.
Important limitation: the backtest uses only technical signals.
Historical P/E ratios, EPS growth, and revenue growth are not available from
free data sources on a point-in-time basis, so the backtest re-weights across
the six technical factors only. This means backtest results won't perfectly match
live performance, but they're still a useful sanity check for the technical side.
Known limitations
Even after pre-screening all 500 stocks, only the top 60 get full scoring.
The true best candidate of the day might finish in positions 61–500 of the
pre-screen occasionally — particularly if it has weak short-term momentum
but strong fundamentals.
Fundamental data (EPS growth, revenue growth) from Yahoo Finance can be
stale or missing for some tickers. When a figure is absent,
a neutral default is applied rather than skipping the stock. Check
picker.py → get_stock_data() to see exactly how missing values
are handled.
The weights were set by judgment, not by optimising against historical data.
Run the backtest and experiment with WEIGHTS in picker.py
to see whether different allocations improve your win rate over time.
Not financial advice. This tool is a personal project for learning
about technical analysis and Python. Past performance of the scoring algorithm does
not predict future results. Never invest money based solely on any automated tool,
including this one.