
Tracking Crypto Prices
- 337 installs
- 2.6k repo stars
- Updated August 5, 2026
- jeremylongshore/claude-code-plugins-plus-skills
tracking-crypto-prices is a developer skill for watching token and pair prices, setting alert thresholds, and summarizing market moves for developers building dashboards, bots, or portfolio monitoring workflows.
About
tracking-crypto-prices is an agent skill for watching cryptocurrency token and trading pair prices, configuring alert thresholds, and summarizing market moves for software workflows. Developers reach for tracking-crypto-prices when building dashboards, trading bots, notification pipelines, or portfolio monitoring tools that need structured price watchlists and move summaries rather than manual chart checking. The skill supports workflows where programmatic alerts fire when tokens cross configured thresholds and aggregated summaries feed into downstream automation. It fits backend and full-stack engineers integrating live market data into applications, cron jobs, or agent-driven monitoring systems. Use it when price observation, threshold alerts, and concise market summaries must plug into existing developer tooling.
- Live price polling patterns
- Alert threshold design
- Multi-asset watchlists
- Summary snippets for dashboards
- API integration guidance
Tracking Crypto Prices by the numbers
- 337 all-time installs (skills.sh)
- Ranked #325 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 tracking-crypto-pricesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 337 |
|---|---|
| repo stars | ★ 2.6k |
| Last updated | August 5, 2026 |
| Repository | jeremylongshore/claude-code-plugins-plus-skills ↗ |
How do you track crypto prices with alerts in code?
Watch token and pair prices, set alert thresholds, and summarize market moves for dashboards, bots, or personal portfolio monitoring workflows.
Who is it for?
Developers building crypto dashboards, bots, or portfolio monitors that need programmatic price watches and alert thresholds.
Skip if: Teams needing licensed financial advisory, exchange trading execution, or non-crypto market data integrations.
When should I use this skill?
A developer asks to track token prices, set crypto alert thresholds, or summarize market moves for a bot or dashboard.
What you get
Price watchlists, alert threshold configurations, and summarized market move reports for downstream automation
- Price watch configuration
- Alert threshold rules
- Market move summary output
Files
Tracking Crypto Prices
Contents
Overview | Prerequisites | Instructions | Output | Error Handling | Examples | Resources
Overview
Foundation skill providing real-time and historical cryptocurrency price data for 10,000+ coins. This is the data layer for the crypto plugin ecosystem -- 10+ other skills depend on it for price information.
Prerequisites
1. Install dependencies: pip install requests pandas yfinance 2. Optional: pip install python-dotenv for API key management 3. Optional: Get free API key from https://www.coingecko.com/en/api for higher rate limits 4. Add API key to ${CLAUDE_SKILL_DIR}/config/settings.yaml or set COINGECKO_API_KEY env var
Instructions
1. Check current prices for one or more symbols:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbols BTC,ETH,SOL2. Use watchlists to scan predefined groups (available: top10, defi, layer2, stablecoins, memecoins):
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist top10 # Top 10 by market cap
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist defi # DeFi tokens
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist layer2 # Layer 2 tokens3. Fetch historical data by period or custom date range:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --period 30d
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --period 90d --output csv
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol ETH --start 2024-01-01 --end 2024-12-31 # 2024 full year4. Configure settings by editing ${CLAUDE_SKILL_DIR}/config/settings.yaml to customize cache TTLs, default currency, and custom watchlists. See references/implementation.md for the full configuration reference.
Output
- Table (default): Symbol, price, 24h change, volume, market cap in formatted columns
- JSON (
--format json): Machine-readable with prices array and metadata - CSV (
--output csv): OHLCV historical data export to${CLAUDE_SKILL_DIR}/data/
See ${CLAUDE_SKILL_DIR}/references/implementation.md for detailed output format examples.
Error Handling
| Error | Cause | Solution |
|---|---|---|
Unknown symbol: XYZ | Invalid ticker | Check spelling, use --list to search |
Rate limit exceeded | Too many API calls | Wait 60s, or add API key for higher limits |
Network error | No internet | Check connection; cached data used automatically |
Cache stale | Data older than TTL | Shown with warning, refreshes on next call |
The skill auto-manages rate limits: cache first, exponential backoff, yfinance fallback, stale cache as last resort.
Examples
Quick price check:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC
# Output: BTC (Bitcoin) $97,234.56 USD +2.34% (24h) | Vol: $28.5B | MCap: $1.92TWatchlist scan:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist top10Historical export:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol ETH --period 90d --output csv
# Creates: ${CLAUDE_SKILL_DIR}/data/ETH_90d_[date].csvResources
${CLAUDE_SKILL_DIR}/references/implementation.md- Output formats, full config, integration guide, file map- CoinGecko API - Primary data source
- yfinance - Fallback for historical data
ARD: Market Price Tracker
Part of Tons of Skills by Intent Solutions | jeremylongshore.com
Version: 2.0.0 Author: Jeremy Longshore <jeremy@intentsolutions.io> Status: In Development Last Updated: 2025-01-14
---
Document Control
| Field | Value |
|---|---|
| Skill Name | tracking-crypto-prices |
| Architectural Pattern | Read-Process-Write with Caching |
| Complexity Level | Medium (4 steps) |
| API Integrations | 2 (CoinGecko, yfinance) |
| Token Budget | ~2,500 tokens / 5,000 max |
| Status | In Development |
| Owner | Jeremy Longshore |
---
1. Architectural Overview
1.1 Skill Purpose
One-Sentence Summary: Fetch, cache, and present cryptocurrency price data from multiple sources with standardized output for both human consumption and programmatic use by dependent skills.
Architectural Pattern: Read-Process-Write with Caching
Why This Pattern:
- Read: Fetch price data from external APIs
- Cache: Store recently fetched data to reduce API calls
- Process: Normalize data from different sources into standard format
- Write: Output to terminal (human) or files (programmatic)
This pattern suits the foundation skill nature - it must be fast, reliable, and provide a consistent interface for 10+ dependent skills.
1.2 High-Level Architecture Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ MARKET PRICE TRACKER │
│ Foundation Skill Architecture │
└─────────────────────────────────────────────────────────────────────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ USER INPUT │ │ CACHE │ │ DEPENDENT │
│ - Symbol │ │ LAYER │ │ SKILLS │
│ - Watchlist │ │ (30s TTL) │ │ (via import) │
│ - Period │ │ │ │ │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└──────────────────────────┼──────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Step 1: VALIDATE INPUT │
│ ├─ Parse symbol/name │
│ ├─ Resolve aliases (BTC → bitcoin) │
│ └─ Output: Normalized symbol list │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Step 2: CHECK CACHE │
│ ├─ Load cache from data/cache.json │
│ ├─ Check TTL (30s for spot, 1h for OHLCV) │
│ └─ Output: Cached data OR cache miss │
└─────────────────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ Cache Hit? │
├─────────────────────────────┤
│ YES: Skip to Step 4 │
│ NO: Continue to Step 3 │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Step 3: FETCH FROM API │
│ ├─ Primary: CoinGecko /simple/price │
│ ├─ Fallback: yfinance (if CoinGecko fails) │
│ ├─ Retry with exponential backoff │
│ └─ Output: Raw price data (JSON) │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Step 4: PROCESS & OUTPUT │
│ ├─ Normalize to standard schema │
│ ├─ Update cache │
│ ├─ Format for human (table) or machine │
│ └─ Output: Formatted response OR JSON file │
└─────────────────────────────────────────────┘1.3 Workflow Summary
Total Steps: 4
| Step | Action | Type | Dependencies | Output |
|---|---|---|---|---|
| 1 | Validate Input | Code | None | Normalized symbols |
| 2 | Check Cache | Code | data/cache.json | Cached data or miss |
| 3 | Fetch from API | API Call | CoinGecko/yfinance | Raw JSON |
| 4 | Process & Output | Code | Step 2 or 3 output | Formatted response |
---
2. Progressive Disclosure Strategy
2.1 Level 1: Frontmatter (Metadata)
What Goes Here: ONLY name and description
---
name: tracking-crypto-prices
description: |
Track real-time cryptocurrency prices across exchanges with historical data and alerts.
Provides price data infrastructure for dependent skills (portfolio, tax, DeFi, arbitrage).
Use when checking crypto prices, monitoring markets, or fetching historical data.
Trigger with phrases like "check price", "BTC price", "crypto prices", "price history",
"get quote for", "what's ETH trading at", or "show me the top 10 prices".
allowed-tools: Read, Write, Edit, Grep, Glob, Bash(python:*)
version: 2.0.0
author: Jeremy Longshore <jeremy@intentsolutions.io>
license: MIT
---Description Quality Target: 85%+ Character Count: 445 characters (within 500 limit for extended description)
2.2 Level 2: SKILL.md (Core Instructions)
Token Budget: ~2,500 tokens (target <500 lines)
Required Sections: 1. Overview (what + who uses this) 2. Prerequisites (pip install) 3. Instructions (4 numbered steps with code) 4. Output (example formats) 5. Configuration (settings.yaml reference) 6. Error Handling (common errors + solutions) 7. Composability (how to use with other skills) 8. Examples (3 concrete use cases)
2.3 Level 3: Resources (Extended Context)
scripts/ Directory:
scripts/
├── price_tracker.py # Main entry point (CLI)
├── api_client.py # CoinGecko/yfinance abstraction
├── cache_manager.py # Cache read/write/invalidation
└── formatters.py # Human-readable output formattingreferences/ Directory:
references/
├── errors.md # Common errors and solutions
├── examples.md # Extended use cases
└── implementation.md # Technical deep-diveconfig/ Directory:
config/
└── settings.yaml # API keys, cache duration, watchlists---
3. Tool Permission Strategy
3.1 Required Tools
Minimal Necessary Set:
Read- Load cache, read config filesWrite- Save cache, export CSV/JSONBash(python:*)- Execute Python scripts
3.2 Tool Usage Justification
| Tool | Why Needed | Usage Pattern |
|---|---|---|
| Read | Load cached prices, read settings | Steps 2, 4 |
| Write | Save cache, export historical data | Steps 3, 4 |
| Bash(python:*) | Execute price fetching scripts | All steps |
3.3 Tools Explicitly NOT Needed
Excluded Tools:
Edit- Scripts generate fresh output, no editing requiredWebFetch- Python scripts handle HTTP directly via requestsGrep- No code search needed for price queriesGlob- No file pattern matching needed
---
4. Directory Structure & File Organization
4.1 Complete Skill Structure
plugins/crypto/market-price-tracker/skills/tracking-crypto-prices/
├── PRD.md # Product Requirements Document
├── ARD.md # Architecture Requirements Document (this file)
├── SKILL.md # Core instructions (<500 lines)
│
├── scripts/ # Executable code
│ ├── price_tracker.py # Main CLI entry point
│ ├── api_client.py # API abstraction layer
│ ├── cache_manager.py # Caching logic
│ └── formatters.py # Output formatting
│
├── references/ # Documentation
│ ├── errors.md # Error handling guide
│ ├── examples.md # Extended examples
│ └── implementation.md # Technical details
│
├── config/ # Configuration
│ └── settings.yaml # User-configurable settings
│
└── data/ # Runtime data (gitignored)
├── cache.json # Price cache
└── *.csv # Exported historical data4.2 File Naming Conventions
Scripts: [noun]_[purpose].py
- ✅
price_tracker.py- Main price tracking logic - ✅
api_client.py- API communication - ✅
cache_manager.py- Cache operations
References: [purpose].md (lowercase)
- ✅
errors.md- Error documentation - ✅
examples.md- Usage examples
4.3 Path Referencing Standard
Always Use: ${CLAUDE_SKILL_DIR} for all file paths in SKILL.md
# Correct
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC
# Incorrect
python scripts/price_tracker.py --symbol BTC # Missing ${CLAUDE_SKILL_DIR}---
5. API Integration Architecture
5.1 External API Integrations
API 1: CoinGecko (Primary)
Purpose: Real-time price data for 10,000+ cryptocurrencies
Integration Details:
- Base URL:
https://api.coingecko.com/api/v3 - Authentication: Optional API key (header:
x-cg-demo-api-keyorx-cg-pro-api-key) - Rate Limits: 10-30 calls/minute (free), 500/minute (Pro)
- Response Format: JSON
Endpoints Used:
| Endpoint | Purpose | Rate Impact |
|---|---|---|
/simple/price | Current prices (batch) | 1 call per request |
/coins/{id} | Detailed coin data | 1 call per coin |
/coins/{id}/market_chart | Historical OHLCV | 1 call per coin/range |
/coins/markets | Top coins by market cap | 1 call per page |
Example Request:
import requests
def get_prices(symbols: list, vs_currency: str = "usd") -> dict:
"""Fetch current prices for multiple symbols."""
ids = ",".join(symbols)
url = f"https://api.coingecko.com/api/v3/simple/price"
params = {
"ids": ids,
"vs_currencies": vs_currency,
"include_24hr_change": "true",
"include_24hr_vol": "true",
"include_market_cap": "true"
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
return response.json()Example Response:
{
"bitcoin": {
"usd": 97234.56,
"usd_24h_change": 2.34,
"usd_24h_vol": 28500000000,
"usd_market_cap": 1920000000000
},
"ethereum": {
"usd": 3456.78,
"usd_24h_change": 1.87,
"usd_24h_vol": 12300000000,
"usd_market_cap": 415200000000
}
}Error Handling:
| Code | Cause | Solution |
|---|---|---|
| 429 | Rate limit exceeded | Exponential backoff (1s, 2s, 4s, 8s) |
| 404 | Unknown coin ID | Return error with suggestion |
| 500 | Server error | Retry 3x, then fallback to yfinance |
---
API 2: yfinance (Fallback)
Purpose: Backup price source, primary for historical OHLCV data
Integration Details:
- Library:
yfinancePython package - Authentication: None required
- Rate Limits: Implicit (respectful usage)
- Response Format: pandas DataFrame
Example Usage:
import yfinance as yf
def get_historical(symbol: str, period: str = "30d") -> pd.DataFrame:
"""Fetch historical OHLCV data."""
ticker = yf.Ticker(f"{symbol}-USD")
df = ticker.history(period=period, interval="1d")
df.columns = [c.lower() for c in df.columns]
return dfSymbol Mapping:
- CoinGecko uses full names:
bitcoin,ethereum - yfinance uses tickers:
BTC-USD,ETH-USD - api_client.py handles translation
5.2 API Call Sequencing
User Request
│
▼
Check Cache (local, fast)
│
├── Cache HIT ────────────────────────────────────────┐
│ │
▼ Cache MISS │
│ │
Try CoinGecko API │
│ │
├── Success ──────────────────────────┐ │
│ │ │
▼ Failure (429/500) │ │
│ │ │
Fallback to yfinance │ │
│ │ │
├── Success ─────────────────┐ │ │
│ │ │ │
▼ Failure │ │ │
│ │ │ │
Return Cached (stale) + Warning │ │ │
│ │ │ │
└─────────────────────────────┴────────┴───────────────┘
│
▼
Update Cache
│
▼
Format & ReturnFallback Strategy: 1. CoinGecko (primary) → yfinance (fallback) → Cache (stale, last resort) 2. Always return something - stale data with warning beats error
---
6. Data Flow Architecture
6.1 Input → Processing → Output Pipeline
INPUT (User Request: "BTC price")
│
▼
Step 1: Parse & Validate
│ Input: "BTC" (string)
│ Processing: Resolve to CoinGecko ID "bitcoin"
│ Output: {"id": "bitcoin", "symbol": "BTC", "name": "Bitcoin"}
│
▼
Step 2: Check Cache
│ Input: Cache key "bitcoin_usd_spot"
│ Processing: Load data/cache.json, check TTL
│ Output: Cached price data OR cache miss flag
│
▼
Step 3: Fetch (if cache miss)
│ Input: Symbol ID "bitcoin"
│ API Call: CoinGecko /simple/price
│ Processing: Parse JSON, handle errors
│ Output: {"price": 97234.56, "change_24h": 2.34, ...}
│
▼
Step 4: Format & Return
│ Input: Price data dict
│ Processing: Update cache, format output
│ Output: Human-readable table OR JSON file
│
▼
FINAL OUTPUT6.2 Data Format Specifications
Format 1: Cache Entry (data/cache.json)
{
"bitcoin_usd_spot": {
"symbol": "BTC",
"name": "Bitcoin",
"price": 97234.56,
"change_24h": 2.34,
"volume_24h": 28500000000,
"market_cap": 1920000000000,
"currency": "usd",
"fetched_at": "2025-01-14T15:30:00Z",
"ttl_seconds": 30
}
}Format 2: Standardized Price Output (returned to dependent skills)
{
"symbol": "BTC",
"name": "Bitcoin",
"price": 97234.56,
"currency": "USD",
"change_24h_percent": 2.34,
"volume_24h": 28500000000,
"market_cap": 1920000000000,
"timestamp": "2025-01-14T15:30:00Z",
"source": "coingecko",
"cached": false
}Format 3: Historical OHLCV (data/BTC_30d.csv)
date,open,high,low,close,volume
2024-12-15,95000.00,96500.00,94200.00,96100.00,25000000000
2024-12-16,96100.00,97800.00,95800.00,97500.00,27000000000
...6.3 Data Validation Rules
Validation Checkpoints:
1. Input Validation (Step 1):
- ✅ Symbol is string, 1-10 characters
- ✅ Symbol resolves to known cryptocurrency
- ✅ Currency is supported (USD, EUR, GBP, etc.)
2. Cache Validation (Step 2):
- ✅ Cache file exists and is valid JSON
- ✅ Entry TTL not exceeded
- ✅ Required fields present
3. API Response Validation (Step 3):
- ✅ HTTP 200 status
- ✅ JSON parseable
- ✅ Price is positive number
- ✅ Required fields present
---
7. Error Handling Strategy
7.1 Error Categories & Responses
Category 1: Input Errors
| Error | Cause | Detection | Solution |
|---|---|---|---|
| Unknown symbol | User typed invalid ticker | Symbol lookup fails | Suggest similar symbols |
| Invalid currency | Unsupported fiat currency | Currency not in list | Show supported currencies |
| Empty input | No symbols provided | Input validation | Show usage example |
Category 2: API Errors
| Error | Cause | Detection | Solution |
|---|---|---|---|
| Rate limited (429) | Too many requests | HTTP status | Exponential backoff + retry |
| Server error (500) | CoinGecko down | HTTP status | Fallback to yfinance |
| Timeout | Slow network | Exception | Retry with longer timeout |
| Auth failed (401) | Invalid API key | HTTP status | Check config, use free tier |
Category 3: Cache Errors
| Error | Cause | Detection | Solution |
|---|---|---|---|
| Cache corrupted | Invalid JSON | Parse exception | Delete cache, refetch |
| Cache missing | First run or deleted | File not found | Create empty cache |
| Cache stale | TTL exceeded | Timestamp check | Refetch from API |
7.2 Graceful Degradation
Primary Path:
CoinGecko API → Cache → Format → User
↓ (if API fails)
Fallback Path 1:
yfinance → Cache → Format → User
↓ (if yfinance fails)
Fallback Path 2:
Stale Cache → Format → User (with warning)
↓ (if no cache)
Error Path:
Clear error message + suggested action7.3 Logging Format
[YYYY-MM-DD HH:MM:SS] [LEVEL] [Component] Message
[2025-01-14 15:30:00] [INFO] [API] Fetching BTC price from CoinGecko
[2025-01-14 15:30:01] [INFO] [API] ✓ Got price: $97,234.56
[2025-01-14 15:30:01] [INFO] [Cache] Updated bitcoin_usd_spot (TTL: 30s)
[2025-01-14 15:30:45] [WARN] [API] Rate limited (429), backing off 2s
[2025-01-14 15:30:47] [INFO] [API] Retry successful---
8. Composability & Stacking Architecture
8.1 Standalone Execution
This skill can run independently for direct user queries:
# Single price
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC
# Multiple prices
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbols BTC,ETH,SOL
# Historical data
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --period 30d --output csv8.2 Skill Stacking Patterns
Pattern 1: Direct Import (Preferred for Python skills)
Other skills import price_tracker functions directly:
# In crypto-portfolio-tracker/scripts/portfolio.py
import sys
sys.path.insert(0, "${CLAUDE_SKILL_DIR}/../market-price-tracker/skills/tracking-crypto-prices/scripts")
from price_tracker import get_current_prices
def calculate_portfolio_value(holdings: dict) -> float:
"""Value portfolio using price tracker."""
prices = get_current_prices(list(holdings.keys()))
total = sum(holdings[sym] * prices[sym]["price"] for sym in holdings)
return totalPattern 2: CLI Subprocess (Cross-language or isolation)
# In another skill's script
PRICES=$(python ${CLAUDE_SKILL_DIR}/../market-price-tracker/scripts/price_tracker.py \
--symbols BTC,ETH \
--format json)Pattern 3: Shared Cache (Efficient for batch operations)
Multiple skills read from the same cache file:
- price-tracker updates
data/cache.json - portfolio-tracker reads from same cache
- Reduces redundant API calls
8.3 Skills That Depend on This
| Skill | Integration Pattern | Data Needed |
|---|---|---|
| market-movers-scanner | Direct import | Batch prices + 24h change |
| crypto-portfolio-tracker | Direct import | Current prices for holdings |
| crypto-tax-calculator | Direct import | Historical prices for cost basis |
| defi-yield-optimizer | CLI subprocess | USD prices for APY calculation |
| liquidity-pool-analyzer | Direct import | Token prices for LP valuation |
| staking-rewards-optimizer | CLI subprocess | Token prices for rewards calc |
| crypto-derivatives-tracker | Direct import | Underlying asset prices |
| dex-aggregator-router | Direct import | CEX prices for comparison |
| options-flow-analyzer | Direct import | Underlying prices |
| arbitrage-opportunity-finder | Direct import | Multi-exchange prices |
8.4 Input/Output Contracts
Input Contract (what this skill expects):
# Function signature for get_current_prices()
def get_current_prices(
symbols: list[str], # Required: ["BTC", "ETH"]
currency: str = "usd", # Optional: Default USD
use_cache: bool = True, # Optional: Default True
cache_ttl: int = 30 # Optional: Seconds
) -> dict[str, PriceData]:
...Output Contract (what this skill guarantees):
@dataclass
class PriceData:
symbol: str # "BTC"
name: str # "Bitcoin"
price: float # 97234.56
currency: str # "USD"
change_24h: float # 2.34 (percent)
volume_24h: float # 28500000000
market_cap: float # 1920000000000
timestamp: str # ISO 8601
source: str # "coingecko" or "yfinance"
cached: bool # True if from cache---
9. Performance & Scalability
9.1 Performance Targets
| Metric | Target | Max Acceptable | Notes |
|---|---|---|---|
| Single price (cached) | < 100ms | < 500ms | Local file read only |
| Single price (API) | < 2s | < 5s | Network dependent |
| Batch 10 prices (API) | < 3s | < 8s | Single API call |
| Batch 50 prices (API) | < 5s | < 15s | May require pagination |
| Historical 30d OHLCV | < 3s | < 10s | yfinance call |
9.2 Scalability Considerations
Cache Strategy:
- Spot prices: 30s TTL (balance freshness vs rate limits)
- Historical data: 1h TTL (rarely changes)
- Cache file: Single JSON file (sufficient for <1000 entries)
Batch Optimization:
- CoinGecko supports 100+ coins per request
- Always batch multiple symbol requests
- Single API call vs N calls = 100x reduction
9.3 Resource Usage
Disk: ~1MB for cache + historical exports Memory: ~50MB during execution Network: ~10KB per API request
---
10. Testing Strategy
10.1 Unit Tests
Test Input Validation:
def test_symbol_resolution():
assert resolve_symbol("BTC") == "bitcoin"
assert resolve_symbol("btc") == "bitcoin"
assert resolve_symbol("Bitcoin") == "bitcoin"
assert resolve_symbol("INVALID") is NoneTest Cache:
def test_cache_hit():
cache.set("bitcoin", price_data, ttl=30)
result = cache.get("bitcoin")
assert result is not None
assert result["cached"] is True
def test_cache_miss():
cache.clear()
result = cache.get("bitcoin")
assert result is None10.2 Integration Tests
# Test full workflow
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --test
# Expected: Fetches real data, validates response format10.3 Acceptance Criteria
- [ ] Single price returns in < 3s
- [ ] Batch 10 prices returns in < 5s
- [ ] Cache reduces repeat queries to < 100ms
- [ ] Fallback to yfinance works when CoinGecko fails
- [ ] Historical data exports valid CSV
- [ ] All error cases return helpful messages
---
11. Security & Compliance
11.1 API Key Management
Storage: Environment variables or config/settings.yaml
# config/settings.yaml
api:
coingecko:
api_key: ${COINGECKO_API_KEY} # From environment
use_pro: falseNever:
- Hardcode API keys in scripts
- Commit API keys to git
- Log API keys
11.2 Data Privacy
User Data: No PII collected Market Data: Public information, no privacy concerns Cache: Local only, user can delete anytime
11.3 Rate Limit Compliance
- Respect CoinGecko rate limits (10-50/min free tier)
- Implement exponential backoff
- Use caching aggressively
---
12. Version History
| Version | Date | Changes | Author |
|---|---|---|---|
| 1.0.0 | 2025-01-01 | Initial stub | Jeremy Longshore |
| 2.0.0 | 2025-01-14 | Full ARD per nixtla standard | Jeremy Longshore |
---
13. Approval
| Role | Name | Approval Date |
|---|---|---|
| Architect | Claude (Opus 4.5) | 2025-01-14 |
| Owner | Jeremy Longshore | 2025-01-14 |
---
Document maintained by: Intent Solutions Standard: Nixtla Enterprise Skill ARD Template v1.0
# Tracking Crypto Prices - Configuration
# Version: 2.0.0
# Author: Jeremy Longshore <jeremy@intentsolutions.io>
# =============================================================================
# API Configuration
# =============================================================================
api:
coingecko:
# API key (optional, for higher rate limits)
# Get free key from: https://www.coingecko.com/en/api
# Can also be set via COINGECKO_API_KEY environment variable
api_key: ${COINGECKO_API_KEY}
# Use Pro API endpoint (requires paid key)
use_pro: false
# Request timeout in seconds
timeout: 10
# Maximum retries on failure
max_retries: 3
yfinance:
# Enable yfinance as fallback data source
enabled: true
# =============================================================================
# Cache Configuration
# =============================================================================
cache:
# Enable caching to reduce API calls
enabled: true
# Time-to-live for spot prices in seconds
# Lower values = fresher data, higher API usage
# Recommended: 30-60 seconds
spot_ttl: 30
# Time-to-live for historical data in seconds
# Historical data changes less frequently
# Recommended: 3600 (1 hour)
historical_ttl: 3600
# Cache storage directory (relative to skill root)
directory: ./data
# =============================================================================
# Currency Configuration
# =============================================================================
currency:
# Default fiat currency for price display
default: usd
# Supported currencies (CoinGecko supports 30+)
supported:
- usd # US Dollar
- eur # Euro
- gbp # British Pound
- jpy # Japanese Yen
- cad # Canadian Dollar
- aud # Australian Dollar
- chf # Swiss Franc
- cny # Chinese Yuan
- inr # Indian Rupee
- krw # Korean Won
- btc # Bitcoin (for crypto-to-crypto)
- eth # Ethereum (for crypto-to-crypto)
# =============================================================================
# Predefined Watchlists
# =============================================================================
# Use CoinGecko IDs (not ticker symbols)
# Find IDs at: https://www.coingecko.com/en/api/documentation
watchlists:
# Top 10 by market cap
top10:
- bitcoin
- ethereum
- tether
- binancecoin
- solana
- ripple
- cardano
- avalanche-2
- dogecoin
- polkadot
# DeFi protocols
defi:
- uniswap
- aave
- chainlink
- maker
- compound-governance-token
- curve-dao-token
- sushi
- 1inch
- pancakeswap-token
# Layer 2 solutions
layer2:
- matic-network
- arbitrum
- optimism
- immutable-x
- starknet
- mantle
# Stablecoins
stablecoins:
- tether
- usd-coin
- dai
- frax
- true-usd
- paxos-standard
# Memecoins
memecoins:
- dogecoin
- shiba-inu
- pepe
- floki
- bonk
- dogwifcoin
# Custom watchlist (user-configurable)
custom: []
# Add your favorite coins here:
# - bitcoin
# - ethereum
# - your-favorite-coin
# =============================================================================
# Display Configuration
# =============================================================================
display:
# Number of decimal places for prices
price_decimals:
high_value: 2 # Prices >= $1
low_value: 6 # Prices < $1
very_low: 8 # Prices < $0.0001
# Show percentage change colors (ANSI)
# Note: Only works in terminal, not in file output
color_output: true
# Table width for aligned output
table_width: 80
# =============================================================================
# Rate Limiting
# =============================================================================
rate_limit:
# Minimum seconds between API requests
# CoinGecko free tier: ~10-50 calls/minute
min_interval: 1.5
# Exponential backoff on rate limit
backoff_factor: 2
max_backoff: 60
PRD: Market Price Tracker
Version: 2.0.0 Author: Jeremy Longshore <jeremy@intentsolutions.io> Status: In Development Last Updated: 2025-01-14
---
Document Control
| Field | Value |
|---|---|
| Skill Name | tracking-crypto-prices |
| Skill Type | Utility Skill |
| Domain | Cryptocurrency / Market Data |
| Target Users | Traders, Investors, Developers, Analysts |
| Priority | Critical (Foundation Skill) |
| Status | In Development |
| Owner | Jeremy Longshore |
---
1. Executive Summary
One-sentence description: Track real-time cryptocurrency prices across multiple exchanges with historical data, price alerts, and multi-currency support.
Value Proposition: This is the foundation skill for the entire crypto plugin ecosystem. It provides the price data infrastructure that 10+ other skills depend on for their functionality. Without reliable price tracking, portfolio management, tax calculation, DeFi optimization, and arbitrage detection are impossible.
Key Metrics:
- Activation accuracy: 95%+
- Price data freshness: < 30 seconds
- API reliability: 99.5%+ uptime
- Supported assets: 10,000+ cryptocurrencies
Dependent Skills (skills that require this one):
- market-movers-scanner
- crypto-portfolio-tracker
- crypto-tax-calculator
- defi-yield-optimizer
- liquidity-pool-analyzer
- staking-rewards-optimizer
- crypto-derivatives-tracker
- dex-aggregator-router
- options-flow-analyzer
- arbitrage-opportunity-finder
---
2. Problem Statement
Current State (Without This Skill)
Pain Points: 1. Fragmented Data Sources: Traders must manually check multiple exchanges and websites for price information, wasting time and risking decisions on stale data 2. No Standardized Format: Price data comes in different formats from different sources, making programmatic analysis difficult 3. Missing Historical Context: Point-in-time prices without historical trends lead to poor trading decisions 4. Alert Fatigue: Without intelligent alerting, users miss important price movements or get overwhelmed by noise 5. Currency Confusion: Prices in USD only ignore users who think in EUR, GBP, or other currencies
Current Workarounds:
- Manually refreshing CoinGecko/CoinMarketCap tabs
- Using spreadsheets with manual data entry
- Writing one-off scripts for each data source
- Subscribing to expensive third-party services
Impact of Problem:
- Time wasted: 30+ minutes daily checking prices across sources
- Error rate: 15% of decisions based on stale/incorrect data
- Missed opportunities: Significant due to delayed information
- User frustration: High
Desired State (With This Skill)
Transformation:
- From: Manual, fragmented, time-consuming price checking
- To: Instant, unified, automated price intelligence with historical context
Expected Benefits: 1. Time Savings: Reduce price checking from 30+ minutes to < 30 seconds 2. Accuracy: 99.9%+ data accuracy with source verification 3. Intelligence: Historical trends and price alerts reduce missed opportunities by 80% 4. Foundation: Enable 10+ dependent skills to function reliably
---
3. Target Users
Primary Users
User Persona 1: Active Cryptocurrency Trader
- Background: Trades crypto daily, uses multiple exchanges, technically competent
- Goals: Get real-time prices quickly, set price alerts, compare across exchanges
- Pain Points: Switching between apps/tabs, missing price movements, stale data
- Use Frequency: 10-50 times daily
User Persona 2: Crypto Investor (HODLer)
- Background: Long-term holder, checks portfolio weekly, moderate technical skills
- Goals: Monitor portfolio value, track historical performance, set major price alerts
- Pain Points: No simple way to see current holdings value, missing major moves
- Use Frequency: 2-5 times weekly
User Persona 3: Developer Building Crypto Tools
- Background: Software developer integrating price data into applications
- Goals: Reliable price API, consistent data format, historical data access
- Pain Points: Inconsistent API responses, rate limits, data normalization
- Use Frequency: Continuous (via other skills)
Secondary Users
- Analysts: Need historical price data for research and modeling
- Content Creators: Need current prices for articles and videos
- Compliance Officers: Need price data for regulatory reporting
---
4. User Stories
Critical User Stories (Must Have)
1. As a trader, I want to get the current price of any cryptocurrency instantly, So that I can make informed trading decisions without delay.
Acceptance Criteria:
- [ ] Price returned in < 3 seconds
- [ ] Price includes 24h change percentage
- [ ] Price includes volume data
- [ ] Works for top 10,000 cryptocurrencies by market cap
2. As a investor, I want to see price history for any cryptocurrency, So that I can understand trends before making buy/sell decisions.
Acceptance Criteria:
- [ ] Historical data available for 1d, 7d, 30d, 90d, 1y, all-time
- [ ] Data includes OHLCV (Open, High, Low, Close, Volume)
- [ ] Data exportable to CSV for analysis
- [ ] Charts/visualizations available
3. As a multi-currency user, I want prices displayed in my preferred currency (EUR, GBP, JPY, etc.), So that I don't have to mentally convert from USD.
Acceptance Criteria:
- [ ] Support for 30+ fiat currencies
- [ ] Currency preference can be set and remembered
- [ ] Conversion rates are current (< 1 hour old)
4. As a developer using other crypto skills, I want a reliable price data interface, So that dependent skills (portfolio tracker, tax calculator, etc.) work correctly.
Acceptance Criteria:
- [ ] Standardized JSON output format
- [ ] Consistent error handling
- [ ] Cached data for rate limit management
- [ ] Clear documentation for integration
High-Priority User Stories (Should Have)
1. As a trader, I want to compare prices across exchanges to find arbitrage opportunities 2. As a investor, I want price alerts when assets hit target prices 3. As a analyst, I want batch price queries for multiple assets simultaneously
Nice-to-Have User Stories (Could Have)
1. As a user, I want price predictions based on historical patterns 2. As a user, I want social sentiment integration with price data
---
5. Functional Requirements
Core Capabilities (Must Have)
REQ-1: Real-Time Price Fetching
- Description: Fetch current price for any cryptocurrency by symbol or name
- Rationale: Core functionality - everything else depends on this
- Acceptance Criteria:
- [ ] Support symbol lookup (BTC, ETH, SOL)
- [ ] Support name lookup (Bitcoin, Ethereum, Solana)
- [ ] Return price, 24h change, volume, market cap
- [ ] Response time < 3 seconds
- Dependencies: CoinGecko API or equivalent
REQ-2: Historical Price Data
- Description: Fetch OHLCV data for specified time ranges
- Rationale: Trend analysis requires historical context
- Acceptance Criteria:
- [ ] Configurable time ranges (1d to all-time)
- [ ] Configurable intervals (1m, 5m, 1h, 1d)
- [ ] OHLCV format output
- [ ] Export to CSV/JSON
- Dependencies: Yahoo Finance, CoinGecko, or exchange APIs
REQ-3: Multi-Currency Support
- Description: Display prices in user's preferred fiat currency
- Rationale: Global user base thinks in different currencies
- Acceptance Criteria:
- [ ] Support 30+ fiat currencies
- [ ] Automatic conversion using current rates
- [ ] Configurable default currency
- Dependencies: Exchange rate API
REQ-4: Watchlist Management
- Description: Track a personalized list of cryptocurrencies
- Rationale: Users care about specific assets, not all 10,000+
- Acceptance Criteria:
- [ ] Create/edit/delete watchlists
- [ ] Predefined watchlists (top 10, DeFi, Layer 2, etc.)
- [ ] Batch price fetch for watchlist
- Dependencies: Local storage for watchlist data
REQ-5: Caching Layer
- Description: Cache price data to reduce API calls and improve speed
- Rationale: Rate limits and latency require intelligent caching
- Acceptance Criteria:
- [ ] Configurable cache duration (default: 30 seconds for spot prices)
- [ ] Cache invalidation on demand
- [ ] Disk-based cache for persistence
- Dependencies: Local file system
Integration Requirements
REQ-API-1: CoinGecko API
- Purpose: Primary source for price data (10,000+ assets, free tier available)
- Endpoints:
/simple/price- Current prices/coins/{id}/market_chart- Historical data/coins/markets- Market data with sorting- Authentication: API key (optional for higher limits)
- Rate Limits: 10-50 calls/minute (free), 500/minute (Pro)
- Error Handling: Exponential backoff on 429, fallback to cache
REQ-API-2: Yahoo Finance (yfinance)
- Purpose: Backup source, especially for historical OHLCV data
- Endpoints: Via yfinance Python library
- Authentication: None required
- Rate Limits: Implicit (be respectful)
- Error Handling: Fallback to CoinGecko
Data Requirements
REQ-DATA-1: Input Data Format
- Format: Command-line arguments or JSON config
- Required Fields:
symbolorsymbols(list) - Optional Fields:
currency,period,interval - Validation Rules: Symbol must be valid crypto ticker
REQ-DATA-2: Output Data Format
- Format: JSON (programmatic) or formatted table (human-readable)
- Fields:
symbol,name,price,change_24h,volume_24h,market_cap,last_updated - Quality Standards: Prices accurate to 8 decimal places for small-cap assets
Performance Requirements
REQ-PERF-1: Response Time
- Target: < 3 seconds for single asset
- Max Acceptable: < 10 seconds for watchlist of 20 assets
REQ-PERF-2: Token Budget
- Description Size: < 250 characters
- SKILL.md Size: < 500 lines
- Total Skill Size: < 5,000 tokens
Quality Requirements
REQ-QUAL-1: Description Quality
- Target Score: 80%+ on quality formula
- Must Include:
- [ ] Action-oriented verbs
- [ ] "Use when [scenarios]" clause
- [ ] "Trigger with '[phrases]'" examples
- [ ] Domain keywords (price, crypto, exchange, market)
REQ-QUAL-2: Data Accuracy
- Price Accuracy: Match exchange prices within 0.5%
- Data Freshness: < 30 seconds for spot prices
- Error Rate: < 1% failed requests after retries
---
6. Non-Goals (Out of Scope)
What This Skill Does NOT Do:
1. Execute Trades
- Rationale: Trading requires exchange authentication and carries financial risk
- Alternative: Use exchange-specific trading bots or manual trading
2. Provide Price Predictions
- Rationale: Prediction is speculative and outside data-fetching scope
- Alternative: May be added in future version (v3.0)
3. Track NFT Prices
- Rationale: NFTs require different data sources and valuation methods
- Alternative: Use nft-rarity-analyzer skill (separate)
4. Aggregate DEX Prices
- Rationale: DEX prices require on-chain queries (different architecture)
- Alternative: Use dex-aggregator-router skill (depends on this skill)
---
7. Success Metrics
Skill Activation Metrics
Metric 1: Activation Accuracy
- Definition: % of times skill activates when user intends to check prices
- Target: 95%+
- Measurement: Manual testing with 50+ trigger phrase variations
Metric 2: False Positive Rate
- Definition: % of times skill activates when user meant something else
- Target: < 2%
- Measurement: User feedback and log analysis
Quality Metrics
Metric 3: Description Quality Score
- Formula: 6-criterion weighted scoring
- Target: 85%+
- Components:
- Action-oriented: 20%
- Clear triggers: 25%
- Comprehensive: 15%
- Natural language: 20%
- Specificity: 10%
- Technical terms: 10%
Usage Metrics
Metric 4: Daily Active Use
- Target: Used 5+ times daily by active users
- Measurement: Skill invocation logs
Performance Metrics
Metric 5: Data Freshness
- Definition: Time since last price update
- Target: < 30 seconds for cached data
- Measurement: Timestamp comparison
---
8. User Experience Flow
Typical Usage Flow
1. User Intent: User wants to know current Bitcoin price 2. Trigger: User says "What's the Bitcoin price?" or "check BTC" 3. Skill Activation: Claude recognizes price query intent 4. Skill Execution:
- Check cache for recent BTC price
- If stale, fetch from CoinGecko API
- Format response with price, change, volume
5. Output Delivered: Formatted price card with key metrics 6. User Action: User uses information for trading decision
Example Scenario
Scenario: Check current prices for a watchlist
Input:
Check prices for my top holdings: BTC, ETH, SOLClaude's Response:
Fetching current prices...
================================================================================
CRYPTO PRICES Updated: [timestamp]
================================================================================
Symbol Price (USD) 24h Change Volume (24h) Market Cap
--------------------------------------------------------------------------------
BTC $97,234.56 +2.34% $28.5B $1.92T
ETH $3,456.78 +1.87% $12.3B $415.2B
SOL $142.34 +5.12% $2.1B $61.4B
--------------------------------------------------------------------------------
Total Portfolio Change: +2.44%
================================================================================User Benefit: Instant visibility into holdings without checking multiple sources
---
9. Integration Points
External Systems
System 1: CoinGecko API
- Purpose: Primary price data source
- Integration Type: REST API
- Authentication: Optional API key for higher limits
- Data Flow: Skill → API → Parse JSON → Cache → Format → User
System 2: Yahoo Finance (via yfinance)
- Purpose: Historical OHLCV data, backup price source
- Integration Type: Python library
- Authentication: None
- Data Flow: Skill → yfinance → DataFrame → CSV/JSON → User
Internal Dependencies
Dependency 1: Local Cache System
- What it provides: Fast access to recently fetched prices
- Why needed: Reduces API calls, improves response time
Dependency 2: Python Libraries
- Libraries: requests, pandas, yfinance
- Versions: requests>=2.28, pandas>=2.0, yfinance>=0.2.30
Skills That Depend on This
| Skill | How It Uses Price Data |
|---|---|
| market-movers-scanner | Compares price changes to find movers |
| crypto-portfolio-tracker | Values holdings at current prices |
| crypto-tax-calculator | Gets cost basis and current values |
| defi-yield-optimizer | Calculates yield in USD terms |
| liquidity-pool-analyzer | Values LP positions |
| staking-rewards-optimizer | Calculates staking APY |
| crypto-derivatives-tracker | Tracks underlying asset prices |
| dex-aggregator-router | Compares DEX prices to CEX |
| options-flow-analyzer | Values options based on underlying |
| arbitrage-opportunity-finder | Detects price discrepancies |
---
10. Constraints & Assumptions
Technical Constraints
1. API Rate Limits: CoinGecko free tier limits to 10-50 calls/minute 2. Token Budget: Must fit in 5,000 token skill discovery limit 3. Processing Time: Max 10 seconds for any operation 4. Dependencies: Requires network access for price fetching
Business Constraints
1. API Costs: Free tier preferred; Pro tier ($129/month) if needed 2. Timeline: Foundation skill - must be complete before dependent skills 3. Resources: Single developer (Claude)
Assumptions
1. Assumption 1: CoinGecko API remains available and free tier sufficient
- Risk if false: Switch to backup provider (CoinMarketCap)
- Mitigation: Implement provider abstraction layer
2. Assumption 2: Users have Python 3.8+ installed
- Risk if false: Scripts won't run
- Mitigation: Document requirements, provide error messages
---
11. Risk Assessment
Technical Risks
Risk 1: API Rate Limiting
- Probability: High (free tier has strict limits)
- Impact: Medium (degraded but not broken experience)
- Mitigation: Aggressive caching, exponential backoff, batch requests
Risk 2: API Downtime
- Probability: Low (CoinGecko has 99.9% uptime)
- Impact: High (skill unusable)
- Mitigation: Multiple fallback providers, cached data with staleness warning
User Experience Risks
Risk 1: Skill Over-Triggering (False Positives)
- Probability: Medium
- Impact: Low (user can clarify)
- Mitigation: Precise trigger phrases, domain-specific keywords
Risk 2: Stale Data Confusion
- Probability: Medium (if cache too aggressive)
- Impact: Medium (bad trading decisions)
- Mitigation: Clear timestamps, configurable cache duration, manual refresh
---
12. Open Questions
Resolved Questions:
1. ✅ Question: Which API should be primary?
- Decision: CoinGecko (largest free tier, most assets)
2. ✅ Question: How long to cache spot prices?
- Decision: 30 seconds (balance between freshness and rate limits)
Pending Questions: None
---
13. Appendix: Examples
Example 1: Single Asset Price Check
User Request:
What's the current Ethereum price?Expected Skill Behavior: 1. Parse "Ethereum" as ETH 2. Check cache for recent ETH price 3. If stale (>30s), fetch from CoinGecko 4. Format response with price card
Expected Output:
ETH (Ethereum)
$3,456.78 USD
+1.87% (24h) | Vol: $12.3B | MCap: $415.2B
Updated: [timestamp]Example 2: Watchlist Scan
User Request:
Check prices for the top 10 cryptosExpected Behavior: 1. Use predefined "crypto_top10" watchlist 2. Batch fetch all 10 prices 3. Format as table with all metrics
Example 3: Historical Data Export
User Request:
Get Bitcoin's price history for the last 30 days and save to CSVExpected Behavior: 1. Fetch 30-day OHLCV data from yfinance 2. Format as DataFrame 3. Export to data/BTC_30d_[date].csv 4. Confirm file location
---
14. Version History
| Version | Date | Changes | Author |
|---|---|---|---|
| 1.0.0 | 2025-01-01 | Initial stub | Jeremy Longshore |
| 2.0.0 | 2025-01-14 | Full PRD per nixtla standard | Jeremy Longshore |
---
15. Approval
| Role | Name | Approval Date | Signature |
|---|---|---|---|
| Product Owner | Jeremy Longshore | 2025-01-14 | ✓ |
| Tech Lead | Claude (Opus 4.5) | 2025-01-14 | ✓ |
---
Document maintained by: Intent Solutions Standard: Nixtla Enterprise Skill PRD Template v1.0
Error Handling Reference
Comprehensive guide to errors, causes, and solutions for the tracking-crypto-prices skill.
Error Categories
1. API Errors
RateLimitError
Message: Rate limit exceeded. Retry after Xs
Cause: CoinGecko API rate limit reached (10-50 calls/minute for free tier).
Solution: 1. Wait for the indicated retry period 2. Enable caching to reduce API calls 3. Consider getting a CoinGecko API key for higher limits
Prevention:
# In config/settings.yaml
cache:
enabled: true
spot_ttl: 30 # Cache spot prices for 30 seconds---
NetworkError
Message: Connection failed: [details] or Request timed out
Cause: No internet connection or CoinGecko API unreachable.
Solution: 1. Check internet connection 2. Cached data will be used automatically if available 3. yfinance fallback will be attempted if configured
Fallback Behavior:
Primary: CoinGecko API
↓ (fails)
Fallback: yfinance (if installed)
↓ (fails)
Last Resort: Stale cache (if --allow-stale)---
SymbolNotFoundError
Message: Unknown symbol: XYZ
Cause: The cryptocurrency ticker or CoinGecko ID doesn't exist.
Solution: 1. Check spelling (symbols are case-insensitive) 2. Use --list to search for valid symbols:
python price_tracker.py --list --query bitcoin3. Use CoinGecko ID instead of ticker (e.g., avalanche-2 not AVAX)
Common Mapping Issues:
| Ticker | CoinGecko ID | Notes |
|---|---|---|
| AVAX | avalanche-2 | Not "avalanche" |
| MATIC | matic-network | Polygon network |
| COMP | compound-governance-token | Full name |
| CRV | curve-dao-token | Full name |
---
2. Cache Errors
Cache Stale Warning
Message: Warning: Using stale cache for XYZ
Cause: Fresh data unavailable, returning expired cache entry.
Solution:
- Not a critical error; stale data is returned with
_stale: trueflag - Use
--no-cacheto force fresh fetch - Clear cache with
--clear-cacheif data seems corrupted
---
Cache Write Failed
Message: Silent failure (logged in verbose mode)
Cause: Cannot write to cache directory (permissions or disk space).
Solution: 1. Check cache directory permissions:
ls -la ./data/2. Ensure disk space available 3. Configure alternate cache directory in settings.yaml
---
3. Configuration Errors
Missing Dependencies
Message: ImportError: No module named 'requests'
Cause: Required Python packages not installed.
Solution:
pip install requests pandas yfinanceOptional packages:
pip install pyyaml python-dotenv---
Invalid Configuration
Message: Various YAML parsing errors
Cause: Malformed settings.yaml file.
Solution: 1. Validate YAML syntax 2. Check indentation (use spaces, not tabs) 3. Delete settings.yaml to use defaults
Validation:
python -c "import yaml; yaml.safe_load(open('config/settings.yaml'))"---
Unknown Watchlist
Message: Error: Unknown watchlist 'xyz'
Cause: Requested watchlist not defined in configuration.
Solution: 1. Use a predefined watchlist: top10, defi, layer2, stablecoins, memecoins 2. Define custom watchlist in settings.yaml:
watchlists:
custom:
- bitcoin
- ethereum
- solana---
4. Output Errors
File Write Error
Message: Error writing to output file
Cause: Cannot write to specified output path.
Solution: 1. Check directory exists 2. Check file permissions 3. Ensure path is valid
---
Error Handling Strategy
Automatic Fallback Chain
1. CoinGecko API (primary)
↓ RateLimitError or NetworkError
2. yfinance (fallback)
↓ ImportError or APIError
3. Stale Cache (last resort)
↓ No cache available
4. Error reported to userGraceful Degradation
| Scenario | Behavior |
|---|---|
| Rate limited | Auto-retry with exponential backoff |
| Network error | Use fallback source or cache |
| Partial failure | Return successful results with warnings |
| Total failure | Clear error message with suggestions |
Retry Logic
# Built-in retry with exponential backoff
Attempt 1: Immediate
Attempt 2: Wait 2 seconds
Attempt 3: Wait 4 seconds
(Give up and try fallback)---
Debugging
Enable Verbose Output
python price_tracker.py --symbol BTC --verboseShows:
- API calls being made
- Cache hits/misses
- Fallback attempts
- Timing information
Check Cache Status
# In Python
from cache_manager import CacheManager
cache = CacheManager()
print(cache.get_stats())Force Fresh Data
# Bypass cache entirely
python price_tracker.py --symbol BTC --no-cache
# Clear all cached data
python price_tracker.py --clear-cache---
Error Codes
| Code | Category | Meaning |
|---|---|---|
| 1 | General | Command-line argument error |
| 2 | Network | API connection failed |
| 3 | API | Rate limit or server error |
| 4 | Data | Symbol not found |
| 5 | Config | Configuration error |
| 6 | Output | File write error |
---
Reporting Issues
When reporting issues, include:
1. Command executed:
python price_tracker.py --symbol XYZ --verbose2. Full error output (with --verbose flag)
3. Python version:
python --version4. Package versions:
pip show requests yfinance pandas5. Configuration (redact API keys):
cat config/settings.yaml--- [Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)
Usage Examples
Comprehensive examples for the tracking-crypto-prices skill.
Quick Start Examples
Example 1: Get Single Price
The simplest use case - check the current price of Bitcoin:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTCOutput:
BTC (Bitcoin)
$97,234.56 USD
+2.34% (24h) | Vol: $28.5B | MCap: $1.92T---
Example 2: Check Multiple Prices
Get prices for a portfolio of assets:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbols BTC,ETH,SOL,AVAX,DOTOutput:
================================================================================
CRYPTO PRICES Updated: 2025-01-14 15:30:00
================================================================================
Symbol Price (USD) 24h Change Volume (24h) Market Cap
--------------------------------------------------------------------------------
BTC $97,234.56 +2.34% $28.5B $1.92T
ETH $3,456.78 +1.87% $12.3B $415.2B
SOL $142.34 +5.12% $2.1B $61.4B
AVAX $38.92 -0.45% $425.6M $14.2B
DOT $7.23 +3.21% $198.3M $9.8B
--------------------------------------------------------------------------------
Total 24h Change: +2.44% (weighted)
================================================================================---
Example 3: Use a Watchlist
Scan predefined watchlists for quick market overview:
# Top 10 by market cap
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist top10
# DeFi tokens
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist defi
# Layer 2 solutions
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist layer2
# Stablecoins
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist stablecoins---
Output Format Examples
Example 4: JSON Output
Machine-readable output for scripting:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol ETH --format jsonOutput:
{
"prices": [
{
"symbol": "ETH",
"name": "Ethereum",
"price": 3456.78,
"currency": "USD",
"change_24h": 1.87,
"change_7d": 5.42,
"volume_24h": 12300000000,
"market_cap": 415200000000,
"timestamp": "2025-01-14T15:30:00.000000",
"source": "coingecko"
}
],
"meta": {
"count": 1,
"currency": "USD",
"timestamp": "2025-01-14T15:30:00.000000"
}
}---
Example 5: CSV Export
Export prices for spreadsheet analysis:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbols BTC,ETH,SOL --format csv --output prices.csvOutput (prices.csv):
symbol,name,price,currency,change_24h,change_7d,volume_24h,market_cap,timestamp,source
BTC,Bitcoin,97234.56,USD,2.34,8.21,28500000000,1920000000000,2025-01-14T15:30:00.000000,coingecko
ETH,Ethereum,3456.78,USD,1.87,5.42,12300000000,415200000000,2025-01-14T15:30:00.000000,coingecko
SOL,Solana,142.34,USD,5.12,12.8,2100000000,61400000000,2025-01-14T15:30:00.000000,coingecko---
Example 6: Minimal Output
Single-line output for shell scripts:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbols BTC,ETH --format minimalOutput:
BTC:$97,234.56(+2.34%) | ETH:$3,456.78(+1.87%)Use in shell scripts:
#!/bin/bash
PRICES=$(python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbols BTC,ETH --format minimal)
echo "Current prices: $PRICES"---
Historical Data Examples
Example 7: 30-Day History
Get price history for the last 30 days:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --period 30dOutput:
================================================================================
HISTORICAL PRICES: BTC
Period: 2024-12-15 to 2025-01-14
================================================================================
Date Price Volume
--------------------------------------------------------------------------------
2024-12-15 $95,123.45 $24.3B
2024-12-16 $94,567.89 $22.1B
...
2025-01-13 $96,789.01 $26.8B
2025-01-14 $97,234.56 $28.5B
--------------------------------------------------------------------------------
Total data points: 30
================================================================================---
Example 8: Custom Date Range
Fetch history for a specific period:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol ETH --start 2024-01-01 --end 2024-12-31---
Example 9: Export Historical Data to CSV
Export OHLCV data for analysis:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --period 90d --format csv --output btc_90d.csvOutput (btc_90d.csv):
date,price,volume
2024-10-16,68234.56,18500000000
2024-10-17,69123.45,19200000000
...---
Currency Examples
Example 10: Different Fiat Currencies
Get prices in alternative currencies:
# Euro
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --currency EUR
# British Pound
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --currency GBP
# Japanese Yen
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --currency JPY---
Search Examples
Example 11: Search for Coins
Find available cryptocurrencies:
# Search by name
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --list --query ethereum
# Search by partial name
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --list --query layerOutput:
================================================================================
SEARCH RESULTS: 'ethereum'
================================================================================
Symbol ID Name
--------------------------------------------------------------------------------
ETH ethereum Ethereum
ETC ethereum-classic Ethereum Classic
ETHW ethereum-pow Ethereum PoW
...
--------------------------------------------------------------------------------
Total: 15 coins
================================================================================---
Cache Management Examples
Example 12: Bypass Cache
Force fresh data fetch:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --no-cache---
Example 13: Clear Cache
Remove all cached data:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --clear-cache---
Integration Examples
Example 14: Portfolio Value Calculation
Use with other skills for portfolio tracking:
# In crypto-portfolio-tracker skill
from price_tracker import get_current_prices
# Get current prices
prices = get_current_prices(["BTC", "ETH", "SOL"])
# Calculate portfolio value
holdings = {"BTC": 0.5, "ETH": 10, "SOL": 100}
total_value = sum(
prices[symbol]["price"] * amount
for symbol, amount in holdings.items()
)
print(f"Portfolio Value: ${total_value:,.2f}")---
Example 15: Shell Script Integration
Use in automated scripts:
#!/bin/bash
# Get BTC price as JSON and extract value
BTC_PRICE=$(python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --format json | jq '.prices[0].price')
# Alert if price drops below threshold
if (( $(echo "$BTC_PRICE < 90000" | bc -l) )); then
echo "ALERT: Bitcoin below $90,000!"
fi---
Example 16: Cron Job for Price Logging
Automated price logging:
# Add to crontab (crontab -e)
# Log prices every 5 minutes
*/5 * * * * python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist top10 --format csv >> /var/log/crypto_prices.csv---
Advanced Examples
Example 17: Multi-Timeframe Analysis
Combine spot and historical data:
# Current price
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC
# 7-day trend
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --period 7d
# 30-day trend
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --period 30d
# Year-to-date
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --start 2025-01-01 --end $(date +%Y-%m-%d)---
Example 18: Verbose Debugging
Debug API and cache behavior:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --verboseOutput:
[DEBUG] Checking cache for spot:btc:usd
[DEBUG] Cache miss
[DEBUG] Fetching from CoinGecko API
[DEBUG] API call: /coins/bitcoin
[DEBUG] Response received in 0.234s
[DEBUG] Caching result (TTL: 30s)
BTC (Bitcoin)
$97,234.56 USD
+2.34% (24h) | Vol: $28.5B | MCap: $1.92T
Cache: 0/1 hits---
Example 19: Custom Watchlist
Create and use a custom watchlist:
1. Edit config/settings.yaml:
watchlists:
custom:
- bitcoin
- ethereum
- solana
- chainlink
- uniswap2. Use it:
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --watchlist custom---
Error Recovery Examples
Example 20: Handling Rate Limits
When rate limited, the skill automatically: 1. Uses cached data if available 2. Falls back to yfinance if installed 3. Shows stale data with warning
# Force yfinance fallback (for testing)
# Temporarily disable CoinGecko by rate limiting
python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbol BTC --verboseOutput with fallback:
[DEBUG] CoinGecko rate limited
[DEBUG] Falling back to yfinance
[DEBUG] yfinance: BTC-USD
BTC (Bitcoin)
$97,234.56 USD (source: yfinance)---
Best Practices
1. Use caching: Default 30-second cache reduces API calls 2. Batch requests: Use --symbols instead of multiple single requests 3. Use watchlists: Predefined lists are optimized for common use cases 4. Export for analysis: Use --format csv for spreadsheet work 5. Script with JSON: Use --format json for programmatic access
--- [Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)
Implementation Details
Output Formats
Price Table (Default)
================================================================================
CRYPTO PRICES Updated: [timestamp]
================================================================================
Symbol Price (USD) 24h Change Volume (24h) Market Cap
--------------------------------------------------------------------------------
BTC $97,234.56 +2.34% $28.5B $1.92T
ETH $3,456.78 +1.87% $12.3B $415.2B
SOL $142.34 +5.12% $2.1B $61.4B
--------------------------------------------------------------------------------
Total 24h Change: +2.44% (weighted)
================================================================================JSON Output (--format json)
{
"prices": [
{
"symbol": "BTC",
"name": "Bitcoin",
"price": 97234.56,
"currency": "USD",
"change_24h": 2.34,
"volume_24h": 28500000000,
"market_cap": 1920000000000,
"timestamp": "[timestamp]",
"source": "coingecko"
}
],
"meta": {
"count": 1,
"currency": "USD",
"cached": false
}
}Historical CSV Export
date,open,high,low,close,volume
[date],95000.00,96500.00,94200.00,96100.00,25000000000
[date],96100.00,97800.00,95800.00,97500.00,27000000000Full Configuration Reference
Edit ${CLAUDE_SKILL_DIR}/config/settings.yaml:
# API Configuration
api:
coingecko:
api_key: ${COINGECKO_API_KEY} # Optional, from env
use_pro: false
yfinance:
enabled: true # Fallback source
# Cache Configuration
cache:
enabled: true
spot_ttl: 30 # Spot price TTL (seconds)
historical_ttl: 3600 # Historical data TTL (seconds)
directory: ./data
# Display Configuration
currency:
default: usd
supported: [usd, eur, gbp, jpy, cad, aud]
# Predefined Watchlists
watchlists:
top10:
- bitcoin
- ethereum
- tether
- binancecoin
- solana
- ripple
- cardano
- avalanche-2
- dogecoin
- polkadot
defi:
- uniswap
- aave
- chainlink
- maker
- compound-governance-token
- curve-dao-token
- sushi
layer2:
- matic-network
- arbitrum
- optimism
- immutable-xRate Limit Handling
The skill automatically manages rate limits through a multi-tier fallback: 1. Uses cached data when available (spot: 30s TTL, historical: 1h TTL) 2. Applies exponential backoff on rate limits (1s, 2s, 4s, max 3 retries) 3. Falls back to yfinance if CoinGecko fails 4. Shows stale cache data with warning as last resort
Integration with Other Skills
This skill provides the price data foundation for 10+ other crypto skills.
Direct Import (recommended for Python skills):
from price_tracker import get_current_prices, get_historical_prices
# Get prices for portfolio valuation
prices = get_current_prices(["BTC", "ETH", "SOL"])CLI Subprocess (for non-Python or isolation):
PRICES=$(python ${CLAUDE_SKILL_DIR}/scripts/price_tracker.py --symbols BTC,ETH --format json)Shared Cache (efficient for batch): Multiple skills can read from ${CLAUDE_SKILL_DIR}/data/cache.json to avoid redundant API calls.
Files
| File | Purpose |
|---|---|
scripts/price_tracker.py | Main CLI entry point |
scripts/api_client.py | CoinGecko/yfinance abstraction |
scripts/cache_manager.py | Cache read/write/invalidation |
scripts/formatters.py | Output formatting |
config/settings.yaml | User configuration |
data/cache.json | Price cache (auto-generated) |
--- [Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)
#!/usr/bin/env python3
"""
Crypto API Client - Multi-Source Price Data Fetcher
Provides unified interface to CoinGecko (primary) and yfinance (fallback)
for cryptocurrency price data retrieval.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import os
import time
from datetime import datetime, timedelta
from typing import Optional, Dict, List, Any
from dataclasses import dataclass
from enum import Enum
try:
import requests
except ImportError:
raise ImportError("Required: pip install requests")
class APIError(Exception):
"""Base exception for API errors."""
pass
class RateLimitError(APIError):
"""Rate limit exceeded."""
pass
class NetworkError(APIError):
"""Network connectivity error."""
pass
class SymbolNotFoundError(APIError):
"""Cryptocurrency symbol not found."""
pass
class DataSource(Enum):
"""Available data sources."""
COINGECKO = "coingecko"
YFINANCE = "yfinance"
@dataclass
class PriceData:
"""Standardized price data structure."""
symbol: str
name: str
price: float
currency: str
change_24h: Optional[float] = None
change_7d: Optional[float] = None
volume_24h: Optional[float] = None
market_cap: Optional[float] = None
timestamp: Optional[str] = None
source: str = "coingecko"
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"symbol": self.symbol,
"name": self.name,
"price": self.price,
"currency": self.currency,
"change_24h": self.change_24h,
"change_7d": self.change_7d,
"volume_24h": self.volume_24h,
"market_cap": self.market_cap,
"timestamp": self.timestamp or datetime.utcnow().isoformat(),
"source": self.source
}
class CryptoAPIClient:
"""
Unified cryptocurrency API client with multi-source support.
Primary: CoinGecko API (free tier or Pro)
Fallback: yfinance (Yahoo Finance)
"""
COINGECKO_BASE = "https://api.coingecko.com/api/v3"
COINGECKO_PRO_BASE = "https://pro-api.coingecko.com/api/v3"
# Common symbol to CoinGecko ID mapping
SYMBOL_MAP = {
"BTC": "bitcoin",
"ETH": "ethereum",
"SOL": "solana",
"XRP": "ripple",
"ADA": "cardano",
"DOGE": "dogecoin",
"DOT": "polkadot",
"AVAX": "avalanche-2",
"MATIC": "matic-network",
"LINK": "chainlink",
"UNI": "uniswap",
"AAVE": "aave",
"MKR": "maker",
"COMP": "compound-governance-token",
"CRV": "curve-dao-token",
"SUSHI": "sushi",
"ARB": "arbitrum",
"OP": "optimism",
"IMX": "immutable-x",
"USDT": "tether",
"USDC": "usd-coin",
"DAI": "dai",
"BNB": "binancecoin",
"SHIB": "shiba-inu",
"PEPE": "pepe",
"BONK": "bonk"
}
def __init__(self, config: Optional[dict] = None):
"""
Initialize the API client.
Args:
config: Optional configuration dictionary with API keys
"""
self.config = config or {}
# CoinGecko API setup
self.api_key = (
os.environ.get("COINGECKO_API_KEY") or
self.config.get("api", {}).get("coingecko", {}).get("api_key")
)
self.use_pro = (
self.api_key and
self.config.get("api", {}).get("coingecko", {}).get("use_pro", False)
)
self.base_url = (
self.COINGECKO_PRO_BASE if self.use_pro else self.COINGECKO_BASE
)
# Rate limiting
self._last_request_time = 0
self._min_request_interval = 1.5 # seconds (CoinGecko free tier)
self._retry_count = 0
self._max_retries = 3
# yfinance availability
self._yfinance_available = None
# Coin list cache
self._coin_list_cache = None
self._coin_list_timestamp = None
def _check_yfinance(self) -> bool:
"""Check if yfinance is available."""
if self._yfinance_available is None:
try:
import yfinance
self._yfinance_available = True
except ImportError:
self._yfinance_available = False
return self._yfinance_available
def _rate_limit(self) -> None:
"""Apply rate limiting between requests."""
elapsed = time.time() - self._last_request_time
if elapsed < self._min_request_interval:
time.sleep(self._min_request_interval - elapsed)
self._last_request_time = time.time()
def _make_request(
self,
endpoint: str,
params: Optional[dict] = None
) -> dict:
"""
Make HTTP request to CoinGecko API with error handling.
Args:
endpoint: API endpoint path
params: Query parameters
Returns:
JSON response data
Raises:
RateLimitError: Rate limit exceeded
NetworkError: Connection failed
APIError: Other API errors
"""
self._rate_limit()
url = f"{self.base_url}{endpoint}"
headers = {}
if self.api_key:
if self.use_pro:
headers["x-cg-pro-api-key"] = self.api_key
else:
headers["x-cg-demo-api-key"] = self.api_key
try:
response = requests.get(
url,
params=params,
headers=headers,
timeout=10
)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
raise RateLimitError(
f"Rate limit exceeded. Retry after {retry_after}s"
)
if response.status_code == 404:
raise SymbolNotFoundError("Resource not found")
response.raise_for_status()
return response.json()
except requests.exceptions.ConnectionError as e:
raise NetworkError(f"Connection failed: {e}")
except requests.exceptions.Timeout:
raise NetworkError("Request timed out")
except requests.exceptions.RequestException as e:
raise APIError(f"Request failed: {e}")
def _symbol_to_id(self, symbol: str) -> str:
"""
Convert ticker symbol to CoinGecko ID.
Args:
symbol: Cryptocurrency ticker (e.g., "BTC")
Returns:
CoinGecko ID (e.g., "bitcoin")
"""
symbol_upper = symbol.upper()
# Check direct mapping
if symbol_upper in self.SYMBOL_MAP:
return self.SYMBOL_MAP[symbol_upper]
# Assume lowercase symbol is the ID
return symbol.lower()
def list_coins(
self,
query: Optional[str] = None,
limit: int = 100
) -> List[dict]:
"""
List available cryptocurrencies.
Args:
query: Optional search query
limit: Maximum results to return
Returns:
List of coin info dictionaries
"""
# Refresh coin list if stale (older than 1 hour)
now = time.time()
if (self._coin_list_cache is None or
self._coin_list_timestamp is None or
now - self._coin_list_timestamp > 3600):
try:
self._coin_list_cache = self._make_request("/coins/list")
self._coin_list_timestamp = now
except APIError:
# Return empty if we can't fetch
return []
coins = self._coin_list_cache
# Filter by query
if query:
query_lower = query.lower()
coins = [
c for c in coins
if (query_lower in c.get("id", "").lower() or
query_lower in c.get("name", "").lower() or
query_lower in c.get("symbol", "").lower())
]
# Limit results
return coins[:limit]
def get_current_price(
self,
symbol: str,
currency: str = "usd"
) -> dict:
"""
Get current price for a cryptocurrency.
Args:
symbol: Cryptocurrency symbol (e.g., "BTC") or CoinGecko ID
currency: Fiat currency code (e.g., "usd")
Returns:
Price data dictionary
"""
coin_id = self._symbol_to_id(symbol)
try:
data = self._make_request(
f"/coins/{coin_id}",
params={
"localization": "false",
"tickers": "false",
"community_data": "false",
"developer_data": "false",
"sparkline": "false"
}
)
market_data = data.get("market_data", {})
current_price = market_data.get("current_price", {})
price = current_price.get(currency.lower())
if price is None:
raise APIError(f"Price not available in {currency}")
return PriceData(
symbol=data.get("symbol", symbol).upper(),
name=data.get("name", symbol),
price=price,
currency=currency.upper(),
change_24h=market_data.get("price_change_percentage_24h"),
change_7d=market_data.get("price_change_percentage_7d"),
volume_24h=market_data.get("total_volume", {}).get(currency.lower()),
market_cap=market_data.get("market_cap", {}).get(currency.lower()),
source="coingecko"
).to_dict()
except SymbolNotFoundError:
# Try yfinance fallback
if self._check_yfinance():
return self._get_price_yfinance(symbol, currency)
raise SymbolNotFoundError(f"Unknown symbol: {symbol}")
except RateLimitError:
# Try yfinance fallback
if self._check_yfinance():
return self._get_price_yfinance(symbol, currency)
raise
def _get_price_yfinance(
self,
symbol: str,
currency: str = "usd"
) -> dict:
"""
Get price using yfinance as fallback.
Args:
symbol: Cryptocurrency symbol
currency: Fiat currency
Returns:
Price data dictionary
"""
try:
import yfinance as yf
except ImportError:
raise APIError("yfinance not installed")
# yfinance uses format like "BTC-USD"
ticker_symbol = f"{symbol.upper()}-{currency.upper()}"
try:
ticker = yf.Ticker(ticker_symbol)
info = ticker.info
if not info or "regularMarketPrice" not in info:
raise SymbolNotFoundError(f"Unknown symbol: {symbol}")
return PriceData(
symbol=symbol.upper(),
name=info.get("shortName", symbol),
price=info.get("regularMarketPrice", 0),
currency=currency.upper(),
change_24h=info.get("regularMarketChangePercent"),
volume_24h=info.get("regularMarketVolume"),
market_cap=info.get("marketCap"),
source="yfinance"
).to_dict()
except Exception as e:
raise APIError(f"yfinance error: {e}")
def get_historical_prices(
self,
symbol: str,
currency: str = "usd",
period: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> List[dict]:
"""
Get historical OHLCV data.
Args:
symbol: Cryptocurrency symbol
currency: Fiat currency
period: Period string (e.g., "7d", "30d", "90d", "1y", "max")
start_date: Custom start date
end_date: Custom end date
Returns:
List of OHLCV data points
"""
coin_id = self._symbol_to_id(symbol)
# Convert period to days
if period:
period_map = {
"1d": 1, "7d": 7, "14d": 14, "30d": 30,
"60d": 60, "90d": 90, "180d": 180,
"1y": 365, "2y": 730, "max": "max"
}
days = period_map.get(period.lower(), 30)
elif start_date and end_date:
days = (end_date - start_date).days
else:
days = 30
try:
# Use market_chart for period-based queries
if isinstance(days, int):
data = self._make_request(
f"/coins/{coin_id}/market_chart",
params={
"vs_currency": currency.lower(),
"days": days,
"interval": "daily" if days > 1 else "hourly"
}
)
prices = data.get("prices", [])
volumes = data.get("total_volumes", [])
results = []
for i, (timestamp, price) in enumerate(prices):
dt = datetime.fromtimestamp(timestamp / 1000)
vol = volumes[i][1] if i < len(volumes) else None
results.append({
"date": dt.strftime("%Y-%m-%d"),
"timestamp": timestamp,
"price": price,
"volume": vol
})
return results
else: # "max" period
data = self._make_request(
f"/coins/{coin_id}/market_chart",
params={
"vs_currency": currency.lower(),
"days": "max"
}
)
prices = data.get("prices", [])
results = []
for timestamp, price in prices:
dt = datetime.fromtimestamp(timestamp / 1000)
results.append({
"date": dt.strftime("%Y-%m-%d"),
"timestamp": timestamp,
"price": price
})
return results
except (RateLimitError, SymbolNotFoundError):
# Try yfinance fallback
if self._check_yfinance():
return self._get_historical_yfinance(
symbol, currency, period, start_date, end_date
)
raise
def _get_historical_yfinance(
self,
symbol: str,
currency: str = "usd",
period: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> List[dict]:
"""
Get historical data using yfinance.
Args:
symbol: Cryptocurrency symbol
currency: Fiat currency
period: Period string
start_date: Start date
end_date: End date
Returns:
List of OHLCV data points
"""
try:
import yfinance as yf
except ImportError:
raise APIError("yfinance not installed")
ticker_symbol = f"{symbol.upper()}-{currency.upper()}"
ticker = yf.Ticker(ticker_symbol)
# Map period to yfinance format
if period:
yf_period_map = {
"1d": "1d", "7d": "7d", "14d": "14d", "30d": "1mo",
"60d": "2mo", "90d": "3mo", "180d": "6mo",
"1y": "1y", "2y": "2y", "max": "max"
}
yf_period = yf_period_map.get(period.lower(), "1mo")
df = ticker.history(period=yf_period)
elif start_date and end_date:
df = ticker.history(start=start_date, end=end_date)
else:
df = ticker.history(period="1mo")
results = []
for date, row in df.iterrows():
results.append({
"date": date.strftime("%Y-%m-%d"),
"open": row.get("Open"),
"high": row.get("High"),
"low": row.get("Low"),
"close": row.get("Close"),
"volume": row.get("Volume")
})
return results
#!/usr/bin/env python3
"""
Cache Manager - TTL-Based Caching for Price Data
Provides intelligent caching with configurable TTL for spot prices
and historical data to minimize API calls and improve response times.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import json
import time
import hashlib
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List
from dataclasses import dataclass, asdict
import threading
@dataclass
class CacheEntry:
"""Represents a cached item with metadata."""
data: Any
timestamp: float
ttl: int
key: str
@property
def is_expired(self) -> bool:
"""Check if cache entry has expired."""
return time.time() - self.timestamp > self.ttl
@property
def age(self) -> float:
"""Get age of cache entry in seconds."""
return time.time() - self.timestamp
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
"data": self.data,
"timestamp": self.timestamp,
"ttl": self.ttl,
"key": self.key
}
@classmethod
def from_dict(cls, d: dict) -> "CacheEntry":
"""Create CacheEntry from dictionary."""
return cls(
data=d["data"],
timestamp=d["timestamp"],
ttl=d["ttl"],
key=d["key"]
)
class CacheManager:
"""
TTL-based cache manager for cryptocurrency price data.
Features:
- Separate TTLs for spot prices (short) and historical data (long)
- File-based persistence
- Thread-safe operations
- Automatic cleanup of expired entries
- Stale data access for fallback scenarios
"""
def __init__(
self,
cache_dir: Optional[Path] = None,
spot_ttl: int = 30,
historical_ttl: int = 3600
):
"""
Initialize the cache manager.
Args:
cache_dir: Directory for cache files (default: ./data)
spot_ttl: TTL for spot prices in seconds (default: 30)
historical_ttl: TTL for historical data in seconds (default: 3600)
"""
self.cache_dir = Path(cache_dir) if cache_dir else Path("./data")
self.spot_ttl = spot_ttl
self.historical_ttl = historical_ttl
# Ensure cache directory exists
self.cache_dir.mkdir(parents=True, exist_ok=True)
# In-memory cache
self._memory_cache: Dict[str, CacheEntry] = {}
self._lock = threading.RLock()
# File paths
self._spot_cache_file = self.cache_dir / "spot_cache.json"
self._historical_cache_file = self.cache_dir / "historical_cache.json"
# Load existing cache
self._load_cache()
def _load_cache(self) -> None:
"""Load cache from disk."""
with self._lock:
# Load spot cache
if self._spot_cache_file.exists():
try:
with open(self._spot_cache_file, "r") as f:
data = json.load(f)
for key, entry_data in data.items():
entry = CacheEntry.from_dict(entry_data)
if not entry.is_expired:
self._memory_cache[key] = entry
except (json.JSONDecodeError, KeyError):
pass
# Load historical cache
if self._historical_cache_file.exists():
try:
with open(self._historical_cache_file, "r") as f:
data = json.load(f)
for key, entry_data in data.items():
entry = CacheEntry.from_dict(entry_data)
if not entry.is_expired:
self._memory_cache[key] = entry
except (json.JSONDecodeError, KeyError):
pass
def _save_cache(self) -> None:
"""Persist cache to disk."""
with self._lock:
spot_data = {}
historical_data = {}
for key, entry in self._memory_cache.items():
if key.startswith("spot:"):
spot_data[key] = entry.to_dict()
elif key.startswith("hist:"):
historical_data[key] = entry.to_dict()
# Write spot cache
try:
with open(self._spot_cache_file, "w") as f:
json.dump(spot_data, f, indent=2)
except IOError:
pass
# Write historical cache
try:
with open(self._historical_cache_file, "w") as f:
json.dump(historical_data, f, indent=2)
except IOError:
pass
def _make_key(self, prefix: str, *parts: str) -> str:
"""
Create a cache key from parts.
Args:
prefix: Key prefix (e.g., "spot", "hist")
*parts: Key components
Returns:
Cache key string
"""
key_str = ":".join([prefix] + [str(p).lower() for p in parts])
return key_str
def get_spot_price(
self,
symbol: str,
currency: str,
allow_stale: bool = False
) -> Optional[dict]:
"""
Get cached spot price.
Args:
symbol: Cryptocurrency symbol
currency: Fiat currency
allow_stale: Return stale data if no fresh data available
Returns:
Cached price data or None
"""
key = self._make_key("spot", symbol, currency)
with self._lock:
entry = self._memory_cache.get(key)
if entry is None:
return None
if entry.is_expired:
if allow_stale:
# Return stale data with warning flag
data = entry.data.copy() if isinstance(entry.data, dict) else entry.data
if isinstance(data, dict):
data["_cache_stale"] = True
data["_cache_age"] = entry.age
return data
return None
data = entry.data.copy() if isinstance(entry.data, dict) else entry.data
if isinstance(data, dict):
data["_cached"] = True
data["_cache_age"] = entry.age
return data
def set_spot_price(
self,
symbol: str,
currency: str,
data: dict
) -> None:
"""
Cache spot price data.
Args:
symbol: Cryptocurrency symbol
currency: Fiat currency
data: Price data to cache
"""
key = self._make_key("spot", symbol, currency)
with self._lock:
self._memory_cache[key] = CacheEntry(
data=data,
timestamp=time.time(),
ttl=self.spot_ttl,
key=key
)
self._save_cache()
def get_historical(
self,
cache_key: str,
allow_stale: bool = False
) -> Optional[List[dict]]:
"""
Get cached historical data.
Args:
cache_key: Unique key for the historical query
allow_stale: Return stale data if no fresh data available
Returns:
Cached historical data or None
"""
key = self._make_key("hist", cache_key)
with self._lock:
entry = self._memory_cache.get(key)
if entry is None:
return None
if entry.is_expired:
if allow_stale:
return entry.data
return None
return entry.data
def set_historical(
self,
cache_key: str,
data: List[dict]
) -> None:
"""
Cache historical data.
Args:
cache_key: Unique key for the historical query
data: Historical data to cache
"""
key = self._make_key("hist", cache_key)
with self._lock:
self._memory_cache[key] = CacheEntry(
data=data,
timestamp=time.time(),
ttl=self.historical_ttl,
key=key
)
self._save_cache()
def invalidate(self, pattern: Optional[str] = None) -> int:
"""
Invalidate cache entries matching a pattern.
Args:
pattern: Key pattern to match (None = all)
Returns:
Number of entries invalidated
"""
with self._lock:
if pattern is None:
count = len(self._memory_cache)
self._memory_cache.clear()
else:
pattern_lower = pattern.lower()
keys_to_remove = [
k for k in self._memory_cache
if pattern_lower in k.lower()
]
count = len(keys_to_remove)
for key in keys_to_remove:
del self._memory_cache[key]
self._save_cache()
return count
def clear(self) -> None:
"""Clear all cached data."""
with self._lock:
self._memory_cache.clear()
# Remove cache files
if self._spot_cache_file.exists():
self._spot_cache_file.unlink()
if self._historical_cache_file.exists():
self._historical_cache_file.unlink()
def cleanup(self) -> int:
"""
Remove all expired entries.
Returns:
Number of entries removed
"""
with self._lock:
expired_keys = [
k for k, v in self._memory_cache.items()
if v.is_expired
]
for key in expired_keys:
del self._memory_cache[key]
if expired_keys:
self._save_cache()
return len(expired_keys)
def get_stats(self) -> dict:
"""
Get cache statistics.
Returns:
Dictionary with cache stats
"""
with self._lock:
spot_entries = [
e for k, e in self._memory_cache.items()
if k.startswith("spot:")
]
hist_entries = [
e for k, e in self._memory_cache.items()
if k.startswith("hist:")
]
spot_expired = sum(1 for e in spot_entries if e.is_expired)
hist_expired = sum(1 for e in hist_entries if e.is_expired)
return {
"total_entries": len(self._memory_cache),
"spot_entries": len(spot_entries),
"spot_expired": spot_expired,
"historical_entries": len(hist_entries),
"historical_expired": hist_expired,
"cache_dir": str(self.cache_dir),
"spot_ttl": self.spot_ttl,
"historical_ttl": self.historical_ttl
}
def get_all_symbols(self) -> List[str]:
"""
Get all symbols currently in cache.
Returns:
List of cached symbols
"""
with self._lock:
symbols = set()
for key in self._memory_cache.keys():
if key.startswith("spot:"):
parts = key.split(":")
if len(parts) >= 2:
symbols.add(parts[1].upper())
return sorted(list(symbols))
#!/usr/bin/env python3
"""
Price Formatters - Output Formatting for Price Data
Provides human-readable table output, JSON formatting, CSV export,
and minimal output modes for cryptocurrency price data.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import json
import csv
import io
from datetime import datetime
from typing import List, Optional, Dict, Any
class PriceFormatter:
"""
Formats cryptocurrency price data for various output modes.
Supports:
- Table: Human-readable aligned tables
- JSON: Machine-readable JSON
- CSV: Spreadsheet-compatible export
- Minimal: Single-line output for scripting
"""
# Currency symbols for formatting
CURRENCY_SYMBOLS = {
"USD": "$",
"EUR": "€",
"GBP": "£",
"JPY": "¥",
"CAD": "C$",
"AUD": "A$",
"CHF": "CHF ",
"CNY": "¥",
"INR": "₹",
"KRW": "₩"
}
def __init__(self, currency: str = "USD"):
"""
Initialize formatter.
Args:
currency: Default currency code
"""
self.currency = currency.upper()
self.currency_symbol = self.CURRENCY_SYMBOLS.get(self.currency, "")
def _format_price(self, price: float) -> str:
"""
Format a price value with appropriate precision.
Args:
price: Price value
Returns:
Formatted price string
"""
if price >= 1000:
return f"{self.currency_symbol}{price:,.2f}"
elif price >= 1:
return f"{self.currency_symbol}{price:.2f}"
elif price >= 0.0001:
return f"{self.currency_symbol}{price:.6f}"
else:
return f"{self.currency_symbol}{price:.8f}"
def _format_large_number(self, value: Optional[float]) -> str:
"""
Format large numbers with K/M/B suffixes.
Args:
value: Number to format
Returns:
Formatted string
"""
if value is None:
return "N/A"
if value >= 1_000_000_000_000:
return f"${value / 1_000_000_000_000:.2f}T"
elif value >= 1_000_000_000:
return f"${value / 1_000_000_000:.2f}B"
elif value >= 1_000_000:
return f"${value / 1_000_000:.2f}M"
elif value >= 1_000:
return f"${value / 1_000:.2f}K"
else:
return f"${value:.2f}"
def _format_change(self, change: Optional[float]) -> str:
"""
Format percentage change with color indicators.
Args:
change: Percentage change
Returns:
Formatted change string
"""
if change is None:
return "N/A"
if change >= 0:
return f"+{change:.2f}%"
else:
return f"{change:.2f}%"
def format_prices(
self,
prices: List[dict],
format_type: str = "table",
verbose: bool = False
) -> str:
"""
Format a list of price data.
Args:
prices: List of price dictionaries
format_type: Output format (table, json, csv, minimal)
verbose: Include extra details
Returns:
Formatted output string
"""
if format_type == "json":
return self._format_json(prices)
elif format_type == "csv":
return self._format_csv_prices(prices)
elif format_type == "minimal":
return self._format_minimal(prices)
else:
return self._format_table(prices, verbose)
def _format_table(self, prices: List[dict], verbose: bool = False) -> str:
"""Format prices as aligned table."""
if not prices:
return "No price data available"
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines = []
# Header
lines.append("=" * 80)
lines.append(f" CRYPTO PRICES{' ' * 45}Updated: {timestamp}")
lines.append("=" * 80)
lines.append("")
# Column headers
if verbose:
header = f" {'Symbol':<10} {'Price':>14} {'24h':>10} {'7d':>10} {'Volume':>12} {'Market Cap':>12}"
else:
header = f" {'Symbol':<10} {'Price':>14} {'24h Change':>12} {'Volume (24h)':>14} {'Market Cap':>12}"
lines.append(header)
lines.append("-" * 80)
# Data rows
total_change = 0
total_weight = 0
for p in prices:
symbol = p.get("symbol", "???").upper()
price = p.get("price", 0)
change_24h = p.get("change_24h")
change_7d = p.get("change_7d")
volume = p.get("volume_24h")
market_cap = p.get("market_cap")
price_str = self._format_price(price)
change_24h_str = self._format_change(change_24h)
change_7d_str = self._format_change(change_7d) if verbose else ""
volume_str = self._format_large_number(volume)
mcap_str = self._format_large_number(market_cap)
# Track weighted average change
if change_24h is not None and market_cap is not None:
total_change += change_24h * market_cap
total_weight += market_cap
# Stale/cached indicators
suffix = ""
if p.get("_cache_stale"):
suffix = " (stale)"
elif p.get("_cached"):
suffix = " (cached)"
if verbose:
row = f" {symbol:<10} {price_str:>14} {change_24h_str:>10} {change_7d_str:>10} {volume_str:>12} {mcap_str:>12}{suffix}"
else:
row = f" {symbol:<10} {price_str:>14} {change_24h_str:>12} {volume_str:>14} {mcap_str:>12}{suffix}"
lines.append(row)
lines.append("-" * 80)
# Summary
if total_weight > 0:
weighted_change = total_change / total_weight
lines.append(f" Total 24h Change: {self._format_change(weighted_change)} (weighted)")
lines.append("")
lines.append("=" * 80)
return "\n".join(lines)
def _format_json(self, prices: List[dict]) -> str:
"""Format prices as JSON."""
# Clean internal fields
cleaned = []
for p in prices:
clean_p = {k: v for k, v in p.items() if not k.startswith("_")}
cleaned.append(clean_p)
output = {
"prices": cleaned,
"meta": {
"count": len(cleaned),
"currency": self.currency,
"timestamp": datetime.utcnow().isoformat()
}
}
return json.dumps(output, indent=2)
def _format_csv_prices(self, prices: List[dict]) -> str:
"""Format prices as CSV."""
if not prices:
return ""
output = io.StringIO()
fieldnames = ["symbol", "name", "price", "currency", "change_24h",
"change_7d", "volume_24h", "market_cap", "timestamp", "source"]
writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore")
writer.writeheader()
for p in prices:
# Filter out internal fields
clean_p = {k: v for k, v in p.items() if not k.startswith("_")}
writer.writerow(clean_p)
return output.getvalue()
def _format_minimal(self, prices: List[dict]) -> str:
"""Format prices as minimal one-line output."""
parts = []
for p in prices:
symbol = p.get("symbol", "???").upper()
price = p.get("price", 0)
change = p.get("change_24h")
if change is not None:
change_str = f"({self._format_change(change)})"
else:
change_str = ""
parts.append(f"{symbol}:{self._format_price(price)}{change_str}")
return " | ".join(parts)
def format_historical(
self,
symbol: str,
data: List[dict],
format_type: str = "table"
) -> str:
"""
Format historical price data.
Args:
symbol: Cryptocurrency symbol
data: List of historical data points
format_type: Output format
Returns:
Formatted output string
"""
if format_type == "json":
return self._format_historical_json(symbol, data)
elif format_type == "csv":
return self._format_historical_csv(data)
else:
return self._format_historical_table(symbol, data)
def _format_historical_table(self, symbol: str, data: List[dict]) -> str:
"""Format historical data as table."""
if not data:
return "No historical data available"
lines = []
lines.append("=" * 70)
lines.append(f" HISTORICAL PRICES: {symbol.upper()}")
lines.append(f" Period: {data[0].get('date', 'N/A')} to {data[-1].get('date', 'N/A')}")
lines.append("=" * 70)
lines.append("")
# Determine columns based on data
has_ohlc = "open" in data[0]
if has_ohlc:
lines.append(f" {'Date':<12} {'Open':>12} {'High':>12} {'Low':>12} {'Close':>12}")
else:
lines.append(f" {'Date':<12} {'Price':>14} {'Volume':>16}")
lines.append("-" * 70)
for d in data[-30:]: # Show last 30 entries
date = d.get("date", "N/A")
if has_ohlc:
open_p = self._format_price(d.get("open", 0))
high = self._format_price(d.get("high", 0))
low = self._format_price(d.get("low", 0))
close = self._format_price(d.get("close", 0))
lines.append(f" {date:<12} {open_p:>12} {high:>12} {low:>12} {close:>12}")
else:
price = self._format_price(d.get("price", 0))
volume = self._format_large_number(d.get("volume"))
lines.append(f" {date:<12} {price:>14} {volume:>16}")
if len(data) > 30:
lines.append(f" ... ({len(data) - 30} more entries)")
lines.append("-" * 70)
lines.append(f" Total data points: {len(data)}")
lines.append("=" * 70)
return "\n".join(lines)
def _format_historical_json(self, symbol: str, data: List[dict]) -> str:
"""Format historical data as JSON."""
output = {
"symbol": symbol.upper(),
"data": data,
"meta": {
"count": len(data),
"start_date": data[0].get("date") if data else None,
"end_date": data[-1].get("date") if data else None,
"timestamp": datetime.utcnow().isoformat()
}
}
return json.dumps(output, indent=2)
def _format_historical_csv(self, data: List[dict]) -> str:
"""Format historical data as CSV."""
if not data:
return ""
output = io.StringIO()
# Determine columns from first row
if "open" in data[0]:
fieldnames = ["date", "open", "high", "low", "close", "volume"]
else:
fieldnames = ["date", "price", "volume"]
writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore")
writer.writeheader()
for d in data:
writer.writerow(d)
return output.getvalue()
def print_coin_list(
self,
coins: List[dict],
query: Optional[str] = None
) -> None:
"""
Print a list of available coins.
Args:
coins: List of coin info dictionaries
query: Search query that was used
"""
if not coins:
if query:
print(f"No coins found matching '{query}'")
else:
print("No coins available")
return
print("=" * 60)
if query:
print(f" SEARCH RESULTS: '{query}'")
else:
print(" AVAILABLE CRYPTOCURRENCIES")
print("=" * 60)
print(f" {'Symbol':<10} {'ID':<25} {'Name':<20}")
print("-" * 60)
for coin in coins[:50]: # Limit to 50 results
symbol = coin.get("symbol", "").upper()
coin_id = coin.get("id", "")
name = coin.get("name", "")
# Truncate long names
if len(name) > 18:
name = name[:17] + "..."
print(f" {symbol:<10} {coin_id:<25} {name:<20}")
if len(coins) > 50:
print(f" ... and {len(coins) - 50} more")
print("-" * 60)
print(f" Total: {len(coins)} coins")
print("=" * 60)
#!/usr/bin/env python3
"""
Crypto Price Tracker - Main CLI Entry Point
Track real-time and historical cryptocurrency prices across exchanges.
Foundation skill for the crypto plugin ecosystem.
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
# Add scripts directory to path for local imports
SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(SCRIPT_DIR))
from api_client import CryptoAPIClient, APIError
from cache_manager import CacheManager
from formatters import PriceFormatter
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Track real-time and historical cryptocurrency prices",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --symbol BTC # Get Bitcoin price
%(prog)s --symbols BTC,ETH,SOL # Get multiple prices
%(prog)s --watchlist top10 # Scan top 10 by market cap
%(prog)s --symbol BTC --period 30d # 30-day history
%(prog)s --symbol ETH --output csv # Export to CSV
%(prog)s --list # Search available coins
"""
)
# Symbol selection (mutually exclusive)
symbol_group = parser.add_mutually_exclusive_group()
symbol_group.add_argument(
"--symbol", "-s",
type=str,
help="Single cryptocurrency symbol (e.g., BTC, ETH)"
)
symbol_group.add_argument(
"--symbols",
type=str,
help="Comma-separated list of symbols (e.g., BTC,ETH,SOL)"
)
symbol_group.add_argument(
"--watchlist", "-w",
type=str,
choices=["top10", "defi", "layer2", "stablecoins", "memecoins", "custom"],
help="Use predefined watchlist"
)
symbol_group.add_argument(
"--list", "-l",
action="store_true",
help="List available cryptocurrencies (search with --query)"
)
# Search options
parser.add_argument(
"--query", "-q",
type=str,
help="Search query for --list mode"
)
# Historical data options
parser.add_argument(
"--period", "-p",
type=str,
help="Historical period (e.g., 7d, 30d, 90d, 1y, max)"
)
parser.add_argument(
"--start",
type=str,
help="Start date for custom range (YYYY-MM-DD)"
)
parser.add_argument(
"--end",
type=str,
help="End date for custom range (YYYY-MM-DD)"
)
# Output options
parser.add_argument(
"--format", "-f",
type=str,
choices=["table", "json", "csv", "minimal"],
default="table",
help="Output format (default: table)"
)
parser.add_argument(
"--output", "-o",
type=str,
help="Output file path (default: stdout)"
)
# Currency options
parser.add_argument(
"--currency", "-c",
type=str,
default="usd",
help="Fiat currency for prices (default: usd)"
)
# Cache control
parser.add_argument(
"--no-cache",
action="store_true",
help="Bypass cache and fetch fresh data"
)
parser.add_argument(
"--clear-cache",
action="store_true",
help="Clear all cached data"
)
# Verbosity
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable verbose output"
)
parser.add_argument(
"--quiet",
action="store_true",
help="Suppress non-essential output"
)
# Version
parser.add_argument(
"--version",
action="version",
version="%(prog)s 2.0.0"
)
return parser.parse_args()
def load_config() -> dict:
"""Load configuration from settings.yaml."""
config_path = SCRIPT_DIR.parent / "config" / "settings.yaml"
if config_path.exists():
try:
import yaml
with open(config_path, "r") as f:
return yaml.safe_load(f) or {}
except ImportError:
# Fallback if PyYAML not installed
pass
except Exception:
pass
# Default configuration
return {
"cache": {
"enabled": True,
"spot_ttl": 30,
"historical_ttl": 3600,
"directory": str(SCRIPT_DIR.parent / "data")
},
"currency": {
"default": "usd"
},
"watchlists": {
"top10": ["bitcoin", "ethereum", "tether", "binancecoin", "solana",
"ripple", "cardano", "avalanche-2", "dogecoin", "polkadot"],
"defi": ["uniswap", "aave", "chainlink", "maker",
"compound-governance-token", "curve-dao-token", "sushi"],
"layer2": ["matic-network", "arbitrum", "optimism", "immutable-x"],
"stablecoins": ["tether", "usd-coin", "dai", "frax", "true-usd"],
"memecoins": ["dogecoin", "shiba-inu", "pepe", "floki", "bonk"]
}
}
def get_watchlist_symbols(watchlist_name: str, config: dict) -> list:
"""Get symbols for a watchlist."""
watchlists = config.get("watchlists", {})
if watchlist_name == "custom":
custom = watchlists.get("custom", [])
if not custom:
print("Error: Custom watchlist is empty. Configure in settings.yaml",
file=sys.stderr)
sys.exit(1)
return custom
symbols = watchlists.get(watchlist_name)
if not symbols:
print(f"Error: Unknown watchlist '{watchlist_name}'", file=sys.stderr)
sys.exit(1)
return symbols
def handle_list_command(
client: CryptoAPIClient,
query: Optional[str],
formatter: PriceFormatter,
args: argparse.Namespace
) -> None:
"""Handle --list command to search available coins."""
try:
coins = client.list_coins(query=query)
if args.format == "json":
print(json.dumps(coins, indent=2))
else:
formatter.print_coin_list(coins, query)
except APIError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def handle_price_command(
symbols: list,
client: CryptoAPIClient,
cache: CacheManager,
formatter: PriceFormatter,
args: argparse.Namespace,
config: dict
) -> None:
"""Handle price lookup for one or more symbols."""
currency = args.currency.lower()
use_cache = not args.no_cache and config.get("cache", {}).get("enabled", True)
results = []
cache_hits = 0
for symbol in symbols:
# Check cache first
if use_cache:
cached = cache.get_spot_price(symbol, currency)
if cached:
results.append(cached)
cache_hits += 1
continue
# Fetch from API
try:
price_data = client.get_current_price(symbol, currency)
results.append(price_data)
# Cache the result
if use_cache:
cache.set_spot_price(symbol, currency, price_data)
except APIError as e:
if args.verbose:
print(f"Warning: Failed to fetch {symbol}: {e}", file=sys.stderr)
# Try cache as fallback
if use_cache:
stale = cache.get_spot_price(symbol, currency, allow_stale=True)
if stale:
stale["_stale"] = True
results.append(stale)
if not args.quiet:
print(f"Warning: Using stale cache for {symbol}",
file=sys.stderr)
if not results:
print("Error: No price data available", file=sys.stderr)
sys.exit(1)
# Output results
output_text = formatter.format_prices(
results,
format_type=args.format,
verbose=args.verbose
)
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_text)
if not args.quiet:
print(f"Output written to {output_path}")
else:
print(output_text)
# Cache stats
if args.verbose and use_cache and len(symbols) > 1:
print(f"\nCache: {cache_hits}/{len(symbols)} hits", file=sys.stderr)
def handle_historical_command(
symbol: str,
client: CryptoAPIClient,
cache: CacheManager,
formatter: PriceFormatter,
args: argparse.Namespace,
config: dict
) -> None:
"""Handle historical price lookup."""
currency = args.currency.lower()
use_cache = not args.no_cache and config.get("cache", {}).get("enabled", True)
# Parse period or date range
if args.start and args.end:
start_date = datetime.strptime(args.start, "%Y-%m-%d")
end_date = datetime.strptime(args.end, "%Y-%m-%d")
period = None
elif args.period:
period = args.period
start_date = None
end_date = None
else:
period = "30d" # Default
start_date = None
end_date = None
# Check cache
cache_key = f"{symbol}_{period or f'{args.start}_{args.end}'}_{currency}"
if use_cache:
cached = cache.get_historical(cache_key)
if cached:
results = cached
else:
results = None
else:
results = None
# Fetch if not cached
if results is None:
try:
results = client.get_historical_prices(
symbol,
currency=currency,
period=period,
start_date=start_date,
end_date=end_date
)
if use_cache:
cache.set_historical(cache_key, results)
except APIError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
# Output results
output_text = formatter.format_historical(
symbol,
results,
format_type=args.format
)
if args.output:
output_path = Path(args.output)
if args.format == "csv" and not output_path.suffix:
output_path = output_path.with_suffix(".csv")
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w") as f:
f.write(output_text)
if not args.quiet:
print(f"Output written to {output_path}")
else:
print(output_text)
def main() -> None:
"""Main entry point."""
args = parse_args()
config = load_config()
# Initialize components
cache_dir = Path(config.get("cache", {}).get("directory",
str(SCRIPT_DIR.parent / "data")))
cache = CacheManager(
cache_dir=cache_dir,
spot_ttl=config.get("cache", {}).get("spot_ttl", 30),
historical_ttl=config.get("cache", {}).get("historical_ttl", 3600)
)
client = CryptoAPIClient(config=config)
formatter = PriceFormatter(currency=args.currency.upper())
# Handle cache operations
if args.clear_cache:
cache.clear()
if not args.quiet:
print("Cache cleared")
return
# Handle list command
if args.list:
handle_list_command(client, args.query, formatter, args)
return
# Determine symbols to fetch
symbols = []
if args.symbol:
symbols = [args.symbol]
elif args.symbols:
symbols = [s.strip() for s in args.symbols.split(",")]
elif args.watchlist:
symbols = get_watchlist_symbols(args.watchlist, config)
else:
# Default: show help
print("Error: Specify --symbol, --symbols, --watchlist, or --list",
file=sys.stderr)
print("Use --help for usage information", file=sys.stderr)
sys.exit(1)
# Handle historical vs current prices
if args.period or args.start:
if len(symbols) > 1:
print("Error: Historical data supports single symbol only",
file=sys.stderr)
sys.exit(1)
handle_historical_command(
symbols[0], client, cache, formatter, args, config
)
else:
handle_price_command(
symbols, client, cache, formatter, args, config
)
if __name__ == "__main__":
main()
Related skills
FAQ
What does tracking-crypto-prices do?
tracking-crypto-prices watches token and pair prices, sets alert thresholds, and summarizes market moves for developer workflows such as dashboards, bots, and portfolio monitoring automation.
When should developers use tracking-crypto-prices?
Developers should use tracking-crypto-prices when building applications or agents that need programmatic crypto price watches, threshold alerts, and concise market summaries instead of manual tracking.