← Be Greedy

How the number is made

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

Two instruments, daily closes, no API keys

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

Each reading becomes a signed sub-score

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.

1 · Drawdown from the 1-year high

range +0 … +55

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
Reading (% off high) → sub-score
% off high−40−30−20−12−7−30
score+55+46+33+20+9+30

Drawdown is always ≤ 0%, so this signal only ever adds to the buy side — a deliberate Buffett tilt toward acting on fear, not euphoria.

2 · Momentum — 14-day RSI

range +35 … −35

Wilder's Relative Strength Index over 14 days. Oversold readings flag panic selling; overbought readings flag crowd euphoria.

RSI → sub-score
RSI102030405060708090
score+35+25+13+40−4−13−25−35

3 · Distance from the 200-day average

range +24 … −24

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
Deviation from SMA₂₀₀ → sub-score
dev %−20−12−50+5+12+20
score+24+15+60−6−15−24

4 · Position in the 52-week range

range +13 … −13

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))
Range position → sub-score
position0%10%30%50%70%90%100%
score+13+9+30−3−9−13

5 · Volatility — the VIX

range +50 … −18

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 level → sub-score
VIX1013172025304055
score−18−100+7+16+26+40+50

The composite

Sum, then clamp

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

Five bands

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

Four gates, all must pass

On the scheduled run, an alert goes out only if every one of these holds. Failing any one means silence.

  1. It's an extreme. The stance is Be Greedy or Be Fearful (not Stand Pat / HOLD).
  2. The score clears the threshold. |composite| ≥ ALERT_THRESHOLD (default 60).
  3. Enough signals corroborate. At least MIN_CORROBORATING (default 2) individual signals point the same way as the composite, each with a sub-score of magnitude ≥ 8. One lone signal can't trip an alert — the move has to be broad-based.
  4. The cooldown is clear. No alert has gone out in the last COOLDOWN_DAYS (default 7). This is the hard guarantee behind "never more than one email a week."

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

After the close, on weekdays

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.