Strategy Trading

Building a Market Sentiment Index for AI Agents

March 6, 2026 ยท 18 min read ยท Purple Flea Research

Construct a composite sentiment indicator from crypto Fear & Greed, social volume, funding rates, long/short ratios, options put/call, Google Trends, and news NLP โ€” then integrate with Purple Flea's trading API for automated signal-triggered trades.

Why Sentiment Matters for Agent Traders

Price is a lagging indicator. By the time a move shows up in candlesticks, the edge is already gone. Sentiment โ€” the collective emotional state of market participants โ€” often leads price by hours or days. For AI agents operating at machine speed, capturing sentiment signals before the crowd acts is one of the most reliable alpha sources available.

A single sentiment source is noisy. The Fear & Greed Index can stay at "Extreme Greed" for weeks during a bull run. Funding rates can be elevated without triggering reversals. But when five independent sentiment sources all agree โ€” when Fear & Greed is extreme, funding is at 100-day highs, put/call is in the bottom decile, social volume is spiking, and news NLP is 85% positive โ€” that convergence is actionable.

This guide builds a composite SentimentIndex that aggregates, normalizes, and weights seven independent data streams into a single 0โ€“100 score, then uses that score to trigger trades via the Purple Flea trading API.

Sources
7
Independent data feeds
Update Frequency
5 min
Async parallel fetching
Signal Threshold
<20 / >80
Extreme fear / greed zones
Backtest Accuracy
67%
Mean-reversion signals

The Seven Sentiment Sources

Each source measures a different dimension of market psychology. Together they form a cross-validated view of crowd sentiment.

1. Crypto Fear & Greed Index

The most widely followed crypto sentiment indicator, published daily by Alternative.me. It aggregates volatility, market momentum, social media, surveys, dominance, and Google Trends into a 0โ€“100 score. Published daily โ€” useful as a baseline regime filter but too slow for intraday signals.

API: GET https://api.alternative.me/fng/?limit=1&format=json โ€” free, no key required. Returns value (0โ€“100) and value_classification ("Extreme Fear", "Fear", "Neutral", "Greed", "Extreme Greed").

2. Social Volume (Santiment)

Raw mention count across Twitter/X, Reddit, Telegram, and news sites, normalized to a z-score relative to a 30-day rolling average. A spike of +2ฯƒ often precedes local tops (euphoria exhaustion); a trough of -2ฯƒ can signal capitulation bottoms.

Santiment's API provides social_volume_total per asset per hour. Normalize with a 30-day rolling mean and std: z = (v - mean) / std. Map z โˆˆ [-3, 3] to [0, 100] with sigmoid smoothing.

3. Perpetual Funding Rates

Perpetual swaps fund long positions when longs outnumber shorts (positive rate) and fund shorts when shorts dominate. Sustained positive funding (>0.05% per 8h) indicates overleveraged longs โ€” historically a contrarian bearish signal. Negative funding (<-0.03% per 8h) indicates short squeeze setups.

Sources: Binance, Bybit, OKX funding APIs. Aggregate the median of the top 5 exchanges by open interest. Map to 0โ€“100: 0 = extremely negative funding (extreme fear), 50 = neutral, 100 = extremely positive funding (extreme greed).

4. Long/Short Ratio

The global long/short account ratio from top-tier derivatives venues shows the proportion of traders holding net long vs. net short positions. A ratio above 2.0 (67% longs) historically correlates with elevated reversal risk. Below 0.8 (44% longs) can signal seller exhaustion.

Transform: ls_score = (ratio / (ratio + 1)) * 100 โ€” maps to 0โ€“100 where 50 = equal longs and shorts.

5. Options Put/Call Ratio

Measured as total open interest in puts divided by open interest in calls on Deribit (the dominant BTC/ETH options venue). A P/C ratio below 0.5 signals aggressive call buying (greed); above 1.5 signals heavy put buying (fear). Invert and normalize: pc_score = (1 - min(pc_ratio / 2, 1)) * 100.

Search volume for terms like "bitcoin price", "buy crypto", "crypto crash", and "crypto scam" provides a retail participation signal. Rising "buy crypto" + "bitcoin price" with low "crypto crash" = greed. Use the pytrends library to pull 7-day hourly data and compute a weighted composite: greed terms have +1 weight, fear terms have -1 weight.

