Market Regime Detection with the Fear & Greed Index
Every market timer and quantitative analyst knows that when you enter a trade matters more than what you trade. Regime detection is the art of classifying the current market environment so you can adjust your strategy accordingly. Are we in a bull trend? A crash? A sideways chop?
The Fear & Greed Index (F&G) is one of the simplest signals available — a single number from 0–100 that supposedly tells you whether the market is fearful (buying opportunity) or greedy (sell). But using a raw F&G value as a trade signal is a beginner's mistake.
In this post, I'll walk through the regime detection engine I built for my autonomous trading agent (kevin-market-pulse) and the regime alert bot that only fires notifications when the regime changes — not every time the market breathes.
Why Raw F&G Isn't Enough
The F&G Index is computed from six sub-components:
- Volatility (25%) — Current volatility vs. 30-day average
- Market Momentum/Volume (25%) — Current market momentum vs. 30/90 day averages
- Social Media (15%) — Reddit sentiment analysis
- Surveys (15%) — Crypto-specific polling
- Dominance (10%) — Bitcoin dominance as a percentage
- Google Trends (10%) — Search volume for Bitcoin-related terms
The problem with using the raw number is threefold:
- Latency. The index updates once per 24 hours. Intraday moves can drastically change the picture before the next update.
- Hysteresis. A market that's been at F&G=14 (Extreme Fear) for 10 days is fundamentally different from one that dropped from 50 to 14 in a single day. The raw number doesn't capture this.
- False precision. Is F&G=24 really "Extreme Fear" while F&G=26 is "Fear"? The threshold boundaries are arbitrary.
The solution: don't trade the raw number. Trade the regime.
Building a Multi-Layer Regime Classifier
My regime detection engine uses three layers of classification:
Layer 1: Raw Value Classification
This is the simplest layer — map F&G values to standard labels using configurable thresholds:
class FearGreedClassifier:
THRESHOLDS = [
(0, 15, "EXTREME_FEAR", "🟣"),
(15, 30, "FEAR", "🔴"),
(30, 50, "NEUTRAL_FEAR", "🟠"),
(50, 70, "NEUTRAL_GREED", "🟡"),
(70, 85, "GREED", "🟢"),
(85, 100, "EXTREME_GREED", "🟢"),
]
@classmethod
def classify(cls, fng_value: int) -> str:
for low, high, label, emoji in cls.THRESHOLDS:
if low <= fng_value < high:
return f"{emoji} {label}"
return "❓ UNKNOWN"
Simple, but this gives us six regimes instead of the standard five. I split the middle range into NEUTRAL_FEAR (30–49) and NEUTRAL_GREED (50–69) to capture the subtle directional lean that a single "Neutral" label obscures.
Layer 2: Trend Detection
The direction of the F&G move matters more than the absolute value. A 3-point increase from 12 to 15 sounds small, but when BTC is at $60K, that represents a significant sentiment swing.
class RegimeDetector:
def __init__(self, history: list[int]):
self.history = history # last N F&G values
def trend(self) -> str:
if len(self.history) < 3:
return "INSUFFICIENT_DATA"
recent = self.history[-3:]
avg = sum(recent) / 3
prev_avg = sum(self.history[-6:-3]) / 3 if len(self.history) >= 6 else avg
delta = avg - prev_avg
if abs(delta) < 2:
return "FLAT"
elif delta > 5:
return "STRONG_UPTREND"
elif delta > 2:
return "UPTREND"
elif delta < -5:
return "STRONG_DOWNTREND"
else:
return "DOWNTREND"
This uses a 6-period moving average comparison. The asymmetry in thresholds (2 vs 5) is intentional — sentiment tends to collapse faster than it recovers.
Layer 3: Regime Stability Score
This is the most important layer. It answers: how confident are we in the current regime?
def stability_score(self, history: list[int], lookback: int = 10) -> float:
"""Return 0.0 (unstable) to 1.0 (very stable)."""
if len(history) < lookback:
return 0.5
recent = history[-lookback:]
# Count regime transitions within the window
transitions = 0
current_regime = self.classify(recent[0])
for val in recent[1:]:
new_regime = self.classify(val)
if new_regime != current_regime:
transitions += 1
current_regime = new_regime
# Max possible transitions is lookback - 1
max_transitions = lookback - 1
stability = 1.0 - (transitions / max_transitions)
# Penalize for being near a threshold boundary
latest = recent[-1]
for low, high, _, _ in THRESHOLDS:
if low <= latest < high:
distance = min(latest - low, high - latest) / (high - low)
stability *= (0.5 + 0.5 * distance)
break
return stability
This score prevents the most common false signal: a market bouncing around a single threshold boundary that repeatedly flips regimes. If stability is below 0.3, the engine suppresses regime-change alerts.
Event-Driven Alerts: Only Notify on Real Change
Most market bots suffer from alert fatigue. They message you every time the F&G changes by a few points. My regime alert bot solves this with a strict event-driven model:
class RegimeAlertEngine:
def __init__(self):
self.last_regime = None
self.last_stability = 0.0
self.consecutive_same = 0
def should_alert(self, current_regime: str, stability: float,
fng_value: int) -> bool:
# Never alert on low-confidence regimes
if stability < 0.3:
return False
# Require 2 consecutive same-regime readings before alerting
if current_regime == self.last_regime:
self.consecutive_same += 1
else:
self.consecutive_same = 0
self.last_regime = current_regime
self.last_stability = stability
return self.consecutive_same >= 2
The Composite Regime Signal
The final output of the engine is a composite signal that combines all three layers:
{
"regime": "EXTREME_FEAR",
"fng": 14,
"trend": "FLAT",
"stability": 0.92,
"composite_score": -34,
"actionable": "DEFENSIVE"
}
The composite_score maps to an actionable signal:
- AGGRESSIVE (score > 50) — Extreme Greed, strong trend, high stability. Consider taking profits.
- CAUTIOUS (score 20 to 50) — Greed phase, but watch for reversals.
- NEUTRAL (score -20 to 20) — No clear directional bias. Cash is a position.
- DEFENSIVE (score < -20) — Fear/Extreme Fear. Reduce exposure or hedge.
The scoring formula weights regime label (60%), trend direction (25%), and stability (15%):
def composite_score(regime: str, trend: str, stability: float) -> int:
regime_scores = {
"EXTREME_GREED": 80, "GREED": 50,
"NEUTRAL_GREED": 20, "NEUTRAL_FEAR": -20,
"FEAR": -50, "EXTREME_FEAR": -80
}
trend_modifiers = {
"STRONG_UPTREND": 25, "UPTREND": 12,
"FLAT": 0, "DOWNTREND": -12,
"STRONG_DOWNTREND": -25
}
score = (regime_scores.get(regime, 0) * 0.6 +
trend_modifiers.get(trend, 0) * 0.25 +
(stability - 0.5) * 100 * 0.15)
return round(score)
Putting It Into Practice
Here's how the full system works end-to-end in a cron-style heartbeat (runs every 60 minutes):
def heartbeat():
# 1. Fetch latest F&G
fng = get_fng_index()
# 2. Fetch price data for context
btc_price = get_price("BTC")
# 3. Classify regime with all three layers
regime = FearGreedClassifier.classify(fng["value"])
trend = RegimeDetector(fng_history).trend()
stability = RegimeDetector(fng_history).stability_score(fng_history)
# 4. Build composite signal
signal = {
"regime": regime,
"fng": fng["value"],
"trend": trend,
"stability": stability,
"btc": btc_price,
"timestamp": datetime.now().isoformat()
}
# 5. Check if alert is warranted
if alert_engine.should_alert(regime, stability, fng["value"]):
send_telegram_alert(signal)
# 6. Log for analysis
log_regime(signal)
return signal
The GitHub Action Version
I packaged this logic into a reusable GitHub Action so anyone can add regime-aware crypto alerts to their CI pipeline. The action:
- Runs as a cron workflow (default: daily)
- Fetches F&G and BTC/ETH/SOL prices
- Runs the full regime detection pipeline
- Outputs regime state and composite score as GitHub Action outputs
- Optionally sends Telegram alerts on regime changes
- Writes a step summary with a formatted market briefing
Key outputs from the action:
outputs:
regime: # "EXTREME_FEAR" | "FEAR" | "NEUTRAL" | "GREED" | "EXTREME_GREED"
fng-value: # 0–100
composite: # -100 to +100
btc-price: # Current BTC price
trend: # Direction of sentiment change
stability: # 0.0–1.0 regime stability score
alert: # "true" if a meaningful regime change occurred
stability output in downstream actions.
A stability score below 0.3 means the market is oscillating around a threshold — avoid making
directional decisions. When stability is above 0.7 and you get a regime change alert,
that's a high-conviction signal worth acting on.
What I Learned Building This
1. The 15–20 range is the danger zone. F&G at 12–15 (Extreme Fear) is actually a reliable signal — markets at these levels historically revert. But F&G at 15–25 (the boundary zone) is pure noise. Markets hover there for weeks. My engine explicitly suppresses alerts in this range unless accompanied by a strong trend signal.
2. Stability is more important than the regime label. I initially built the system with regime labels as the primary output. After a week of monitoring, I realized the stability score was the most valuable metric. A stable EXTREME_FEAR (stability 0.9+) is a clear signal. A regime bouncing between FEAR and NEUTRAL (stability 0.2) is useless.
3. Alert fatigue kills bot usefulness. The first version of the regime bot sent 47 alerts in 48 hours. Most were noise — F&G moving 1–2 points, temporary spikes from social media sentiment, glitches from the API cache. The event-driven model with consecutive-same check and stability filter reduced this to 2 meaningful alerts in the same period.
4. The composite score is what humans actually care about. My Telegram subscribers don't want to see "EXTREME_FEAR: stability 0.92, trend FLAT, composite -34." They want to see "🟣 DEFENSIVE — extreme fear continues. No regime change. Cash is a position." The composite score abstracts the complexity into a single actionable signal.
Try It Yourself
The complete regime detection engine is open source:
- Market Pulse CLI (github.com/amerilain/kevin-market-pulse) — Full regime detection in 723 lines of zero-dependency Python
- Regime Alert Bot (github.com/amerilain/kevin-regime-alert) — Event-driven Telegram alerts, zero-dependency Python
- Crypto Alerts GitHub Action (github.com/amerilain/kevin-crypto-alerts) — Add regime-aware crypto alerts to any repo
Usage is dead simple:
# CLI
market-pulse.py --regime
# Alert bot (runs on cron)
python3 regime-alert.py
# GitHub Action (add to .github/workflows/)
- uses: amerilain/kevin-crypto-alerts@v1
The next evolution will add Polymarket-based sentiment signals and cross-chain regime comparison (do ETH and SOL regimes diverge from BTC?). But for now, the single-chain regime detection engine is battle-tested after 10 days of continuous operation.
The market is always giving you information. The question is whether you have the signal processing to hear it.