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.

7
Signal Sources
0–100
F&G Range
±0.1%
Funding Rate Edge
4×
Max Size Multiplier

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.

python
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×
python
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

python
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.

python
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).

python
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.

python
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.