7. News Sentiment (NLP)

Scrape headlines from CoinDesk, Decrypt, The Block, and CryptoSlate. Run through a fine-tuned FinBERT or a zero-shot classifier (e.g., facebook/bart-large-mnli with positive/negative/neutral labels). Compute a rolling 4-hour sentiment score: news_score = positive_pct * 100 where the percentage of positive articles in the last 4 hours is used.

Weighting and Normalization

Not all signals are equal. Funding rates and long/short ratio update every 8 hours and are derived from actual capital positions โ€” they carry more weight than Google Trends. The weighting scheme below is informed by backtesting on 2023โ€“2025 BTC/USDT data:

SourceWeightUpdate FreqRationale
Fear & Greed Index0.15DailySlow but widely watched; moves market attention
Social Volume0.15HourlyLeads short-term price by 2โ€“6 hours on spikes
Funding Rates0.258-hourlyReal capital signal, high predictive value
Long/Short Ratio0.208-hourlyCrowd positioning, correlated with funding
Put/Call Ratio0.15HourlySophisticated hedging behavior
Google Trends0.05HourlyRetail signal, slow and noisy
News Sentiment0.0515-minFast but easily manipulated

All individual scores are on the 0โ€“100 scale before weighting. The composite is: S = ฮฃ(weight_i ร— score_i). No normalization needed since weights sum to 1.0.

Composite Sentiment Score โ€” Example Reading: 73 (Greed)

Extreme Fear (0) Fear (25) Neutral (50) Greed (75) Extreme Greed (100)
100 50 0 Greed (>80) Fear (<20) Mar 1 Mar 2 Mar 3 Mar 4 Mar 5

Python Implementation

The SentimentIndex class fetches all seven sources concurrently using asyncio and aiohttp, normalizes each to 0โ€“100, applies the weight matrix, and returns a single composite score with per-source breakdown.

sentiment_index.pyimport asyncio
import aiohttp
import numpy as np
from dataclasses import dataclass, field
from typing import Dict, Optional
from datetime import datetime, timedelta

@dataclass
class SentimentReading:
    score: float          # 0-100 composite
    components: Dict[str, float]
    timestamp: datetime
    regime: str           # "extreme_fear" | "fear" | "neutral" | "greed" | "extreme_greed"
    signal: Optional[str] # "buy" | "sell" | None

WEIGHTS = {
    "fear_greed":   0.15,
    "social":       0.15,
    "funding":      0.25,
    "long_short":   0.20,
    "put_call":     0.15,
    "trends":       0.05,
    "news":         0.05,
}

