
Aggregating Crypto News
- 227 installs
- 2.6k repo stars
- Updated August 5, 2026
- jeremylongshore/claude-code-plugins-plus-skills
Wire crypto news feeds and sources into a pipeline that normalizes headlines and summaries for dashboards, bots, or content sites.
About
Builds integrations that pull crypto news from multiple sources, normalize and dedupe headlines, and route summaries into dashboards, agents, newsletters, or content sites for Web3 products and media workflows.
- Feed ingestion
- Source normalization
- Deduping headlines
- API wiring
- Publish-ready summaries
Aggregating Crypto News by the numbers
- 227 all-time installs (skills.sh)
- Ranked #84 of 479 Web3 & Blockchain 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 aggregating-crypto-newsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 227 |
|---|---|
| repo stars | ★ 2.6k |
| Last updated | August 5, 2026 |
| Repository | jeremylongshore/claude-code-plugins-plus-skills ↗ |
What it does
Wire crypto news feeds and sources into a pipeline that normalizes headlines and summaries for dashboards, bots, or content sites.
Files
Aggregating Crypto News
Overview
Aggregate cryptocurrency news from 50+ authoritative sources via RSS feeds with real-time scanning, coin/category filtering, and relevance scoring.
Prerequisites
1. Python 3.8+ installed 2. Dependencies: pip install feedparser requests 3. Internet connectivity for RSS feed access
Instructions
1. Assess user intent - determine filters needed:
- General news: no filters, use defaults
- Coin-specific: extract symbol (BTC, ETH, etc.)
- Category-specific: extract category (defi, nft, regulatory, etc.)
- Time-specific: extract window (1h, 4h, 24h, 7d)
2. Run the aggregator with appropriate filters:
# Default scan (top 20, past 24h, relevance sorted)
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py
# Coin-specific scan
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --coin BTC --period 4h
# Category filter
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --category defi --top 30
# Multiple filters
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --coin ETH --category defi --period 24h --top 153. Export results for downstream processing:
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --format json --output news.json4. Present results to the user:
- Show source, title, age, and relevance score
- Highlight market-moving keywords if present
- Provide links for full articles
- Summarize meta information (sources checked, articles found)
Output
Table showing articles ranked by relevance score (0-100) based on market-moving keyword detection, source authority, and recency:
==============================================================================
CRYPTO NEWS AGGREGATOR Updated: 2026-01-14 15:30 # 2026 - current year timestamp
==============================================================================
TOP CRYPTO NEWS (24h)
------------------------------------------------------------------------------
Rank Source Title Age Score
------------------------------------------------------------------------------
1 CoinDesk Bitcoin Breaks $100K ATH 2h 95.0
2 The Block SEC Approves ETH ETF 4h 92.5
3 Decrypt Solana DeFi TVL Surges 6h 78.3
------------------------------------------------------------------------------
Summary: 20 articles shown | Scanned: 50 sources | Matched: 187
==============================================================================Error Handling
| Error | Cause | Solution |
|---|---|---|
| Network timeout | RSS feed unreachable | Uses cached data; skips unavailable sources |
| Parse error | Malformed RSS | Skips entry; continues with valid articles |
| No results | Filters too strict | Suggest relaxing filters |
| Invalid coin | Unknown symbol | List similar valid symbols |
See ${CLAUDE_SKILL_DIR}/references/errors.md for comprehensive error handling.
Examples
Filtering patterns for common news monitoring scenarios:
# Latest crypto news (defaults)
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py
# Bitcoin news from past 4 hours
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --coin BTC --period 4h
# DeFi category news
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --category defi
# High-relevance news only
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --min-score 70 --top 10
# Multiple coins
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --coins BTC,ETH,SOLResources
${CLAUDE_SKILL_DIR}/references/implementation.md- CLI options, categories, JSON format, advanced filtering${CLAUDE_SKILL_DIR}/references/errors.md- Comprehensive error handling${CLAUDE_SKILL_DIR}/references/examples.md- Detailed usage examples${CLAUDE_SKILL_DIR}/config/sources.yaml- Full source registry- CoinDesk RSS: https://www.coindesk.com/arc/outboundfeeds/rss/
- feedparser docs: https://feedparser.readthedocs.io/
ARD: Aggregating Crypto News
Part of Tons of Skills by Intent Solutions | jeremylongshore.com
Document Control
| Field | Value |
|---|---|
| Skill Name | aggregating-crypto-news |
| Architecture Pattern | Data Aggregation Pipeline |
| Version | 2.0.0 |
| Author | Jeremy Longshore <jeremy@intentsolutions.io> |
---
Architectural Overview
Pattern: Multi-Source Data Aggregation
This skill implements a parallel fetching, aggregation, and filtering pipeline for RSS-based news sources.
┌─────────────────────────────────────────────────────────────────────────────┐
│ CRYPTO NEWS AGGREGATOR ARCHITECTURE │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ RSS Source 1 │ │ RSS Source 2 │ │ RSS Source N │
│ (CoinDesk) │ │ (CoinTelegraph) │ │ (The Block) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
└────────────────────────┼────────────────────────┘
│
▼
┌─────────────────────────┐
│ FEED FETCHER │
│ (Parallel Requests) │
│ - Timeout handling │
│ - Caching layer │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ FEED PARSER │
│ - XML/RSS parsing │
│ - Date normalization │
│ - Content extraction │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ DEDUPLICATOR │
│ - Title similarity │
│ - URL matching │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ SCORER │
│ - Keyword matching │
│ - Market-moving boost │
│ - Source quality │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ FILTER ENGINE │
│ - Time window │
│ - Coin matching │
│ - Category filtering │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ FORMATTER │
│ - Table output │
│ - JSON export │
│ - CSV export │
└─────────────────────────┘Workflow
1. Fetch: Parallel HTTP requests to RSS feeds with timeout handling 2. Parse: Extract title, content, date, source from XML 3. Dedupe: Remove duplicate articles based on title similarity 4. Score: Calculate relevance score based on keywords and source 5. Filter: Apply user filters (time, coin, category) 6. Format: Output in requested format
---
Progressive Disclosure Strategy
Level 1: Simple Scan (Default)
python news_aggregator.pyReturns top 20 news from past 24h, relevance sorted.
Level 2: Filtered Scan
python news_aggregator.py --coin BTC --period 4h --top 10Adds coin and time filtering.
Level 3: Category + Export
python news_aggregator.py --category defi --format json --output news.jsonFull filtering with export.
Level 4: Advanced Configuration
python news_aggregator.py --sources coindesk,theblock --min-score 50 --verboseSource selection and score thresholds.
---
Tool Permission Strategy
Allowed Tools (Scoped)
allowed-tools: Read, Bash(crypto:news-*)| Tool | Scope | Purpose |
|---|---|---|
| Read | Unrestricted | Read config, sources list |
| Bash | crypto:news-* | Execute news aggregation scripts |
Why These Tools
- Read: Load source configuration and settings
- *Bash(crypto:news-)**: Execute Python scripts for fetching and parsing
- No Write: Aggregation is read-only; exports use script file output
---
Directory Structure
plugins/crypto/crypto-news-aggregator/
└── skills/
└── aggregating-crypto-news/
├── PRD.md # Product requirements
├── ARD.md # This file
├── SKILL.md # Core instructions
├── scripts/
│ ├── news_aggregator.py # Main CLI entry point
│ ├── feed_fetcher.py # Parallel RSS fetching
│ ├── feed_parser.py # XML parsing and normalization
│ ├── scorer.py # Relevance scoring
│ └── formatters.py # Output formatting
├── references/
│ ├── errors.md # Error handling guide
│ └── examples.md # Usage examples
└── config/
├── settings.yaml # Configuration options
└── sources.yaml # RSS source registry---
Data Flow Architecture
Input
- User request with optional filters (--coin, --period, --category)
- RSS feed URLs from source registry
Processing Pipeline
Input Request
│
├──► Load sources.yaml
│ │
│ ▼
├──► Parallel fetch (ThreadPoolExecutor)
│ │
│ ▼
├──► Parse each feed (feedparser library)
│ │
│ ▼
├──► Normalize entries to common schema:
│ {
│ "title": str,
│ "url": str,
│ "source": str,
│ "published": datetime,
│ "summary": str,
│ "category": str,
│ "relevance_score": float
│ }
│ │
│ ▼
├──► Deduplicate by title similarity (>80% match)
│ │
│ ▼
├──► Score relevance:
│ - Base score from source quality (1-10)
│ - Keyword boost (+10 per market-moving term)
│ - Recency boost (newer = higher)
│ │
│ ▼
├──► Apply filters:
│ - Time window (published > now - period)
│ - Coin match (title/summary contains symbol)
│ - Category match (source category or keywords)
│ │
│ ▼
└──► Format and outputOutput Schema
{
"articles": [
{
"rank": 1,
"title": "Bitcoin Hits New All-Time High",
"url": "https://coindesk.com/...",
"source": "CoinDesk",
"published": "2026-01-14T10:30:00Z",
"age": "2h ago",
"category": "market",
"relevance_score": 85.5,
"coins_mentioned": ["BTC"]
}
],
"meta": {
"total_fetched": 500,
"after_dedup": 380,
"after_filter": 25,
"sources_used": 12,
"period": "24h",
"filters": {"category": "market"}
}
}---
Error Handling Strategy
Error Categories
| Category | Examples | Strategy |
|---|---|---|
| Network | Timeout, DNS failure | Skip source, continue with others |
| Parse | Malformed XML, missing fields | Skip entry, log warning |
| Filter | Invalid coin symbol | Warn and proceed with partial filter |
| Export | File permission denied | Error with clear message |
Graceful Degradation
Full Success (all sources)
│
▼
Partial Success (some sources failed)
│ → Log warnings, return available data
│
▼
Cached Fallback (all sources failed)
│ → Return cached data with stale warning
│
▼
Complete Failure
│ → Clear error message with remediation---
Composability & Stacking
As a Standalone Skill
- Fully functional for news aggregation
- No dependencies on other skills
As Data Provider
Skills that can consume news data:
- market-sentiment-analyzer: Feed articles for sentiment scoring
- crypto-signal-generator: Use news as signal input
- whale-alert-monitor: Correlate whale activity with news
Integration Pattern
# In another skill's script:
from pathlib import Path
import subprocess
import json
def get_news_for_coin(coin: str, period: str = "4h") -> list:
"""Fetch news using crypto-news-aggregator skill."""
news_script = Path(__file__).parent.parent.parent.parent / \
"crypto-news-aggregator/skills/aggregating-crypto-news/scripts/news_aggregator.py"
result = subprocess.run(
["python3", str(news_script), "--coin", coin, "--period", period, "--format", "json"],
capture_output=True, text=True
)
if result.returncode == 0:
return json.loads(result.stdout).get("articles", [])
return []---
Performance & Scalability
Performance Targets
| Metric | Target | Approach |
|---|---|---|
| Fetch time (50 sources) | < 10s | Parallel fetching with ThreadPoolExecutor |
| Parse time (500 articles) | < 2s | Efficient XML parsing with feedparser |
| Memory usage | < 100MB | Stream processing, no full corpus in memory |
Optimization Strategies
1. Parallel Fetching: Use ThreadPoolExecutor(max_workers=10) 2. Caching: Cache responses with 5-minute TTL 3. Lazy Parsing: Parse only after filter candidates identified 4. Early Exit: Stop processing when --top limit reached
Scalability Considerations
- More Sources: Linear scaling with parallel fetch
- Higher Volume: Pagination and streaming for large exports
- Frequency: Caching prevents redundant fetches
---
Testing Strategy
Unit Tests
| Component | Test Cases |
|---|---|
| FeedFetcher | Timeout handling, cache hit/miss, parallel execution |
| FeedParser | Valid RSS, malformed XML, missing fields |
| Scorer | Keyword matching, source quality, recency boost |
| Formatters | Table, JSON, CSV output validation |
Integration Tests
- End-to-end with mock RSS server
- Real feeds (selected stable sources)
- Export file validation
Manual Testing
# Default scan
python news_aggregator.py
# Coin filter
python news_aggregator.py --coin BTC
# Category filter
python news_aggregator.py --category defi
# Export
python news_aggregator.py --format json --output test.json---
Security & Compliance
Security Considerations
- No Authentication: RSS feeds are public, no credentials needed
- Input Validation: Sanitize coin symbols and category names
- Output Sanitization: Escape HTML in article titles/summaries
Data Privacy
- No user data collected or stored
- No tracking or analytics
- Cached data is ephemeral (5-minute TTL)
Rate Limiting
- Respect source rate limits (1 req/source/5min default)
- No aggressive crawling
- Caching reduces request volume
# Aggregating Crypto News - Configuration
# Version: 2.0.0
# Author: Jeremy Longshore <jeremy@intentsolutions.io>
# =============================================================================
# Default Settings
# =============================================================================
defaults:
# Default time period for news scan
period: 24h
# Number of articles to return
top: 20
# Sort order (relevance or recency)
sort_by: relevance
# Output format (table, json, csv)
format: table
# =============================================================================
# Fetch Settings
# =============================================================================
fetch:
# Request timeout in seconds
timeout: 10
# Maximum parallel requests
max_workers: 10
# User agent string
user_agent: "CryptoNewsAggregator/2.0 (RSS Feed Reader)"
# =============================================================================
# Cache Settings
# =============================================================================
cache:
# Enable response caching
enabled: true
# Cache time-to-live in seconds (5 minutes)
ttl: 300
# Cache directory (relative to skill root)
directory: ./data/cache
# =============================================================================
# Scoring Weights
# =============================================================================
scoring:
# Source quality weight (0-30 points)
source_quality_multiplier: 3
# Maximum keyword score
max_keyword_score: 50
# Recency bonuses by age
recency_bonus:
1h: 20
4h: 15
12h: 10
24h: 5
3d: 2
# =============================================================================
# Categories
# =============================================================================
categories:
market:
description: "General market news and price movements"
keywords: []
defi:
description: "DeFi protocols, yield farming, DEXes"
keywords:
- defi
- yield farming
- liquidity pool
- dex
- lending protocol
nft:
description: "NFT projects, marketplaces, collections"
keywords:
- nft
- opensea
- blur
- collection
- mint
regulatory:
description: "Government, SEC, legal developments"
keywords:
- sec
- cftc
- regulation
- congress
- compliance
layer1:
description: "L1 blockchain news"
keywords:
- ethereum
- solana
- bitcoin
- cardano
- avalanche
layer2:
description: "L2 scaling solutions"
keywords:
- arbitrum
- optimism
- polygon
- zksync
- base
- rollup
exchange:
description: "Exchange news, listings, delistings"
keywords:
- binance
- coinbase
- kraken
- listing
- delist
security:
description: "Hacks, exploits, vulnerabilities"
keywords:
- hack
- exploit
- vulnerability
- audit
- breach
- stolen
# =============================================================================
# Deduplication
# =============================================================================
deduplication:
# Enable title-based deduplication
enabled: true
# Similarity threshold (0-1)
threshold: 0.8
# RSS News Sources Registry
# Version: 2.0.0
# Author: Jeremy Longshore <jeremy@intentsolutions.io>
#
# Quality score (1-10):
# 9-10: Tier 1 - Major outlets, fast, reliable
# 7-8: Tier 2 - Good coverage, some delay
# 5-6: Tier 3 - Niche or aggregated content
# 1-4: Tier 4 - Lower quality, promotional
sources:
# ==========================================================================
# Tier 1: Major Crypto News Outlets
# ==========================================================================
- name: CoinDesk
url: https://www.coindesk.com/arc/outboundfeeds/rss/
category: market
quality: 9
description: Leading crypto news, fast on breaking stories
- name: The Block
url: https://www.theblock.co/rss.xml
category: market
quality: 9
description: Institutional-grade crypto journalism
- name: Decrypt
url: https://decrypt.co/feed
category: market
quality: 8
description: Accessible crypto news for all levels
- name: CoinTelegraph
url: https://cointelegraph.com/rss
category: market
quality: 8
description: High-volume crypto news coverage
- name: Blockworks
url: https://blockworks.co/feed/
category: market
quality: 8
description: Institutional crypto and DeFi news
# ==========================================================================
# Tier 2: Quality Crypto Publications
# ==========================================================================
- name: Bitcoin Magazine
url: https://bitcoinmagazine.com/feed
category: market
quality: 8
description: Bitcoin-focused coverage
- name: CryptoSlate
url: https://cryptoslate.com/feed/
category: market
quality: 7
description: Comprehensive crypto coverage
- name: DL News
url: https://www.dlnews.com/feed/
category: market
quality: 7
description: DeFi and web3 focused
- name: Unchained
url: https://unchainedcrypto.com/feed/
category: market
quality: 7
description: In-depth crypto analysis
# ==========================================================================
# Tier 3: Aggregators and Niche Sites
# ==========================================================================
- name: NewsBTC
url: https://www.newsbtc.com/feed/
category: market
quality: 6
description: General crypto news
- name: CryptoPotato
url: https://cryptopotato.com/feed/
category: market
quality: 6
description: Trading and market news
- name: U.Today
url: https://u.today/rss
category: market
quality: 6
description: High-volume crypto news
- name: Bitcoinist
url: https://bitcoinist.com/feed/
category: market
quality: 6
description: Bitcoin and altcoin news
- name: AMBCrypto
url: https://ambcrypto.com/feed/
category: market
quality: 5
description: Altcoin coverage
# ==========================================================================
# DeFi Specific
# ==========================================================================
- name: DeFi Pulse
url: https://defipulse.com/blog/feed/
category: defi
quality: 8
description: DeFi protocol analysis
- name: The Defiant
url: https://thedefiant.io/feed/
category: defi
quality: 8
description: DeFi news and research
# ==========================================================================
# NFT Specific
# ==========================================================================
- name: NFT Now
url: https://nftnow.com/feed/
category: nft
quality: 7
description: NFT news and culture
# ==========================================================================
# Layer 2 and Ethereum
# ==========================================================================
- name: Week in Ethereum
url: https://weekinethereumnews.com/feed/
category: layer1
quality: 8
description: Weekly Ethereum digest
# ==========================================================================
# Regulatory and Institutional
# ==========================================================================
- name: Crypto Law Insider
url: https://cryptolawinsider.com/feed/
category: regulatory
quality: 7
description: Crypto legal developments
# =============================================================================
# Source Categories Reference
# =============================================================================
# market: General market news, price movements
# defi: DeFi protocols, yield farming, DEXes
# nft: NFT projects, marketplaces
# regulatory: Government, SEC, legal
# layer1: L1 blockchain news
# layer2: L2 scaling solutions
# exchange: Exchange news
# security: Hacks, exploits
PRD: Aggregating Crypto News
Document Control
| Field | Value |
|---|---|
| Skill Name | aggregating-crypto-news |
| Type | Data Aggregation & Information |
| Domain | Cryptocurrency News & Media |
| Target Users | Traders, Analysts, Researchers, Portfolio Managers |
| Priority | P1 - Foundation Skill |
| Version | 2.0.0 |
| Author | Jeremy Longshore <jeremy@intentsolutions.io> |
---
Executive Summary
The aggregating-crypto-news skill provides real-time cryptocurrency news aggregation from multiple authoritative sources including RSS feeds, news APIs, and social media. It enables traders and analysts to stay informed about market-moving events, protocol updates, regulatory changes, and ecosystem developments without manually monitoring dozens of sources.
Value Proposition: Consolidate crypto news from 50+ sources into a single, filterable feed with relevance scoring and coin-specific filtering.
---
Problem Statement
Current Pain Points
1. Information Overload: Crypto news is scattered across hundreds of sources - Twitter, Discord, Telegram, news sites, protocol blogs, exchange announcements 2. Signal vs Noise: Separating market-moving news from promotional content and spam is time-consuming 3. Missed Events: Important announcements (airdrops, forks, delistings) can be missed without constant monitoring 4. No Central Feed: No single tool aggregates RSS feeds, Twitter spaces, Discord announcements, and official blogs
Impact of Not Solving
- Traders miss alpha from early news detection
- Portfolio managers react late to regulatory changes
- Researchers waste hours manually checking sources
- Teams lack shared news intelligence
---
Target Users
Persona 1: Active Day Trader
- Name: Marcus
- Role: Full-time crypto day trader
- Goals: Catch news before price moves; identify catalysts
- Pain Points: Misses announcements; overwhelmed by noise
- Usage: Runs news scan every 30 minutes during active sessions
Persona 2: Research Analyst
- Name: Sarah
- Role: Crypto research analyst at investment firm
- Goals: Comprehensive coverage for reports; track regulatory changes
- Pain Points: Manual source monitoring; inconsistent coverage
- Usage: Daily digest with category filters; export for reports
Persona 3: Portfolio Manager
- Name: David
- Role: DeFi portfolio manager
- Goals: Monitor protocol updates; track governance proposals
- Pain Points: Misses critical updates buried in Discord/forums
- Usage: Protocol-specific feeds with alerts for high-priority items
---
User Stories
US-1: Breaking News Scan (Critical)
As a day trader I want to scan for breaking crypto news from the past hour So that I can identify market-moving events before they're priced in
Acceptance Criteria:
- Fetch news from past 1h/4h/24h windows
- Display source, title, timestamp, relevance score
- Highlight market-moving keywords (listing, delisting, hack, exploit)
- Complete scan in under 10 seconds
US-2: Coin-Specific News (Critical)
As a researcher I want to filter news for specific coins or tokens So that I can focus on assets in my coverage universe
Acceptance Criteria:
- Filter by single coin (--coin BTC) or multiple (--coins BTC,ETH,SOL)
- Match against title, body, and tags
- Include protocol-specific sources (e.g., Solana newsletters for SOL)
- Return results sorted by relevance or recency
US-3: Category Filtering (Important)
As a portfolio manager I want to filter news by category (regulatory, DeFi, NFT, etc.) So that I can focus on my domain without irrelevant noise
Acceptance Criteria:
- Categories: regulatory, defi, nft, layer1, layer2, exchange, security
- Multiple categories can be combined
- Category detected via keyword matching and source classification
US-4: Export to Multiple Formats (Important)
As a analyst I want to export news to JSON/CSV So that I can integrate with my research tools and create reports
Acceptance Criteria:
- Support table, JSON, CSV output formats
- Include all metadata (source, timestamp, category, relevance)
- Export to file with --output flag
US-5: Source Management (Nice-to-Have)
As a user I want to manage which sources are included in my feed So that I can customize quality and focus
Acceptance Criteria:
- List available sources with categories
- Enable/disable specific sources
- Save source preferences to config
---
Functional Requirements
REQ-1: Multi-Source Aggregation
- Fetch from RSS feeds (CoinDesk, CoinTelegraph, Decrypt, The Block, etc.)
- Parse feed entries with proper date handling
- Deduplicate across sources based on title similarity
REQ-2: Relevance Scoring
- Score articles based on keyword matches
- Boost scores for market-moving terms (exploit, hack, listing, partnership)
- Penalize promotional/sponsored content
REQ-3: Filtering System
- Time-based filtering (1h, 4h, 24h, 7d)
- Coin/token filtering with symbol matching
- Category filtering with multi-select
- Source filtering
REQ-4: Output Formatting
- Table format for terminal display
- JSON format for programmatic use
- CSV format for spreadsheet analysis
- Minimal format for quick scanning
REQ-5: Caching
- Cache feed responses to reduce API calls
- Configurable TTL (default 5 minutes)
- Cache invalidation on demand
---
Non-Goals
- Social Media Scraping: No Twitter/Discord/Telegram scraping (API access required, separate skill)
- Sentiment Analysis: Basic keyword detection only (sentiment-analyzer is separate skill)
- Alerts/Notifications: No push notifications (separate alerting system)
- Historical Archive: No long-term storage (scan only, not archive)
- Translation: No multi-language support (English sources only)
---
Success Metrics
| Metric | Target | Measurement |
|---|---|---|
| News fetch time | < 10s for 50 sources | Script execution time |
| Source coverage | 50+ curated sources | Count in config |
| Relevance accuracy | > 80% relevant in top 10 | Manual review |
| User activation | Triggered by relevant phrases | Plugin analytics |
| Export success | 100% valid JSON/CSV | Format validation |
---
UX Flow
User: "get latest crypto news"
│
├─► Parse intent (default: 24h, all categories)
│
├─► Fetch from RSS sources (parallel)
│
├─► Parse and normalize entries
│
├─► Score relevance
│
├─► Apply filters (time, category, coin)
│
├─► Sort by relevance or recency
│
├─► Format output (table/JSON/CSV)
│
└─► Display or export---
Integration Points
Dependencies
- None (standalone skill, no other skills required)
Consumers (Skills that can use this)
- market-sentiment-analyzer: Can consume news feed for sentiment analysis
- crypto-signal-generator: Can use news as signal input
- whale-alert-monitor: Can correlate whale moves with news
External APIs
- RSS feeds (no API key required)
- CryptoCompare News API (optional, for enhanced coverage)
---
Constraints & Assumptions
Constraints
- RSS feed availability (some sources may block or change URLs)
- Rate limiting on news APIs
- No real-time push (polling model only)
Assumptions
- User has internet connectivity
- English language sources are sufficient
- 5-minute cache TTL is acceptable for most use cases
---
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| RSS feed URL changes | Medium | Medium | Maintain source registry; auto-detection |
| Feed parsing errors | Medium | Low | Graceful degradation; skip malformed feeds |
| Network timeouts | Low | Low | Timeout per source; parallel fetching |
| Duplicate articles | Medium | Low | Title-based deduplication |
---
Examples
Example 1: Default News Scan
python news_aggregator.pyReturns top 20 news items from past 24h, sorted by relevance.
Example 2: Bitcoin-Specific News
python news_aggregator.py --coin BTC --period 4hReturns Bitcoin news from past 4 hours.
Example 3: DeFi Category with Export
python news_aggregator.py --category defi --format json --output defi_news.jsonExports DeFi news to JSON file.
---
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 guide to errors, causes, and solutions for the aggregating-crypto-news skill.
Error Categories
1. Network Errors
ConnectionError
Message: Connection failed or Failed to establish connection
Cause: No internet connectivity or DNS resolution failure.
Solution:
1. Check internet connection 2. Verify DNS settings 3. Try again after connection is restored 4. Cached data will be used if available
---
TimeoutError
Message: Request timed out for [source]
Cause: RSS feed server not responding within timeout window.
Solution:
1. Source is automatically skipped 2. Other sources continue to be fetched 3. Cached data used if available for that source 4. Check if source is down: visit URL in browser
Mitigation:
- Default timeout is 10 seconds per source
- Parallel fetching prevents one slow source from blocking others
---
HTTPError (4xx/5xx)
Message: HTTP 403 Forbidden or HTTP 500 Server Error
Cause: Source blocking requests or server issues.
Solution:
- 403: Source may be blocking automated access
- 404: RSS feed URL has changed
- 5xx: Server-side issue, try later
Note: Aggregator continues with available sources; failed sources are logged.
---
2. Parse Errors
FeedParseError
Message: Failed to parse feed from [source]
Cause: Malformed RSS/Atom XML that feedparser cannot process.
Solution:
1. Source is skipped 2. Check if source has valid RSS: validate at https://validator.w3.org/feed/ 3. Report issue if feed URL has changed
---
MissingFieldError
Message: Article missing required field: title
Cause: Feed entry lacks required fields (title, link).
Solution:
1. Entry is skipped 2. Other entries from source continue to process 3. No user action needed
---
DateParseError
Message: Failed to parse date for article
Cause: Non-standard date format in feed entry.
Solution:
1. Article is included without date 2. Recency scoring disabled for that article 3. May appear out of order when sorted by recency
---
3. Filter Errors
InvalidCoinSymbol
Message: Unknown coin symbol: [symbol]
Cause: User provided unrecognized coin symbol.
Solution:
1. Check spelling (case-insensitive: btc, BTC, Btc all work) 2. Use common symbols: BTC, ETH, SOL, BNB, XRP, ADA, DOGE, etc. 3. Filter will attempt to match anyway against article text
Note: This is a warning, not a fatal error. Filter proceeds.
---
InvalidCategory
Message: Invalid category: [category]
Cause: User provided category not in allowed list.
Solution: Use one of: market, defi, nft, regulatory, layer1, layer2, exchange, security
---
NoResultsFound
Message: No articles found matching your criteria
Cause: Filters are too restrictive for current news.
Solution:
1. Relax time window: --period 24h instead of --period 1h 2. Remove coin filter: drop --coin option 3. Lower score threshold: --min-score 0 4. Try different category or remove category filter
Suggested Relaxations:
# If no results with strict filters:
python news_aggregator.py --coin BTC --period 1h --min-score 50
# Try relaxing step by step:
python news_aggregator.py --coin BTC --period 4h # Longer window
python news_aggregator.py --period 1h --min-score 0 # Lower threshold
python news_aggregator.py --period 4h # Both relaxed---
4. Export Errors
FileWriteError
Message: Failed to write to output file: [path]
Cause: Cannot write to specified path.
Solution:
1. Check directory exists 2. Check write permissions 3. Ensure disk space available 4. Use absolute path if relative fails
---
InvalidFormat
Message: Invalid output format: [format]
Cause: Unrecognized format option.
Solution: Use one of: table, json, csv
---
5. Dependency Errors
ImportError: feedparser
Message: Error: feedparser library required
Cause: feedparser package not installed.
Solution:
pip install feedparser---
ImportError: requests
Message: Error: requests library required
Cause: requests package not installed.
Solution:
pip install requests---
Error Handling Strategy
Graceful Degradation
Full Success (all sources fetched)
│
▼
Partial Success (some sources failed)
│ → Continue with available data
│ → Log warnings for failed sources
│
▼
Cached Fallback (network issues)
│ → Return cached data
│ → Warn about stale data
│
▼
Complete Failure (no data available)
│ → Clear error message
│ → Suggest remediation stepsBehavior by Error Type
| Error Type | Behavior | User Impact |
|---|---|---|
| Network timeout | Skip source, continue | Partial results |
| Parse error | Skip entry, continue | Missing some articles |
| Invalid filter | Warn, proceed with partial filter | Approximate results |
| Export error | Fail with clear message | Must fix path/permissions |
| Missing dependency | Fail with install command | Must install package |
---
Debugging
Enable Verbose Mode
python news_aggregator.py --verboseShows:
- Which sources are being fetched
- Cache hits/misses
- Parse results per source
- Filter statistics
- Timing information
Check Individual Source
# Validate RSS feed
curl -s "https://www.coindesk.com/arc/outboundfeeds/rss/" | head -50
# Or use a feed validator
# https://validator.w3.org/feed/check.cgiTest with Single Source
Edit config/sources.yaml to include only one source for testing.
---
Error Codes
| Code | Category | Meaning |
|---|---|---|
| 0 | Success | Completed without errors |
| 1 | General | Unknown error or argument error |
| 2 | Dependency | Required library not installed |
| 3 | Network | All sources failed |
| 4 | Filter | No results after filtering |
| 5 | Export | File write error |
---
Reporting Issues
When reporting issues, include:
1. Command executed:
python news_aggregator.py --verbose [your options]2. Full error output (with --verbose)
3. Environment:
python --version
pip list | grep -E "feedparser|requests"4. Network test:
curl -I https://www.coindesk.com/arc/outboundfeeds/rss/--- [Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)
Usage Examples
Comprehensive examples for the aggregating-crypto-news skill.
Quick Start Examples
Example 1: Default News Scan
The simplest use case - get the latest crypto news:
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.pyOutput:
==============================================================================
CRYPTO NEWS AGGREGATOR Updated: 2026-01-14 15:30
==============================================================================
TOP CRYPTO NEWS (24h)
------------------------------------------------------------------------------
Rank Source Title Age Score
------------------------------------------------------------------------------
1 CoinDesk Bitcoin Breaks $100K, Sets New ATH 2h 95.0
2 The Block SEC Approves Spot ETH ETF Applications 4h 92.5
3 Decrypt Solana DeFi TVL Surges Past $15 Billion 6h 78.3
4 CoinTelegraph Binance Announces Major Listing Event 8h 75.0
5 Blockworks Arbitrum DAO Passes $50M Grant Proposal 12h 68.5
------------------------------------------------------------------------------
Summary: 20 articles shown | Scanned: 12 sources | Matched: 156
==============================================================================---
Example 2: Bitcoin-Specific News
Filter for Bitcoin-related news only:
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --coin BTCOnly shows articles mentioning Bitcoin, BTC, or related terms.
---
Example 3: Multiple Coins
Track news for your portfolio:
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --coins BTC,ETH,SOLShows articles mentioning any of the specified coins.
---
Example 4: Recent Breaking News
Get news from the past hour only:
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --period 1h --top 10Perfect for catching breaking news during active trading.
---
Category Filtering
Example 5: DeFi News
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --category defiShows DeFi protocol news, yield farming updates, DEX announcements.
---
Example 6: Regulatory News
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --category regulatorySEC, CFTC, congressional hearings, legal developments.
---
Example 7: Security Alerts
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --category security --period 4hHacks, exploits, vulnerabilities - time-sensitive security news.
---
Example 8: Exchange News
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --category exchangeListings, delistings, exchange announcements.
---
Example 9: Layer 2 Updates
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --category layer2Arbitrum, Optimism, Base, zkSync news.
---
Export Options
Example 10: JSON Export
For programmatic processing:
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --format json --output crypto_news.jsonOutput (crypto_news.json):
{
"articles": [
{
"rank": 1,
"title": "Bitcoin Breaks $100K, Sets New ATH",
"url": "https://coindesk.com/markets/2026/01/14/bitcoin-100k",
"source": "CoinDesk",
"published": "2026-01-14T13:30:00Z",
"age": "2h ago",
"category": "market",
"relevance_score": 95.0,
"coins_mentioned": ["BTC"],
"summary": "Bitcoin surpassed $100,000 for the first time..."
}
],
"meta": {
"period": "24h",
"sources_checked": 12,
"total_fetched": 500,
"after_dedup": 380,
"after_filter": 156,
"shown": 20,
"generated_at": "2026-01-14T15:30:00Z"
}
}---
Example 11: CSV Export
For spreadsheet analysis:
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --format csv --output crypto_news.csvOutput (crypto_news.csv):
rank,title,url,source,published,age,category,relevance_score,coins_mentioned
1,Bitcoin Breaks $100K Sets New ATH,https://coindesk.com/...,CoinDesk,2026-01-14T13:30:00,2h ago,market,95.0,BTC
2,SEC Approves Spot ETH ETF,https://theblock.co/...,The Block,2026-01-14T11:30:00,4h ago,regulatory,92.5,ETH---
Sorting Options
Example 12: Sort by Recency
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --sort-by recencyNewest articles first, regardless of relevance score.
---
Example 13: High-Score News Only
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --min-score 70Only shows articles with relevance score >= 70.
---
Combined Filters
Example 14: DeFi Breaking News
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--category defi \
--period 4h \
--min-score 50 \
--top 10DeFi news from past 4 hours with decent relevance.
---
Example 15: Bitcoin Regulatory Updates
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--coin BTC \
--category regulatory \
--period 7d \
--format json \
--output btc_regulatory.jsonBitcoin-related regulatory news from past week.
---
Example 16: Exchange Security Alerts
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--category security \
--period 1h \
--sort-by recencyRecent security-related news, perfect for immediate alerts.
---
Example 17: Solana Ecosystem News
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--coin SOL \
--period 24h \
--top 15All Solana-related news from past day.
---
Example 18: Multi-Coin DeFi
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--coins ETH,AAVE,UNI,MKR \
--category defi \
--period 24hDeFi news mentioning major DeFi tokens.
---
Verbose Mode
Example 19: Debug with Verbose
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --verboseOutput:
Loaded 12 sources
Fetching feeds...
Fetching: CoinDesk
Fetching: CoinTelegraph
Fetching: The Block
Cache hit: Decrypt
...
Fetched 11 feeds successfully
Parsed 487 total articles
After deduplication: 352 articles
After filtering: 156 articles
==============================================================================
...---
Shell Script Integration
Example 20: Scheduled Scan
#!/bin/bash
# morning_scan.sh - Run at market open
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR=~/crypto_news
mkdir -p $OUTPUT_DIR
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--period 4h \
--format json \
--output "$OUTPUT_DIR/news_$TIMESTAMP.json"
echo "Scan complete: $OUTPUT_DIR/news_$TIMESTAMP.json"---
Example 21: Alert on Market-Moving News
#!/bin/bash
# Check for high-scoring news
RESULT=$(python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--period 1h \
--min-score 80 \
--format json)
COUNT=$(echo "$RESULT" | jq '.meta.shown')
if [ "$COUNT" -gt 0 ]; then
echo "ALERT: $COUNT high-importance articles found!"
echo "$RESULT" | jq '.articles[0].title'
fi---
Example 22: Daily Digest Email
#!/bin/bash
# daily_digest.sh - Generate daily news digest
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--period 24h \
--top 30 \
--format json \
--output /tmp/daily_digest.json
# Process with jq or send to email service
cat /tmp/daily_digest.json | jq '.articles[] | "\(.rank). \(.title) - \(.source)"'---
Practical Use Cases
Example 23: Pre-Trading Research
# Morning routine before trading
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--period 12h \
--min-score 60 \
--top 15
# Check specific coins in watchlist
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--coins BTC,ETH,SOL,ARB \
--period 12h---
Example 24: Security Monitoring
# Run every hour via cron
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--category security \
--period 1h \
--sort-by recency \
--format json \
--output /var/log/crypto_security.json---
Example 25: Research Report Data
# Generate data for weekly report
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py \
--period 7d \
--top 100 \
--format csv \
--output weekly_news.csv---
Best Practices
1. Start with defaults: Run without filters first to see what's available 2. Use verbose mode: Add --verbose when debugging or learning 3. Cache is your friend: Re-running within 5 minutes uses cached data 4. Combine filters carefully: Too many filters = no results 5. Export for analysis: JSON/CSV exports enable deeper processing 6. Schedule regular scans: Automate morning/evening news checks 7. Monitor security category: Set up alerts for security news 8. Check multiple timeframes: 1h for breaking, 24h for context
--- [Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)
Aggregating Crypto News - Implementation Reference
Command-Line Options
| Option | Description | Default |
|---|---|---|
--coin | Filter by coin symbol (BTC, ETH, etc.) | None |
--coins | Filter by multiple coins (comma-separated) | None |
--category | Filter by category | None |
--period | Time window (1h, 4h, 24h, 7d) | 24h |
--top | Number of results to return | 20 |
--min-score | Minimum relevance score | 0 |
--format | Output format (table, json, csv) | table |
--output | Output file path | stdout |
--sort-by | Sort by (relevance, recency) | relevance |
--verbose | Enable verbose output | false |
Categories Available
market: General market news, price movementsdefi: DeFi protocols, yield farming, DEXesnft: NFT projects, marketplaces, collectionsregulatory: Government, SEC, legal developmentslayer1: L1 blockchain news (Ethereum, Solana, etc.)layer2: L2 scaling solutions (Arbitrum, Optimism, etc.)exchange: Exchange news, listings, delistingssecurity: Hacks, exploits, vulnerabilities
JSON Output Format
{
"articles": [
{
"rank": 1,
"title": "Bitcoin Breaks $100K ATH",
"url": "https://coindesk.com/...",
"source": "CoinDesk",
"published": "2026-01-14T13:30:00Z",
"age": "2h ago",
"category": "market",
"relevance_score": 95.0,
"coins_mentioned": ["BTC"]
}
],
"meta": {
"period": "24h",
"sources_checked": 50,
"total_articles": 187,
"shown": 20
}
}Advanced Filtering Examples
# Multiple filters combined
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --coin ETH --category defi --period 24h --top 15
# High-relevance news only
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --min-score 70 --top 10
# Multiple coins
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --coins BTC,ETH,SOL
# Export to JSON file
python ${CLAUDE_SKILL_DIR}/scripts/news_aggregator.py --format json --output crypto_news.json--- [Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)
#!/usr/bin/env python3
"""
Feed Fetcher - Parallel RSS Feed Fetching
Fetch multiple RSS feeds in parallel with timeout handling and caching.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import sys
import hashlib
import json
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Dict, List, Any, Optional
from datetime import datetime, timedelta
try:
import requests
except ImportError:
print("Error: requests library required. Install with: pip install requests", file=sys.stderr)
sys.exit(1)
class FeedFetcher:
"""Fetch multiple RSS feeds in parallel with caching."""
def __init__(
self,
timeout: int = 10,
max_workers: int = 10,
cache_ttl: int = 300, # 5 minutes
verbose: bool = False,
):
"""
Initialize feed fetcher.
Args:
timeout: Request timeout in seconds
max_workers: Max parallel requests
cache_ttl: Cache time-to-live in seconds
verbose: Enable verbose logging
"""
self.timeout = timeout
self.max_workers = max_workers
self.cache_ttl = cache_ttl
self.verbose = verbose
self.cache_dir = Path(__file__).parent.parent / "data" / "cache"
self.cache_dir.mkdir(parents=True, exist_ok=True)
def fetch_all(self, sources: List[Dict[str, Any]]) -> Dict[str, str]:
"""
Fetch all RSS feeds in parallel.
Args:
sources: List of source dictionaries with 'name' and 'url'
Returns:
Dictionary mapping source name to feed content
"""
results = {}
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
future_to_source = {executor.submit(self._fetch_one, source): source for source in sources}
for future in as_completed(future_to_source):
source = future_to_source[future]
try:
content = future.result()
if content:
results[source["name"]] = content
except Exception as e:
if self.verbose:
print(f"Error fetching {source['name']}: {e}", file=sys.stderr)
return results
def _fetch_one(self, source: Dict[str, Any]) -> Optional[str]:
"""
Fetch a single RSS feed.
Args:
source: Source dictionary with 'name' and 'url'
Returns:
Feed content string or None if failed
"""
name = source.get("name", "unknown")
url = source.get("url", "")
if not url:
return None
# Check cache first
cached = self._get_cached(url)
if cached:
if self.verbose:
print(f" Cache hit: {name}", file=sys.stderr)
return cached
# Fetch from network
try:
if self.verbose:
print(f" Fetching: {name}", file=sys.stderr)
headers = {
"User-Agent": "CryptoNewsAggregator/2.0 (RSS Feed Reader)",
"Accept": "application/rss+xml, application/xml, text/xml, */*",
}
response = requests.get(url, timeout=self.timeout, headers=headers)
response.raise_for_status()
content = response.text
# Cache the response
self._set_cached(url, content)
return content
except requests.exceptions.Timeout:
if self.verbose:
print(f" Timeout: {name}", file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
if self.verbose:
print(f" Error: {name} - {e}", file=sys.stderr)
return None
def _get_cache_path(self, url: str) -> Path:
"""Get cache file path for URL."""
url_hash = hashlib.md5(url.encode()).hexdigest()
return self.cache_dir / f"{url_hash}.json"
def _get_cached(self, url: str) -> Optional[str]:
"""Get cached content if valid."""
cache_path = self._get_cache_path(url)
if not cache_path.exists():
return None
try:
with open(cache_path, "r") as f:
cached = json.load(f)
# Check if expired
cached_time = datetime.fromisoformat(cached.get("timestamp", ""))
if datetime.utcnow() - cached_time > timedelta(seconds=self.cache_ttl):
return None
return cached.get("content")
except Exception:
return None
def _set_cached(self, url: str, content: str) -> None:
"""Cache content for URL."""
cache_path = self._get_cache_path(url)
try:
with open(cache_path, "w") as f:
json.dump({"url": url, "timestamp": datetime.utcnow().isoformat(), "content": content}, f)
except Exception:
pass
def clear_cache(self) -> int:
"""Clear all cached feeds. Returns count of files deleted."""
count = 0
for cache_file in self.cache_dir.glob("*.json"):
try:
cache_file.unlink()
count += 1
except Exception:
pass
return count
#!/usr/bin/env python3
"""
Feed Parser - RSS Feed Parsing and Normalization
Parse RSS/Atom feeds and normalize entries to common schema.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import sys
import re
import html
from datetime import datetime
from typing import List, Dict, Any, Optional
from difflib import SequenceMatcher
try:
import feedparser
except ImportError:
print("Error: feedparser library required. Install with: pip install feedparser", file=sys.stderr)
sys.exit(1)
class FeedParser:
"""Parse RSS/Atom feeds and normalize to common schema."""
def __init__(self):
"""Initialize feed parser."""
self.html_tag_pattern = re.compile(r"<[^>]+>")
def parse_feed(
self, content: str, source_name: str = "Unknown", source_category: str = "market", source_quality: int = 5
) -> List[Dict[str, Any]]:
"""
Parse RSS/Atom feed content into article list.
Args:
content: Raw feed XML content
source_name: Name of the source
source_category: Default category for this source
source_quality: Quality score (1-10)
Returns:
List of normalized article dictionaries
"""
articles = []
try:
feed = feedparser.parse(content)
for entry in feed.entries:
article = self._parse_entry(
entry, source_name=source_name, source_category=source_category, source_quality=source_quality
)
if article:
articles.append(article)
except Exception:
pass
return articles
def _parse_entry(
self, entry: Any, source_name: str, source_category: str, source_quality: int
) -> Optional[Dict[str, Any]]:
"""
Parse a single feed entry.
Args:
entry: feedparser entry object
source_name: Name of the source
source_category: Default category
source_quality: Quality score
Returns:
Normalized article dictionary or None if invalid
"""
# Extract title
title = entry.get("title", "")
if not title:
return None
title = self._clean_text(title)
# Extract URL
url = entry.get("link", "")
if not url:
return None
# Extract summary
summary = ""
if "summary" in entry:
summary = self._clean_text(entry.summary)
elif "description" in entry:
summary = self._clean_text(entry.description)
elif "content" in entry and entry.content:
summary = self._clean_text(entry.content[0].get("value", ""))
# Truncate summary if too long
if len(summary) > 500:
summary = summary[:500] + "..."
# Extract published date
published = self._parse_date(entry)
# Detect coins mentioned
coins_mentioned = self._detect_coins(f"{title} {summary}")
# Detect category from content
category = self._detect_category(f"{title} {summary}", source_category)
return {
"title": title,
"url": url,
"source": source_name,
"source_quality": source_quality,
"published": published,
"summary": summary,
"category": category,
"coins_mentioned": coins_mentioned,
}
def _clean_text(self, text: str) -> str:
"""Remove HTML tags and clean whitespace."""
if not text:
return ""
# Decode HTML entities
text = html.unescape(text)
# Remove HTML tags
text = self.html_tag_pattern.sub("", text)
# Normalize whitespace
text = " ".join(text.split())
return text.strip()
def _parse_date(self, entry: Any) -> Optional[datetime]:
"""Parse published date from entry."""
# Try published_parsed first
if hasattr(entry, "published_parsed") and entry.published_parsed:
try:
return datetime(*entry.published_parsed[:6])
except Exception:
pass
# Try updated_parsed
if hasattr(entry, "updated_parsed") and entry.updated_parsed:
try:
return datetime(*entry.updated_parsed[:6])
except Exception:
pass
# Try parsing string dates
for field in ["published", "updated", "date"]:
date_str = entry.get(field)
if date_str:
for fmt in [
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S%z",
"%a, %d %b %Y %H:%M:%S %z",
"%a, %d %b %Y %H:%M:%S GMT",
"%Y-%m-%d %H:%M:%S",
]:
try:
return datetime.strptime(date_str[:24], fmt[:24])
except Exception:
continue
return None
def _detect_coins(self, text: str) -> List[str]:
"""Detect cryptocurrency symbols mentioned in text."""
text_upper = text.upper()
# Common coin patterns
coins = []
coin_patterns = [
("BTC", ["BITCOIN", "BTC"]),
("ETH", ["ETHEREUM", "ETH", "ETHER"]),
("SOL", ["SOLANA", "SOL"]),
("BNB", ["BINANCE", "BNB"]),
("XRP", ["RIPPLE", "XRP"]),
("ADA", ["CARDANO", "ADA"]),
("DOGE", ["DOGECOIN", "DOGE"]),
("DOT", ["POLKADOT", "DOT"]),
("AVAX", ["AVALANCHE", "AVAX"]),
("LINK", ["CHAINLINK", "LINK"]),
("MATIC", ["POLYGON", "MATIC"]),
("UNI", ["UNISWAP", "UNI"]),
("AAVE", ["AAVE"]),
("MKR", ["MAKER", "MKR"]),
("CRV", ["CURVE", "CRV"]),
("LDO", ["LIDO", "LDO"]),
("ARB", ["ARBITRUM", "ARB"]),
("OP", ["OPTIMISM", " OP "]),
]
for symbol, patterns in coin_patterns:
for pattern in patterns:
if pattern in text_upper:
if symbol not in coins:
coins.append(symbol)
break
return coins
def _detect_category(self, text: str, default: str) -> str:
"""Detect news category from content."""
text_lower = text.lower()
category_keywords = {
"defi": ["defi", "yield farming", "liquidity pool", "dex", "lending protocol", "aave", "uniswap", "curve"],
"nft": ["nft", "opensea", "blur", "collection", "mint", "pfp", "digital art"],
"regulatory": ["sec", "cftc", "regulation", "congress", "senator", "compliance", "lawsuit", "settlement"],
"exchange": ["binance", "coinbase", "kraken", "listing", "delist", "exchange", "trading pair"],
"security": ["hack", "exploit", "vulnerability", "audit", "breach", "stolen", "attack", "rug pull"],
"layer2": ["arbitrum", "optimism", "polygon", "zksync", "base", "layer 2", "l2", "rollup"],
}
for category, keywords in category_keywords.items():
if any(kw in text_lower for kw in keywords):
return category
return default
def deduplicate(self, articles: List[Dict[str, Any]], threshold: float = 0.8) -> List[Dict[str, Any]]:
"""
Remove duplicate articles based on title similarity.
Args:
articles: List of article dictionaries
threshold: Similarity threshold (0-1) for considering duplicates
Returns:
Deduplicated list of articles
"""
if not articles:
return []
unique = []
seen_titles = []
for article in articles:
title = article.get("title", "").lower()
# Check similarity against all seen titles
is_duplicate = False
for seen in seen_titles:
similarity = SequenceMatcher(None, title, seen).ratio()
if similarity >= threshold:
is_duplicate = True
break
if not is_duplicate:
unique.append(article)
seen_titles.append(title)
return unique
#!/usr/bin/env python3
"""
News Formatters - Output Formatting for Crypto News
Format news results for table, JSON, and CSV output.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import json
import csv
import io
from datetime import datetime, timedelta
from typing import Dict, Any
class NewsFormatter:
"""Format crypto news results for various output types."""
def format(self, result: Dict[str, Any], format_type: str = "table") -> str:
"""
Format news results.
Args:
result: Dictionary with articles and meta
format_type: Output format (table, json, csv)
Returns:
Formatted output string
"""
if format_type == "json":
return self._format_json(result)
elif format_type == "csv":
return self._format_csv(result)
else:
return self._format_table(result)
def _format_table(self, result: Dict[str, Any]) -> str:
"""Format results as aligned table."""
lines = []
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
meta = result.get("meta", {})
# Header
lines.append("=" * 90)
lines.append(f" CRYPTO NEWS AGGREGATOR{' ' * 42}Updated: {timestamp}")
lines.append("=" * 90)
articles = result.get("articles", [])
if articles:
lines.append("")
period = meta.get("period", "24h")
lines.append(f" TOP CRYPTO NEWS ({period})")
lines.append("-" * 90)
lines.append(f" {'Rank':<6}{'Source':<16}{'Title':<44}{'Age':<10}{'Score':>8}")
lines.append("-" * 90)
for article in articles:
rank = article.get("rank", "-")
source = article.get("source", "Unknown")[:14]
title = article.get("title", "")[:42]
age = self._format_age(article.get("published"))
score = f"{article.get('relevance_score', 0):.1f}"
lines.append(f" {rank:<6}{source:<16}{title:<44}{age:<10}{score:>8}")
lines.append("-" * 90)
else:
lines.append("")
lines.append(" No articles found matching your criteria.")
lines.append("")
# Summary
lines.append("")
shown = meta.get("shown", 0)
sources = meta.get("sources_checked", 0)
total = meta.get("after_filter", 0)
lines.append(f" Summary: {shown} articles shown | Scanned: {sources} sources | Matched: {total}")
# Filters applied
filters = meta.get("filters", {})
filter_parts = []
if filters.get("coins"):
filter_parts.append(f"coins={','.join(filters['coins'])}")
if filters.get("category"):
filter_parts.append(f"category={filters['category']}")
if filters.get("min_score"):
filter_parts.append(f"min_score={filters['min_score']}")
if filter_parts:
lines.append(f" Filters: {', '.join(filter_parts)}")
lines.append("=" * 90)
return "\n".join(lines)
def _format_json(self, result: Dict[str, Any]) -> str:
"""Format results as JSON."""
# Prepare articles for JSON serialization
output = {"articles": [], "meta": result.get("meta", {})}
for article in result.get("articles", []):
serializable = {
"rank": article.get("rank"),
"title": article.get("title"),
"url": article.get("url"),
"source": article.get("source"),
"published": None,
"age": self._format_age(article.get("published")),
"category": article.get("category"),
"relevance_score": article.get("relevance_score"),
"coins_mentioned": article.get("coins_mentioned", []),
"summary": article.get("summary", "")[:200],
}
# Convert datetime to ISO format
pub = article.get("published")
if pub:
serializable["published"] = pub.isoformat() + "Z"
output["articles"].append(serializable)
# Add timestamp
output["meta"]["generated_at"] = datetime.utcnow().isoformat() + "Z"
return json.dumps(output, indent=2)
def _format_csv(self, result: Dict[str, Any]) -> str:
"""Format results as CSV."""
output = io.StringIO()
fieldnames = [
"rank",
"title",
"url",
"source",
"published",
"age",
"category",
"relevance_score",
"coins_mentioned",
]
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
for article in result.get("articles", []):
pub = article.get("published")
published_str = pub.isoformat() if pub else ""
writer.writerow(
{
"rank": article.get("rank", ""),
"title": article.get("title", ""),
"url": article.get("url", ""),
"source": article.get("source", ""),
"published": published_str,
"age": self._format_age(pub),
"category": article.get("category", ""),
"relevance_score": article.get("relevance_score", ""),
"coins_mentioned": ",".join(article.get("coins_mentioned", [])),
}
)
return output.getvalue()
def _format_age(self, published: datetime) -> str:
"""Format datetime as human-readable age."""
if not published:
return "unknown"
now = datetime.utcnow()
age = now - published
if age < timedelta(minutes=1):
return "just now"
elif age < timedelta(hours=1):
minutes = int(age.total_seconds() / 60)
return f"{minutes}m ago"
elif age < timedelta(hours=24):
hours = int(age.total_seconds() / 3600)
return f"{hours}h ago"
elif age < timedelta(days=7):
days = int(age.total_seconds() / 86400)
return f"{days}d ago"
else:
return published.strftime("%Y-%m-%d")
def format_article_detail(article: Dict[str, Any]) -> str:
"""Format a single article with full details."""
lines = []
lines.append(f"Title: {article.get('title', 'Unknown')}")
lines.append(f"Source: {article.get('source', 'Unknown')}")
lines.append(f"URL: {article.get('url', '')}")
pub = article.get("published")
if pub:
lines.append(f"Published: {pub.strftime('%Y-%m-%d %H:%M:%S UTC')}")
lines.append(f"Category: {article.get('category', 'Unknown')}")
lines.append(f"Score: {article.get('relevance_score', 0):.1f}")
coins = article.get("coins_mentioned", [])
if coins:
lines.append(f"Coins: {', '.join(coins)}")
summary = article.get("summary", "")
if summary:
lines.append("")
lines.append("Summary:")
lines.append(summary)
return "\n".join(lines)
#!/usr/bin/env python3
"""
Crypto News Aggregator - Main CLI Entry Point
Aggregate breaking cryptocurrency news from multiple RSS sources.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import argparse
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Dict, Any
# Add scripts directory to path for local imports
SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(SCRIPT_DIR))
from feed_fetcher import FeedFetcher
from feed_parser import FeedParser
from scorer import NewsScorer
from formatters import NewsFormatter
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Aggregate breaking cryptocurrency news from multiple sources",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s # Default scan (24h, top 20)
%(prog)s --coin BTC --period 4h # Bitcoin news, past 4 hours
%(prog)s --category defi # DeFi category only
%(prog)s --format json --output news.json # Export to JSON
""",
)
# Filtering options
parser.add_argument("--coin", type=str, help="Filter by single coin symbol (e.g., BTC, ETH)")
parser.add_argument("--coins", type=str, help="Filter by multiple coins (comma-separated, e.g., BTC,ETH,SOL)")
parser.add_argument(
"--category",
type=str,
choices=["market", "defi", "nft", "regulatory", "layer1", "layer2", "exchange", "security"],
help="Filter by news category",
)
parser.add_argument(
"--period",
type=str,
choices=["1h", "4h", "24h", "7d"],
default="24h",
help="Time window for news (default: 24h)",
)
# Output options
parser.add_argument("--top", type=int, default=20, help="Number of results to return (default: 20)")
parser.add_argument("--min-score", type=float, default=0, help="Minimum relevance score (default: 0)")
parser.add_argument(
"--sort-by",
type=str,
choices=["relevance", "recency"],
default="relevance",
help="Sort results by (default: relevance)",
)
# Format and export
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 get_time_threshold(period: str) -> datetime:
"""Convert period string to datetime threshold."""
now = datetime.utcnow()
if period == "1h":
return now - timedelta(hours=1)
elif period == "4h":
return now - timedelta(hours=4)
elif period == "24h":
return now - timedelta(hours=24)
elif period == "7d":
return now - timedelta(days=7)
else:
return now - timedelta(hours=24)
def load_sources() -> List[Dict[str, Any]]:
"""Load RSS sources from config."""
config_path = SCRIPT_DIR.parent / "config" / "sources.yaml"
if config_path.exists():
try:
import yaml
with open(config_path, "r") as f:
config = yaml.safe_load(f)
return config.get("sources", [])
except ImportError:
pass
except Exception:
pass
# Default sources if config not available
return [
{
"name": "CoinDesk",
"url": "https://www.coindesk.com/arc/outboundfeeds/rss/",
"category": "market",
"quality": 9,
},
{"name": "CoinTelegraph", "url": "https://cointelegraph.com/rss", "category": "market", "quality": 8},
{"name": "The Block", "url": "https://www.theblock.co/rss.xml", "category": "market", "quality": 9},
{"name": "Decrypt", "url": "https://decrypt.co/feed", "category": "market", "quality": 8},
{"name": "Bitcoin Magazine", "url": "https://bitcoinmagazine.com/feed", "category": "market", "quality": 8},
{"name": "CryptoSlate", "url": "https://cryptoslate.com/feed/", "category": "market", "quality": 7},
{"name": "NewsBTC", "url": "https://www.newsbtc.com/feed/", "category": "market", "quality": 6},
{"name": "CryptoPotato", "url": "https://cryptopotato.com/feed/", "category": "market", "quality": 6},
{"name": "U.Today", "url": "https://u.today/rss", "category": "market", "quality": 6},
{"name": "Blockworks", "url": "https://blockworks.co/feed/", "category": "market", "quality": 8},
{"name": "DeFi Pulse", "url": "https://defipulse.com/blog/feed/", "category": "defi", "quality": 8},
{"name": "DL News", "url": "https://www.dlnews.com/feed/", "category": "market", "quality": 7},
]
def main() -> None:
"""Main entry point."""
args = parse_args()
# Parse coin filters
coins = []
if args.coin:
coins = [args.coin.upper()]
elif args.coins:
coins = [c.strip().upper() for c in args.coins.split(",")]
# Get time threshold
time_threshold = get_time_threshold(args.period)
# Load sources
sources = load_sources()
if args.verbose:
print(f"Loaded {len(sources)} sources", file=sys.stderr)
# Initialize components
fetcher = FeedFetcher(timeout=10, verbose=args.verbose)
parser = FeedParser()
scorer = NewsScorer()
formatter = NewsFormatter()
# Fetch feeds
if args.verbose:
print("Fetching feeds...", file=sys.stderr)
raw_feeds = fetcher.fetch_all(sources)
if args.verbose:
print(f"Fetched {len(raw_feeds)} feeds successfully", file=sys.stderr)
# Parse feeds into articles
all_articles = []
for source_name, feed_content in raw_feeds.items():
source_info = next((s for s in sources if s["name"] == source_name), {})
articles = parser.parse_feed(
feed_content,
source_name=source_name,
source_category=source_info.get("category", "market"),
source_quality=source_info.get("quality", 5),
)
all_articles.extend(articles)
if args.verbose:
print(f"Parsed {len(all_articles)} total articles", file=sys.stderr)
# Deduplicate
unique_articles = parser.deduplicate(all_articles)
if args.verbose:
print(f"After deduplication: {len(unique_articles)} articles", file=sys.stderr)
# Score articles
for article in unique_articles:
article["relevance_score"] = scorer.calculate_score(
title=article.get("title", ""),
summary=article.get("summary", ""),
source_quality=article.get("source_quality", 5),
published=article.get("published"),
)
# Apply filters
filtered = []
for article in unique_articles:
# Time filter
pub_date = article.get("published")
if pub_date and pub_date < time_threshold:
continue
# Coin filter
if coins:
text = f"{article.get('title', '')} {article.get('summary', '')}".upper()
if not any(coin in text for coin in coins):
continue
# Category filter
if args.category and article.get("category") != args.category:
# Also check for category keywords in content
category_keywords = {
"defi": ["defi", "yield", "lending", "dex", "liquidity"],
"nft": ["nft", "opensea", "blur", "collection", "mint"],
"regulatory": ["sec", "regulation", "congress", "law", "compliance"],
"exchange": ["binance", "coinbase", "kraken", "listing", "delist"],
"security": ["hack", "exploit", "vulnerability", "audit", "breach"],
"layer1": ["ethereum", "solana", "bitcoin", "cardano", "avalanche"],
"layer2": ["arbitrum", "optimism", "polygon", "zksync", "base"],
}
keywords = category_keywords.get(args.category, [])
text = f"{article.get('title', '')} {article.get('summary', '')}".lower()
if not any(kw in text for kw in keywords):
continue
# Score filter
if article.get("relevance_score", 0) < args.min_score:
continue
filtered.append(article)
if args.verbose:
print(f"After filtering: {len(filtered)} articles", file=sys.stderr)
# Sort
if args.sort_by == "relevance":
filtered.sort(key=lambda x: x.get("relevance_score", 0), reverse=True)
else:
filtered.sort(key=lambda x: x.get("published") or datetime.min, reverse=True)
# Limit results
result_articles = filtered[: args.top]
# Add ranks
for i, article in enumerate(result_articles, 1):
article["rank"] = i
# Prepare result
result = {
"articles": result_articles,
"meta": {
"period": args.period,
"sources_checked": len(sources),
"total_fetched": len(all_articles),
"after_dedup": len(unique_articles),
"after_filter": len(filtered),
"shown": len(result_articles),
"filters": {"coins": coins if coins else None, "category": args.category, "min_score": args.min_score},
},
}
# Format output
output = formatter.format(result, format_type=args.format)
# 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()
#!/usr/bin/env python3
"""
News Scorer - Relevance Scoring for Crypto News
Calculate relevance scores based on keywords, source quality, and recency.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
from datetime import datetime, timedelta
from typing import Optional, List, Dict
class NewsScorer:
"""Calculate relevance scores for crypto news articles."""
def __init__(self):
"""Initialize news scorer with keyword weights."""
# High-impact keywords (market-moving)
self.high_impact_keywords = {
"hack": 15,
"exploit": 15,
"breach": 12,
"stolen": 12,
"listing": 10,
"delist": 12,
"sec": 12,
"lawsuit": 10,
"settlement": 10,
"partnership": 8,
"acquisition": 10,
"merger": 10,
"etf": 12,
"approval": 10,
"rejection": 10,
"all-time high": 10,
"ath": 8,
"crash": 10,
"surge": 8,
"plunge": 8,
"rally": 8,
"dump": 8,
"pump": 8,
"airdrop": 10,
"fork": 10,
"upgrade": 8,
"mainnet": 8,
"launch": 8,
"bankruptcy": 12,
"insolvent": 12,
"freeze": 10,
"halting": 10,
}
# Medium-impact keywords
self.medium_impact_keywords = {
"bitcoin": 5,
"ethereum": 5,
"defi": 4,
"nft": 4,
"regulation": 6,
"institutional": 5,
"whale": 6,
"binance": 4,
"coinbase": 4,
"stablecoin": 5,
"usdt": 4,
"usdc": 4,
"layer 2": 4,
"rollup": 4,
"yield": 4,
"staking": 4,
"validator": 4,
"governance": 4,
"proposal": 4,
"vote": 4,
}
# Negative keywords (reduce score for promotional content)
self.negative_keywords = {
"sponsored": -10,
"partner content": -10,
"advertisement": -15,
"promotion": -5,
"giveaway": -5,
"free": -3,
"sign up": -3,
"discount": -5,
}
def calculate_score(
self, title: str, summary: str = "", source_quality: int = 5, published: Optional[datetime] = None
) -> float:
"""
Calculate relevance score for an article.
Args:
title: Article title
summary: Article summary/description
source_quality: Source quality rating (1-10)
published: Publication datetime
Returns:
Relevance score (0-100)
"""
text = f"{title} {summary}".lower()
score = 0.0
# Base score from source quality (0-30 points)
score += source_quality * 3
# Keyword scoring (0-50 points)
keyword_score = 0.0
# High-impact keywords
for keyword, weight in self.high_impact_keywords.items():
if keyword in text:
keyword_score += weight
# Medium-impact keywords
for keyword, weight in self.medium_impact_keywords.items():
if keyword in text:
keyword_score += weight
# Negative keywords
for keyword, weight in self.negative_keywords.items():
if keyword in text:
keyword_score += weight # weight is negative
# Cap keyword score
keyword_score = min(50, max(0, keyword_score))
score += keyword_score
# Recency bonus (0-20 points)
if published:
recency_score = self._calculate_recency_score(published)
score += recency_score
# Ensure score is in range
score = min(100, max(0, score))
return round(score, 1)
def _calculate_recency_score(self, published: datetime) -> float:
"""Calculate recency bonus based on publication time."""
now = datetime.utcnow()
age = now - published
if age < timedelta(hours=1):
return 20 # Very fresh
elif age < timedelta(hours=4):
return 15
elif age < timedelta(hours=12):
return 10
elif age < timedelta(hours=24):
return 5
elif age < timedelta(days=3):
return 2
else:
return 0
def explain_score(
self, title: str, summary: str = "", source_quality: int = 5, published: Optional[datetime] = None
) -> Dict[str, any]:
"""
Generate explanation of score components.
Args:
title: Article title
summary: Article summary
source_quality: Source quality rating
published: Publication datetime
Returns:
Dictionary with score breakdown
"""
text = f"{title} {summary}".lower()
explanation = {
"base_score": source_quality * 3,
"source_quality": source_quality,
"keywords_found": [],
"negative_keywords_found": [],
"keyword_score": 0,
"recency_score": 0,
"final_score": 0,
}
# Find matching keywords
keyword_score = 0.0
for keyword, weight in self.high_impact_keywords.items():
if keyword in text:
explanation["keywords_found"].append({"keyword": keyword, "weight": weight})
keyword_score += weight
for keyword, weight in self.medium_impact_keywords.items():
if keyword in text:
explanation["keywords_found"].append({"keyword": keyword, "weight": weight})
keyword_score += weight
for keyword, weight in self.negative_keywords.items():
if keyword in text:
explanation["negative_keywords_found"].append({"keyword": keyword, "weight": weight})
keyword_score += weight
explanation["keyword_score"] = min(50, max(0, keyword_score))
if published:
explanation["recency_score"] = self._calculate_recency_score(published)
explanation["final_score"] = self.calculate_score(title, summary, source_quality, published)
return explanation
def get_market_moving_keywords() -> List[str]:
"""Return list of market-moving keywords for highlighting."""
return [
"hack",
"exploit",
"breach",
"stolen",
"listing",
"delist",
"sec",
"lawsuit",
"etf",
"approval",
"rejection",
"all-time high",
"ath",
"crash",
"surge",
"airdrop",
"fork",
"upgrade",
"mainnet",
"bankruptcy",
"insolvent",
"freeze",
"halting",
]