
Analyzing Market Sentiment
- 79 installs
- 2.6k repo stars
- Updated August 5, 2026
- jeremylongshore/claude-code-plugins-plus-skills
Scores crypto market sentiment by combining the Fear & Greed Index, news keyword analysis, and price/volume momentum into a 0-100 score.
About
Analyzes cryptocurrency market sentiment overall or per-coin using Fear & Greed, news, and momentum into a composite 0-100 score. A developer uses it to gauge whether the market is fearful or greedy before making decisions.
- Composite 0-100 sentiment from multiple sources
- Overall or coin-specific with detailed component breakdown
Analyzing Market Sentiment by the numbers
- 79 all-time installs (skills.sh)
- Ranked #548 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jeremylongshore/claude-code-plugins-plus-skills --skill analyzing-market-sentimentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 79 |
|---|---|
| repo stars | ★ 2.6k |
| Last updated | August 5, 2026 |
| Repository | jeremylongshore/claude-code-plugins-plus-skills ↗ |
What it does
Scores crypto market sentiment by combining the Fear & Greed Index, news keyword analysis, and price/volume momentum into a 0-100 score.
Files
Analyzing Market Sentiment
Overview
Cryptocurrency market sentiment analysis combining Fear & Greed Index, news keyword analysis, and price/volume momentum into a composite 0-100 score.
Prerequisites
1. Python 3.8+ installed 2. Dependencies: pip install requests 3. Internet connectivity for API access (Alternative.me, CoinGecko) 4. Optional: crypto-news-aggregator skill for enhanced news analysis
Instructions
1. Assess user intent - determine what analysis is needed:
- Overall market: no specific coin, general sentiment
- Coin-specific: extract symbol (BTC, ETH, etc.)
- Quick vs detailed: quick score or full component breakdown
2. Run sentiment analysis with appropriate options:
# Quick market sentiment check
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py
# Coin-specific sentiment
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --coin BTC
# Detailed breakdown with all components
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --detailed
# Custom time period
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --period 7d --detailed3. Export results for trading models or analysis:
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --format json --output sentiment.json4. Present results to the user:
- Show composite score and classification prominently
- Explain what the sentiment reading means
- Highlight extreme readings (potential contrarian signals)
- For detailed mode, show component breakdown with weights
Output
Composite sentiment score (0-100) with classification and weighted component breakdown. Extreme readings serve as contrarian indicators:
==============================================================================
MARKET SENTIMENT ANALYZER Updated: 2026-01-14 15:30 # 2026 - current year timestamp
==============================================================================
COMPOSITE SENTIMENT
------------------------------------------------------------------------------
Score: 65.5 / 100 Classification: GREED
Component Breakdown:
- Fear & Greed Index: 72.0 (weight: 40%) -> 28.8 pts
- News Sentiment: 58.5 (weight: 40%) -> 23.4 pts
- Market Momentum: 66.5 (weight: 20%) -> 13.3 pts
Interpretation: Market is moderately greedy. Consider taking profits or
reducing position sizes. Watch for reversal signals.
==============================================================================Error Handling
| Error | Cause | Solution |
|---|---|---|
| Fear & Greed unavailable | API down | Uses cached value with warning |
| News fetch failed | Network issue | Reduces weight of news component |
| Invalid coin | Unknown symbol | Proceeds with market-wide analysis |
See ${CLAUDE_SKILL_DIR}/references/errors.md for comprehensive error handling.
Examples
Sentiment analysis patterns from quick checks to custom-weighted deep analysis:
# Quick market sentiment
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py
# Bitcoin-specific sentiment
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --coin BTC
# Detailed analysis with component breakdown
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --detailed
# Custom weights emphasizing news
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --weights "news:0.5,fng:0.3,momentum:0.2"
# Weekly sentiment trend
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --period 7d --detailedResources
${CLAUDE_SKILL_DIR}/references/implementation.md- CLI options, classifications, JSON format, contrarian theory${CLAUDE_SKILL_DIR}/references/errors.md- Comprehensive error handling${CLAUDE_SKILL_DIR}/references/examples.md- Detailed usage examples- Alternative.me Fear & Greed: https://alternative.me/crypto/fear-and-greed-index/
- CoinGecko API: https://www.coingecko.com/en/api
${CLAUDE_SKILL_DIR}/config/settings.yaml- Configuration options
ARD: Analyzing Market Sentiment
Part of Tons of Skills by Intent Solutions | jeremylongshore.com
Document Control
| Field | Value |
|---|---|
| Skill Name | analyzing-market-sentiment |
| Architecture Pattern | Multi-Source Aggregation + Scoring |
| Version | 2.0.0 |
| Author | Jeremy Longshore <jeremy@intentsolutions.io> |
---
Architectural Overview
Pattern: Composite Scoring Pipeline
This skill implements a multi-source data aggregation pattern with weighted composite scoring.
┌─────────────────────────────────────────────────────────────────────────────┐
│ MARKET SENTIMENT ANALYZER ARCHITECTURE │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Fear & Greed │ │ News Articles │ │ Market Data │
│ Index │ │ (RSS/Aggregator)│ │ (CoinGecko) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ F&G FETCHER │ │ NEWS FETCHER │ │ MARKET FETCHER │
│ - API call │ │ - RSS parsing │ │ - Price data │
│ - Caching │ │ - Deduplication │ │ - Volume data │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ NEWS SCORER │ │
│ │ - Keyword match │ │
│ │ - Sentiment │ │
│ │ - Aggregation │ │
│ └────────┬─────────┘ │
│ │ │
└────────────────────────┼────────────────────────┘
│
▼
┌─────────────────────────┐
│ COMPOSITE CALCULATOR │
│ - Weight application │
│ - Score normalization │
│ - Classification │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ FORMATTER │
│ - Dashboard view │
│ - JSON export │
│ - CSV export │
└─────────────────────────┘Workflow
1. Fetch: Parallel data retrieval from multiple sources 2. Score: Analyze news sentiment using keyword matching 3. Calculate: Compute weighted composite score 4. Classify: Map score to sentiment classification 5. Format: Output in requested format
---
Progressive Disclosure Strategy
Level 1: Quick Sentiment Check (Default)
python sentiment_analyzer.pyReturns overall sentiment score and classification.
Level 2: Coin-Specific Analysis
python sentiment_analyzer.py --coin BTCAdds coin-specific sentiment breakdown.
Level 3: Detailed Analysis
python sentiment_analyzer.py --detailed --period 24hFull breakdown with news article scores.
Level 4: Export with Custom Weights
python sentiment_analyzer.py --format json --weights "news:0.5,fng:0.3,momentum:0.2"Custom weighting and JSON export.
---
Tool Permission Strategy
Allowed Tools (Scoped)
allowed-tools: Read, Bash(crypto:sentiment-*)| Tool | Scope | Purpose |
|---|---|---|
| Read | Unrestricted | Read config, cached data |
| Bash | crypto:sentiment-* | Execute sentiment analysis scripts |
Why These Tools
- Read: Load configuration and cached sentiment data
- *Bash(crypto:sentiment-)**: Execute Python scripts for analysis
- No Write: Analysis is read-only; exports use script file output
---
Directory Structure
plugins/crypto/market-sentiment-analyzer/
└── skills/
└── analyzing-market-sentiment/
├── PRD.md # Product requirements
├── ARD.md # This file
├── SKILL.md # Core instructions
├── scripts/
│ ├── sentiment_analyzer.py # Main CLI entry point
│ ├── fear_greed.py # Fear & Greed Index fetcher
│ ├── news_sentiment.py # News sentiment scoring
│ ├── market_momentum.py # Market momentum calculation
│ └── formatters.py # Output formatting
├── references/
│ ├── errors.md # Error handling guide
│ └── examples.md # Usage examples
└── config/
└── settings.yaml # Configuration options---
Data Flow Architecture
Input
- User request with optional filters (--coin, --period, --detailed)
- External API data (Fear & Greed, news, market)
Processing Pipeline
Input Request
│
├──► Fetch Fear & Greed Index
│ │
│ ▼
│ Score: 0-100, Classification
│
├──► Fetch News (RSS or aggregator)
│ │
│ ▼
│ Score each article (-1 to +1)
│ │
│ ▼
│ Aggregate: weighted average
│ │
│ ▼
│ Normalize to 0-100
│
├──► Fetch Market Data
│ │
│ ▼
│ Calculate momentum (price change, volume)
│ │
│ ▼
│ Normalize to 0-100
│
└──► Calculate Composite Score
│
▼
Weighted Sum:
- News: 40%
- Fear & Greed: 40%
- Momentum: 20%
│
▼
Classify:
- 0-20: Extreme Fear
- 21-40: Fear
- 41-60: Neutral
- 61-80: Greed
- 81-100: Extreme GreedOutput Schema
{
"composite_score": 65.5,
"classification": "Greed",
"components": {
"fear_greed": {
"score": 72,
"classification": "Greed",
"weight": 0.40,
"contribution": 28.8
},
"news_sentiment": {
"score": 58.5,
"articles_analyzed": 25,
"positive": 12,
"negative": 5,
"neutral": 8,
"weight": 0.40,
"contribution": 23.4
},
"market_momentum": {
"score": 66.5,
"btc_change_24h": 3.5,
"volume_ratio": 1.2,
"weight": 0.20,
"contribution": 13.3
}
},
"meta": {
"timestamp": "2026-01-14T15:30:00Z",
"period": "24h",
"coin_filter": null,
"data_freshness": {
"fear_greed": "5 min",
"news": "2 min",
"market": "1 min"
}
}
}---
Sentiment Scoring Algorithm
News Sentiment Scoring
# Per-article scoring
def score_article(title: str, summary: str) -> float:
"""Score article from -1 (bearish) to +1 (bullish)."""
text = f"{title} {summary}".lower()
score = 0.0
# Positive keywords
positive = ["bullish", "surge", "rally", "breakout", "adoption",
"partnership", "approval", "milestone", "record"]
for kw in positive:
if kw in text:
score += 0.2
# Negative keywords
negative = ["bearish", "crash", "dump", "hack", "exploit",
"ban", "lawsuit", "fraud", "bankruptcy"]
for kw in negative:
if kw in text:
score -= 0.2
return max(-1, min(1, score))
# Aggregate scoring
def aggregate_news_sentiment(articles: List[dict]) -> float:
"""Aggregate article scores to 0-100 scale."""
if not articles:
return 50 # Neutral
# Weight by recency
weighted_sum = 0
weight_total = 0
for article in articles:
age_hours = article.get("age_hours", 24)
weight = 1 / (1 + age_hours / 12) # Decay over 12 hours
weighted_sum += article["score"] * weight
weight_total += weight
avg_score = weighted_sum / weight_total # -1 to +1
return (avg_score + 1) * 50 # Convert to 0-100Composite Score Calculation
def calculate_composite(
fear_greed: float, # 0-100
news_sentiment: float, # 0-100
momentum: float, # 0-100
weights: dict = None
) -> float:
"""Calculate weighted composite sentiment score."""
weights = weights or {
"fear_greed": 0.40,
"news": 0.40,
"momentum": 0.20
}
return (
fear_greed * weights["fear_greed"] +
news_sentiment * weights["news"] +
momentum * weights["momentum"]
)---
Error Handling Strategy
Error Categories
| Category | Examples | Strategy |
|---|---|---|
| API Error | Fear & Greed API down | Use cached value with stale warning |
| Network | Timeout, DNS failure | Skip component, partial analysis |
| Parse | Invalid JSON response | Log warning, use default |
| Data | No news found | Reduce weight, increase other components |
Graceful Degradation
Full Analysis (all components)
│
▼
Partial Analysis (some components failed)
│ → Recalculate weights for available data
│ → Warn user about missing components
│
▼
Cached Fallback (API failures)
│ → Use recent cached values
│ → Show "stale data" warning
│
▼
Minimal Analysis (only Fear & Greed)
│ → Single source analysis
│ → Clear limitations disclosure---
Composability & Stacking
Optional Dependencies
- crypto-news-aggregator: Enhanced news analysis
- If available: Uses aggregator's scored news
- If unavailable: Falls back to direct RSS fetch
As Data Provider
Skills that can consume sentiment data:
- crypto-signal-generator: Sentiment as signal input
- trading-strategy-backtester: Historical sentiment data
Integration Pattern
# Check for news aggregator
NEWS_AGGREGATOR = None
try:
from pathlib import Path
aggregator_path = Path(__file__).parent.parent.parent.parent / \
"crypto-news-aggregator/skills/aggregating-crypto-news/scripts"
if (aggregator_path / "news_aggregator.py").exists():
NEWS_AGGREGATOR = aggregator_path
except Exception:
pass
def get_news_for_sentiment(coin: str = None, period: str = "24h"):
"""Fetch news using aggregator if available, else direct RSS."""
if NEWS_AGGREGATOR:
# Use aggregator
return fetch_via_aggregator(coin, period)
else:
# Direct RSS fallback
return fetch_direct_rss(coin, period)---
Performance & Scalability
Performance Targets
| Metric | Target | Approach |
|---|---|---|
| Full analysis | < 15s | Parallel API calls |
| Quick check | < 5s | Cached Fear & Greed only |
| News scoring | < 2s per 100 articles | Efficient keyword matching |
Optimization Strategies
1. Parallel Fetching: Concurrent API calls with asyncio/threading 2. Caching: Cache Fear & Greed (5 min TTL), news (2 min TTL) 3. Lazy Loading: Only fetch detailed data if --detailed flag 4. Early Exit: Return cached data for repeated calls
---
Testing Strategy
Unit Tests
| Component | Test Cases |
|---|---|
| FearGreedFetcher | API response parsing, caching, classification |
| NewsSentiment | Keyword matching, aggregation, normalization |
| CompositeCalculator | Weight application, edge cases |
| Formatters | All output formats, empty data handling |
Integration Tests
- End-to-end with mock APIs
- Real API tests (daily CI)
- Export format validation
Manual Testing
# Quick check
python sentiment_analyzer.py
# Coin-specific
python sentiment_analyzer.py --coin BTC
# Detailed with export
python sentiment_analyzer.py --detailed --format json --output sentiment.json---
Security & Compliance
Security Considerations
- No Authentication: All APIs used are public
- No User Data: No personal information collected
- Output Only: Read-only analysis, no state modification
Rate Limiting
- Alternative.me: Respect 10 req/min limit
- CoinGecko: Respect free tier limits
- Caching reduces API calls significantly
# Market Sentiment Analyzer Configuration
# Version: 2.0.0
# =============================================================================
# Component Weights
# =============================================================================
# Default weights for composite score calculation
# Must sum to 1.0 (will be normalized if not)
weights:
fear_greed: 0.40 # Alternative.me Fear & Greed Index
news: 0.40 # News sentiment from RSS/aggregator
momentum: 0.20 # Market price/volume momentum
# =============================================================================
# API Configuration
# =============================================================================
apis:
# Alternative.me Fear & Greed Index
fear_greed:
url: "https://api.alternative.me/fng/"
timeout: 10 # Request timeout in seconds
rate_limit: 10 # Max requests per minute
# CoinGecko for market data
coingecko:
url: "https://api.coingecko.com/api/v3"
timeout: 10
rate_limit: 30 # Free tier limit
# =============================================================================
# Caching
# =============================================================================
cache:
# Time-to-live in seconds
fear_greed_ttl: 300 # 5 minutes
news_ttl: 120 # 2 minutes
momentum_ttl: 60 # 1 minute
# Cache file locations (relative to scripts/)
fear_greed_file: ".fng_cache.json"
news_file: ".news_cache.json"
momentum_file: ".momentum_cache.json"
# =============================================================================
# News Sources
# =============================================================================
news_sources:
- url: "https://cointelegraph.com/rss"
name: "CoinTelegraph"
reliability: 0.9
- url: "https://www.coindesk.com/arc/outboundfeeds/rss/"
name: "CoinDesk"
reliability: 0.9
- url: "https://decrypt.co/feed"
name: "Decrypt"
reliability: 0.85
# =============================================================================
# Sentiment Classification
# =============================================================================
classification:
thresholds:
extreme_fear: 20 # 0-20
fear: 40 # 21-40
neutral: 60 # 41-60
greed: 80 # 61-80
# 81-100 = extreme_greed
# =============================================================================
# Keyword Scoring
# =============================================================================
keywords:
# Strong positive (weight: 0.3)
strong_positive:
- bullish
- surge
- soar
- rally
- breakout
- "record high"
- "all-time high"
- ath
- moon
# Medium positive (weight: 0.2)
medium_positive:
- adoption
- partnership
- approval
- milestone
- upgrade
- launch
- integration
- institutional
- etf
- accumulation
- inflow
- buy
# Mild positive (weight: 0.1)
mild_positive:
- gain
- rise
- up
- growth
- positive
- optimistic
- recovery
- support
# Strong negative (weight: -0.3)
strong_negative:
- bearish
- crash
- dump
- collapse
- hack
- exploit
- scam
- fraud
- bankruptcy
- insolvent
- "rug pull"
- rugpull
# Medium negative (weight: -0.2)
medium_negative:
- ban
- lawsuit
- investigation
- sec
- regulation
- crackdown
- sell-off
- selloff
- outflow
- withdrawal
- liquidation
# Mild negative (weight: -0.1)
mild_negative:
- decline
- drop
- fall
- down
- loss
- concern
- risk
- uncertain
- volatility
- correction
- dip
- fear
- weak
# =============================================================================
# Coin Mappings
# =============================================================================
coin_mappings:
BTC:
coingecko_id: bitcoin
aliases: [bitcoin, btc, satoshi]
ETH:
coingecko_id: ethereum
aliases: [ethereum, eth, ether]
SOL:
coingecko_id: solana
aliases: [solana, sol]
XRP:
coingecko_id: ripple
aliases: [ripple, xrp]
ADA:
coingecko_id: cardano
aliases: [cardano, ada]
DOGE:
coingecko_id: dogecoin
aliases: [dogecoin, doge]
DOT:
coingecko_id: polkadot
aliases: [polkadot, dot]
LINK:
coingecko_id: chainlink
aliases: [chainlink, link]
AVAX:
coingecko_id: avalanche-2
aliases: [avalanche, avax]
MATIC:
coingecko_id: matic-network
aliases: [polygon, matic]
BNB:
coingecko_id: binancecoin
aliases: [binance, bnb]
LTC:
coingecko_id: litecoin
aliases: [litecoin, ltc]
# =============================================================================
# Output Formatting
# =============================================================================
output:
default_format: table # table, json, csv
table_width: 78
show_colors: true # ANSI colors in terminal output
decimal_places: 1 # For scores
# =============================================================================
# Performance
# =============================================================================
performance:
max_articles: 50 # Maximum articles to analyze
article_truncate: 500 # Max characters per article summary
parallel_fetches: true # Fetch APIs concurrently
PRD: Analyzing Market Sentiment
Document Control
| Field | Value |
|---|---|
| Skill Name | analyzing-market-sentiment |
| Type | Analysis & Intelligence |
| Domain | Cryptocurrency Sentiment Analysis |
| Target Users | Traders, Analysts, Portfolio Managers |
| Priority | P1 - Core Analytics Skill |
| Version | 2.0.0 |
| Author | Jeremy Longshore <jeremy@intentsolutions.io> |
---
Executive Summary
The analyzing-market-sentiment skill provides comprehensive cryptocurrency market sentiment analysis by combining news sentiment scoring, Fear & Greed index integration, and keyword-based sentiment detection. It enables traders and analysts to gauge market mood before making trading decisions.
Value Proposition: Quantify market sentiment with a 0-100 score combining news analysis, social indicators, and market metrics into actionable intelligence.
---
Problem Statement
Current Pain Points
1. Subjective Assessment: "Feeling bullish" is not quantifiable; traders need measurable sentiment 2. Information Fragmentation: Sentiment signals scattered across news, social media, and market data 3. Delayed Reaction: Manual sentiment assessment is slow; markets move fast 4. No Historical Context: Hard to compare current sentiment to historical norms
Impact of Not Solving
- Traders enter positions against prevailing sentiment
- Missed contrarian opportunities at sentiment extremes
- Emotional decisions override data-driven analysis
- No systematic approach to sentiment tracking
---
Target Users
Persona 1: Swing Trader
- Name: Alex
- Role: Part-time crypto trader
- Goals: Time entries/exits with sentiment extremes
- Pain Points: Doesn't have time to read all news; needs quick sentiment check
- Usage: Daily sentiment scan before placing trades
Persona 2: Quantitative Analyst
- Name: Rachel
- Role: Quant at crypto hedge fund
- Goals: Incorporate sentiment into trading models
- Pain Points: Needs structured, exportable sentiment data
- Usage: Hourly sentiment data in JSON format for model input
Persona 3: Research Analyst
- Name: Kevin
- Role: Crypto research analyst
- Goals: Include sentiment analysis in reports
- Pain Points: Needs both aggregate and coin-specific sentiment
- Usage: Weekly sentiment reports with historical comparison
---
User Stories
US-1: Overall Market Sentiment (Critical)
As a trader I want to see a single sentiment score for the crypto market So that I can quickly gauge if the market is fearful or greedy
Acceptance Criteria:
- Display composite sentiment score (0-100)
- Show Fear & Greed classification (Extreme Fear, Fear, Neutral, Greed, Extreme Greed)
- Include component breakdown (news, market metrics)
- Complete analysis in under 15 seconds
US-2: Coin-Specific Sentiment (Critical)
As a trader I want to analyze sentiment for a specific coin So that I can make informed decisions on that asset
Acceptance Criteria:
- Filter sentiment analysis by coin symbol
- Show news sentiment specific to that coin
- Include social mention volume if available
- Compare to overall market sentiment
US-3: News Sentiment Analysis (Important)
As a analyst I want to see sentiment scores for recent news articles So that I can understand what's driving market mood
Acceptance Criteria:
- Score each article as positive, negative, or neutral
- Show aggregate news sentiment
- Highlight most positive and negative articles
- Support time window filtering (1h, 4h, 24h, 7d)
US-4: Export Sentiment Data (Important)
As a quant I want to export sentiment data in JSON format So that I can feed it into my trading models
Acceptance Criteria:
- JSON output with all sentiment components
- Include timestamps for time-series analysis
- Support multiple export formats (JSON, CSV)
- Include metadata (sources, confidence)
US-5: Historical Comparison (Nice-to-Have)
As a analyst I want to see how current sentiment compares to historical averages So that I can identify sentiment extremes
Acceptance Criteria:
- Show current vs 7-day average
- Flag extreme readings (top/bottom 10%)
- Visual indicators for sentiment trends
---
Functional Requirements
REQ-1: Fear & Greed Index Integration
- Fetch Alternative.me Fear & Greed Index
- Parse historical values for comparison
- Map to 0-100 score with classification
REQ-2: News Sentiment Analysis
- Integrate with crypto-news-aggregator skill (optional dependency)
- Perform keyword-based sentiment scoring
- Aggregate across multiple articles
- Weight by source quality and recency
REQ-3: Sentiment Scoring Algorithm
- Composite score combining multiple indicators:
- News sentiment (40% weight)
- Fear & Greed Index (40% weight)
- Market momentum (20% weight)
- Configurable weights via settings
REQ-4: Coin-Specific Analysis
- Filter news by coin symbol
- Calculate coin-specific sentiment
- Compare to market-wide sentiment
REQ-5: Output Formatting
- Table format for terminal display
- JSON format for programmatic use
- CSV format for spreadsheet analysis
- Summary format for quick reads
---
Non-Goals
- Social Media Scraping: No Twitter/Discord/Telegram scraping (API access required)
- Real-time Streaming: Polling model, not push notifications
- Machine Learning: Keyword-based only, no ML sentiment models
- Order Flow Analysis: Pure sentiment, no trade flow data
- Portfolio Integration: Analysis only, no trading recommendations
---
Success Metrics
| Metric | Target | Measurement |
|---|---|---|
| Analysis time | < 15s | Script execution time |
| Sentiment accuracy | Correlates with price direction > 60% | Backtest validation |
| Data freshness | < 5 min old | Timestamp comparison |
| User activation | Triggered by sentiment phrases | Plugin analytics |
| Export completeness | All components included | Schema validation |
---
UX Flow
User: "analyze crypto sentiment"
│
├─► Fetch Fear & Greed Index
│
├─► Fetch news (via aggregator or direct)
│
├─► Score news articles
│
├─► Fetch market momentum data
│
├─► Calculate composite score
│
├─► Format output
│
└─► Display sentiment dashboard---
Integration Points
Optional Dependencies
- crypto-news-aggregator: For news feed (can work standalone)
- tracking-crypto-prices: For market momentum data (can use CoinGecko directly)
External APIs
- Alternative.me: Fear & Greed Index (free, no API key)
- CoinGecko: Market data for momentum calculation (free tier)
Consumers (Skills that can use this)
- crypto-signal-generator: Sentiment as signal input
- trading-strategy-backtester: Historical sentiment for backtesting
---
Constraints & Assumptions
Constraints
- Alternative.me API availability (single source for F&G)
- No social media access without API keys
- Keyword-based sentiment is less accurate than ML
Assumptions
- Fear & Greed Index is representative of market sentiment
- News sentiment correlates with market direction
- Users want quick, actionable sentiment scores
---
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Fear & Greed API down | Low | High | Cache recent values; show stale warning |
| News aggregator unavailable | Medium | Medium | Direct RSS fallback; reduced accuracy |
| Sentiment accuracy issues | Medium | Medium | Clear methodology disclosure |
| API rate limiting | Low | Low | Caching; respect rate limits |
---
Examples
Example 1: Quick Sentiment Check
python sentiment_analyzer.pyReturns overall market sentiment with Fear & Greed classification.
Example 2: Bitcoin-Specific Sentiment
python sentiment_analyzer.py --coin BTCReturns Bitcoin-specific sentiment with news analysis.
Example 3: Detailed Analysis with Export
python sentiment_analyzer.py --detailed --format json --output sentiment.jsonFull sentiment breakdown exported to JSON.
---
Version History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2025-10-16 | Jeremy Longshore | Initial stub |
| 2.0.0 | 2026-01-14 | Jeremy Longshore | Full PRD, implementation |
Error Handling Reference
Comprehensive error handling guide for the Market Sentiment Analyzer.
---
API Errors
Fear & Greed Index Unavailable
Error: Cannot fetch Fear & Greed Index from Alternative.me API
Symptoms:
Fear & Greed Index unavailable
Using cached/fallback valueCauses:
- API rate limiting (>10 requests/minute)
- API server downtime
- Network connectivity issues
Solutions: 1. Automatic: Uses cached value with "stale" warning 2. Manual: Wait 1-2 minutes and retry 3. Persistent: Check https://alternative.me/crypto/fear-and-greed-index/ status
Impact: Composite score uses cached or neutral (50) value
---
CoinGecko API Rate Limit
Error: Market momentum data unavailable
Symptoms:
API request failed: 429 Too Many Requests
Market data unavailableCauses:
- Exceeded free tier limit (~10-30 calls/minute)
- Too many concurrent requests
Solutions: 1. Automatic: Falls back to cached data 2. Manual: Wait 60 seconds before retry 3. Upgrade: CoinGecko Pro API for higher limits
Impact: Momentum component uses neutral (50) value
---
News Feed Unavailable
Error: Cannot fetch news from RSS feeds
Symptoms:
Failed to fetch [feed_url]: Connection timed out
No articles availableCauses:
- RSS feed URL changed
- Feed server downtime
- Network firewall blocking
Solutions: 1. Automatic: Skips unavailable feeds, uses others 2. Check: Verify feed URLs in settings 3. Network: Test connectivity to feed domains
Impact: News sentiment based on fewer articles; score may be less representative
---
Data Parsing Errors
Invalid JSON Response
Error: Failed to parse API response
Symptoms:
json.JSONDecodeError: Expecting value
Failed to parse response: [error details]Causes:
- API returning HTML error page instead of JSON
- Malformed API response
- Incomplete response due to timeout
Solutions: 1. Check API status pages 2. Verify API endpoint URLs 3. Increase timeout in settings
---
Missing Data Fields
Error: Expected field not in response
Symptoms:
KeyError: 'price_change_percentage_24h'Causes:
- API schema changed
- New coin not fully supported
- Delisted coin
Solutions: 1. Update to latest script version 2. Use market-wide analysis instead of coin-specific 3. Report issue if persistent
---
Configuration Errors
Invalid Weights
Error: Custom weights don't sum to 1.0
Note: This is auto-corrected, not a fatal error
Causes:
- User-provided weights like "news:0.5,fng:0.5,momentum:0.5" (sum > 1)
Solutions: 1. Automatic: Weights are normalized 2. Manual: Ensure weights sum to 1.0
---
Unknown Coin Symbol
Error: Coin not found in known mappings
Symptoms:
No data for [COIN], using market analysisCauses:
- Coin symbol not in known mappings
- New coin not yet added
- Typo in symbol
Solutions: 1. Use common symbols (BTC, ETH, SOL, etc.) 2. Try full CoinGecko ID instead of symbol 3. Fall back to market-wide analysis
---
Graceful Degradation
The analyzer implements graceful degradation:
Full Analysis (all 3 components)
│
├─► Fear & Greed fails → Use cached/neutral (50)
│
├─► News fails → Use remaining components with adjusted weights
│
├─► Momentum fails → Use remaining components with adjusted weights
│
└─► All fail → Return neutral (50) with error warningsComponent Failure Handling
| Component | Fallback | Score Used |
|---|---|---|
| Fear & Greed | Cached → Neutral | Cached value or 50 |
| News Sentiment | Skip | 50 (neutral) |
| Market Momentum | Cached → Neutral | Cached value or 50 |
---
Diagnostic Commands
Test Individual Components
# Test Fear & Greed
python fear_greed.py -v
# Test News Sentiment
python news_sentiment.py -v
# Test Market Momentum
python market_momentum.py -v
# Full analysis with verbose
python sentiment_analyzer.py -vCheck Cache Files
# List cache files
ls -la scripts/.*.json
# View cached data
cat scripts/.fng_cache.json | python -m json.toolClear Caches
rm scripts/.fng_cache.json
rm scripts/.news_cache.json
rm scripts/.momentum_cache.json---
Common Issues
Issue: Score Always Neutral (50)
Causes:
- All APIs unavailable
- Network connectivity issues
- VPN blocking API access
Diagnosis:
# Run with verbose
python sentiment_analyzer.py -v
# Check network
curl -I https://api.alternative.me/fng/
curl -I https://api.coingecko.com/api/v3/pingIssue: News Sentiment Skewed
Causes:
- Only one RSS feed responding
- Feed returns old articles
- Keyword bias in recent news
Diagnosis:
python news_sentiment.py -v
# Check "articles_analyzed" count
# Should be 15-50 for balanced sentimentIssue: Slow Response Time
Causes:
- Cache expired, fetching fresh data
- Network latency to APIs
- API rate limiting delays
Solutions: 1. Increase cache TTL in settings 2. Use --verbose to see timing 3. Consider local caching proxy
--- [Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)
Usage Examples
Comprehensive examples for the Market Sentiment Analyzer.
---
Quick Start Examples
1. Basic Sentiment Check
python sentiment_analyzer.pyOutput:
==============================================================================
MARKET SENTIMENT ANALYZER 2026-01-14 15:30 UTC
==============================================================================
COMPOSITE SENTIMENT
------------------------------------------------------------------------------
Score: 58.5 / 100 Classification: NEUTRAL
[------------|----------------------█-----------------|------------------]
0 -------- 25 -------- 50 -------- 75 -------- 100
FEAR NEUTRAL GREED
COMPONENTS
------------------------------------------------------------------------------
Fear & Greed Index: 62.0 (weight: 40%) → 24.8 pts [Greed]
News Sentiment: 55.0 (weight: 40%) → 22.0 pts [15 articles]
Market Momentum: 58.5 (weight: 20%) → 11.7 pts [BTC: +1.2%]
INTERPRETATION
------------------------------------------------------------------------------
Market sentiment is balanced with no strong directional bias.
Wait for clearer signals before making major decisions.
==============================================================================---
2. Bitcoin-Specific Sentiment
python sentiment_analyzer.py --coin BTCFilters news and momentum specifically for Bitcoin.
---
3. Detailed Analysis
python sentiment_analyzer.py --detailedShows full component breakdown including:
- News sentiment distribution (positive/negative/neutral)
- Top positive and negative headlines
- Volume ratio analysis
- ETH price change
---
4. Different Time Periods
# Last hour
python sentiment_analyzer.py --period 1h
# Last 4 hours
python sentiment_analyzer.py --period 4h
# Last 24 hours (default)
python sentiment_analyzer.py --period 24h
# Last 7 days
python sentiment_analyzer.py --period 7d---
Export Examples
5. JSON Export
python sentiment_analyzer.py --format jsonOutput:
{
"composite_score": 58.5,
"classification": "Neutral",
"interpretation": "Market sentiment is balanced...",
"components": {
"fear_greed": {
"score": 62,
"classification": "Greed",
"weight": 0.40,
"contribution": 24.8
},
"news_sentiment": {
"score": 55.0,
"articles_analyzed": 15,
"positive": 6,
"negative": 4,
"neutral": 5,
"weight": 0.40,
"contribution": 22.0
},
"market_momentum": {
"score": 58.5,
"btc_change_24h": 1.2,
"weight": 0.20,
"contribution": 11.7
}
},
"meta": {
"timestamp": "2026-01-14T15:30:00Z",
"period": "24h"
}
}---
6. CSV Export
python sentiment_analyzer.py --format csv --output sentiment.csvCreates spreadsheet-compatible output for analysis.
---
7. Save to File
# JSON to file
python sentiment_analyzer.py --format json --output daily_sentiment.json
# Detailed CSV to file
python sentiment_analyzer.py --format csv --detailed --output sentiment_log.csv---
Advanced Examples
8. Custom Weights
Emphasize news over Fear & Greed:
python sentiment_analyzer.py --weights "news:0.5,fng:0.3,momentum:0.2"Emphasize market momentum:
python sentiment_analyzer.py --weights "news:0.3,fng:0.3,momentum:0.4"---
9. Verbose Mode (Debugging)
python sentiment_analyzer.py -vShows:
- Which APIs are being called
- Cache hits/misses
- Individual component fetch times
- Any errors or warnings
---
10. Ethereum Analysis with Export
python sentiment_analyzer.py --coin ETH --detailed --format json --output eth_sentiment.json---
Integration Examples
11. Cron Job (Hourly Tracking)
# Add to crontab
0 * * * * cd /path/to/skill && python sentiment_analyzer.py --format json --output /data/sentiment/$(date +\%Y\%m\%d_\%H).json---
12. Combined with News Aggregator
When crypto-news-aggregator skill is installed:
# Aggregator provides enhanced news data
python sentiment_analyzer.py --detailed
# Will show "source: aggregator" in verbose mode---
13. Trading Signal Input
# Get sentiment for trading bot
SENTIMENT=$(python sentiment_analyzer.py --format json | jq -r '.classification')
if [ "$SENTIMENT" = "Extreme Fear" ]; then
echo "Potential buying opportunity"
fi---
Interpretation Guide
Reading the Output
| Score Range | Classification | Trading Implication |
|---|---|---|
| 0-20 | Extreme Fear | Potential bottom, accumulation zone |
| 21-40 | Fear | Cautious, wait for reversal signals |
| 41-60 | Neutral | No strong bias, follow other indicators |
| 61-80 | Greed | Consider taking profits |
| 81-100 | Extreme Greed | High risk, potential top |
Component Weights
Default weights:
- Fear & Greed Index: 40%
- News Sentiment: 40%
- Market Momentum: 20%
These can be adjusted with --weights based on your trading style.
---
Troubleshooting Examples
Check Component Status
# Individual component tests
python fear_greed.py -v
python news_sentiment.py -v
python market_momentum.py -vForce Fresh Data
# Clear caches
rm scripts/.*.json
python sentiment_analyzer.pyDebug Network Issues
python sentiment_analyzer.py -v 2>&1 | grep -E "(Fetching|failed|error)"--- [Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)
Analyzing Market Sentiment - Implementation Reference
Command-Line Options
| Option | Description | Default |
|---|---|---|
--coin | Analyze specific coin (BTC, ETH, etc.) | All market |
--period | Time period (1h, 4h, 24h, 7d) | 24h |
--detailed | Show full component breakdown | false |
--format | Output format (table, json, csv) | table |
--output | Output file path | stdout |
--weights | Custom weights (e.g., "news:0.5,fng:0.3,momentum:0.2") | Default |
--verbose | Enable verbose output | false |
Sentiment Classifications
| Score Range | Classification | Description |
|---|---|---|
| 0-20 | Extreme Fear | Market panic, potential bottom |
| 21-40 | Fear | Cautious sentiment, bearish |
| 41-60 | Neutral | Balanced, no strong bias |
| 61-80 | Greed | Optimistic, bullish sentiment |
| 81-100 | Extreme Greed | Euphoria, potential top |
JSON Output Format
{
"composite_score": 65.5,
"classification": "Greed",
"components": {
"fear_greed": {
"score": 72,
"classification": "Greed",
"weight": 0.40,
"contribution": 28.8
},
"news_sentiment": {
"score": 58.5,
"articles_analyzed": 25,
"positive": 12,
"negative": 5,
"neutral": 8,
"weight": 0.40,
"contribution": 23.4
},
"market_momentum": {
"score": 66.5,
"btc_change_24h": 3.5,
"weight": 0.20,
"contribution": 13.3
}
},
"meta": {
"timestamp": "2026-01-14T15:30:00Z",
"period": "24h"
}
}Advanced Examples
# Custom weights (emphasize news)
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --weights "news:0.5,fng:0.3,momentum:0.2"
# Weekly sentiment comparison
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --period 7d --detailed
# Export for trading model
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --format json --output sentiment.json
# Bitcoin-specific detailed analysis
python ${CLAUDE_SKILL_DIR}/scripts/sentiment_analyzer.py --coin BTC --detailedContrarian Indicator Theory
Sentiment is often used as a contrarian indicator:
- Extreme Fear readings historically correlate with market bottoms and buying opportunities
- Extreme Greed readings historically correlate with market tops and selling opportunities
- The Fear & Greed Index has shown predictive value when combined with technical analysis
- Best used in conjunction with other analysis tools rather than as a sole decision driver
--- [Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)
#!/usr/bin/env python3
"""
Fear & Greed Index Fetcher
Fetches the Crypto Fear & Greed Index from Alternative.me API.
Provides historical data and classification.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import json
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List
try:
import requests
except ImportError:
print("Error: requests library required. Install with: pip install requests", file=sys.stderr)
sys.exit(1)
class FearGreedFetcher:
"""Fetches Fear & Greed Index from Alternative.me API."""
API_URL = "https://api.alternative.me/fng/"
CACHE_FILE = Path(__file__).parent / ".fng_cache.json"
CACHE_TTL = 300 # 5 minutes
def __init__(self, verbose: bool = False):
"""Initialize fetcher.
Args:
verbose: Enable verbose output
"""
self.verbose = verbose
self._cache: Optional[Dict[str, Any]] = None
self._cache_time: float = 0
def fetch(self, limit: int = 1) -> Optional[Dict[str, Any]]:
"""Fetch Fear & Greed Index data.
Args:
limit: Number of historical values to fetch (default: 1 for current)
Returns:
Dict with value, classification, timestamp, or None on error
"""
# Check memory cache first
if self._is_cache_valid():
if self.verbose:
print("Using memory cache for Fear & Greed Index", file=sys.stderr)
return self._cache
# Check file cache
cached = self._load_file_cache()
if cached:
if self.verbose:
print("Using file cache for Fear & Greed Index", file=sys.stderr)
self._cache = cached
self._cache_time = time.time()
return cached
# Fetch from API
try:
if self.verbose:
print(f"Fetching Fear & Greed Index from {self.API_URL}", file=sys.stderr)
response = requests.get(
self.API_URL,
params={"limit": limit},
timeout=10
)
response.raise_for_status()
data = response.json()
if data.get("metadata", {}).get("error"):
if self.verbose:
print(f"API error: {data['metadata']['error']}", file=sys.stderr)
return self._get_fallback()
if not data.get("data"):
if self.verbose:
print("No data in API response", file=sys.stderr)
return self._get_fallback()
# Parse current value
current = data["data"][0]
result = {
"value": int(current.get("value", 50)),
"classification": current.get("value_classification", "Neutral"),
"timestamp": self._parse_timestamp(current.get("timestamp")),
"time_until_update": current.get("time_until_update"),
"source": "alternative.me"
}
# Add historical data if requested
if limit > 1 and len(data["data"]) > 1:
result["history"] = [
{
"value": int(item.get("value", 50)),
"classification": item.get("value_classification", "Neutral"),
"timestamp": self._parse_timestamp(item.get("timestamp"))
}
for item in data["data"][1:]
]
# Update caches
self._cache = result
self._cache_time = time.time()
self._save_file_cache(result)
return result
except requests.exceptions.Timeout:
if self.verbose:
print("API request timed out", file=sys.stderr)
return self._get_fallback()
except requests.exceptions.RequestException as e:
if self.verbose:
print(f"API request failed: {e}", file=sys.stderr)
return self._get_fallback()
except (json.JSONDecodeError, KeyError, ValueError) as e:
if self.verbose:
print(f"Failed to parse API response: {e}", file=sys.stderr)
return self._get_fallback()
def fetch_historical(self, days: int = 7) -> List[Dict[str, Any]]:
"""Fetch historical Fear & Greed data.
Args:
days: Number of days of history
Returns:
List of historical data points
"""
result = self.fetch(limit=days)
if not result:
return []
history = [
{
"value": result["value"],
"classification": result["classification"],
"timestamp": result["timestamp"]
}
]
if "history" in result:
history.extend(result["history"])
return history
def get_average(self, days: int = 7) -> float:
"""Get average Fear & Greed value over specified days.
Args:
days: Number of days to average
Returns:
Average value (0-100)
"""
history = self.fetch_historical(days)
if not history:
return 50.0
values = [item["value"] for item in history]
return sum(values) / len(values)
def _is_cache_valid(self) -> bool:
"""Check if memory cache is still valid."""
if self._cache is None:
return False
return (time.time() - self._cache_time) < self.CACHE_TTL
def _load_file_cache(self) -> Optional[Dict[str, Any]]:
"""Load cached data from file."""
try:
if not self.CACHE_FILE.exists():
return None
with open(self.CACHE_FILE, "r") as f:
data = json.load(f)
# Check cache freshness
cache_time = data.get("_cache_time", 0)
if (time.time() - cache_time) > self.CACHE_TTL:
return None
return data.get("data")
except Exception:
return None
def _save_file_cache(self, data: Dict[str, Any]) -> None:
"""Save data to file cache."""
try:
cache_data = {
"_cache_time": time.time(),
"data": data
}
with open(self.CACHE_FILE, "w") as f:
json.dump(cache_data, f)
except Exception:
pass # Cache write failure is non-fatal
def _parse_timestamp(self, ts: Optional[str]) -> str:
"""Parse Unix timestamp to ISO format."""
if not ts:
return datetime.utcnow().isoformat() + "Z"
try:
dt = datetime.utcfromtimestamp(int(ts))
return dt.isoformat() + "Z"
except (ValueError, TypeError):
return datetime.utcnow().isoformat() + "Z"
def _get_fallback(self) -> Optional[Dict[str, Any]]:
"""Get fallback data when API is unavailable."""
# Try file cache even if stale
try:
if self.CACHE_FILE.exists():
with open(self.CACHE_FILE, "r") as f:
data = json.load(f)
result = data.get("data", {})
result["stale"] = True
result["stale_reason"] = "API unavailable, using cached data"
if self.verbose:
print("Using stale cache as fallback", file=sys.stderr)
return result
except Exception:
pass
# Return neutral fallback
if self.verbose:
print("Returning neutral fallback value", file=sys.stderr)
return {
"value": 50,
"classification": "Neutral",
"timestamp": datetime.utcnow().isoformat() + "Z",
"source": "fallback",
"stale": True,
"stale_reason": "API unavailable, no cache available"
}
def main():
"""CLI entry point for testing."""
import argparse
parser = argparse.ArgumentParser(description="Fetch Fear & Greed Index")
parser.add_argument("--history", type=int, default=1, help="Days of history")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
args = parser.parse_args()
fetcher = FearGreedFetcher(verbose=args.verbose)
if args.history > 1:
data = fetcher.fetch_historical(args.history)
print(json.dumps(data, indent=2))
else:
data = fetcher.fetch()
print(json.dumps(data, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Sentiment Output Formatters
Formats sentiment analysis results in various output formats:
- Table (default): Terminal-friendly dashboard
- JSON: Machine-readable export
- CSV: Spreadsheet-compatible
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import csv
import io
import json
from datetime import datetime
from typing import Dict, Any
class SentimentFormatter:
"""Formats sentiment analysis results."""
# Sentiment classification colors (ANSI)
COLORS = {
"Extreme Fear": "\033[91m", # Red
"Fear": "\033[93m", # Yellow
"Neutral": "\033[0m", # Default
"Greed": "\033[92m", # Green
"Extreme Greed": "\033[96m", # Cyan
"reset": "\033[0m"
}
def format(
self,
data: Dict[str, Any],
format_type: str = "table",
detailed: bool = False
) -> str:
"""Format sentiment data for output.
Args:
data: Sentiment analysis result dict
format_type: Output format ("table", "json", "csv")
detailed: Include component breakdown
Returns:
Formatted string output
"""
if format_type == "json":
return self._format_json(data)
elif format_type == "csv":
return self._format_csv(data, detailed)
else:
return self._format_table(data, detailed)
def _format_table(self, data: Dict[str, Any], detailed: bool) -> str:
"""Format as terminal table/dashboard."""
lines = []
w = 78 # Width
# Header
timestamp = data.get("meta", {}).get("timestamp", "")
if timestamp:
try:
dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
time_str = dt.strftime("%Y-%m-%d %H:%M UTC")
except ValueError:
time_str = timestamp[:19]
else:
time_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
lines.append("=" * w)
lines.append(f" MARKET SENTIMENT ANALYZER{' ' * (w - 42)}{time_str}")
lines.append("=" * w)
lines.append("")
# Composite Score
score = data.get("composite_score", 50)
classification = data.get("classification", "Neutral")
color = self.COLORS.get(classification, "")
reset = self.COLORS["reset"]
lines.append(" COMPOSITE SENTIMENT")
lines.append("-" * w)
lines.append(f" Score: {color}{score:.1f}{reset} / 100{' ' * 30}Classification: {color}{classification.upper()}{reset}")
lines.append("")
# Sentiment gauge
gauge = self._create_gauge(score)
lines.append(f" {gauge}")
lines.append(" 0 -------- 25 -------- 50 -------- 75 -------- 100")
lines.append(" FEAR NEUTRAL GREED")
lines.append("")
# Component breakdown (always show basic, detailed shows more)
components = data.get("components", {})
if components:
lines.append(" COMPONENTS")
lines.append("-" * w)
# Fear & Greed
fg = components.get("fear_greed", {})
fg_score = fg.get("score", 50)
fg_weight = fg.get("weight", 0.4) * 100
fg_contrib = fg.get("contribution", 20)
fg_class = fg.get("classification", "Neutral")
lines.append(f" Fear & Greed Index: {fg_score:5.1f} (weight: {fg_weight:.0f}%) → {fg_contrib:5.1f} pts [{fg_class}]")
# News Sentiment
ns = components.get("news_sentiment", {})
ns_score = ns.get("score", 50)
ns_weight = ns.get("weight", 0.4) * 100
ns_contrib = ns.get("contribution", 20)
ns_articles = ns.get("articles_analyzed", 0)
lines.append(f" News Sentiment: {ns_score:5.1f} (weight: {ns_weight:.0f}%) → {ns_contrib:5.1f} pts [{ns_articles} articles]")
# Market Momentum
mm = components.get("market_momentum", {})
mm_score = mm.get("score", 50)
mm_weight = mm.get("weight", 0.2) * 100
mm_contrib = mm.get("contribution", 10)
btc_change = mm.get("btc_change_24h")
btc_str = f"{btc_change:+.1f}%" if btc_change is not None else "N/A"
lines.append(f" Market Momentum: {mm_score:5.1f} (weight: {mm_weight:.0f}%) → {mm_contrib:5.1f} pts [BTC: {btc_str}]")
lines.append("")
# Detailed breakdown
if detailed:
lines.append(" DETAILED BREAKDOWN")
lines.append("-" * w)
# News details
ns = components.get("news_sentiment", {})
if ns.get("articles_analyzed", 0) > 0:
pos = ns.get("positive", 0)
neg = ns.get("negative", 0)
neu = ns.get("neutral", 0)
lines.append(f" News Analysis:")
lines.append(f" Positive: {pos} | Negative: {neg} | Neutral: {neu}")
top_pos = ns.get("top_positive", [])
if top_pos:
lines.append(f" Top Positive Headlines:")
for headline in top_pos[:2]:
lines.append(f" + {headline[:60]}...")
top_neg = ns.get("top_negative", [])
if top_neg:
lines.append(f" Top Negative Headlines:")
for headline in top_neg[:2]:
lines.append(f" - {headline[:60]}...")
lines.append("")
# Market details
mm = components.get("market_momentum", {})
eth_change = mm.get("eth_change_24h")
vol_ratio = mm.get("volume_ratio")
if eth_change is not None or vol_ratio is not None:
lines.append(f" Market Data:")
if eth_change is not None:
lines.append(f" ETH 24h: {eth_change:+.1f}%")
if vol_ratio is not None:
vol_desc = "high" if vol_ratio > 1.2 else "normal" if vol_ratio > 0.8 else "low"
lines.append(f" Volume Ratio: {vol_ratio:.2f}x ({vol_desc})")
lines.append("")
# Interpretation
interpretation = data.get("interpretation", "")
if interpretation:
lines.append(" INTERPRETATION")
lines.append("-" * w)
# Word wrap interpretation
words = interpretation.split()
line = " "
for word in words:
if len(line) + len(word) + 1 > w - 2:
lines.append(line)
line = " " + word
else:
line += " " + word if line.strip() else word
if line.strip():
lines.append(line)
lines.append("")
# Errors
errors = data.get("meta", {}).get("errors")
if errors:
lines.append(" WARNINGS")
lines.append("-" * w)
for error in errors:
lines.append(f" ⚠ {error}")
lines.append("")
# Footer
lines.append("=" * w)
coin_filter = data.get("meta", {}).get("coin_filter")
period = data.get("meta", {}).get("period", "24h")
filter_str = f"Coin: {coin_filter} | " if coin_filter else ""
lines.append(f" {filter_str}Period: {period} | Sources: Fear & Greed, News, Market Data")
lines.append("=" * w)
return "\n".join(lines)
def _format_json(self, data: Dict[str, Any]) -> str:
"""Format as JSON."""
return json.dumps(data, indent=2, default=str)
def _format_csv(self, data: Dict[str, Any], detailed: bool) -> str:
"""Format as CSV."""
output = io.StringIO()
writer = csv.writer(output)
# Header row
headers = [
"timestamp",
"composite_score",
"classification",
"fear_greed_score",
"fear_greed_weight",
"news_score",
"news_weight",
"news_articles",
"momentum_score",
"momentum_weight",
"btc_change_24h",
"period",
"coin_filter"
]
if detailed:
headers.extend([
"news_positive",
"news_negative",
"news_neutral",
"eth_change_24h",
"volume_ratio"
])
writer.writerow(headers)
# Data row
components = data.get("components", {})
fg = components.get("fear_greed", {})
ns = components.get("news_sentiment", {})
mm = components.get("market_momentum", {})
meta = data.get("meta", {})
row = [
meta.get("timestamp", ""),
data.get("composite_score", 50),
data.get("classification", "Neutral"),
fg.get("score", 50),
fg.get("weight", 0.4),
ns.get("score", 50),
ns.get("weight", 0.4),
ns.get("articles_analyzed", 0),
mm.get("score", 50),
mm.get("weight", 0.2),
mm.get("btc_change_24h", ""),
meta.get("period", "24h"),
meta.get("coin_filter", "")
]
if detailed:
row.extend([
ns.get("positive", 0),
ns.get("negative", 0),
ns.get("neutral", 0),
mm.get("eth_change_24h", ""),
mm.get("volume_ratio", "")
])
writer.writerow(row)
return output.getvalue()
def _create_gauge(self, score: float) -> str:
"""Create ASCII gauge for sentiment score."""
# 50 character gauge
width = 50
position = int((score / 100) * width)
position = max(0, min(width - 1, position))
gauge = ["-"] * width
gauge[position] = "█"
# Add markers
if position > 0:
gauge[0] = "|"
if position < width - 1:
gauge[width - 1] = "|"
return "[" + "".join(gauge) + "]"
def main():
"""CLI entry point for testing."""
# Test with sample data
sample_data = {
"composite_score": 65.5,
"classification": "Greed",
"interpretation": "Market is moderately greedy. Consider taking some profits or reducing position sizes. Watch for reversal signals.",
"components": {
"fear_greed": {
"score": 72,
"classification": "Greed",
"weight": 0.40,
"contribution": 28.8
},
"news_sentiment": {
"score": 58.5,
"articles_analyzed": 25,
"positive": 12,
"negative": 5,
"neutral": 8,
"weight": 0.40,
"contribution": 23.4,
"top_positive": ["Bitcoin breaks $50k resistance", "Institutional adoption surges"],
"top_negative": ["SEC delays ETF decision"]
},
"market_momentum": {
"score": 66.5,
"btc_change_24h": 3.5,
"eth_change_24h": 2.1,
"volume_ratio": 1.2,
"weight": 0.20,
"contribution": 13.3
}
},
"meta": {
"timestamp": "2026-01-14T15:30:00Z",
"period": "24h",
"coin_filter": None
}
}
formatter = SentimentFormatter()
print("=== TABLE FORMAT ===")
print(formatter.format(sample_data, "table", detailed=True))
print()
print("=== JSON FORMAT ===")
print(formatter.format(sample_data, "json"))
print()
print("=== CSV FORMAT ===")
print(formatter.format(sample_data, "csv", detailed=True))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Market Momentum Analyzer
Calculates market momentum from price and volume data using CoinGecko API.
Converts momentum indicators to sentiment-compatible scores.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import json
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List
try:
import requests
except ImportError:
print("Error: requests library required. Install with: pip install requests", file=sys.stderr)
sys.exit(1)
class MarketMomentumAnalyzer:
"""Calculates market momentum from price and volume data."""
COINGECKO_API = "https://api.coingecko.com/api/v3"
CACHE_FILE = Path(__file__).parent / ".momentum_cache.json"
CACHE_TTL = 60 # 1 minute
# Map symbols to CoinGecko IDs
COIN_IDS = {
"BTC": "bitcoin",
"ETH": "ethereum",
"SOL": "solana",
"XRP": "ripple",
"ADA": "cardano",
"DOGE": "dogecoin",
"DOT": "polkadot",
"LINK": "chainlink",
"AVAX": "avalanche-2",
"MATIC": "matic-network",
"BNB": "binancecoin",
"LTC": "litecoin",
"ATOM": "cosmos",
"UNI": "uniswap",
"ARB": "arbitrum",
}
def __init__(self, verbose: bool = False):
"""Initialize analyzer.
Args:
verbose: Enable verbose output
"""
self.verbose = verbose
self._cache: Dict[str, Any] = {}
self._cache_time: float = 0
def analyze(self, coin: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""Analyze market momentum.
Args:
coin: Specific coin to analyze (default: market overall)
Returns:
Dict with score (0-100), price changes, volume ratio
"""
cache_key = coin or "market"
if self._is_cache_valid(cache_key):
if self.verbose:
print(f"Using cached momentum for {cache_key}", file=sys.stderr)
return self._cache.get(cache_key)
if coin:
result = self._analyze_coin(coin.upper())
else:
result = self._analyze_market()
if result:
self._cache[cache_key] = result
self._cache_time = time.time()
return result
def _analyze_market(self) -> Optional[Dict[str, Any]]:
"""Analyze overall market momentum using BTC and ETH."""
btc_data = self._fetch_coin_data("bitcoin")
eth_data = self._fetch_coin_data("ethereum")
if not btc_data:
return self._get_fallback()
# Calculate market momentum from major coins
btc_change = btc_data.get("price_change_percentage_24h", 0)
eth_change = eth_data.get("price_change_percentage_24h", 0) if eth_data else 0
# Volume analysis
btc_volume_ratio = self._calculate_volume_ratio(btc_data)
# Convert to momentum score (0-100)
# Price change component (60% weight)
avg_change = (btc_change * 0.7 + eth_change * 0.3) # BTC-weighted
price_score = self._change_to_score(avg_change)
# Volume component (40% weight)
volume_score = self._volume_ratio_to_score(btc_volume_ratio)
# Combined score
momentum_score = (price_score * 0.6) + (volume_score * 0.4)
return {
"score": round(momentum_score, 1),
"btc_change_24h": round(btc_change, 2),
"eth_change_24h": round(eth_change, 2),
"btc_price": btc_data.get("current_price"),
"eth_price": eth_data.get("current_price") if eth_data else None,
"volume_ratio": round(btc_volume_ratio, 2),
"market_cap": btc_data.get("market_cap"),
"timestamp": datetime.utcnow().isoformat() + "Z"
}
def _analyze_coin(self, symbol: str) -> Optional[Dict[str, Any]]:
"""Analyze momentum for specific coin."""
coin_id = self.COIN_IDS.get(symbol, symbol.lower())
data = self._fetch_coin_data(coin_id)
if not data:
# Fall back to market analysis
if self.verbose:
print(f"No data for {symbol}, using market analysis", file=sys.stderr)
return self._analyze_market()
change_24h = data.get("price_change_percentage_24h", 0)
volume_ratio = self._calculate_volume_ratio(data)
# Convert to score
price_score = self._change_to_score(change_24h)
volume_score = self._volume_ratio_to_score(volume_ratio)
momentum_score = (price_score * 0.6) + (volume_score * 0.4)
return {
"score": round(momentum_score, 1),
"coin": symbol,
"change_24h": round(change_24h, 2),
"current_price": data.get("current_price"),
"volume_ratio": round(volume_ratio, 2),
"market_cap": data.get("market_cap"),
"market_cap_rank": data.get("market_cap_rank"),
"timestamp": datetime.utcnow().isoformat() + "Z"
}
def _fetch_coin_data(self, coin_id: str) -> Optional[Dict[str, Any]]:
"""Fetch coin data from CoinGecko."""
try:
if self.verbose:
print(f"Fetching data for {coin_id}", file=sys.stderr)
url = f"{self.COINGECKO_API}/coins/markets"
params = {
"vs_currency": "usd",
"ids": coin_id,
"order": "market_cap_desc",
"sparkline": "false",
"price_change_percentage": "24h"
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if data and len(data) > 0:
return data[0]
return None
except requests.exceptions.RequestException as e:
if self.verbose:
print(f"API request failed: {e}", file=sys.stderr)
return None
except (json.JSONDecodeError, IndexError) as e:
if self.verbose:
print(f"Failed to parse response: {e}", file=sys.stderr)
return None
def _calculate_volume_ratio(self, data: Dict[str, Any]) -> float:
"""Calculate volume to market cap ratio.
Higher ratio suggests more activity relative to size.
"""
volume = data.get("total_volume", 0)
market_cap = data.get("market_cap", 1)
if market_cap == 0:
return 1.0
# Typical ratio is around 0.02-0.05 (2-5%)
ratio = volume / market_cap
# Normalize to 0-2 range (1.0 being average)
return min(2.0, ratio / 0.03)
def _change_to_score(self, change: float) -> float:
"""Convert price change percentage to 0-100 score.
-20% or worse = 0 (extreme bearish)
0% = 50 (neutral)
+20% or better = 100 (extreme bullish)
"""
# Clamp to -20 to +20 range
clamped = max(-20, min(20, change))
# Linear mapping to 0-100
return (clamped + 20) * 2.5
def _volume_ratio_to_score(self, ratio: float) -> float:
"""Convert volume ratio to 0-100 score.
Low volume (< 0.5) = bearish (30-50)
Normal volume (0.5-1.5) = neutral (40-60)
High volume (> 1.5) = can go either way, slight bullish (60-70)
"""
if ratio < 0.5:
return 30 + (ratio * 40) # 30-50
elif ratio < 1.5:
return 40 + (ratio - 0.5) * 20 # 40-60
else:
return min(70, 60 + (ratio - 1.5) * 20) # 60-70
def _is_cache_valid(self, key: str) -> bool:
"""Check if cache is valid."""
if key not in self._cache:
return False
return (time.time() - self._cache_time) < self.CACHE_TTL
def _get_fallback(self) -> Dict[str, Any]:
"""Return fallback data when API unavailable."""
return {
"score": 50,
"btc_change_24h": None,
"eth_change_24h": None,
"volume_ratio": None,
"error": "Market data unavailable",
"timestamp": datetime.utcnow().isoformat() + "Z"
}
def main():
"""CLI entry point for testing."""
import argparse
parser = argparse.ArgumentParser(description="Analyze market momentum")
parser.add_argument("--coin", type=str, help="Specific coin (BTC, ETH, etc.)")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
args = parser.parse_args()
analyzer = MarketMomentumAnalyzer(verbose=args.verbose)
result = analyzer.analyze(coin=args.coin)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
News Sentiment Analyzer
Analyzes cryptocurrency news sentiment using keyword-based scoring.
Optionally integrates with crypto-news-aggregator skill.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import json
import re
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, Dict, Any, List
try:
import requests
except ImportError:
print("Error: requests library required. Install with: pip install requests", file=sys.stderr)
sys.exit(1)
# Sentiment keywords with weights
POSITIVE_KEYWORDS = {
# Strong positive (0.3)
"bullish": 0.3, "surge": 0.3, "soar": 0.3, "rally": 0.3, "breakout": 0.3,
"record high": 0.3, "all-time high": 0.3, "ath": 0.3, "moon": 0.3,
# Medium positive (0.2)
"adoption": 0.2, "partnership": 0.2, "approval": 0.2, "milestone": 0.2,
"upgrade": 0.2, "launch": 0.2, "integration": 0.2, "institutional": 0.2,
"etf": 0.2, "accumulation": 0.2, "inflow": 0.2, "buy": 0.2,
# Mild positive (0.1)
"gain": 0.1, "rise": 0.1, "up": 0.1, "growth": 0.1, "positive": 0.1,
"optimistic": 0.1, "recovery": 0.1, "support": 0.1, "bullrun": 0.1,
}
NEGATIVE_KEYWORDS = {
# Strong negative (-0.3)
"bearish": -0.3, "crash": -0.3, "dump": -0.3, "collapse": -0.3,
"hack": -0.3, "exploit": -0.3, "scam": -0.3, "fraud": -0.3,
"bankruptcy": -0.3, "insolvent": -0.3, "rug pull": -0.3, "rugpull": -0.3,
# Medium negative (-0.2)
"ban": -0.2, "lawsuit": -0.2, "investigation": -0.2, "sec": -0.2,
"regulation": -0.2, "crackdown": -0.2, "sell-off": -0.2, "selloff": -0.2,
"outflow": -0.2, "withdrawal": -0.2, "liquidation": -0.2,
# Mild negative (-0.1)
"decline": -0.1, "drop": -0.1, "fall": -0.1, "down": -0.1, "loss": -0.1,
"concern": -0.1, "risk": -0.1, "uncertain": -0.1, "volatility": -0.1,
"correction": -0.1, "dip": -0.1, "fear": -0.1, "weak": -0.1,
}
# Coin-specific keywords
COIN_ALIASES = {
"BTC": ["bitcoin", "btc", "satoshi"],
"ETH": ["ethereum", "eth", "ether"],
"SOL": ["solana", "sol"],
"XRP": ["ripple", "xrp"],
"ADA": ["cardano", "ada"],
"DOGE": ["dogecoin", "doge"],
"DOT": ["polkadot", "dot"],
"LINK": ["chainlink", "link"],
"AVAX": ["avalanche", "avax"],
"MATIC": ["polygon", "matic"],
}
class NewsSentimentAnalyzer:
"""Analyzes news sentiment for cryptocurrency markets."""
RSS_FEEDS = [
"https://cointelegraph.com/rss",
"https://www.coindesk.com/arc/outboundfeeds/rss/",
"https://decrypt.co/feed",
]
CACHE_FILE = Path(__file__).parent / ".news_cache.json"
CACHE_TTL = 120 # 2 minutes
def __init__(self, verbose: bool = False):
"""Initialize analyzer.
Args:
verbose: Enable verbose output
"""
self.verbose = verbose
self._cache: Dict[str, Any] = {}
self._cache_time: float = 0
def analyze(
self,
coin: Optional[str] = None,
period: str = "24h"
) -> Optional[Dict[str, Any]]:
"""Analyze news sentiment.
Args:
coin: Filter by specific coin (e.g., "BTC", "ETH")
period: Time period ("1h", "4h", "24h", "7d")
Returns:
Dict with score (0-100), articles_analyzed, positive, negative, neutral
"""
# Check cache
cache_key = f"{coin or 'all'}:{period}"
if self._is_cache_valid(cache_key):
if self.verbose:
print(f"Using cached news sentiment for {cache_key}", file=sys.stderr)
return self._cache.get(cache_key)
# Try news aggregator skill first
aggregator_result = self._try_news_aggregator(coin, period)
if aggregator_result:
self._cache[cache_key] = aggregator_result
self._cache_time = time.time()
return aggregator_result
# Fallback to direct RSS fetch
articles = self._fetch_rss_articles(period)
if not articles:
if self.verbose:
print("No articles fetched, returning neutral sentiment", file=sys.stderr)
return {
"score": 50,
"articles_analyzed": 0,
"positive": 0,
"negative": 0,
"neutral": 0,
"error": "No articles available"
}
# Filter by coin if specified
if coin:
articles = self._filter_by_coin(articles, coin.upper())
# Score articles
scored = self._score_articles(articles)
# Calculate aggregate score
result = self._aggregate_scores(scored)
result["source"] = "rss"
self._cache[cache_key] = result
self._cache_time = time.time()
return result
def _try_news_aggregator(
self,
coin: Optional[str],
period: str
) -> Optional[Dict[str, Any]]:
"""Try to use crypto-news-aggregator skill if available."""
# Check for news aggregator in sibling plugin
aggregator_path = (
Path(__file__).parent.parent.parent.parent.parent /
"crypto-news-aggregator" / "skills" / "aggregating-crypto-news" / "scripts"
)
if not aggregator_path.exists():
if self.verbose:
print("News aggregator skill not found", file=sys.stderr)
return None
try:
sys.path.insert(0, str(aggregator_path))
from news_aggregator import NewsAggregator
sys.path.pop(0)
if self.verbose:
print("Using news aggregator skill", file=sys.stderr)
aggregator = NewsAggregator(verbose=self.verbose)
news_data = aggregator.fetch(coin=coin, period=period, limit=50)
if not news_data or not news_data.get("articles"):
return None
# Score the aggregated articles
articles = [
{"title": a.get("title", ""), "summary": a.get("summary", "")}
for a in news_data.get("articles", [])
]
scored = self._score_articles(articles)
result = self._aggregate_scores(scored)
result["source"] = "aggregator"
return result
except Exception as e:
if self.verbose:
print(f"News aggregator failed: {e}", file=sys.stderr)
return None
def _fetch_rss_articles(self, period: str) -> List[Dict[str, Any]]:
"""Fetch articles from RSS feeds."""
articles = []
cutoff = self._get_cutoff_time(period)
for feed_url in self.RSS_FEEDS:
try:
if self.verbose:
print(f"Fetching RSS: {feed_url}", file=sys.stderr)
response = requests.get(feed_url, timeout=10)
response.raise_for_status()
# Simple XML parsing for RSS
parsed = self._parse_rss_xml(response.text, cutoff)
articles.extend(parsed)
except Exception as e:
if self.verbose:
print(f"Failed to fetch {feed_url}: {e}", file=sys.stderr)
continue
# Deduplicate by title
seen = set()
unique = []
for article in articles:
title_key = article.get("title", "").lower()[:50]
if title_key and title_key not in seen:
seen.add(title_key)
unique.append(article)
return unique
def _parse_rss_xml(
self,
xml_content: str,
cutoff: datetime
) -> List[Dict[str, Any]]:
"""Parse RSS XML content."""
articles = []
# Extract items using regex (simple approach)
items = re.findall(r"<item>(.*?)</item>", xml_content, re.DOTALL)
for item in items:
# Extract title
title_match = re.search(r"<title>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?</title>", item, re.DOTALL)
title = title_match.group(1).strip() if title_match else ""
# Extract description
desc_match = re.search(r"<description>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?</description>", item, re.DOTALL)
description = desc_match.group(1).strip() if desc_match else ""
# Clean HTML
title = re.sub(r"<[^>]+>", "", title)
description = re.sub(r"<[^>]+>", "", description)
if title:
articles.append({
"title": title,
"summary": description[:500]
})
return articles[:30] # Limit per feed
def _filter_by_coin(
self,
articles: List[Dict[str, Any]],
coin: str
) -> List[Dict[str, Any]]:
"""Filter articles mentioning specific coin."""
keywords = COIN_ALIASES.get(coin, [coin.lower()])
filtered = []
for article in articles:
text = f"{article.get('title', '')} {article.get('summary', '')}".lower()
if any(kw in text for kw in keywords):
filtered.append(article)
return filtered if filtered else articles # Return all if no matches
def _score_articles(self, articles: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Score each article for sentiment."""
scored = []
for article in articles:
text = f"{article.get('title', '')} {article.get('summary', '')}".lower()
score = 0.0
# Check positive keywords
for keyword, weight in POSITIVE_KEYWORDS.items():
if keyword in text:
score += weight
# Check negative keywords
for keyword, weight in NEGATIVE_KEYWORDS.items():
if keyword in text:
score += weight # weight is already negative
# Clamp to [-1, 1]
score = max(-1, min(1, score))
# Classify
if score > 0.1:
sentiment = "positive"
elif score < -0.1:
sentiment = "negative"
else:
sentiment = "neutral"
scored.append({
**article,
"score": score,
"sentiment": sentiment
})
return scored
def _aggregate_scores(self, scored: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Aggregate article scores to overall sentiment."""
if not scored:
return {
"score": 50,
"articles_analyzed": 0,
"positive": 0,
"negative": 0,
"neutral": 0
}
positive = sum(1 for a in scored if a["sentiment"] == "positive")
negative = sum(1 for a in scored if a["sentiment"] == "negative")
neutral = sum(1 for a in scored if a["sentiment"] == "neutral")
# Calculate weighted average score
total_score = sum(a["score"] for a in scored)
avg_score = total_score / len(scored) # -1 to +1
# Convert to 0-100 scale
normalized_score = (avg_score + 1) * 50
return {
"score": round(normalized_score, 1),
"articles_analyzed": len(scored),
"positive": positive,
"negative": negative,
"neutral": neutral,
"top_positive": [
a["title"] for a in sorted(scored, key=lambda x: x["score"], reverse=True)[:3]
if a["sentiment"] == "positive"
],
"top_negative": [
a["title"] for a in sorted(scored, key=lambda x: x["score"])[:3]
if a["sentiment"] == "negative"
]
}
def _get_cutoff_time(self, period: str) -> datetime:
"""Get cutoff datetime for period."""
now = datetime.utcnow()
deltas = {
"1h": timedelta(hours=1),
"4h": timedelta(hours=4),
"24h": timedelta(days=1),
"7d": timedelta(days=7),
}
return now - deltas.get(period, timedelta(days=1))
def _is_cache_valid(self, key: str) -> bool:
"""Check if cache entry is valid."""
if key not in self._cache:
return False
return (time.time() - self._cache_time) < self.CACHE_TTL
def main():
"""CLI entry point for testing."""
import argparse
parser = argparse.ArgumentParser(description="Analyze crypto news sentiment")
parser.add_argument("--coin", type=str, help="Filter by coin (BTC, ETH, etc.)")
parser.add_argument("--period", type=str, default="24h", help="Time period")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
args = parser.parse_args()
analyzer = NewsSentimentAnalyzer(verbose=args.verbose)
result = analyzer.analyze(coin=args.coin, period=args.period)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Market Sentiment Analyzer - Main CLI Entry Point
Analyze cryptocurrency market sentiment using Fear & Greed Index,
news sentiment, and market momentum.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import argparse
import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any
# Add scripts directory to path for local imports
SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(SCRIPT_DIR))
from fear_greed import FearGreedFetcher
from news_sentiment import NewsSentimentAnalyzer
from market_momentum import MarketMomentumAnalyzer
from formatters import SentimentFormatter
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Analyze cryptocurrency market sentiment",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s # Quick sentiment check
%(prog)s --coin BTC # Bitcoin-specific sentiment
%(prog)s --detailed # Full component breakdown
%(prog)s --format json --output s.json # Export to JSON
"""
)
# Analysis options
parser.add_argument(
"--coin",
type=str,
help="Analyze specific coin (e.g., BTC, ETH)"
)
parser.add_argument(
"--period",
type=str,
choices=["1h", "4h", "24h", "7d"],
default="24h",
help="Time period for analysis (default: 24h)"
)
parser.add_argument(
"--detailed",
action="store_true",
help="Show detailed component breakdown"
)
# Weight customization
parser.add_argument(
"--weights",
type=str,
help="Custom weights (e.g., 'news:0.5,fng:0.3,momentum:0.2')"
)
# Output options
parser.add_argument(
"--format", "-f",
type=str,
choices=["table", "json", "csv"],
default="table",
help="Output format (default: table)"
)
parser.add_argument(
"--output", "-o",
type=str,
help="Output file path (default: stdout)"
)
# Debug options
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable verbose output"
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 2.0.0"
)
return parser.parse_args()
def parse_weights(weights_str: Optional[str]) -> Dict[str, float]:
"""Parse custom weights string."""
default_weights = {
"fear_greed": 0.40,
"news": 0.40,
"momentum": 0.20
}
if not weights_str:
return default_weights
try:
weights = {}
for part in weights_str.split(","):
key, value = part.strip().split(":")
key = key.strip().lower()
# Map shorthand names
if key == "fng":
key = "fear_greed"
weights[key] = float(value.strip())
# Fill in missing weights with defaults
for key in default_weights:
if key not in weights:
weights[key] = default_weights[key]
# Normalize to sum to 1.0
total = sum(weights.values())
if total > 0:
weights = {k: v / total for k, v in weights.items()}
return weights
except Exception:
return default_weights
def classify_sentiment(score: float) -> str:
"""Classify sentiment score into category."""
if score <= 20:
return "Extreme Fear"
elif score <= 40:
return "Fear"
elif score <= 60:
return "Neutral"
elif score <= 80:
return "Greed"
else:
return "Extreme Greed"
def get_interpretation(score: float, classification: str) -> str:
"""Generate interpretation text for sentiment score."""
interpretations = {
"Extreme Fear": (
"Market is in extreme fear. Historically, this has been a good "
"buying opportunity for long-term investors. However, prices may "
"continue falling in the short term."
),
"Fear": (
"Market sentiment is fearful. Caution is advised, but oversold "
"conditions may present opportunities for those with higher risk "
"tolerance."
),
"Neutral": (
"Market sentiment is balanced with no strong directional bias. "
"Wait for clearer signals before making major decisions."
),
"Greed": (
"Market is moderately greedy. Consider taking some profits or "
"reducing position sizes. Watch for reversal signals."
),
"Extreme Greed": (
"Market is in extreme greed territory. Exercise caution - this "
"level often precedes corrections. Consider defensive positioning."
)
}
return interpretations.get(classification, "")
def main() -> None:
"""Main entry point."""
args = parse_args()
# Parse custom weights
weights = parse_weights(args.weights)
if args.verbose:
print(f"Using weights: {weights}", file=sys.stderr)
# Initialize analyzers
fng_fetcher = FearGreedFetcher(verbose=args.verbose)
news_analyzer = NewsSentimentAnalyzer(verbose=args.verbose)
momentum_analyzer = MarketMomentumAnalyzer(verbose=args.verbose)
formatter = SentimentFormatter()
# Collect components
components = {}
errors = []
# 1. Fear & Greed Index
if args.verbose:
print("Fetching Fear & Greed Index...", file=sys.stderr)
fng_data = fng_fetcher.fetch()
if fng_data:
components["fear_greed"] = {
"score": fng_data.get("value", 50),
"classification": fng_data.get("classification", "Neutral"),
"weight": weights.get("fear_greed", 0.4),
"contribution": fng_data.get("value", 50) * weights.get("fear_greed", 0.4),
"timestamp": fng_data.get("timestamp")
}
else:
errors.append("Fear & Greed Index unavailable")
components["fear_greed"] = {
"score": 50,
"classification": "Unknown",
"weight": weights.get("fear_greed", 0.4),
"contribution": 50 * weights.get("fear_greed", 0.4),
"error": "API unavailable"
}
# 2. News Sentiment
if args.verbose:
print("Analyzing news sentiment...", file=sys.stderr)
news_data = news_analyzer.analyze(
coin=args.coin,
period=args.period
)
if news_data:
components["news_sentiment"] = {
"score": news_data.get("score", 50),
"articles_analyzed": news_data.get("articles_analyzed", 0),
"positive": news_data.get("positive", 0),
"negative": news_data.get("negative", 0),
"neutral": news_data.get("neutral", 0),
"weight": weights.get("news", 0.4),
"contribution": news_data.get("score", 50) * weights.get("news", 0.4)
}
else:
errors.append("News sentiment analysis unavailable")
components["news_sentiment"] = {
"score": 50,
"articles_analyzed": 0,
"weight": weights.get("news", 0.4),
"contribution": 50 * weights.get("news", 0.4),
"error": "Analysis unavailable"
}
# 3. Market Momentum
if args.verbose:
print("Calculating market momentum...", file=sys.stderr)
momentum_data = momentum_analyzer.analyze(coin=args.coin)
if momentum_data:
components["market_momentum"] = {
"score": momentum_data.get("score", 50),
"btc_change_24h": momentum_data.get("btc_change_24h"),
"eth_change_24h": momentum_data.get("eth_change_24h"),
"volume_ratio": momentum_data.get("volume_ratio"),
"weight": weights.get("momentum", 0.2),
"contribution": momentum_data.get("score", 50) * weights.get("momentum", 0.2)
}
else:
errors.append("Market momentum unavailable")
components["market_momentum"] = {
"score": 50,
"weight": weights.get("momentum", 0.2),
"contribution": 50 * weights.get("momentum", 0.2),
"error": "Data unavailable"
}
# Calculate composite score
composite_score = sum(c.get("contribution", 0) for c in components.values())
composite_score = round(composite_score, 1)
classification = classify_sentiment(composite_score)
interpretation = get_interpretation(composite_score, classification)
# Prepare result
result = {
"composite_score": composite_score,
"classification": classification,
"interpretation": interpretation,
"components": components,
"meta": {
"timestamp": datetime.utcnow().isoformat() + "Z",
"period": args.period,
"coin_filter": args.coin,
"weights": weights,
"errors": errors if errors else None
}
}
# Format output
output = formatter.format(
result,
format_type=args.format,
detailed=args.detailed
)
# Write output
if args.output:
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w") as f:
f.write(output)
print(f"Output written to {output_path}", file=sys.stderr)
else:
print(output)
if __name__ == "__main__":
main()