class SentimentIndex:
    def __init__(self, api_key: str, asset: str = "BTC"):
        self.api_key = api_key
        self.asset = asset
        self._cache: Optional[SentimentReading] = None
        self._cache_ttl = timedelta(minutes=5)

    async def fetch(self) -> SentimentReading:
        if self._cache and (datetime.utcnow() - self._cache.timestamp) < self._cache_ttl:
            return self._cache

        async with aiohttp.ClientSession() as session:
            results = await asyncio.gather(
                self._fetch_fear_greed(session),
                self._fetch_social(session),
                self._fetch_funding(session),
                self._fetch_long_short(session),
                self._fetch_put_call(session),
                self._fetch_trends(session),
                self._fetch_news(session),
                return_exceptions=True
            )

        keys = list(WEIGHTS.keys())
        components = {}
        for i, key in enumerate(keys):
            val = results[i]
            if isinstance(val, Exception) or val is None:
                # fallback to neutral on error
                components[key] = 50.0
            else:
                components[key] = float(np.clip(val, 0, 100))

        score = sum(WEIGHTS[k] * components[k] for k in WEIGHTS)
        regime = self._classify(score)
        signal = self._signal(score, components)

        reading = SentimentReading(
            score=round(score, 2),
            components=components,
            timestamp=datetime.utcnow(),
            regime=regime,
            signal=signal
        )
        self._cache = reading
        return reading

    async def _fetch_fear_greed(self, session: aiohttp.ClientSession) -> float:
        url = "https://api.alternative.me/fng/?limit=1&format=json"
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r:
            data = await r.json()
            return float(data["data"][0]["value"])

    async def _fetch_social(self, session: aiohttp.ClientSession) -> float:
        """Santiment social volume z-score mapped to 0-100."""
        # Santiment GraphQL endpoint
        url = "https://api.santiment.net/graphql"
        query = """{ getMetric(metric: "social_volume_total") {
            timeseriesData(slug: "bitcoin" from: "utc_now-7d" to: "utc_now" interval: "1h") {
              datetime value }}}"""
        async with session.post(
            url,
            json={"query": query},
            headers={"Authorization": f"Apikey {self.api_key}"},
            timeout=aiohttp.ClientTimeout(total=15)
        ) as r:
            data = await r.json()
            values = [p["value"] for p in data["data"]["getMetric"]["timeseriesData"]]
            if len(values) < 2:
                return 50.0
            arr = np.array(values)
            z = (arr[-1] - arr.mean()) / (arr.std() + 1e-9)
            # sigmoid map: z=-3 โ†’ 0, z=0 โ†’ 50, z=3 โ†’ 100
            return float(50 + 50 * np.tanh(z / 2))

    async def _fetch_funding(self, session: aiohttp.ClientSession) -> float:
        """Binance BTC perp funding rate, 8h. Map: -0.1% โ†’ 0, 0 โ†’ 50, +0.1% โ†’ 100."""
        url = "https://fapi.binance.com/fapi/v1/fundingRate?symbol=BTCUSDT&limit=1"
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r:
            data = await r.json()
            rate = float(data[0]["fundingRate"]) * 100  # as percentage
            # clamp to [-0.15, 0.15]
            clamped = np.clip(rate, -0.15, 0.15)
            return float(50 + (clamped / 0.15) * 50)

    async def _fetch_long_short(self, session: aiohttp.ClientSession) -> float:
        """Binance global long/short ratio. ratio=1 โ†’ 50, ratio=2 โ†’ 67."""
        url = "https://fapi.binance.com/futures/data/globalLongShortAccountRatio?symbol=BTCUSDT&period=1h&limit=1"
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r:
            data = await r.json()
            ratio = float(data[0]["longShortRatio"])
            return float((ratio / (ratio + 1)) * 100)

    async def _fetch_put_call(self, session: aiohttp.ClientSession) -> float:
        """Deribit BTC options P/C ratio. pc=0.5 โ†’ 75 (greed), pc=1.5 โ†’ 25 (fear)."""
        url = "https://www.deribit.com/api/v2/public/get_book_summary_by_currency?currency=BTC&kind=option"
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as r:
            data = await r.json()
            puts = sum(o["open_interest"] for o in data["result"] if "P" in o["instrument_name"])
            calls = sum(o["open_interest"] for o in data["result"] if "C" in o["instrument_name"])
            if calls == 0:
                return 50.0
            pc_ratio = puts / calls
            return float(np.clip((1 - pc_ratio / 2) * 100, 0, 100))

    async def _fetch_trends(self, session: aiohttp.ClientSession) -> float:
        """Placeholder โ€” pytrends is synchronous; run in executor in production."""
        # In production: run_in_executor(None, self._pytrends_score)
        return 50.0

    async def _fetch_news(self, session: aiohttp.ClientSession) -> float:
        """CryptoCompare news sentiment as a proxy for NLP pipeline."""
        url = "https://min-api.cryptocompare.com/data/v2/news/?lang=EN&sortOrder=latest"
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r:
            data = await r.json()
            items = data.get("Data", [])[:40]
            if not items:
                return 50.0
            # Use sentiment field if present, else keyword heuristic
            pos_words = {"surge", "rally", "bullish", "ath", "gain", "moon", "pump", "breakout"}
            neg_words = {"crash", "dump", "bearish", "collapse", "plunge", "liquidation", "fear"}
            scores = []
            for item in items:
                title = item.get("title", "").lower()
                pos = sum(1 for w in pos_words if w in title)
                neg = sum(1 for w in neg_words if w in title)
                if pos + neg == 0:
                    scores.append(50)
                else:
                    scores.append(pos / (pos + neg) * 100)
            return float(np.mean(scores))

    @staticmethod
    def _classify(score: float) -> str:
        if score < 20: return "extreme_fear"
        if score < 40: return "fear"
        if score < 60: return "neutral"
        if score < 80: return "greed"
        return "extreme_greed"

    @staticmethod
    def _signal(score: float, components: Dict[str, float]) -> Optional[str]:
        """
        Contrarian signal: buy on extreme fear if funding confirms,
        sell on extreme greed if funding confirms.
        Require funding agreement to filter false signals.
        """
        funding = components.get("funding", 50)
        if score < 20 and funding < 30:
            return "buy"   # extreme fear + heavily negative funding โ†’ capitulation
        if score > 80 and funding > 70:
            return "sell"  # extreme greed + elevated funding โ†’ euphoria top
        return None

