Be Greedy compresses the state of the broad US market into a single conviction score from −100 to +100. Here is exactly how — the inputs, the math, and the rules that decide whether your inbox hears from us at all. Nothing is hidden; the scoring engine is open source.
The inputs
Every weekday after the US close we pull the daily closing-price history of two things: the S&P 500 (the market) and the VIX (its 30-day implied-volatility "fear gauge"). Data comes from Stooq, falling back to Yahoo Finance if Stooq rate-limits the runner — both keyless, so there's no paid data dependency. We keep roughly two years of history so the trailing windows below are always fully populated.
The VIX is treated as a nice-to-have: if it can't be fetched, the score is computed from the four price-based signals alone rather than failing the run.
The five signals
The market is scored on five orthogonal signals — each capturing a different facet of fear or greed, so no single noisy reading dominates. Every raw reading is passed through a piecewise-linear curve that maps it to a signed sub-score:
Sign convention. A positive sub-score means fear / cheapness and leans toward buying. A negative sub-score means greed / richness and leans toward trimming. Readings between the listed knots are linearly interpolated; beyond the ends they're clamped.
How far below its trailing 252-trading-day high the index sits. Deep declines put great businesses on sale.
drawdown % = (last_close − max(last 252 closes)) / max(last 252 closes) × 100| % off high | −40 | −30 | −20 | −12 | −7 | −3 | 0 |
|---|---|---|---|---|---|---|---|
| score | +55 | +46 | +33 | +20 | +9 | +3 | 0 |
Drawdown is always ≤ 0%, so this signal only ever adds to the buy side — a deliberate Buffett tilt toward acting on fear, not euphoria.
Wilder's Relative Strength Index over 14 days. Oversold readings flag panic selling; overbought readings flag crowd euphoria.
| RSI | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 |
|---|---|---|---|---|---|---|---|---|---|
| score | +35 | +25 | +13 | +4 | 0 | −4 | −13 | −25 | −35 |
How far price has stretched from its 200-day simple moving average. Far below trend is bargain territory; far above is stretched.
deviation % = (price − SMA₂₀₀) / SMA₂₀₀ × 100| dev % | −20 | −12 | −5 | 0 | +5 | +12 | +20 |
|---|---|---|---|---|---|---|---|
| score | +24 | +15 | +6 | 0 | −6 | −15 | −24 |
Where the latest price sits between its trailing-year low (0%) and high (100%). Near the lows the crowd is fearful; near the highs, greedy.
position = (last − min(252)) / (max(252) − min(252))| position | 0% | 10% | 30% | 50% | 70% | 90% | 100% |
|---|---|---|---|---|---|---|---|
| score | +13 | +9 | +3 | 0 | −3 | −9 | −13 |
The latest VIX close. A spiking VIX is the market screaming; a sleepy VIX is complacency. Note the asymmetry — fear (high VIX) is rewarded more heavily than complacency (low VIX) is penalised.
| VIX | 10 | 13 | 17 | 20 | 25 | 30 | 40 | 55 |
|---|---|---|---|---|---|---|---|---|
| score | −18 | −10 | 0 | +7 | +16 | +26 | +40 | +50 |
The composite
The five sub-scores are simply added and the total is clamped to the ±100 range.
composite = clamp( Σ sub-scores , −100 , +100 )The weights are deliberately asymmetric. The buy side (drawdown +55, RSI +35, trend +24, range +13, VIX +50) can sum past +100 and clamp at the ceiling, while the sell side bottoms out around −90 (drawdown contributes nothing to greed). In plain terms: the model is built to shout "be greedy" in a true panic more readily than it whispers "be fearful" in a melt-up — by design.
From score to stance
Why the ±40 bands don't email. The classifier recognises a "Lean In / Trim" middle ground, but the default alert threshold is 60 — so only genuine extremes (the ±60 bands) ever reach your inbox. The milder tiers exist for reporting and tuning, not notification.
When an email actually sends
On the scheduled run, an alert goes out only if every one of these holds. Failing any one means silence.
The knobs. ALERT_THRESHOLD, MIN_CORROBORATING and COOLDOWN_DAYS are all environment variables — raise the threshold for rarer, higher-conviction calls; lower it to hear more.
Cadence & delivery
A GitHub Action runs at 21:30 UTC, Monday–Friday — shortly after the 4pm ET US close. It fetches the day's data, scores the market, and applies the four gates above. When an alert fires it goes out as a single broadcast to every subscriber, each with their own one-click unsubscribe link.
The cooldown timestamp is persisted between runs (committed back to the repo as state/last_alert.json), so the once-a-week guarantee survives even though each run starts on a fresh, ephemeral machine.