Most trading agents react to price. The best ones react to information. When a headline reads "Bitcoin ETF approved by SEC," a price-only agent sees nothing — the sentiment agent sees a strong buy signal and has a position open before the candle closes.
This guide walks through a complete sentiment-driven trading agent: fetching headlines, scoring them with an LLM, sizing positions using the Kelly Criterion, and executing trades via the Purple Flea Trading API. The full agent fits in 60 lines of Python.
The Four-Step Pipeline
Fetch Sentiment Data
Pull crypto headlines from news APIs and social feeds. Filter for relevant assets.
Score with LLM
Send headlines to Claude. Get back a sentiment score from -1.0 (bearish) to +1.0 (bullish) with confidence.
Size the Position
Apply fractional Kelly to map confidence to position size. Cap at 5% of portfolio per trade.
Execute the Trade
POST to Purple Flea Trading API. Set stop-loss at 2% below entry. Close when sentiment reverses.
Step 1: Fetching Sentiment Data
You need a stream of crypto headlines. CryptoCompare News API is free and returns structured JSON with asset tags. For social sentiment, LunarCrush and Santiment offer agent-friendly APIs.
import requests CRYPTOCOMPARE_API = "https://min-api.cryptocompare.com/data/v2/news/?lang=EN" def fetch_headlines(asset: str = "BTC", limit: int = 10) -> list[str]: """Fetch recent crypto headlines filtered by asset.""" r = requests.get( CRYPTOCOMPARE_API, params={"categories": asset, "lTs": 0}, timeout=10, ) r.raise_for_status() articles = r.json()["Data"][:limit] return [a["title"] for a in articles] # Example output: # ["Bitcoin ETF Approved by SEC — Markets React", # "BTC breaks $100K resistance on institutional flows", # "Whale moves 10,000 BTC to exchange — sell signal?"]
Step 2: Scoring Headlines with Claude
This is where the LLM does the heavy lifting. Rather than a keyword-based approach, we send all recent headlines to Claude and ask for a structured JSON response: a direction, a score from -1.0 to +1.0, and a confidence from 0 to 1.
Confidence matters as much as direction — a weak signal should produce a small position. A high-confidence, strongly bullish signal justifies a larger allocation.
import anthropic, json CLIENT = anthropic.Anthropic() # uses ANTHROPIC_API_KEY env var SCORE_PROMPT = """You are a crypto trading analyst. Given these recent headlines about {asset}, return a JSON object with: - "direction": "long" or "short" or "neutral" - "score": float from -1.0 (strongly bearish) to +1.0 (strongly bullish) - "confidence": float from 0.0 (uncertain) to 1.0 (high confidence) - "rationale": one sentence explaining the signal Headlines: {headlines} Respond ONLY with valid JSON.""" def score_sentiment(asset: str, headlines: list[str]) -> dict: """Score sentiment of headlines using Claude. Returns structured signal.""" prompt = SCORE_PROMPT.format( asset=asset, headlines="\n".join(f"- {h}" for h in headlines), ) msg = CLIENT.messages.create( model="claude-opus-4-6", max_tokens=256, messages=[{"role": "user", "content": prompt}], ) return json.loads(msg.content[0].text) # Example: "Bitcoin ETF Approved by SEC" headlines # Returns: {"direction": "long", "score": 0.87, "confidence": 0.91, # "rationale": "SEC ETF approval is a strong institutional bullish catalyst."}
Keywords miss context. "Bitcoin crash feared" is bearish. "Bitcoin crash fears overblown" is actually bullish. LLMs understand negation, hedging, and nuance that regex cannot capture. Claude scores correctly in both cases.
Step 3: Kelly Fraction Position Sizing
The Kelly Criterion tells you what fraction of your portfolio to risk on a bet, given your edge and win probability. For sentiment trading, we adapt it: the LLM confidence becomes the win probability estimate, and we use a fractional Kelly (half-Kelly) to reduce variance.
The formula: f = (p * b - q) / b where p = win probability, q = 1-p, b = win/loss ratio. We then cap at 5% of portfolio per trade regardless of Kelly output.
def kelly_fraction( confidence: float, score: float, win_loss_ratio: float = 1.5, kelly_divisor: float = 2.0, max_fraction: float = 0.05, ) -> float: """ Compute fractional Kelly position size as a fraction of portfolio. Args: confidence: LLM confidence score 0.0-1.0 score: sentiment score -1.0 to +1.0 (absolute value used) win_loss_ratio: expected win / expected loss (default 1.5:1) kelly_divisor: fraction of full Kelly to use (2.0 = half-Kelly) max_fraction: hard cap on position size (default 5%) """ # Use confidence as win probability, modulated by score magnitude p = confidence * abs(score) # effective probability q = 1.0 - p b = win_loss_ratio full_kelly = (p * b - q) / b fractional = full_kelly / kelly_divisor # Never go negative (no trade if signal is weak) return max(0.0, min(fractional, max_fraction)) # Example: # score=0.87, confidence=0.91 → p=0.79, kelly=0.26, half-kelly=0.13, capped at 0.05 # score=0.30, confidence=0.50 → p=0.15, kelly negative → no trade
Step 4: Executing the Trade
With a direction and position size, we execute via the Purple Flea Trading API. The API supports 275+ Hyperliquid perpetual markets. We open the position with a market order and immediately set a stop-loss 2% below entry to cap downside risk.
import os, requests TRADING_BASE = "https://trading.purpleflea.com" TRADING_KEY = os.environ["PURPLE_FLEA_TRADING_KEY"] def get_portfolio_value() -> float: """Fetch current portfolio USDC value.""" r = requests.get( f"{TRADING_BASE}/api/v1/account", headers={"Authorization": f"Bearer {TRADING_KEY}"}, ) r.raise_for_status() return float(r.json()["equity_usdc"]) def open_position( market: str, direction: str, size_usdc: float, stop_loss_pct: float = 0.02, ) -> dict: """Open a perpetual futures position with stop-loss.""" side = "buy" if direction == "long" else "sell" # Place market order order = requests.post( f"{TRADING_BASE}/api/v1/orders", headers={"Authorization": f"Bearer {TRADING_KEY}"}, json={ "market": market, "side": side, "type": "market", "size_usdc": size_usdc, "stop_loss_pct": stop_loss_pct, }, ) order.raise_for_status() result = order.json() print(f"Opened {direction} {market}: ${size_usdc:.2f} @ {result['fill_price']}") print(f"Stop-loss set at {result['stop_loss_price']}") return result
The Complete Agent (60 Lines)
Putting all four steps together into a runnable agent that polls for new sentiment signals and acts on them. The agent runs in a loop, checking for new headlines every 5 minutes and trading when it finds a high-confidence signal.
import os, time, json, requests, anthropic # Config TRADING_BASE = "https://trading.purpleflea.com" TRADING_KEY = os.environ["PURPLE_FLEA_TRADING_KEY"] CLIENT = anthropic.Anthropic() ASSETS = {"BTC": "BTC/USDC", "ETH": "ETH/USDC", "SOL": "SOL/USDC"} MIN_CONFIDENCE = 0.65 # only trade if LLM is 65%+ confident POLL_INTERVAL = 300 # check every 5 minutes def fetch_headlines(asset): r = requests.get("https://min-api.cryptocompare.com/data/v2/news/?lang=EN", params={"categories": asset}, timeout=10) return [a["title"] for a in r.json()["Data"][:10]] def score_sentiment(asset, headlines): prompt = f"""Analyze these {asset} headlines. Return JSON only: {{"direction":"long"|"short"|"neutral","score":-1.0..1.0,"confidence":0..1,"rationale":"..."}} Headlines: {chr(10).join("- "+h for h in headlines)}""" msg = CLIENT.messages.create(model="claude-opus-4-6", max_tokens=200, messages=[{"role":"user","content":prompt}]) return json.loads(msg.content[0].text) def kelly_size(confidence, score, portfolio): p = confidence * abs(score) full_kelly = (p * 1.5 - (1 - p)) / 1.5 fraction = max(0.0, min(full_kelly / 2.0, 0.05)) return round(portfolio * fraction, 2) def get_portfolio(): r = requests.get(f"{TRADING_BASE}/api/v1/account", headers={"Authorization": f"Bearer {TRADING_KEY}"}) return float(r.json()["equity_usdc"]) def open_trade(market, direction, size): side = "buy" if direction == "long" else "sell" r = requests.post(f"{TRADING_BASE}/api/v1/orders", headers={"Authorization": f"Bearer {TRADING_KEY}"}, json={"market":market,"side":side,"type":"market", "size_usdc":size,"stop_loss_pct":0.02}) r.raise_for_status() return r.json() if __name__ == "__main__": print("Sentiment trading agent running...") while True: portfolio = get_portfolio() for asset, market in ASSETS.items(): headlines = fetch_headlines(asset) signal = score_sentiment(asset, headlines) print(f"{asset}: {signal['direction']} (score={signal['score']:.2f}, conf={signal['confidence']:.2f})") print(f" {signal['rationale']}") if signal["direction"] != "neutral" and signal["confidence"] >= MIN_CONFIDENCE: size = kelly_size(signal["confidence"], signal["score"], portfolio) if size >= 1.0: # minimum $1 trade result = open_trade(market, signal["direction"], size) print(f" Traded: ${size:.2f} {signal['direction']} @ {result['fill_price']}") time.sleep(POLL_INTERVAL)
Example: "Bitcoin ETF Approved" Signal
Let's trace through a concrete example. The headline "Bitcoin Spot ETF Approved by SEC — Institutional Floodgates Open" arrives in the news feed.
| Stage | Output | Notes |
|---|---|---|
| Headline fetch | "Bitcoin Spot ETF Approved..." | Top CryptoCompare result |
| LLM score | direction=long, score=0.92, confidence=0.94 | Strong institutional catalyst |
| Kelly fraction | p=0.865, full_kelly=0.30, half=0.15, capped=0.05 | 5% cap kicks in |
| Position size | $500 on $10,000 portfolio | 5% of equity |
| Stop-loss | -2% from entry | Max $10 loss per trade |
| Market | BTC/USDC perpetual long | via trading.purpleflea.com |
Risk Management
The agent has two layers of risk control:
- Position cap: Never risk more than 5% of portfolio on a single trade, regardless of Kelly output. This prevents a single high-confidence wrong call from causing large drawdown.
- Stop-loss: Every trade sets a stop-loss at 2% below the entry price. The maximum loss per trade is therefore 2% of 5% = 0.1% of total portfolio per trade. Even 10 consecutive wrong calls only costs 1% of the portfolio.
For more advanced stop-loss patterns (trailing stops, volatility-adjusted stops, time-based exits), see our Kelly Criterion trading guide. The 2% fixed stop is a conservative starting point — experienced agents often use ATR-based dynamic stops.
Beyond Headlines: Social Sentiment
Crypto Twitter and Reddit move faster than news wires. To capture social sentiment, use the same scoring approach but source from:
- LunarCrush API: Aggregated social volume and sentiment scores by asset. Updates every hour.
- Santiment API: Social dominance and developer activity metrics. Excellent for altcoins.
- Nitter scrape: Real-time Twitter/X crypto influencer posts if you need sub-minute latency.
The same four-step pipeline applies — just replace the fetch_headlines() function with your social data source. The LLM scoring and Kelly sizing code is identical.
Get Started
The agent needs three things: a Purple Flea Trading API key, an Anthropic API key, and Python 3.11+. Registration takes one API call:
# Register curl -s -X POST https://trading.purpleflea.com/api/v1/auth/register \ -H 'Content-Type: application/json' \ -d '{"agent_name": "my-sentiment-agent"}' # Returns: {"agent_id":"ag_xxx","api_key":"pf_xxx","equity_usdc":"100.00"}
- Trading API: trading.purpleflea.com
- Free $1 for new agents: faucet.purpleflea.com
- Full docs: purpleflea.com/docs