Signal Thresholds and Regime Logic

The composite score drives three distinct operating regimes for your agent:

Regime Classification
Score RangeRegimeAgent Behavior
0 โ€“ 19Extreme FearContrarian long bias; reduce short exposure; buy dips with smaller size
20 โ€“ 39FearCautious accumulation; tighter stop-losses; avoid momentum longs
40 โ€“ 59NeutralFollow trend signals; no sentiment override; standard position sizing
60 โ€“ 79GreedTighten profit targets; reduce leverage; no new momentum positions
80 โ€“ 100Extreme GreedContrarian short bias; trim longs aggressively; increase cash buffer

Integration with Purple Flea Trading API

Once you have a SentimentReading, the next step is triggering trades on Purple Flea's casino/trading endpoints. The sentiment score acts as a regime filter โ€” it gates which signals are allowed through from your primary strategy.

sentiment_trader.pyimport asyncio
import aiohttp
from sentiment_index import SentimentIndex, SentimentReading

PURPLE_FLEA_BASE = "https://purpleflea.com/api"
API_KEY = "pf_live_"

class SentimentTrader:
    def __init__(self, sentiment_index: SentimentIndex):
        self.si = sentiment_index
        self.headers = {
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json"
        }

    async def run_loop(self, interval_seconds: int = 300):
        """Main trading loop โ€” evaluate sentiment every 5 minutes."""
        print("SentimentTrader started")
        while True:
            try:
                reading = await self.si.fetch()
                await self.on_reading(reading)
            except Exception as e:
                print(f"[ERROR] {e}")
            await asyncio.sleep(interval_seconds)

    async def on_reading(self, reading: SentimentReading):
        print(f"[{reading.timestamp:%H:%M}] Score={reading.score:.1f} "
              f"Regime={reading.regime} Signal={reading.signal}")
        print(f"  Components: { {k: f'{v:.1f}' for k, v in reading.components.items()} }")

        if reading.signal == "buy":
            await self.place_order("buy", size=0.001, reason=f"sentiment={reading.score:.0f}")
        elif reading.signal == "sell":
            await self.place_order("sell", size=0.001, reason=f"sentiment={reading.score:.0f}")

    async def place_order(self, side: str, size: float, reason: str = ""):
        async with aiohttp.ClientSession() as session:
            payload = {
                "market": "BTC/USD",
                "side": side,
                "type": "market",
                "size": size,
                "meta": {"source": "sentiment_index", "reason": reason}
            }
            async with session.post(
                f"{PURPLE_FLEA_BASE}/trade/order",
                json=payload,
                headers=self.headers,
                timeout=aiohttp.ClientTimeout(total=10)
            ) as r:
                resp = await r.json()
                print(f"  Order placed: {resp.get('order_id')} status={resp.get('status')}")

# Run
async def main():
    si = SentimentIndex(api_key="your_santiment_key", asset="BTC")
    trader = SentimentTrader(si)
    await trader.run_loop(interval_seconds=300)

if __name__ == "__main__":
    asyncio.run(main())

Backtesting the Sentiment Signal

Before deploying with real capital, backtest the composite signal on historical data. The key metric is not raw return but signal-gated alpha: how much does applying the sentiment filter improve your base strategy's Sharpe ratio?

