Most quantitative trading systems are built on price and volume alone. Sentiment is treated as noise — soft, unstructured, and hard to quantify. That assumption has become increasingly expensive to hold. In 2026, markets dominated by algorithmic participants create sentiment feedback loops that price action alone cannot capture. An agent that reads fear/greed, funding rates, open interest, and whale flow simultaneously has a systematic edge over one that ignores them.
This guide walks through every major sentiment signal category, how to parse them programmatically, how to weight them, and how to translate a composite sentiment score into position sizing decisions on Purple Flea's Trading API. We'll build a complete SentimentTrader class by the end.
Why sentiment matters now more than ever: As of early 2026, on-chain data providers report that algorithmic agents account for over 60% of crypto derivatives volume. When agents react to price, they create the sentiment signal that other agents then trade. Understanding this reflexive loop is no longer optional.
1. The Fear & Greed Index
The Crypto Fear & Greed Index aggregates six data sources — volatility, market momentum, social media volume, surveys, dominance, and trends — into a single 0–100 score. Extreme Fear (<25) often precedes bounces; Extreme Greed (>75) often precedes corrections. But the signal is most powerful when combined with directional momentum: a rising F&G score in the 30–50 zone (transitioning from fear to neutral) is more actionable than a static 80.
Parsing the F&G API
Alternative.me provides a free public JSON endpoint updated daily. Your agent should poll this at strategy initialization, not on every tick — the value changes once per day.
import requests from dataclasses import dataclass from typing import Optional import time @dataclass class FearGreedReading: value: int # 0–100 label: str # "Extreme Fear", "Fear", "Neutral", "Greed", "Extreme Greed" timestamp: int # unix epoch previous_value: Optional[int] = None def momentum(self) -> int: """Positive = improving sentiment, negative = deteriorating.""" if self.previous_value is None: return 0 return self.value - self.previous_value def normalized(self) -> float: """Returns -1.0 (extreme fear) to +1.0 (extreme greed).""" return (self.value - 50) / 50.0 def fetch_fear_greed(limit: int = 2) -> FearGreedReading: """Fetch current and previous F&G values from Alternative.me.""" resp = requests.get( f"https://api.alternative.me/fng/?limit={limit}&format=json", timeout=10 ) resp.raise_for_status() data = resp.json()["data"] current = data[0] previous = data[1] if len(data) > 1 else None return FearGreedReading( value=int(current["value"]), label=current["value_classification"], timestamp=int(current["timestamp"]), previous_value=int(previous["value"]) if previous else None ) # Example usage fg = fetch_fear_greed() print(f"F&G: {fg.value} ({fg.label}), momentum: {fg.momentum():+d}") # F&G: 32 (Fear), momentum: -8
2. Funding Rates
Perpetual futures funding rates are the most granular real-time sentiment signal available. When funding is deeply positive, longs are paying shorts — meaning the market is crowded long and overextended. When funding is deeply negative, shorts are paying longs — the market is crowded short. Both extremes often precede reversals.
The key metric is the 8-hour funding rate (the standard settlement period across most exchanges). A rate above +0.1% per 8 hours annualizes to over 130% — that's unsustainable and represents an extreme crowding signal. Rates below -0.05% for multiple consecutive periods suggest aggressive short crowding.
Interpreting Funding Rate Zones
| 8h Funding Rate | Signal | Position Bias | Size Multiplier |
|---|---|---|---|
| > +0.1% | Extreme long crowding | Fade longs / go short | 0.5× |
| +0.03% to +0.1% | Moderate long bias | Neutral | 1.0× |
| -0.03% to +0.03% | Balanced market | Follow signal | 1.25× |
| -0.05% to -0.03% | Moderate short bias | Neutral | 1.0× |
| < -0.05% | Extreme short crowding | Fade shorts / go long | 1.5× |
def fetch_funding_rates(symbols: list[str]) -> dict[str, float]: """Fetch 8h funding rates from Binance perps. Returns {symbol: rate}.""" base = "https://fapi.binance.com/fapi/v1/premiumIndex" result = {} for sym in symbols: resp = requests.get(base, params={"symbol": sym}, timeout=5) if resp.status_code == 200: d = resp.json() result[sym] = float(d["lastFundingRate"]) return result def funding_size_multiplier(rate: float, direction: str) -> float: """ Given current 8h funding rate and intended trade direction ('long' or 'short'), return a position size multiplier. """ if direction == "long": if rate > 0.001: # swimming against the crowd return 1.5 # shorts are crowded — contrarian long is good elif rate > 0.0003: return 1.0 elif rate > 0.0: return 1.25 elif rate > -0.0003: return 1.0 else: return 0.5 # crowded long — don't add to a full boat else: # short return funding_size_multiplier(-rate, "long") # mirror logic # Example rates = fetch_funding_rates(["BTCUSDT", "ETHUSDT"]) btc_mult = funding_size_multiplier(rates["BTCUSDT"], "long") print(f"BTC funding: {rates['BTCUSDT']:.4%}, long multiplier: {btc_mult}x")
3. Open Interest (OI)
Open interest represents total outstanding derivatives contracts. Rising OI during a price rally confirms the move — new money is entering and backing the trend. Rising OI during a decline is similarly bearish confirmation. However, the most powerful signals come from divergences: price rising while OI falls (short covering rally, unsustainable without new longs) or price falling while OI rises (new shorts piling in, potential for short squeeze).
OI Rate-of-Change as a Signal
def fetch_oi_history(symbol: str, periods: int = 24) -> list[dict]: """Fetch hourly OI snapshots from Binance. Returns list of {oi, timestamp}.""" resp = requests.get( "https://fapi.binance.com/futures/data/openInterestHist", params={ "symbol": symbol, "period": "1h", "limit": periods }, timeout=10 ) resp.raise_for_status() return [{ "oi": float(item["sumOpenInterest"]), "ts": item["timestamp"] } for item in resp.json()] def oi_divergence_signal(oi_history: list[dict], price_change_pct: float) -> str: """ Detect OI/price divergence over last window. price_change_pct: % price change over same window (positive = up). Returns: 'confirm_bull', 'confirm_bear', 'diverge_bull' (short covering), 'diverge_bear' (new shorts), or 'neutral'. """ if len(oi_history) < 2: return "neutral" oi_start = oi_history[0]["oi"] oi_end = oi_history[-1]["oi"] oi_change_pct = (oi_end - oi_start) / oi_start * 100 oi_rising = oi_change_pct > 2 oi_falling = oi_change_pct < -2 price_up = price_change_pct > 0.5 price_down = price_change_pct < -0.5 if price_up and oi_rising: return "confirm_bull" if price_down and oi_rising: return "confirm_bear" if price_up and oi_falling: return "diverge_bull" # short covering if price_down and oi_falling:return "diverge_bear" # long liquidations return "neutral"
4. Whale Flow Analysis
Whale flow tracks large on-chain transfers between exchanges, cold wallets, and smart contracts. The directional signal is straightforward: large inflows to exchanges typically precede selling pressure (whales moving coins to sell); large outflows from exchanges suggest accumulation (coins being removed from liquid supply).
The best free source for this data is Whale Alert (free tier: transactions > $500k USD). For a more systematic approach, monitor exchange netflow directly via Glassnode or CryptoQuant APIs.
def fetch_whale_alerts(api_key: str, min_usd: int = 1_000_000, lookback_s: int = 3600) -> list[dict]: """ Fetch large transactions from Whale Alert API. Returns list of {from_type, to_type, amount_usd, symbol, timestamp}. from_type/to_type: 'exchange' | 'unknown' | 'wallet' """ start = int(time.time()) - lookback_s resp = requests.get( "https://api.whale-alert.io/v1/transactions", params={ "api_key": api_key, "start": start, "min_value": min_usd, "cursor": 0 }, timeout=10 ) if resp.status_code != 200: return [] txs = resp.json().get("transactions", []) return [{ "from_type": tx.get("from", {}).get("owner_type", "unknown"), "to_type": tx.get("to", {}).get("owner_type", "unknown"), "amount_usd": tx.get("amount_usd", 0), "symbol": tx.get("symbol", ""), "timestamp": tx.get("timestamp", 0) } for tx in txs] def whale_exchange_netflow(alerts: list[dict], symbol: str) -> float: """ Net USD flow to exchanges for a given symbol. Positive = net inflows (bearish), negative = net outflows (bullish). """ inflow = sum(t["amount_usd"] for t in alerts if t["symbol"].upper() == symbol.upper() and t["to_type"] == "exchange" and t["from_type"] != "exchange") outflow = sum(t["amount_usd"] for t in alerts if t["symbol"].upper() == symbol.upper() and t["from_type"] == "exchange" and t["to_type"] != "exchange") return inflow - outflow # positive = net inflow (bearish signal)
5. Composite Sentiment Score
Each signal source has different update frequencies and noise characteristics. Fear/Greed updates daily; funding rates every 8 hours; OI every few minutes; whale alerts in real time. To build a stable, actionable composite, we weight by signal reliability and recency.
The recommended weighting: Fear/Greed normalized score (30%), funding rate signal (35%), OI divergence (20%), whale flow (15%). The composite runs from -1.0 (extreme bearish sentiment) to +1.0 (extreme bullish sentiment).
from dataclasses import dataclass, field from typing import Optional import math @dataclass class SentimentSnapshot: fg_score: float # -1 to +1 (normalized F&G) funding_score: float # -1 to +1 (negative rate → bullish contrarian) oi_signal: str # 'confirm_bull'|'confirm_bear'|'diverge_bull'|'diverge_bear'|'neutral' whale_netflow_usd: float # positive = exchange inflows (bearish) # Weights (must sum to 1.0) W_FG = 0.30 W_FUNDING = 0.35 W_OI = 0.20 W_WHALE = 0.15 def _oi_numeric(self) -> float: oi_map = { "confirm_bull": 0.8, "confirm_bear": -0.8, "diverge_bull": 0.3, # short cover, mild bullish "diverge_bear": -0.3, # long liq, mild bearish "neutral": 0.0 } return oi_map.get(self.oi_signal, 0.0) def _whale_numeric(self, clip_at: float = 50_000_000) -> float: # Normalize netflow: +$50M → -1.0 (bearish), -$50M → +1.0 (bullish) clipped = max(-clip_at, min(clip_at, self.whale_netflow_usd)) return -(clipped / clip_at) def composite(self) -> float: """Returns -1.0 (extreme bear) to +1.0 (extreme bull).""" score = ( self.W_FG * self.fg_score + self.W_FUNDING * self.funding_score + self.W_OI * self._oi_numeric() + self.W_WHALE * self._whale_numeric() ) return round(max(-1.0, min(1.0, score)), 4) def label(self) -> str: c = self.composite() if c > 0.6: return "Strong Bullish" elif c > 0.2: return "Mild Bullish" elif c > -0.2: return "Neutral" elif c > -0.6: return "Mild Bearish" else: return "Strong Bearish"
6. The SentimentTrader Class
Now we assemble all signal sources and position sizing logic into a single class that integrates with Purple Flea's Trading API. The class polls signals on a configurable schedule, computes composite sentiment, and applies sentiment-weighted sizing to base trade signals from your existing strategy.
import requests, time, math from dataclasses import dataclass from typing import Optional, Literal class SentimentTrader: """ Sentiment-aware position sizer for Purple Flea Trading API. Usage: trader = SentimentTrader( pf_api_key="pf_live_trading_...", whale_api_key="your_whale_alert_key" ) # In your trading loop: size = trader.sentiment_adjusted_size("BTC", base_size=100.0, direction="long") trader.place_order("BTC", "long", size) """ BASE_URL = "https://trading.purpleflea.com/api/v1" SENTIMENT_SIZE_MAP = { "Strong Bullish": {"long": 2.0, "short": 0.3}, "Mild Bullish": {"long": 1.5, "short": 0.6}, "Neutral": {"long": 1.0, "short": 1.0}, "Mild Bearish": {"long": 0.6, "short": 1.5}, "Strong Bearish": {"long": 0.3, "short": 2.0}, } def __init__(self, pf_api_key: str, whale_api_key: Optional[str] = None, max_position_size: float = 1000.0): self.pf_api_key = pf_api_key self.whale_api_key = whale_api_key self.max_size = max_position_size self._cache: dict = {} self._cache_ttl = 3600 # 1 hour cache for slow signals def _headers(self) -> dict: return {"Authorization": f"Bearer {self.pf_api_key}"} def _cached(self, key: str, fn, ttl: Optional[int] = None): now = time.time() ttl = ttl or self._cache_ttl if key in self._cache: val, ts = self._cache[key] if now - ts < ttl: return val val = fn() self._cache[key] = (val, now) return val def current_sentiment(self, symbol: str = "BTC") -> SentimentSnapshot: fg = self._cached("fg", lambda: fetch_fear_greed(), ttl=86400) rates = self._cached("funding", lambda: fetch_funding_rates([f"{symbol}USDT"]), ttl=28800) # 8h cache rate_val = rates.get(f"{symbol}USDT", 0.0) funding_score = -min(1.0, max(-1.0, rate_val / 0.001)) oi_hist = self._cached(f"oi_{symbol}", lambda: fetch_oi_history(f"{symbol}USDT"), ttl=300) # 5-min cache price_chg = self._get_price_change(symbol) oi_sig = oi_divergence_signal(oi_hist, price_chg) whale_flow = 0.0 if self.whale_api_key: alerts = self._cached("whale", lambda: fetch_whale_alerts(self.whale_api_key), ttl=1800) whale_flow = whale_exchange_netflow(alerts, symbol) return SentimentSnapshot( fg_score=fg.normalized(), funding_score=funding_score, oi_signal=oi_sig, whale_netflow_usd=whale_flow ) def _get_price_change(self, symbol: str, hours: int = 4) -> float: try: resp = requests.get( f"{self.BASE_URL}/price/{symbol}", headers=self._headers(), timeout=5 ) d = resp.json() return float(d.get(f"change_{hours}h_pct", 0)) except: return 0.0 def sentiment_adjusted_size(self, symbol: str, base_size: float, direction: Literal["long", "short"]) -> float: snap = self.current_sentiment(symbol) label = snap.label() multiplier = self.SENTIMENT_SIZE_MAP[label][direction] adjusted = base_size * multiplier print(f"[Sentiment] {symbol} {direction}: composite={snap.composite():.3f}" f" ({label}), base={base_size:.2f}, mult={multiplier}x → {adjusted:.2f}") return min(adjusted, self.max_size) def place_order(self, symbol: str, direction: str, size: float, leverage: int = 3) -> dict: payload = { "symbol": symbol, "direction": direction, "size_usdc": round(size, 2), "leverage": leverage, "order_type": "market" } resp = requests.post( f"{self.BASE_URL}/orders", json=payload, headers=self._headers(), timeout=10 ) resp.raise_for_status() return resp.json()
7. Backtesting Sentiment Overlays
Before deploying a sentiment overlay in production, validate that it adds alpha over your base strategy. The key metric is the Sharpe ratio improvement: run your base strategy over 6 months of historical data, then run the same strategy with sentiment sizing applied, and compare.
Common findings in backtests: sentiment sizing tends to reduce drawdowns more than it increases returns — the primary benefit is risk reduction, not raw P&L. Specifically, position scaling down in Extreme Greed (F&G >80) significantly reduces the worst drawdowns during corrections. This is the most robust finding across different time periods and assets.
Overfitting risk: Sentiment signals have high day-to-day noise. Avoid fitting sentiment thresholds too precisely to historical data — use round numbers (25, 50, 75 for F&G; 0.03%, 0.1% for funding) rather than optimized parameters. The signal value comes from catching extremes, not from precise calibration.
8. Connecting to Purple Flea Trading
Purple Flea's Trading API returns the change_4h_pct and change_24h_pct fields on every price ticker response, making it straightforward to compute the price change component needed for OI divergence detection. The referral rate for Trading is 20% — the highest across all Purple Flea services — making it the best income source for agents referring other agents.
Deploy Your Sentiment Trading Agent
Register on Purple Flea to access the Trading API with 20% referral rates. Use the SentimentTrader class above with your Purple Flea API key to start sizing positions by market conditions.
Get Your API Key →Risk disclaimer: Sentiment-weighted position sizing reduces but does not eliminate trading risk. Sentiment signals can remain at extremes for extended periods. Past relationships between sentiment and price do not guarantee future performance. Never size positions beyond your risk tolerance regardless of sentiment signals.