
Sentiment Analysis
- 234 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
sentiment-analysis is a Claude Code skill that extracts and scores crypto market sentiment from social media, news, and on-chain data.
About
sentiment-analysis is a Claude Code skill that quantifies crypto market sentiment. It pulls from social media, news, and on-chain sources to compute mention velocity, sentiment polarity, the Fear & Greed index, and on-chain proxies like funding rates and exchange flows, then combines them into a composite score. A developer uses it to gauge crowd positioning and find contrarian extremes.
- Computes mention velocity, polarity, Fear & Greed, and on-chain proxies into a composite score
- Keyword-based, LLM-free scoring plus free-API sources (Alternative.me, CoinGecko)
- Ships keyword_sentiment.py and sentiment_scanner.py with live free-API fetching
Sentiment Analysis by the numbers
- 234 all-time installs (skills.sh)
- Ranked #391 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
sentiment-analysis capabilities & compatibility
Free using no-auth APIs (Alternative.me, CoinGecko); social sources like Twitter/X and Reddit require paid API tiers or keys.
- Capabilities
- sentiment analysis · social monitoring · onchain analysis · signal scoring
- Use cases
- data analysis · web scraping
- Pricing
- Freemium
What sentiment-analysis says it does
Extract and quantify market sentiment from social media, news feeds, and on-chain data to identify crowd positioning and potential contrarian opportunities.
Detect euphoria/panic extremes that precede reversals
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill sentiment-analysisAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 234 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Quantify crypto market sentiment from social media, news, and on-chain data to spot crowd extremes.
Who is it for?
Gauging crowd sentiment and detecting euphoria or panic extremes before entering or exiting positions.
Skip if: Executing trades or computing price-based technical indicators.
When should I use this skill?
You want a quantified sentiment or Fear & Greed reading for a token before a trade decision.
What you get
A composite sentiment score and component signals to inform contrarian or momentum decisions.
- Composite sentiment score
- Fear & Greed reading
- Mention velocity and on-chain sentiment metrics
By the numbers
- 8 sentiment data sources documented
- Composite score range -100 to +100
- Fear & Greed 0-100 with 5 bands
Files
Sentiment Analysis
Extract and quantify market sentiment from social media, news feeds, and on-chain data to identify crowd positioning and potential contrarian opportunities.
When to Use This Skill
- Gauge crowd sentiment before entering or exiting a position
- Detect euphoria/panic extremes that precede reversals
- Monitor social mention velocity for early trend detection
- Track influencer activity around specific tokens
- Build composite sentiment scores for systematic strategies
Core Concepts
Sentiment Data Sources
| Source | Data Type | Access |
|---|---|---|
| Twitter/X | Post text, engagement, follower counts | API (paid tiers) |
| Subreddit posts, comments, upvotes | Reddit API | |
| Telegram | Channel messages, member counts | Bot API or scraping |
| Discord | Server activity, message volume | Bot integration |
| News | Headlines, article text | NewsAPI, RSS feeds |
| CoinGecko | Community stats, developer activity | Free API |
| Alternative.me | Fear & Greed Index | Free API |
| On-chain | Funding rates, exchange flows | Exchange APIs |
See references/data_sources.md for complete API details, rate limits, and access patterns for each source.
Sentiment Metrics
Mention Velocity — Rate of token mentions over time:
mention_velocity = mentions_last_hour / baseline_hourly_mentions
# > 3.0 = trending, > 10.0 = viralSentiment Polarity — Positive vs negative tone:
polarity = (positive_count - negative_count) / total_count
# Range: -1.0 (all negative) to +1.0 (all positive)Fear & Greed Index — Composite market mood (0-100):
| Range | Label | Typical Signal |
|---|---|---|
| 0-24 | Extreme Fear | Potential accumulation zone |
| 25-44 | Fear | Below-average sentiment |
| 45-55 | Neutral | No strong directional bias |
| 56-74 | Greed | Above-average sentiment |
| 75-100 | Extreme Greed | Potential distribution zone |
Social Volume — Total mentions across platforms:
social_volume_z = (current_volume - mean_30d) / std_30d
# z > 2.0 suggests unusual activityOn-Chain Sentiment Proxies
On-chain data reveals what participants are doing, not just saying:
Funding Rates — Perpetual futures cost of carry:
# Positive funding = longs pay shorts (bullish crowding)
# Negative funding = shorts pay longs (bearish crowding)
funding_sentiment = -1.0 * normalize(funding_rate, -0.1, 0.1)
# Inverted: high positive funding is contrarian bearishLong/Short Ratio — Proportion of leveraged positions:
ls_ratio = long_accounts / short_accounts
# > 2.0 = crowded long, < 0.5 = crowded short
ls_sentiment = -1.0 * normalize(ls_ratio, 0.5, 2.0)Exchange Flows — Net deposits/withdrawals:
net_flow = exchange_inflows - exchange_outflows
# Positive net flow (deposits) = bearish (selling pressure)
# Negative net flow (withdrawals) = bullish (accumulation)
flow_sentiment = -1.0 * normalize(net_flow, -threshold, threshold)Keyword-Based Sentiment Scoring
A simple, LLM-free approach using curated word lists:
BULLISH_KEYWORDS = {
"moon": 2, "bullish": 2, "pump": 1, "breakout": 2,
"buy": 1, "long": 1, "accumulate": 2, "undervalued": 2,
"gem": 1, "rocket": 1, "ath": 1, "rally": 2,
}
BEARISH_KEYWORDS = {
"dump": 2, "bearish": 2, "crash": 2, "scam": 3,
"rug": 3, "sell": 1, "short": 1, "overvalued": 2,
"dead": 2, "rekt": 1, "ponzi": 3, "exit": 1,
}
def score_text(text: str) -> float:
"""Score text from -1.0 (bearish) to +1.0 (bullish)."""
words = text.lower().split()
bull_score = sum(BULLISH_KEYWORDS.get(w, 0) for w in words)
bear_score = sum(BEARISH_KEYWORDS.get(w, 0) for w in words)
total = bull_score + bear_score
if total == 0:
return 0.0
return (bull_score - bear_score) / totalSee references/scoring_methods.md for the full methodology, temporal decay weighting, and composite score construction.
Composite Sentiment Score
Combine multiple signals into a single score:
def composite_sentiment(
social_polarity: float, # -1.0 to +1.0
mention_velocity: float, # 0 to inf
fear_greed: int, # 0 to 100
funding_rate: float, # -0.1 to +0.1
weights: dict | None = None,
) -> float:
"""Compute weighted composite sentiment score (-100 to +100).
Args:
social_polarity: Average polarity of social mentions.
mention_velocity: Current velocity vs baseline.
fear_greed: Fear & Greed index reading.
funding_rate: Current perpetual funding rate.
weights: Optional custom weights.
Returns:
Composite score from -100 (extreme fear) to +100 (extreme greed).
"""
w = weights or {
"social": 0.30,
"velocity": 0.15,
"fear_greed": 0.30,
"funding": 0.25,
}
# Normalize each component to -1.0 to +1.0
s_social = social_polarity
s_velocity = min(mention_velocity / 10.0, 1.0) # Cap at 10x
s_fg = (fear_greed - 50) / 50.0 # 0-100 -> -1 to +1
s_funding = -10.0 * funding_rate # Contrarian: high funding = bearish
s_funding = max(-1.0, min(1.0, s_funding))
raw = (
w["social"] * s_social
+ w["velocity"] * s_velocity
+ w["fear_greed"] * s_fg
+ w["funding"] * s_funding
)
return round(raw * 100, 1)Contrarian Signals
Extreme sentiment readings often precede reversals:
| Condition | Interpretation |
|---|---|
| Composite < -70 | Extreme fear — historically a buying zone |
| Composite > +70 | Extreme greed — historically a selling zone |
| Velocity > 10x + polarity > 0.6 | Euphoric spike — fade potential |
| Velocity > 10x + polarity < -0.6 | Panic spike — bounce potential |
| Funding > 0.05% + LS ratio > 2.0 | Crowded long — liquidation risk |
| Funding < -0.05% + LS ratio < 0.5 | Crowded short — squeeze risk |
Key principle: Sentiment is most useful at extremes. Neutral readings (composite between -30 and +30) have low predictive value.
Influencer Tracking
Monitor high-follower accounts for early signal detection:
def influencer_signal(
posts: list[dict],
min_followers: int = 50_000,
lookback_hours: int = 24,
) -> dict:
"""Detect influencer activity around a token.
Args:
posts: List of posts with 'followers', 'timestamp', 'sentiment'.
min_followers: Minimum follower count to qualify as influencer.
lookback_hours: Time window in hours.
Returns:
Dict with influencer_count, avg_sentiment, total_reach.
"""
cutoff = time.time() - (lookback_hours * 3600)
relevant = [
p for p in posts
if p["followers"] >= min_followers and p["timestamp"] >= cutoff
]
if not relevant:
return {"influencer_count": 0, "avg_sentiment": 0.0, "total_reach": 0}
return {
"influencer_count": len(relevant),
"avg_sentiment": sum(p["sentiment"] for p in relevant) / len(relevant),
"total_reach": sum(p["followers"] for p in relevant),
}Integration With Other Skills
| Skill | Integration Point |
|---|---|
position-sizing | Reduce size in extreme greed, increase in extreme fear |
risk-management | Tighten stops when sentiment diverges from price |
regime-detection | Sentiment confirms or contradicts regime classification |
feature-engineering | Sentiment metrics as ML features |
signal-classification | Sentiment as input to signal scoring models |
whale-tracking | Combine whale activity with social sentiment |
token-holder-analysis | Holder growth/decline as sentiment proxy |
Practical Workflow
1. Fetch fear/greed index → Market-wide mood
2. Pull social data for token → Token-specific sentiment
3. Score text with keyword method → Polarity scores
4. Compute mention velocity → Trending detection
5. Check on-chain proxies → Funding, flows
6. Calculate composite score → Single decision input
7. Flag contrarian signals → Extreme readings
8. Integrate with position sizing → Adjust allocationLimitations and Warnings
- Sentiment is noisy. Individual readings are unreliable — use trends and extremes.
- Social data is gameable. Bot activity can inflate mention counts.
- Keyword scoring is crude. It misses sarcasm, context, and nuance.
- Lag exists. By the time sentiment is measurable, price may have moved.
- Not financial advice. Sentiment data is for informational and analytical purposes only.
- API access varies. Twitter/X API pricing has changed frequently. Budget accordingly.
- Survivorship bias. Tokens that go to zero stop being discussed — absence of mentions is also a signal.
Files
References
references/data_sources.md— API details, rate limits, and access patterns for all sentiment data sourcesreferences/scoring_methods.md— Keyword lists, composite scoring methodology, temporal decay, contrarian logic
Scripts
scripts/sentiment_scanner.py— Fetches live sentiment data from free APIs, computes composite scores, flags contrarian signalsscripts/keyword_sentiment.py— Standalone keyword-based text sentiment analyzer with synthetic demo data
Sentiment Data Sources
Complete reference for social, aggregator, and on-chain sentiment data APIs.
Social Media APIs
Twitter/X API
- Base URL:
https://api.twitter.com/2/ - Auth: OAuth 2.0 Bearer Token
- Key Endpoints:
GET /tweets/search/recent— Search tweets from last 7 daysGET /tweets/counts/recent— Tweet volume over timeGET /users/:id/tweets— User timeline- Rate Limits:
- Free tier: 1 app, read-only, 1,500 tweets/month
- Basic ($100/mo): 10,000 tweets/month, 2 apps
- Pro ($5,000/mo): 1M tweets/month, full archive search
- Search Query Examples:
$SOL lang:en -is:retweet # SOL mentions, English, no RTs
(solana OR $SOL) (pump OR moon) # Bullish keywords- Response Fields:
id,text,created_at,public_metrics.like_count,
public_metrics.retweet_count, author_id
- Notes: Pricing changed significantly in 2023. Free tier is very limited.
Consider third-party aggregators for cost-effective access.
Reddit API
- Base URL:
https://oauth.reddit.com/ - Auth: OAuth2 with client ID and secret
- Key Endpoints:
GET /r/{subreddit}/search— Search within subredditGET /r/{subreddit}/hot— Hot postsGET /r/{subreddit}/new— New postsGET /r/{subreddit}/comments/{article}— Post comments- Rate Limits: 100 requests/minute per OAuth client
- Relevant Subreddits:
r/cryptocurrency— General crypto discussionr/solana— Solana ecosystemr/defi— DeFi protocolsr/CryptoMoonShots— Speculative tokens (high noise)- Response Fields:
title,selftext,score,num_comments,
created_utc, author, upvote_ratio
- Notes: Free and accessible. High signal in r/cryptocurrency daily threads.
Telegram
- Bot API URL:
https://api.telegram.org/bot{token}/ - Key Methods:
getUpdates— Receive messages from channels the bot is ingetChatMemberCount— Channel subscriber count- Rate Limits: 30 messages/second, 20 messages/minute to same chat
- Access Pattern: Create a bot via @BotFather, add to target channels.
Cannot search across public channels without scraping.
- Monitoring Approach:
# Poll for new messages in monitored channels
updates = httpx.get(f"{BOT_URL}/getUpdates?offset={last_id}").json()
for update in updates["result"]:
text = update.get("channel_post", {}).get("text", "")
# Score text for sentiment- Notes: Many crypto alpha groups use Telegram. Signal quality varies.
Discord
- Bot Gateway: WebSocket-based, requires bot token
- REST API:
https://discord.com/api/v10/ - Key Endpoints:
GET /channels/{id}/messages— Channel message historyGET /guilds/{id}— Server info including member count- Rate Limits: 50 requests/second globally
- Notes: Requires bot to be invited to servers. Good for project-specific
sentiment (e.g., monitoring a token's official Discord).
Aggregator APIs
Alternative.me Fear & Greed Index
- Endpoint:
GET https://api.alternative.me/fng/ - Auth: None required (free)
- Parameters:
limit— Number of days (default 1, max ~4000)date_format—us,cn,kr, orworld- Rate Limits: Undocumented, ~100 requests/minute observed
- Response:
{
"data": [
{
"value": "25",
"value_classification": "Extreme Fear",
"timestamp": "1709856000"
}
]
}- Composition: Volatility (25%), market momentum/volume (25%),
social media (15%), surveys (15%), Bitcoin dominance (10%), trends (10%)
- Notes: Bitcoin-focused but correlates with altcoin sentiment. Updated daily.
LunarCrush
- Base URL:
https://lunarcrush.com/api4/public/ - Auth: API key via
Authorization: Bearer {key} - Key Endpoints:
GET /coins/{symbol}/time-series/v2— Social metrics over timeGET /coins/list/v2— All tracked coins with social statsGET /coins/{symbol}/meta/v1— Coin metadata and current social stats- Rate Limits: Free tier 100 calls/day, Pro tier 10,000 calls/day
- Key Metrics:
galaxy_score(0-100 composite),alt_rank,
social_volume, social_score, social_dominance
- Notes: Best aggregated social data source. Free tier is usable for
periodic checks.
Santiment
- Base URL:
https://api.santiment.net/graphql - Auth: API key via header
- Key Queries (GraphQL):
{
getMetric(metric: "social_volume_total") {
timeseriesData(slug: "solana", from: "2025-01-01", to: "2025-01-31") {
datetime
value
}
}
}- Key Metrics:
social_volume_total,social_dominance,
sentiment_positive_total, sentiment_negative_total, weighted_social_sentiment
- Rate Limits: Free tier 100 API calls/month, paid tiers higher
- Notes: Highest quality sentiment data but expensive for full access.
On-Chain Sentiment Proxies
Binance Funding Rates
- Endpoint:
GET https://fapi.binance.com/fapi/v1/fundingRate - Auth: None required for public endpoints
- Parameters:
symbol(e.g.,SOLUSDT),limit(default 100, max 1000) - Rate Limits: 2400 request weight/minute
- Response:
[
{
"symbol": "SOLUSDT",
"fundingRate": "0.00010000",
"fundingTime": 1709856000000
}
]- Interpretation:
> 0.01%— Longs paying premium, bullish crowding< -0.01%— Shorts paying premium, bearish crowding> 0.05%— Extreme: high liquidation risk for longs< -0.05%— Extreme: short squeeze risk- Notes: Funding settles every 8 hours. Use
fundingTimefor alignment.
Binance Long/Short Ratio
- Endpoint:
GET https://fapi.binance.com/futures/data/globalLongShortAccountRatio - Auth: None required
- Parameters:
symbol,period(5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d) - Response:
[
{
"symbol": "SOLUSDT",
"longShortRatio": "1.5000",
"longAccount": "0.6000",
"shortAccount": "0.4000",
"timestamp": 1709856000000
}
]- Notes: Ratio > 2.0 or < 0.5 indicates crowded positioning.
CoinGecko Community Data
- Endpoint:
GET https://api.coingecko.com/api/v3/coins/{id} - Auth: None for free tier, API key for Pro
- Rate Limits: 10-30 calls/minute (free), 500/minute (Pro)
- Community Fields (in response):
{
"community_data": {
"twitter_followers": 1234567,
"reddit_subscribers": 456789,
"reddit_accounts_active_48h": 12345,
"telegram_channel_user_count": 67890
},
"developer_data": {
"forks": 1234,
"stars": 5678,
"commit_count_4_weeks": 200
},
"sentiment_votes_up_percentage": 75.5,
"sentiment_votes_down_percentage": 24.5
}- Notes: Free and comprehensive. Community data updates every few hours.
The sentiment_votes_up_percentage field is a direct crowd sentiment gauge.
CryptoQuant / Glassnode (Exchange Flows)
- CryptoQuant API:
https://api.cryptoquant.com/v1/ GET /btc/exchange-flows/netflow— Net exchange flow- Auth: API key, free tier available with limited data
- Glassnode API:
https://api.glassnode.com/v1/metrics/ GET /transactions/transfers_volume_exchanges_net— Net transfer volume- Auth: API key, free tier with 24h delay
- Interpretation:
- Positive net flow (inflows > outflows) = coins moving to exchanges = sell pressure
- Negative net flow (outflows > inflows) = coins leaving exchanges = accumulation
- Notes: Bitcoin and Ethereum focused. Limited Solana coverage on free tiers.
Free-Tier Strategy
For cost-effective sentiment monitoring, use these free sources:
1. Alternative.me Fear & Greed — Daily market-wide mood (no auth) 2. CoinGecko community data — Token-specific social stats (no auth, rate limited) 3. Binance funding rates — On-chain positioning (no auth) 4. Binance long/short ratio — Crowd positioning (no auth) 5. Reddit API — Social text data (free OAuth)
This combination provides market-wide sentiment, token-specific social metrics, and on-chain positioning data at zero cost.
Sentiment Scoring Methods
Comprehensive reference for keyword-based scoring, composite indices, temporal decay, and contrarian signal detection.
Keyword-Based Sentiment
Approach
Score text by matching tokens against curated positive/negative word lists weighted by intensity. No ML model required — runs anywhere with zero dependencies.
Crypto-Specific Word Lists
Bullish Keywords (word: weight):
moon: 2, bullish: 2, pump: 1, breakout: 2, buy: 1, long: 1,
accumulate: 2, undervalued: 2, gem: 1, rocket: 1, ath: 1, rally: 2,
surge: 2, soar: 2, lambo: 1, diamond: 1, hodl: 1, dip: 1 (as in "buy the dip"),
alpha: 1, gainz: 1, moonshot: 2, parabolic: 2, reversal: 1, recovery: 1,
bottom: 1, support: 1, accumulation: 2, institutional: 1, adoption: 2Bearish Keywords (word: weight):
dump: 2, bearish: 2, crash: 2, scam: 3, rug: 3, sell: 1, short: 1,
overvalued: 2, dead: 2, rekt: 1, ponzi: 3, exit: 1, fraud: 3,
bubble: 2, collapse: 2, liquidation: 2, capitulation: 2, resistance: 1,
distribution: 1, top: 1, overbought: 1, bagholding: 2, worthless: 3,
hack: 3, exploit: 3, insolvent: 3, bankrupt: 3, warning: 2Scoring Formula
def keyword_score(text: str, bullish: dict, bearish: dict) -> float:
"""Score text from -1.0 (bearish) to +1.0 (bullish).
Formula: (bull_sum - bear_sum) / (bull_sum + bear_sum)
Returns 0.0 if no keywords matched.
"""
words = text.lower().split()
bull = sum(bullish.get(w, 0) for w in words)
bear = sum(bearish.get(w, 0) for w in words)
total = bull + bear
if total == 0:
return 0.0
return (bull - bear) / totalLimitations
- Misses sarcasm: "Great, another rug pull" scores partially bullish
- Context-blind: "not bullish" scores as bullish
- Language-specific: English only without translation
- Gameable: Bots can flood channels with keyword-loaded posts
Mitigation Strategies
- Require minimum text length (> 10 words) to reduce noise
- Weight by engagement: higher-engagement posts count more
- Apply author reputation scoring (account age, follower count)
- Use bigrams for negation detection: "not bullish" -> bearish
Mention Velocity
Definition
Rate of token mentions compared to a historical baseline:
velocity = mentions_current_period / baseline_mentions_per_periodBaseline Computation
Use a 30-day rolling average as baseline:
def compute_velocity(
current_mentions: int,
period_hours: float,
daily_baseline: float,
) -> float:
"""Compute mention velocity as multiple of baseline.
Args:
current_mentions: Mentions observed in the current period.
period_hours: Length of current observation period in hours.
daily_baseline: 30-day average daily mentions.
Returns:
Velocity as a multiple of baseline (1.0 = normal).
"""
hourly_baseline = daily_baseline / 24.0
if hourly_baseline <= 0:
return 0.0
hourly_current = current_mentions / max(period_hours, 0.1)
return hourly_current / hourly_baselineInterpretation
| Velocity | Meaning |
|---|---|
| 0.0-0.5 | Below normal — low interest |
| 0.5-1.5 | Normal range |
| 1.5-3.0 | Elevated — growing interest |
| 3.0-10.0 | Trending — significant event |
| > 10.0 | Viral — major event or coordinated activity |
Combined Velocity + Polarity Signals
| Velocity | Polarity | Signal |
|---|---|---|
| > 5x | > +0.5 | Euphoria — potential top |
| > 5x | < -0.5 | Panic — potential bottom |
| > 5x | Near 0 | Controversy — uncertain |
| < 0.3x | Any | Forgotten — check if project is dead |
Fear & Greed Index
Alternative.me Composition
The crypto Fear & Greed Index uses these components:
| Component | Weight | Source |
|---|---|---|
| Volatility | 25% | Current vs 30/90-day avg |
| Market Momentum/Volume | 25% | Current vs 30/90-day avg |
| Social Media | 15% | Twitter/Reddit mentions + engagement |
| Surveys | 15% | Community polls (when available) |
| Bitcoin Dominance | 10% | BTC market cap share |
| Google Trends | 10% | Search volume for crypto terms |
Building a Custom Fear/Greed Score
For token-specific sentiment, build a custom index:
def custom_fear_greed(
price_vs_sma30: float, # price / SMA(30) - 1.0
volume_vs_avg: float, # volume / avg_volume(30)
social_polarity: float, # -1.0 to +1.0
funding_rate: float, # perpetual funding rate
holder_growth_pct: float, # 7d holder count change %
) -> int:
"""Compute custom fear/greed index (0-100).
Each component maps to 0-100, then weighted average.
"""
# Price momentum: -20% below SMA = 0, +20% above = 100
c_price = min(100, max(0, (price_vs_sma30 + 0.2) / 0.4 * 100))
# Volume: 0.5x avg = 0, 2.0x avg = 100
c_volume = min(100, max(0, (volume_vs_avg - 0.5) / 1.5 * 100))
# Social: -1.0 = 0, +1.0 = 100
c_social = (social_polarity + 1.0) / 2.0 * 100
# Funding: -0.05% = 0 (fear), +0.05% = 100 (greed)
c_funding = min(100, max(0, (funding_rate + 0.0005) / 0.001 * 100))
# Holder growth: -5% = 0, +5% = 100
c_holders = min(100, max(0, (holder_growth_pct + 5) / 10 * 100))
score = int(
0.25 * c_price
+ 0.20 * c_volume
+ 0.25 * c_social
+ 0.15 * c_funding
+ 0.15 * c_holders
)
return min(100, max(0, score))Composite Sentiment Score
Construction
Combine all available signals into a single -100 to +100 score:
composite = w1 * social_polarity_norm
+ w2 * velocity_norm
+ w3 * fear_greed_norm
+ w4 * funding_normDefault Weights
| Component | Weight | Rationale |
|---|---|---|
| Social polarity | 0.30 | Direct sentiment measure |
| Fear/greed index | 0.30 | Broad market context |
| Funding rate | 0.25 | Reveals leveraged positioning |
| Mention velocity | 0.15 | Activity level indicator |
Normalization
All components must be normalized to the -1.0 to +1.0 range before weighting:
- Social polarity: already -1.0 to +1.0
- Velocity:
min(velocity / 10.0, 1.0)(caps at 10x baseline) - Fear/greed:
(value - 50) / 50(0-100 -> -1 to +1) - Funding:
-10.0 * rate, clamped to [-1, 1] (contrarian: inverted)
Temporal Decay
Recent data points carry more weight than older ones:
def temporal_weight(age_hours: float, half_life_hours: float = 6.0) -> float:
"""Exponential decay weight based on data age.
Args:
age_hours: How old the data point is in hours.
half_life_hours: Hours until weight drops to 50%.
Returns:
Weight from 0.0 to 1.0.
"""
import math
return math.exp(-0.693 * age_hours / half_life_hours)| Age (hours) | Weight (6h half-life) |
|---|---|
| 0 | 1.000 |
| 3 | 0.707 |
| 6 | 0.500 |
| 12 | 0.250 |
| 24 | 0.063 |
| 48 | 0.004 |
Contrarian Signal Detection
Rules
1. Extreme Fear Buy Signal: Composite < -70 AND velocity > 5x
- Crowd is panicking — historically precedes bounces
2. Extreme Greed Sell Signal: Composite > +70 AND velocity > 5x
- Crowd is euphoric — historically precedes corrections
3. Funding Divergence: Funding > 0.05% but price declining
- Longs trapped — liquidation cascade risk
4. Silent Accumulation: Velocity < 0.3x but holder count increasing
- Smart money accumulating while retail is disinterested
Confidence Levels
| Signals Aligned | Confidence |
|---|---|
| 1 | Low — single indicator, could be noise |
| 2 | Moderate — worth monitoring |
| 3+ | High — strong contrarian setup |
Historical Context
Extreme Fear & Greed readings (< 10 or > 90) have historically occurred during 5-10% of trading days. The signal is valuable precisely because it is rare. Do not lower thresholds to generate more signals.
#!/usr/bin/env python3
"""Keyword-based sentiment analyzer for crypto social media text.
Scores text using curated bullish/bearish word lists without any ML model
or external dependencies. Includes a demo mode with 20 synthetic crypto
social media posts showing sentiment analysis in action.
Usage:
python scripts/keyword_sentiment.py # Demo with 20 synthetic posts
python scripts/keyword_sentiment.py --demo # Same as above (explicit)
python scripts/keyword_sentiment.py --text "SOL is going to moon, super bullish"
Dependencies:
None (stdlib only)
"""
import argparse
import math
import re
import sys
import time
from collections import Counter
from dataclasses import dataclass, field
from typing import Optional
# ── Keyword Dictionaries ───────────────────────────────────────────
BULLISH_KEYWORDS: dict[str, int] = {
# Strong bullish (weight 3)
"moonshot": 3, "parabolic": 3, "generational": 3,
# Moderate bullish (weight 2)
"moon": 2, "bullish": 2, "breakout": 2, "accumulate": 2,
"undervalued": 2, "rally": 2, "surge": 2, "soar": 2,
"adoption": 2, "partnership": 2, "institutional": 2,
"recovery": 2, "reversal": 2, "pumping": 2,
# Mild bullish (weight 1)
"buy": 1, "long": 1, "gem": 1, "rocket": 1, "ath": 1,
"hodl": 1, "alpha": 1, "dip": 1, "support": 1,
"bottom": 1, "green": 1, "gains": 1, "profit": 1,
"lambo": 1, "diamond": 1, "strong": 1, "growth": 1,
"upgrade": 1, "launch": 1, "listing": 1, "explosive": 1,
"promising": 1, "opportunity": 1, "underrated": 1,
}
BEARISH_KEYWORDS: dict[str, int] = {
# Strong bearish (weight 3)
"scam": 3, "rug": 3, "fraud": 3, "ponzi": 3, "worthless": 3,
"hack": 3, "exploit": 3, "insolvent": 3, "bankrupt": 3,
# Moderate bearish (weight 2)
"dump": 2, "bearish": 2, "crash": 2, "collapse": 2,
"liquidation": 2, "capitulation": 2, "bagholding": 2,
"bubble": 2, "warning": 2, "overvalued": 2, "rugpull": 2,
"dumping": 2,
# Mild bearish (weight 1)
"sell": 1, "short": 1, "dead": 1, "rekt": 1, "exit": 1,
"resistance": 1, "top": 1, "overbought": 1, "red": 1,
"loss": 1, "falling": 1, "decline": 1, "weak": 1,
"fear": 1, "risk": 1, "delay": 1, "concern": 1,
}
# Negation words that flip the sentiment of the next keyword
NEGATION_WORDS: set[str] = {
"not", "no", "never", "dont", "doesn't", "isn't", "wasn't",
"won't", "can't", "couldn't", "shouldn't", "wouldn't", "neither",
"hardly", "barely", "without",
}
# ── Data Classes ────────────────────────────────────────────────────
@dataclass
class PostScore:
"""Sentiment score for a single post."""
text: str
score: float # -1.0 to +1.0
label: str # "bullish", "bearish", "neutral"
bullish_words: list[str]
bearish_words: list[str]
confidence: float # 0.0 to 1.0 based on keyword density
@dataclass
class AggregateResult:
"""Aggregated sentiment across multiple posts."""
total_posts: int
bullish_count: int
bearish_count: int
neutral_count: int
average_score: float
median_score: float
score_std: float
top_bullish_words: list[tuple[str, int]]
top_bearish_words: list[tuple[str, int]]
sentiment_distribution: dict[str, float]
# ── Scoring Functions ───────────────────────────────────────────────
def tokenize(text: str) -> list[str]:
"""Tokenize text into lowercase words, removing punctuation.
Args:
text: Raw input text.
Returns:
List of lowercase word tokens.
"""
cleaned = re.sub(r"[^\w\s$#@]", " ", text.lower())
# Handle cashtags: $SOL -> sol
cleaned = cleaned.replace("$", "")
return cleaned.split()
def score_text(
text: str,
bullish: Optional[dict[str, int]] = None,
bearish: Optional[dict[str, int]] = None,
use_negation: bool = True,
) -> PostScore:
"""Score a text string for crypto sentiment.
Uses keyword matching with optional negation detection.
Negation flips the sentiment of the next keyword found.
Args:
text: The text to analyze.
bullish: Bullish keyword dict (word -> weight). Defaults to built-in.
bearish: Bearish keyword dict (word -> weight). Defaults to built-in.
use_negation: Whether to apply negation detection.
Returns:
PostScore with score, label, matched words, and confidence.
"""
bull_dict = bullish or BULLISH_KEYWORDS
bear_dict = bearish or BEARISH_KEYWORDS
words = tokenize(text)
bull_score = 0
bear_score = 0
bull_words: list[str] = []
bear_words: list[str] = []
negated = False
for word in words:
if use_negation and word in NEGATION_WORDS:
negated = True
continue
is_bull = word in bull_dict
is_bear = word in bear_dict
if is_bull:
weight = bull_dict[word]
if negated:
bear_score += weight
bear_words.append(f"~{word}") # ~ prefix = negated
else:
bull_score += weight
bull_words.append(word)
negated = False
elif is_bear:
weight = bear_dict[word]
if negated:
bull_score += weight
bull_words.append(f"~{word}")
else:
bear_score += weight
bear_words.append(word)
negated = False
else:
# Non-keyword resets negation after 1 intervening word
# (e.g., "not very bullish" should still negate)
pass
total_weight = bull_score + bear_score
if total_weight == 0:
score = 0.0
label = "neutral"
confidence = 0.0
else:
score = (bull_score - bear_score) / total_weight
keyword_density = len(bull_words + bear_words) / max(len(words), 1)
confidence = min(1.0, keyword_density * 5) # 20% density = full confidence
if score > 0.1:
label = "bullish"
elif score < -0.1:
label = "bearish"
else:
label = "neutral"
return PostScore(
text=text,
score=round(score, 3),
label=label,
bullish_words=bull_words,
bearish_words=bear_words,
confidence=round(confidence, 3),
)
def aggregate_scores(posts: list[PostScore]) -> AggregateResult:
"""Aggregate sentiment scores across multiple posts.
Args:
posts: List of PostScore objects.
Returns:
AggregateResult with counts, averages, and word frequencies.
"""
if not posts:
return AggregateResult(
total_posts=0, bullish_count=0, bearish_count=0, neutral_count=0,
average_score=0.0, median_score=0.0, score_std=0.0,
top_bullish_words=[], top_bearish_words=[],
sentiment_distribution={},
)
scores = [p.score for p in posts]
bullish_count = sum(1 for p in posts if p.label == "bullish")
bearish_count = sum(1 for p in posts if p.label == "bearish")
neutral_count = sum(1 for p in posts if p.label == "neutral")
avg = sum(scores) / len(scores)
sorted_scores = sorted(scores)
n = len(sorted_scores)
median = (
sorted_scores[n // 2]
if n % 2 == 1
else (sorted_scores[n // 2 - 1] + sorted_scores[n // 2]) / 2.0
)
variance = sum((s - avg) ** 2 for s in scores) / len(scores)
std = math.sqrt(variance)
# Word frequency
all_bull: Counter[str] = Counter()
all_bear: Counter[str] = Counter()
for p in posts:
for w in p.bullish_words:
all_bull[w.lstrip("~")] += 1
for w in p.bearish_words:
all_bear[w.lstrip("~")] += 1
total = len(posts)
distribution = {
"bullish": round(bullish_count / total * 100, 1),
"neutral": round(neutral_count / total * 100, 1),
"bearish": round(bearish_count / total * 100, 1),
}
return AggregateResult(
total_posts=total,
bullish_count=bullish_count,
bearish_count=bearish_count,
neutral_count=neutral_count,
average_score=round(avg, 3),
median_score=round(median, 3),
score_std=round(std, 3),
top_bullish_words=all_bull.most_common(10),
top_bearish_words=all_bear.most_common(10),
sentiment_distribution=distribution,
)
# ── Display ─────────────────────────────────────────────────────────
def display_post_score(post: PostScore, index: int) -> None:
"""Display a single post's sentiment analysis.
Args:
post: The scored post.
index: Post number for display.
"""
indicator = {
"bullish": "[+]",
"bearish": "[-]",
"neutral": "[=]",
}.get(post.label, "[?]")
# Truncate text for display
display_text = post.text if len(post.text) <= 80 else post.text[:77] + "..."
print(f"\n {indicator} Post {index}: {display_text}")
print(f" Score: {post.score:>+.3f} | Label: {post.label} | Confidence: {post.confidence:.1%}")
if post.bullish_words:
print(f" Bullish words: {', '.join(post.bullish_words)}")
if post.bearish_words:
print(f" Bearish words: {', '.join(post.bearish_words)}")
def display_aggregate(agg: AggregateResult) -> None:
"""Display aggregated sentiment results.
Args:
agg: AggregateResult to display.
"""
print("\n" + "=" * 60)
print(" AGGREGATE SENTIMENT ANALYSIS")
print("=" * 60)
print(f"\n Total Posts Analyzed: {agg.total_posts}")
print(f"\n --- Distribution ---")
# Visual bar chart
bar_width = 40
for label in ["bullish", "neutral", "bearish"]:
pct = agg.sentiment_distribution.get(label, 0)
bar_len = int(pct / 100 * bar_width)
bar = "#" * bar_len + "." * (bar_width - bar_len)
count = {"bullish": agg.bullish_count, "neutral": agg.neutral_count,
"bearish": agg.bearish_count}[label]
print(f" {label:>8}: [{bar}] {pct:5.1f}% ({count})")
print(f"\n --- Score Statistics ---")
print(f" Average Score: {agg.average_score:>+.3f}")
print(f" Median Score: {agg.median_score:>+.3f}")
print(f" Std Deviation: {agg.score_std:>.3f}")
# Overall label
if agg.average_score > 0.1:
overall = "BULLISH"
elif agg.average_score < -0.1:
overall = "BEARISH"
else:
overall = "NEUTRAL"
print(f" Overall: {overall}")
print(f"\n --- Top Bullish Words ---")
if agg.top_bullish_words:
for word, count in agg.top_bullish_words[:7]:
print(f" {word:<20} x{count}")
else:
print(" (none)")
print(f"\n --- Top Bearish Words ---")
if agg.top_bearish_words:
for word, count in agg.top_bearish_words[:7]:
print(f" {word:<20} x{count}")
else:
print(" (none)")
print("\n" + "=" * 60)
print(" NOTE: This is analytical information only, not financial advice.")
print("=" * 60 + "\n")
# ── Demo Mode ───────────────────────────────────────────────────────
DEMO_POSTS: list[str] = [
# Bullish posts
"SOL is going parabolic! This breakout is massive, accumulate before the moon!",
"$SOL partnership with Visa is bullish af, institutional adoption incoming",
"Just bought the dip on Solana. This gem is so undervalued right now. Diamond hands hodl!",
"SOL rally looking strong, support held perfectly. Green candles everywhere",
"Solana TPS hitting new ATH, this is bullish for the ecosystem growth",
"Massive gains on my SOL long position, this pump is just getting started",
"New DeFi launch on Solana looks promising, real opportunity here",
# Bearish posts
"Another Solana outage? This chain is dead, total scam. Selling everything.",
"$SOL is dumping hard, this crash could get worse. Overvalued garbage",
"Warning: SOL looks like a classic bubble, bearish divergence on the 4h chart",
"Rugpull on another Solana memecoin, this ecosystem is full of fraud and exploits",
"Liquidation cascade incoming for SOL longs. Overbought and at resistance.",
"SOL is weak, declining volume, capitulation hasnt even started yet",
# Neutral / mixed posts
"Solana TVL is at 4.2B, interesting to see how it compares to Ethereum",
"Just deployed my first smart contract on Solana. The developer experience is smooth.",
"SOL trading at $150, volume is average. Waiting for a clear direction.",
"Not bullish on SOL short term but long term the technology is solid",
"Comparing Solana vs Avalanche transaction costs for my research paper",
"The new Solana phone looks cool but not sure how it affects the token price",
"Attending Solana Breakpoint conference next week, should be informative",
]
def run_demo() -> None:
"""Run demo mode with 20 synthetic crypto social media posts."""
print("\n" + "=" * 60)
print(" KEYWORD SENTIMENT ANALYZER — DEMO MODE")
print(" Analyzing 20 synthetic crypto social media posts")
print("=" * 60)
scored_posts: list[PostScore] = []
for i, text in enumerate(DEMO_POSTS, 1):
post_score = score_text(text)
scored_posts.append(post_score)
display_post_score(post_score, i)
agg = aggregate_scores(scored_posts)
display_aggregate(agg)
# ── Single Text Mode ───────────────────────────────────────────────
def analyze_single(text: str) -> None:
"""Analyze a single text input.
Args:
text: The text to analyze.
"""
print("\n" + "=" * 60)
print(" KEYWORD SENTIMENT ANALYZER — SINGLE TEXT")
print("=" * 60)
result = score_text(text)
display_post_score(result, 1)
print("\n" + "=" * 60)
print(" NOTE: This is analytical information only, not financial advice.")
print("=" * 60 + "\n")
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point: parse arguments and run analyzer."""
parser = argparse.ArgumentParser(
description="Keyword-based crypto sentiment analyzer"
)
parser.add_argument(
"--text",
type=str,
default=None,
help="Single text to analyze",
)
parser.add_argument(
"--demo",
action="store_true",
help="Run demo mode with 20 synthetic posts (default if no --text)",
)
args = parser.parse_args()
if args.text:
analyze_single(args.text)
else:
run_demo()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Sentiment scanner that fetches live data from free APIs and computes composite scores.
Pulls data from:
- Alternative.me Fear & Greed Index (no auth required)
- CoinGecko community data (no auth required, rate limited)
- Binance funding rates and long/short ratios (no auth required)
Computes a composite sentiment score from -100 (extreme fear) to +100 (extreme greed)
and flags contrarian opportunities.
Usage:
python scripts/sentiment_scanner.py # Live scan for SOL
python scripts/sentiment_scanner.py --symbol ETH # Live scan for ETH
python scripts/sentiment_scanner.py --demo # Demo with synthetic data
Dependencies:
uv pip install httpx
"""
import argparse
import math
import sys
import time
from dataclasses import dataclass, field
from typing import Optional
try:
import httpx
except ImportError:
print("Missing dependency. Install with: uv pip install httpx")
sys.exit(1)
# ── Configuration ───────────────────────────────────────────────────
COINGECKO_BASE = "https://api.coingecko.com/api/v3"
FEAR_GREED_URL = "https://api.alternative.me/fng/"
BINANCE_FUTURES_BASE = "https://fapi.binance.com"
# CoinGecko ID mapping for common tokens
COINGECKO_IDS: dict[str, str] = {
"SOL": "solana",
"BTC": "bitcoin",
"ETH": "ethereum",
"AVAX": "avalanche-2",
"MATIC": "matic-network",
"ARB": "arbitrum",
"OP": "optimism",
"LINK": "chainlink",
"DOT": "polkadot",
"ADA": "cardano",
}
# Binance futures symbol mapping
BINANCE_SYMBOLS: dict[str, str] = {
"SOL": "SOLUSDT",
"BTC": "BTCUSDT",
"ETH": "ETHUSDT",
"AVAX": "AVAXUSDT",
"MATIC": "MATICUSDT",
"ARB": "ARBUSDT",
"OP": "OPUSDT",
"LINK": "LINKUSDT",
"DOT": "DOTUSDT",
"ADA": "ADAUSDT",
}
DEFAULT_WEIGHTS: dict[str, float] = {
"social": 0.30,
"velocity": 0.15,
"fear_greed": 0.30,
"funding": 0.25,
}
REQUEST_TIMEOUT = 15.0
# ── Data Classes ────────────────────────────────────────────────────
@dataclass
class FearGreedData:
"""Fear & Greed Index reading."""
value: int
classification: str
timestamp: int
@dataclass
class CommunityData:
"""CoinGecko community statistics."""
twitter_followers: int = 0
reddit_subscribers: int = 0
reddit_active_48h: int = 0
telegram_members: int = 0
sentiment_up_pct: float = 50.0
sentiment_down_pct: float = 50.0
developer_commits_4w: int = 0
@dataclass
class FundingData:
"""Binance futures funding rate and long/short ratio."""
funding_rate: float = 0.0
funding_time: int = 0
long_short_ratio: float = 1.0
long_account_pct: float = 0.5
short_account_pct: float = 0.5
@dataclass
class SentimentResult:
"""Complete sentiment analysis result."""
symbol: str
fear_greed: Optional[FearGreedData] = None
community: Optional[CommunityData] = None
funding: Optional[FundingData] = None
social_polarity: float = 0.0
mention_velocity: float = 1.0
composite_score: float = 0.0
contrarian_signals: list[str] = field(default_factory=list)
errors: list[str] = field(default_factory=list)
# ── API Fetchers ────────────────────────────────────────────────────
def fetch_fear_greed(client: httpx.Client, limit: int = 7) -> Optional[FearGreedData]:
"""Fetch the crypto Fear & Greed Index from alternative.me.
Args:
client: HTTP client instance.
limit: Number of historical days to fetch.
Returns:
Most recent FearGreedData or None on failure.
"""
try:
resp = client.get(FEAR_GREED_URL, params={"limit": limit})
resp.raise_for_status()
data = resp.json()
latest = data["data"][0]
return FearGreedData(
value=int(latest["value"]),
classification=latest["value_classification"],
timestamp=int(latest["timestamp"]),
)
except (httpx.HTTPError, KeyError, IndexError, ValueError) as exc:
print(f" [WARN] Fear & Greed fetch failed: {exc}")
return None
def fetch_community_data(
client: httpx.Client, coingecko_id: str
) -> Optional[CommunityData]:
"""Fetch community statistics from CoinGecko.
Args:
client: HTTP client instance.
coingecko_id: CoinGecko coin identifier (e.g., 'solana').
Returns:
CommunityData or None on failure.
"""
try:
resp = client.get(
f"{COINGECKO_BASE}/coins/{coingecko_id}",
params={
"localization": "false",
"tickers": "false",
"market_data": "false",
"community_data": "true",
"developer_data": "true",
},
)
resp.raise_for_status()
data = resp.json()
cd = data.get("community_data", {})
dd = data.get("developer_data", {})
return CommunityData(
twitter_followers=cd.get("twitter_followers") or 0,
reddit_subscribers=cd.get("reddit_subscribers") or 0,
reddit_active_48h=cd.get("reddit_accounts_active_48h") or 0,
telegram_members=cd.get("telegram_channel_user_count") or 0,
sentiment_up_pct=data.get("sentiment_votes_up_percentage") or 50.0,
sentiment_down_pct=data.get("sentiment_votes_down_percentage") or 50.0,
developer_commits_4w=dd.get("commit_count_4_weeks") or 0,
)
except (httpx.HTTPError, KeyError, ValueError) as exc:
print(f" [WARN] CoinGecko community fetch failed: {exc}")
return None
def fetch_funding_data(
client: httpx.Client, binance_symbol: str
) -> Optional[FundingData]:
"""Fetch funding rate and long/short ratio from Binance Futures.
Args:
client: HTTP client instance.
binance_symbol: Binance futures symbol (e.g., 'SOLUSDT').
Returns:
FundingData or None on failure.
"""
funding_rate = 0.0
funding_time = 0
ls_ratio = 1.0
long_pct = 0.5
short_pct = 0.5
# Fetch funding rate
try:
resp = client.get(
f"{BINANCE_FUTURES_BASE}/fapi/v1/fundingRate",
params={"symbol": binance_symbol, "limit": 1},
)
resp.raise_for_status()
data = resp.json()
if data:
funding_rate = float(data[-1]["fundingRate"])
funding_time = int(data[-1]["fundingTime"])
except (httpx.HTTPError, KeyError, IndexError, ValueError) as exc:
print(f" [WARN] Funding rate fetch failed: {exc}")
# Fetch long/short ratio
try:
resp = client.get(
f"{BINANCE_FUTURES_BASE}/futures/data/globalLongShortAccountRatio",
params={"symbol": binance_symbol, "period": "1h", "limit": 1},
)
resp.raise_for_status()
data = resp.json()
if data:
ls_ratio = float(data[-1]["longShortRatio"])
long_pct = float(data[-1]["longAccount"])
short_pct = float(data[-1]["shortAccount"])
except (httpx.HTTPError, KeyError, IndexError, ValueError) as exc:
print(f" [WARN] Long/short ratio fetch failed: {exc}")
return FundingData(
funding_rate=funding_rate,
funding_time=funding_time,
long_short_ratio=ls_ratio,
long_account_pct=long_pct,
short_account_pct=short_pct,
)
# ── Scoring Functions ───────────────────────────────────────────────
def compute_social_polarity(community: Optional[CommunityData]) -> float:
"""Derive social polarity from CoinGecko sentiment votes.
Args:
community: CommunityData with sentiment percentages.
Returns:
Polarity from -1.0 (bearish) to +1.0 (bullish).
"""
if community is None:
return 0.0
up = community.sentiment_up_pct
down = community.sentiment_down_pct
total = up + down
if total == 0:
return 0.0
return (up - down) / total
def estimate_mention_velocity(community: Optional[CommunityData]) -> float:
"""Estimate mention velocity from Reddit active users.
Uses reddit_active_48h relative to subscriber count as a proxy.
Normal engagement is ~0.5-2% of subscribers active.
Args:
community: CommunityData with Reddit stats.
Returns:
Velocity estimate (1.0 = normal, >3.0 = elevated).
"""
if community is None or community.reddit_subscribers == 0:
return 1.0
active_ratio = community.reddit_active_48h / community.reddit_subscribers
# Baseline: 1% of subscribers active in 48h is normal
baseline_ratio = 0.01
if baseline_ratio <= 0:
return 1.0
return active_ratio / baseline_ratio
def compute_composite_score(
social_polarity: float,
mention_velocity: float,
fear_greed_value: int,
funding_rate: float,
weights: Optional[dict[str, float]] = None,
) -> float:
"""Compute weighted composite sentiment score.
Args:
social_polarity: Social sentiment from -1.0 to +1.0.
mention_velocity: Mention rate vs baseline (1.0 = normal).
fear_greed_value: Fear & Greed Index (0-100).
funding_rate: Perpetual futures funding rate.
weights: Custom component weights.
Returns:
Composite score from -100.0 (extreme fear) to +100.0 (extreme greed).
"""
w = weights or DEFAULT_WEIGHTS
s_social = max(-1.0, min(1.0, social_polarity))
s_velocity = min(mention_velocity / 10.0, 1.0)
s_fg = (fear_greed_value - 50) / 50.0
s_funding = max(-1.0, min(1.0, -10.0 * funding_rate))
raw = (
w["social"] * s_social
+ w["velocity"] * s_velocity
+ w["fear_greed"] * s_fg
+ w["funding"] * s_funding
)
return round(raw * 100, 1)
def detect_contrarian_signals(result: SentimentResult) -> list[str]:
"""Identify contrarian trading signals from sentiment data.
Args:
result: SentimentResult with all computed metrics.
Returns:
List of contrarian signal descriptions.
"""
signals: list[str] = []
# Extreme composite scores
if result.composite_score <= -70:
signals.append(
f"EXTREME FEAR (composite={result.composite_score:.1f}): "
"Historically a potential accumulation zone"
)
elif result.composite_score >= 70:
signals.append(
f"EXTREME GREED (composite={result.composite_score:.1f}): "
"Historically a potential distribution zone"
)
# Funding rate extremes
if result.funding and result.funding.funding_rate > 0.0005:
signals.append(
f"HIGH FUNDING ({result.funding.funding_rate:.4%}): "
"Longs paying significant premium — crowded long risk"
)
elif result.funding and result.funding.funding_rate < -0.0005:
signals.append(
f"NEGATIVE FUNDING ({result.funding.funding_rate:.4%}): "
"Shorts paying premium — potential short squeeze"
)
# Long/short ratio extremes
if result.funding and result.funding.long_short_ratio > 2.0:
signals.append(
f"CROWDED LONG (L/S ratio={result.funding.long_short_ratio:.2f}): "
"Liquidation cascade risk if price drops"
)
elif result.funding and result.funding.long_short_ratio < 0.5:
signals.append(
f"CROWDED SHORT (L/S ratio={result.funding.long_short_ratio:.2f}): "
"Short squeeze risk if price rises"
)
# High velocity + extreme polarity
if result.mention_velocity > 5.0 and result.social_polarity > 0.6:
signals.append(
f"EUPHORIC SPIKE (velocity={result.mention_velocity:.1f}x, "
f"polarity={result.social_polarity:.2f}): Potential fade opportunity"
)
elif result.mention_velocity > 5.0 and result.social_polarity < -0.6:
signals.append(
f"PANIC SPIKE (velocity={result.mention_velocity:.1f}x, "
f"polarity={result.social_polarity:.2f}): Potential bounce opportunity"
)
# Fear & Greed extremes
if result.fear_greed and result.fear_greed.value <= 10:
signals.append(
f"EXTREME FEAR INDEX ({result.fear_greed.value}): "
"Rare reading — bottom formation historically likely"
)
elif result.fear_greed and result.fear_greed.value >= 90:
signals.append(
f"EXTREME GREED INDEX ({result.fear_greed.value}): "
"Rare reading — top formation historically likely"
)
return signals
# ── Display ─────────────────────────────────────────────────────────
def classify_score(score: float) -> str:
"""Classify a composite score into a human-readable label.
Args:
score: Composite score from -100 to +100.
Returns:
Classification string.
"""
if score <= -70:
return "Extreme Fear"
elif score <= -30:
return "Fear"
elif score <= 30:
return "Neutral"
elif score <= 70:
return "Greed"
else:
return "Extreme Greed"
def display_result(result: SentimentResult) -> None:
"""Print a formatted sentiment analysis report.
Args:
result: Complete SentimentResult.
"""
print("\n" + "=" * 60)
print(f" SENTIMENT ANALYSIS: {result.symbol}")
print("=" * 60)
# Fear & Greed
print("\n--- Market Fear & Greed Index ---")
if result.fear_greed:
bar_len = result.fear_greed.value // 2
bar = "#" * bar_len + "." * (50 - bar_len)
print(f" Value: {result.fear_greed.value}/100 ({result.fear_greed.classification})")
print(f" [{bar}]")
else:
print(" Not available")
# Community Data
print("\n--- Social / Community Data ---")
if result.community:
print(f" Twitter Followers: {result.community.twitter_followers:>12,}")
print(f" Reddit Subscribers: {result.community.reddit_subscribers:>12,}")
print(f" Reddit Active (48h): {result.community.reddit_active_48h:>12,}")
print(f" Telegram Members: {result.community.telegram_members:>12,}")
print(f" Sentiment Up: {result.community.sentiment_up_pct:>11.1f}%")
print(f" Sentiment Down: {result.community.sentiment_down_pct:>11.1f}%")
print(f" Dev Commits (4w): {result.community.developer_commits_4w:>12,}")
else:
print(" Not available")
# Funding & Positioning
print("\n--- On-Chain Positioning ---")
if result.funding:
print(f" Funding Rate: {result.funding.funding_rate:>12.4%}")
print(f" Long/Short Ratio: {result.funding.long_short_ratio:>12.2f}")
print(f" Long Accounts: {result.funding.long_account_pct:>11.1%}")
print(f" Short Accounts: {result.funding.short_account_pct:>11.1%}")
else:
print(" Not available")
# Derived Metrics
print("\n--- Derived Metrics ---")
print(f" Social Polarity: {result.social_polarity:>+.3f} (-1 to +1)")
print(f" Mention Velocity: {result.mention_velocity:>.2f}x (1.0 = normal)")
# Composite Score
label = classify_score(result.composite_score)
print("\n--- Composite Sentiment Score ---")
score_bar_pos = int((result.composite_score + 100) / 4)
score_bar_pos = max(0, min(50, score_bar_pos))
bar = "." * score_bar_pos + "|" + "." * (50 - score_bar_pos)
print(f" Score: {result.composite_score:>+6.1f} ({label})")
print(f" -100 [{bar}] +100")
# Contrarian Signals
print("\n--- Contrarian Signals ---")
if result.contrarian_signals:
for sig in result.contrarian_signals:
print(f" >> {sig}")
else:
print(" No contrarian signals detected (sentiment in normal range)")
# Errors
if result.errors:
print("\n--- Warnings ---")
for err in result.errors:
print(f" [!] {err}")
print("\n" + "=" * 60)
print(" NOTE: This is analytical information only, not financial advice.")
print("=" * 60 + "\n")
# ── Demo Mode ───────────────────────────────────────────────────────
def run_demo() -> None:
"""Run demo mode with synthetic sentiment data showing a fear/greed cycle.
Generates 5 synthetic scenarios to illustrate different sentiment regimes.
"""
print("\n" + "=" * 60)
print(" SENTIMENT SCANNER — DEMO MODE")
print(" Synthetic data showing different sentiment regimes")
print("=" * 60)
scenarios: list[dict] = [
{
"name": "Extreme Fear (Capitulation)",
"symbol": "SOL",
"fear_greed": FearGreedData(value=8, classification="Extreme Fear", timestamp=0),
"community": CommunityData(
twitter_followers=2_500_000, reddit_subscribers=300_000,
reddit_active_48h=45_000, telegram_members=100_000,
sentiment_up_pct=20.0, sentiment_down_pct=80.0,
developer_commits_4w=150,
),
"funding": FundingData(
funding_rate=-0.0008, funding_time=0,
long_short_ratio=0.4, long_account_pct=0.286, short_account_pct=0.714,
),
},
{
"name": "Fear (Declining Market)",
"symbol": "SOL",
"fear_greed": FearGreedData(value=30, classification="Fear", timestamp=0),
"community": CommunityData(
twitter_followers=2_500_000, reddit_subscribers=300_000,
reddit_active_48h=6_000, telegram_members=100_000,
sentiment_up_pct=35.0, sentiment_down_pct=65.0,
developer_commits_4w=160,
),
"funding": FundingData(
funding_rate=-0.0002, funding_time=0,
long_short_ratio=0.8, long_account_pct=0.444, short_account_pct=0.556,
),
},
{
"name": "Neutral (Consolidation)",
"symbol": "SOL",
"fear_greed": FearGreedData(value=50, classification="Neutral", timestamp=0),
"community": CommunityData(
twitter_followers=2_500_000, reddit_subscribers=300_000,
reddit_active_48h=3_000, telegram_members=100_000,
sentiment_up_pct=52.0, sentiment_down_pct=48.0,
developer_commits_4w=180,
),
"funding": FundingData(
funding_rate=0.0001, funding_time=0,
long_short_ratio=1.1, long_account_pct=0.524, short_account_pct=0.476,
),
},
{
"name": "Greed (Bull Market)",
"symbol": "SOL",
"fear_greed": FearGreedData(value=72, classification="Greed", timestamp=0),
"community": CommunityData(
twitter_followers=2_500_000, reddit_subscribers=300_000,
reddit_active_48h=15_000, telegram_members=100_000,
sentiment_up_pct=78.0, sentiment_down_pct=22.0,
developer_commits_4w=200,
),
"funding": FundingData(
funding_rate=0.0004, funding_time=0,
long_short_ratio=1.8, long_account_pct=0.643, short_account_pct=0.357,
),
},
{
"name": "Extreme Greed (Euphoria)",
"symbol": "SOL",
"fear_greed": FearGreedData(value=92, classification="Extreme Greed", timestamp=0),
"community": CommunityData(
twitter_followers=2_500_000, reddit_subscribers=300_000,
reddit_active_48h=60_000, telegram_members=100_000,
sentiment_up_pct=92.0, sentiment_down_pct=8.0,
developer_commits_4w=120,
),
"funding": FundingData(
funding_rate=0.0012, funding_time=0,
long_short_ratio=3.5, long_account_pct=0.778, short_account_pct=0.222,
),
},
]
for i, scenario in enumerate(scenarios, 1):
print(f"\n{'~' * 60}")
print(f" Scenario {i}/5: {scenario['name']}")
print(f"{'~' * 60}")
result = SentimentResult(symbol=scenario["symbol"])
result.fear_greed = scenario["fear_greed"]
result.community = scenario["community"]
result.funding = scenario["funding"]
result.social_polarity = compute_social_polarity(result.community)
result.mention_velocity = estimate_mention_velocity(result.community)
fg_value = result.fear_greed.value if result.fear_greed else 50
fr_value = result.funding.funding_rate if result.funding else 0.0
result.composite_score = compute_composite_score(
social_polarity=result.social_polarity,
mention_velocity=result.mention_velocity,
fear_greed_value=fg_value,
funding_rate=fr_value,
)
result.contrarian_signals = detect_contrarian_signals(result)
display_result(result)
# ── Live Scanner ────────────────────────────────────────────────────
def run_live_scan(symbol: str) -> SentimentResult:
"""Run a live sentiment scan for the given symbol.
Fetches data from all free API sources, computes derived metrics,
and identifies contrarian signals.
Args:
symbol: Token symbol (e.g., 'SOL', 'BTC', 'ETH').
Returns:
Complete SentimentResult.
"""
symbol = symbol.upper()
result = SentimentResult(symbol=symbol)
coingecko_id = COINGECKO_IDS.get(symbol)
binance_symbol = BINANCE_SYMBOLS.get(symbol)
if not coingecko_id:
result.errors.append(
f"No CoinGecko mapping for {symbol}. "
f"Supported: {', '.join(sorted(COINGECKO_IDS.keys()))}"
)
if not binance_symbol:
result.errors.append(
f"No Binance mapping for {symbol}. "
f"Supported: {', '.join(sorted(BINANCE_SYMBOLS.keys()))}"
)
print(f"\nScanning sentiment for {symbol}...")
with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
# Fetch Fear & Greed (market-wide)
print(" Fetching Fear & Greed Index...")
result.fear_greed = fetch_fear_greed(client)
# Fetch community data (token-specific)
if coingecko_id:
print(f" Fetching CoinGecko community data for {coingecko_id}...")
result.community = fetch_community_data(client, coingecko_id)
# Fetch funding data (token-specific)
if binance_symbol:
print(f" Fetching Binance futures data for {binance_symbol}...")
result.funding = fetch_funding_data(client, binance_symbol)
# Compute derived metrics
result.social_polarity = compute_social_polarity(result.community)
result.mention_velocity = estimate_mention_velocity(result.community)
fg_value = result.fear_greed.value if result.fear_greed else 50
fr_value = result.funding.funding_rate if result.funding else 0.0
result.composite_score = compute_composite_score(
social_polarity=result.social_polarity,
mention_velocity=result.mention_velocity,
fear_greed_value=fg_value,
funding_rate=fr_value,
)
result.contrarian_signals = detect_contrarian_signals(result)
return result
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point: parse arguments and run scanner."""
parser = argparse.ArgumentParser(
description="Crypto sentiment scanner using free APIs"
)
parser.add_argument(
"--symbol",
type=str,
default="SOL",
help="Token symbol to scan (default: SOL)",
)
parser.add_argument(
"--demo",
action="store_true",
help="Run demo mode with synthetic data",
)
args = parser.parse_args()
if args.demo:
run_demo()
else:
result = run_live_scan(args.symbol)
display_result(result)
if __name__ == "__main__":
main()
Related skills
FAQ
Does it need paid APIs?
No; it includes a keyword-based scorer with no ML and a scanner using free no-auth APIs (Alternative.me Fear & Greed, CoinGecko), though social sources like Twitter/X need paid tiers.
What on-chain proxies does it use?
Funding rates, long/short ratio, and exchange net flows, interpreted contrarily (e.g., high positive funding is contrarian bearish).