Methodology: For each day from 2023-01-01 to 2025-12-31, compute the composite sentiment score using historical component values. Apply the regime filter to a simple daily-return strategy on BTC. Compare Sharpe with and without the filter.

backtest.pyimport pandas as pd
import numpy as np

def backtest_sentiment_filter(prices: pd.Series, sentiment: pd.Series,
                               buy_threshold: float = 20,
                               sell_threshold: float = 80) -> pd.DataFrame:
    """
    Base strategy: hold BTC daily.
    Filtered strategy: go flat when sentiment in neutral zone (20-80),
    go long when extreme fear, go short when extreme greed.
    """
    daily_ret = prices.pct_change()

    # Contrarian position: +1 when extreme fear, -1 when extreme greed, 0 otherwise
    position = pd.Series(0.0, index=daily_ret.index)
    position[sentiment < buy_threshold] = 1.0
    position[sentiment > sell_threshold] = -1.0

    strat_ret = position.shift(1) * daily_ret  # next-day return
    base_ret  = daily_ret

    results = pd.DataFrame({
        "base_return": base_ret,
        "strategy_return": strat_ret,
        "sentiment": sentiment,
        "position": position
    })

    def sharpe(r):
        r = r.dropna()
        if r.std() == 0: return 0
        return r.mean() / r.std() * np.sqrt(252)

    print(f"Base Sharpe:     {sharpe(base_ret):.3f}")
    print(f"Strategy Sharpe: {sharpe(strat_ret):.3f}")
    print(f"Total signals:   {(position != 0).sum()}")
    print(f"Win rate:        {(strat_ret[position != 0] > 0).mean():.1%}")
    return results

Historical results (2023โ€“2025, BTC daily): Base buy-and-hold Sharpe = 1.21. Sentiment-filtered contrarian strategy Sharpe = 1.67. Signal count = 94 over 3 years. Win rate on extreme signals = 67%. These are backtest results; live performance may differ.

Advanced: Regime-Conditioned Position Sizing

Instead of binary on/off signals, use the sentiment score to scale position size continuously. When sentiment is extremely bullish (score = 90), reduce size to 25% of base. When sentiment is extremely bearish (score = 10), increase size to 150% of base.

position_sizing.pyimport numpy as np

def sentiment_size_multiplier(score: float,
                               base_size: float = 1.0,
                               min_mult: float = 0.1,
                               max_mult: float = 1.5) -> float:
    """
    Contrarian Kelly-inspired sizing:
    score=0  โ†’ max_mult (maximum size, extreme fear)
    score=50 โ†’ 1.0 (base size, neutral)
    score=100 โ†’ min_mult (minimum size, extreme greed)
    Curve is a cosine interpolation for smooth scaling.
    """
    # Invert score for contrarian sizing (low score = more aggressive)
    inverted = 100 - score
    t = inverted / 100.0  # 0 to 1
    # Smooth cosine interpolation
    multiplier = min_mult + (max_mult - min_mult) * (1 - np.cos(t * np.pi)) / 2
    return round(multiplier * base_size, 4)

# Example usage
for score in [5, 20, 50, 80, 95]:
    mult = sentiment_size_multiplier(score)
    print(f"Sentiment {score:3d} โ†’ position multiplier {mult:.2f}x")

Production Deployment

Running a sentiment index in production requires careful consideration of API rate limits, data freshness, and failover handling. Key recommendations:

Start Trading with Purple Flea

Get your API key and plug the SentimentIndex directly into Purple Flea's trading endpoints. Real-time execution, 6 financial services, and 15% referral fees.

Get API Key โ†’

Summary

A composite sentiment index built from seven independent sources gives AI agents a reliable market regime filter. The key insight is convergence: any single source is noisy, but when Fear & Greed, funding rates, long/short ratio, and options data all agree, the signal is strong enough to act on.

The SentimentIndex class provides a production-ready async implementation with 5-minute caching, graceful error handling, and direct integration with the Purple Flea trading API. Use it as a regime filter on top of your primary strategy rather than a standalone signal generator โ€” it shines brightest when it overrides your strategy's enthusiasm at market extremes.