
Tracking Crypto Portfolio
- 9 installs
- 82 repo stars
- Updated August 2, 2026
- aaaaqwq/agi-super-skills
tracking-crypto-portfolio is a Claude Code skill that tracks a crypto portfolio with real-time CoinGecko valuations, allocation analysis, and P&L against cost basis.
About
tracking-crypto-portfolio is a Claude Code skill for tracking a cryptocurrency portfolio. It reads a holdings JSON file, fetches current prices from CoinGecko, and reports total value, per-asset allocation, and unrealized P&L against cost basis. It flags concentration risk above configurable thresholds and exports results as a table, JSON, or CSV.
- Values crypto holdings in real time via CoinGecko
- Tracks P&L against cost basis and allocation per asset
- Flags concentration risk and exports to table, JSON, or CSV
Tracking Crypto Portfolio by the numbers
- 9 all-time installs (skills.sh)
- Ranked #812 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
tracking-crypto-portfolio capabilities & compatibility
Free; uses the public CoinGecko API and local Python.
- Capabilities
- portfolio tracking · valuation · allocation analysis · pnl tracking
- Works with
- excel
- Use cases
- trading · data analysis
- Pricing
- Free
What tracking-crypto-portfolio says it does
Track cryptocurrency portfolio with real-time valuations, allocation analysis, and P&L tracking.
**Real-Time Valuations**: Current prices from CoinGecko
npx skills add https://github.com/aaaaqwq/agi-super-skills --skill tracking-crypto-portfolioAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 82 |
| Last updated | August 2, 2026 |
| Repository | aaaaqwq/agi-super-skills ↗ |
What it does
Use when a developer or investor wants to value a crypto portfolio, see holdings breakdown, or analyze allocation and P&L.
Who is it for?
Valuing crypto holdings, analyzing allocation, and tracking unrealized P&L from a holdings file.
Skip if: Derivatives, funding rates, or open interest, which the companion crypto-derivatives skill covers.
When should I use this skill?
When the user says show my portfolio, check crypto holdings, portfolio allocation, or export portfolio.
What you get
A valued portfolio with per-asset allocation, P&L, concentration flags, and exportable output.
- portfolio valuation
- holdings breakdown
- P&L report
By the numbers
- Default concentration warning threshold: 25% allocation
- 3 export formats: table, JSON, CSV
Files
Tracking Crypto Portfolio
Overview
This skill provides comprehensive cryptocurrency portfolio tracking with:
- Real-Time Valuations: Current prices from CoinGecko
- Holdings Breakdown: Quantity, value, and allocation per asset
- P&L Tracking: Unrealized gains/losses with cost basis
- Allocation Analysis: Category breakdown and concentration flags
- Multiple Export Formats: Table, JSON, CSV
Key Capabilities:
- Track holdings across multiple assets
- Calculate portfolio total value in USD
- Identify overweight positions (concentration risk)
- Export for analysis tools and tax reporting
Prerequisites
Before using this skill, ensure:
1. Python 3.8+ is installed 2. requests library is available: pip install requests 3. Internet connectivity for CoinGecko API access 4. A portfolio JSON file with your holdings
Portfolio File Format
Create a portfolio file (e.g., holdings.json):
{
"name": "My Portfolio",
"holdings": [
{"coin": "BTC", "quantity": 0.5, "cost_basis": 25000},
{"coin": "ETH", "quantity": 10, "cost_basis": 2000},
{"coin": "SOL", "quantity": 100}
]
}Fields:
coin: Symbol (BTC, ETH, etc.) - requiredquantity: Amount held - requiredcost_basis: Average purchase price per coin (optional, for P&L)acquired: Date acquired (optional, for records)
Instructions
Step 1: Assess User Intent
Determine what portfolio information the user needs:
- Quick check: Total value and top holdings
- Holdings list: Full breakdown of all positions
- Detailed analysis: Allocations, P&L, risk flags
- Export: JSON or CSV for external tools
Step 2: Execute Portfolio Tracking
Run the tracker with appropriate options:
# Quick portfolio summary
python {baseDir}/scripts/portfolio_tracker.py --portfolio holdings.json
# Full holdings breakdown
python {baseDir}/scripts/portfolio_tracker.py --portfolio holdings.json --holdings
# Detailed analysis with P&L and allocations
python {baseDir}/scripts/portfolio_tracker.py --portfolio holdings.json --detailed
# Export to JSON
python {baseDir}/scripts/portfolio_tracker.py --portfolio holdings.json --format json --output portfolio_export.json
# Export to CSV
python {baseDir}/scripts/portfolio_tracker.py --portfolio holdings.json --format csv --output portfolio.csvStep 3: Present Results
Format and explain the portfolio data:
- Show total portfolio value prominently
- Highlight 24h and 7d changes
- Explain allocation percentages
- Flag any concentration risks
- For detailed mode, explain P&L calculations
Command-Line Options
| Option | Description | Default |
|---|---|---|
--portfolio | Path to portfolio JSON file | Required |
--holdings | Show all holdings breakdown | false |
--detailed | Full analysis with P&L | false |
--sort | Sort by: value, allocation, name, change | value |
--format | Output format (table, json, csv) | table |
--output | Output file path | stdout |
--threshold | Allocation warning threshold | 25% |
--verbose | Enable verbose output | false |
Allocation Thresholds
By default, positions > 25% allocation are flagged:
| Allocation | Risk Level | Action |
|---|---|---|
| < 10% | Low | Normal position |
| 10-25% | Medium | Monitor closely |
| 25-50% | High | Consider rebalancing |
| > 50% | Very High | Significant concentration risk |
Output
Table Format (Default)
==============================================================================
CRYPTO PORTFOLIO TRACKER Updated: 2026-01-14 15:30
==============================================================================
PORTFOLIO SUMMARY: My Portfolio
------------------------------------------------------------------------------
Total Value: $125,450.00 USD
24h Change: +$2,540.50 (+2.07%)
7d Change: +$8,125.00 (+6.92%)
Holdings: 8 assets
TOP HOLDINGS
------------------------------------------------------------------------------
Coin Quantity Price Value Alloc 24h
BTC 0.500 $95,000.00 $47,500.00 37.9% +2.5%
ETH 10.000 $3,200.00 $32,000.00 25.5% +1.8%
SOL 100.000 $180.00 $18,000.00 14.4% +4.2%
⚠ CONCENTRATION WARNING: BTC (37.9%) exceeds 25% threshold
==============================================================================JSON Format
{
"portfolio_name": "My Portfolio",
"total_value_usd": 125450.00,
"change_24h": {"amount": 2540.50, "percent": 2.07},
"holdings": [
{
"coin": "BTC",
"quantity": 0.5,
"price_usd": 95000,
"value_usd": 47500,
"allocation_pct": 37.9,
"change_24h_pct": 2.5
}
],
"meta": {
"timestamp": "2026-01-14T15:30:00Z",
"holdings_count": 8
}
}Error Handling
See {baseDir}/references/errors.md for comprehensive error handling.
| Error | Cause | Solution |
|---|---|---|
| Portfolio file not found | Invalid path | Check file path exists |
| Invalid JSON | Malformed file | Validate JSON syntax |
| Coin not found | Unknown symbol | Check symbol spelling, use standard symbols |
| API rate limited | Too many requests | Wait and retry, use caching |
Examples
See {baseDir}/references/examples.md for detailed examples.
Quick Examples
# Basic portfolio check
python {baseDir}/scripts/portfolio_tracker.py --portfolio ~/crypto/holdings.json
# Show all holdings sorted by allocation
python {baseDir}/scripts/portfolio_tracker.py --portfolio holdings.json --holdings --sort allocation
# Detailed analysis with 15% threshold
python {baseDir}/scripts/portfolio_tracker.py --portfolio holdings.json --detailed --threshold 15
# Export for tax software
python {baseDir}/scripts/portfolio_tracker.py --portfolio holdings.json --format csv --output tax_export.csv
# JSON export for trading bot
python {baseDir}/scripts/portfolio_tracker.py --portfolio holdings.json --format json --output portfolio_data.jsonResources
- CoinGecko API: https://www.coingecko.com/en/api - Free crypto market data
- Portfolio Schema: See PRD.md for complete portfolio file format
- Configuration: See
{baseDir}/config/settings.yamlfor options - See
{baseDir}/references/examples.mdfor integration examples
ARD: Tracking Crypto Portfolio
Document Control
| Field | Value |
|---|---|
| Skill Name | tracking-crypto-portfolio |
| Architecture Pattern | Data Aggregation + Valuation Engine |
| Version | 2.0.0 |
| Author | Jeremy Longshore <jeremy@intentsolutions.io> |
---
Architectural Overview
Pattern: Portfolio Valuation Pipeline
This skill implements a data aggregation pattern with real-time price enrichment and valuation calculation.
┌─────────────────────────────────────────────────────────────────────────────┐
│ CRYPTO PORTFOLIO TRACKER ARCHITECTURE │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Portfolio JSON │ │ CoinGecko API │ │ User Config │
│ (Holdings) │ │ (Prices) │ │ (Settings) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ PORTFOLIO │ │ PRICE FETCHER │ │ CONFIG LOADER │
│ LOADER │ │ - Batch fetch │ │ - Categories │
│ - Validation │ │ - Caching │ │ - Thresholds │
│ - Normalization │ │ - Fallbacks │ │ - Display opts │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
└────────────────────────┼────────────────────────┘
│
▼
┌─────────────────────────┐
│ VALUATION ENGINE │
│ - Price × Quantity │
│ - Allocation calc │
│ - P&L calculation │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ ANALYTICS ENGINE │
│ - Allocation analysis │
│ - Risk flags │
│ - Performance metrics │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ FORMATTER │
│ - Table view │
│ - JSON export │
│ - CSV export │
└─────────────────────────┘Workflow
1. Load: Parse portfolio JSON file and validate holdings 2. Fetch: Batch fetch prices from CoinGecko API 3. Calculate: Compute valuations and allocations 4. Analyze: Generate performance metrics and risk flags 5. Format: Output in requested format
---
Progressive Disclosure Strategy
Level 1: Portfolio Summary (Default)
python portfolio_tracker.py --portfolio holdings.jsonReturns total value, 24h change, top 5 holdings.
Level 2: Full Holdings List
python portfolio_tracker.py --portfolio holdings.json --holdingsLists all holdings with prices and allocations.
Level 3: Detailed Analysis
python portfolio_tracker.py --portfolio holdings.json --detailedFull breakdown with P&L, risk flags, category allocation.
Level 4: Export
python portfolio_tracker.py --portfolio holdings.json --format json --output portfolio.jsonMachine-readable export for integration.
---
Tool Permission Strategy
Allowed Tools (Scoped)
allowed-tools: Read, Write, Bash(crypto:portfolio-*)| Tool | Scope | Purpose |
|---|---|---|
| Read | Unrestricted | Read portfolio files, config |
| Write | Unrestricted | Write export files |
| Bash | crypto:portfolio-* | Execute portfolio tracker scripts |
Why These Tools
- Read: Load portfolio JSON and configuration
- Write: Save exported portfolio data
- *Bash(crypto:portfolio-)**: Execute Python scripts for tracking
---
Directory Structure
plugins/crypto/crypto-portfolio-tracker/
└── skills/
└── tracking-crypto-portfolio/
├── PRD.md # Product requirements
├── ARD.md # This file
├── SKILL.md # Core instructions
├── scripts/
│ ├── portfolio_tracker.py # Main CLI entry point
│ ├── portfolio_loader.py # Portfolio file parsing
│ ├── price_fetcher.py # CoinGecko price fetching
│ ├── valuation_engine.py # Value calculations
│ └── formatters.py # Output formatting
├── references/
│ ├── errors.md # Error handling guide
│ └── examples.md # Usage examples
└── config/
└── settings.yaml # Configuration options---
Data Flow Architecture
Input
- Portfolio JSON file with holdings
- Optional: Configuration settings
- External: CoinGecko price API
Processing Pipeline
Portfolio JSON
│
├──► Parse and validate holdings
│ │
│ ▼
│ Normalize coin symbols
│ Calculate total quantity per coin
│
├──► Fetch prices from CoinGecko
│ │
│ ▼
│ Batch request (up to 250 coins)
│ Cache results (60s TTL)
│
├──► Calculate valuations
│ │
│ ▼
│ Value = Quantity × Price
│ Total = Sum(all values)
│
├──► Calculate allocations
│ │
│ ▼
│ Allocation = Value / Total × 100
│ Flag if > threshold (default 25%)
│
├──► Calculate P&L (if cost basis)
│ │
│ ▼
│ Unrealized P&L = Value - (Quantity × Cost Basis)
│ % Change = (Price - Cost Basis) / Cost Basis × 100
│
└──► Format and outputOutput Schema
{
"portfolio_name": "Main Portfolio",
"total_value_usd": 125000.50,
"change_24h": {
"amount": 2500.25,
"percent": 2.04
},
"holdings": [
{
"coin": "BTC",
"coingecko_id": "bitcoin",
"quantity": 0.5,
"price_usd": 95000,
"value_usd": 47500,
"allocation_pct": 38.0,
"change_24h_pct": 2.5,
"cost_basis": 25000,
"unrealized_pnl": 22500,
"pnl_pct": 90.0
}
],
"allocation_by_category": {
"Layer 1": 65.0,
"DeFi": 20.0,
"Stablecoins": 15.0
},
"risk_flags": [
"BTC allocation > 25% threshold"
],
"meta": {
"timestamp": "2026-01-14T15:30:00Z",
"prices_updated": "2026-01-14T15:29:45Z",
"holdings_count": 12
}
}---
API Integration Architecture
CoinGecko API
Endpoint: https://api.coingecko.com/api/v3/coins/markets
Method: GET
Parameters:
- vs_currency: usd
- ids: bitcoin,ethereum,... (comma-separated)
- order: market_cap_desc
- sparkline: false
- price_change_percentage: 24h,7d
Rate Limits:
- Free tier: ~10-30 calls/minute
- Batch up to 250 coins per requestRequest Strategy
def fetch_prices(coin_ids: List[str]) -> Dict[str, Dict]:
"""Batch fetch prices with rate limiting."""
# Split into batches of 250
batches = [coin_ids[i:i+250] for i in range(0, len(coin_ids), 250)]
results = {}
for batch in batches:
response = requests.get(
f"{COINGECKO_API}/coins/markets",
params={
"vs_currency": "usd",
"ids": ",".join(batch),
"price_change_percentage": "24h,7d"
}
)
for coin in response.json():
results[coin["id"]] = {
"price": coin["current_price"],
"change_24h": coin["price_change_percentage_24h"],
"change_7d": coin["price_change_percentage_7d"],
"market_cap": coin["market_cap"]
}
time.sleep(0.5) # Rate limit protection
return results---
Portfolio File Schema
Minimal Format
{
"holdings": [
{"coin": "BTC", "quantity": 0.5},
{"coin": "ETH", "quantity": 10}
]
}Full Format
{
"name": "Main Portfolio",
"currency": "USD",
"holdings": [
{
"coin": "BTC",
"quantity": 0.5,
"cost_basis": 25000,
"acquired": "2024-01-15",
"wallet": "Ledger",
"notes": "DCA purchase"
}
],
"categories": {
"BTC": "Layer 1",
"ETH": "Layer 1",
"UNI": "DeFi",
"USDC": "Stablecoin"
}
}---
Error Handling Strategy
Error Categories
| Category | Examples | Strategy |
|---|---|---|
| File Error | Portfolio not found, invalid JSON | Exit with clear error |
| API Error | Rate limit, timeout | Retry with backoff, then cache fallback |
| Data Error | Unknown coin symbol | Warn, skip coin, continue |
| Validation | Negative quantity | Reject with explanation |
Graceful Degradation
Full Analysis (prices + allocations + P&L)
│
├─► API unavailable → Use cached prices (if available)
│ │
│ ▼
│ Show "stale prices" warning
│
├─► Unknown coin → Skip coin, warn user
│ │
│ ▼
│ Show "X coins not found" message
│
└─► No cost basis → Skip P&L calculation
│
▼
Show allocations only---
Coin Symbol Mapping
# Map common symbols to CoinGecko IDs
SYMBOL_TO_ID = {
"BTC": "bitcoin",
"ETH": "ethereum",
"SOL": "solana",
"XRP": "ripple",
"ADA": "cardano",
"DOGE": "dogecoin",
"DOT": "polkadot",
"LINK": "chainlink",
"AVAX": "avalanche-2",
"MATIC": "matic-network",
"UNI": "uniswap",
"AAVE": "aave",
"USDC": "usd-coin",
"USDT": "tether",
"DAI": "dai",
# ... more mappings
}---
Performance & Scalability
Performance Targets
| Metric | Target | Approach |
|---|---|---|
| Full analysis | < 10s | Batch API requests |
| Summary only | < 5s | Minimal API call |
| Large portfolio (100+ coins) | < 30s | Batching + caching |
Optimization Strategies
1. Batch Requests: Fetch up to 250 coins per API call 2. Caching: Cache prices with 60s TTL 3. Lazy Loading: Only fetch what's needed for output level 4. Symbol Pre-mapping: Resolve symbols before API call
---
Testing Strategy
Unit Tests
| Component | Test Cases |
|---|---|
| PortfolioLoader | Valid JSON, invalid JSON, missing fields, negative quantities |
| PriceFetcher | API success, rate limit, timeout, unknown coin |
| ValuationEngine | Basic calc, zero quantity, zero price |
| AllocationAnalyzer | Normal distribution, single coin, empty portfolio |
| Formatters | All output formats, empty data, special characters |
Integration Tests
- End-to-end with mock API
- Real API tests (daily CI with sample portfolio)
- Export format validation
Sample Test Portfolio
{
"name": "Test Portfolio",
"holdings": [
{"coin": "BTC", "quantity": 1.0, "cost_basis": 50000},
{"coin": "ETH", "quantity": 10, "cost_basis": 2500},
{"coin": "SOL", "quantity": 100, "cost_basis": 100},
{"coin": "USDC", "quantity": 10000}
]
}---
Security & Compliance
Security Considerations
- No Private Keys: Portfolio files contain quantities only
- No Exchange APIs: No authentication credentials stored
- Local Data: All portfolio data stays local
- Public API Only: CoinGecko is public, no secrets needed
Data Privacy
- Portfolio files should not be committed to git
- Add
*.portfolio.jsonto.gitignore - No telemetry or data collection
# Crypto Portfolio Tracker Configuration
# Version: 2.0.0
# =============================================================================
# Display Settings
# =============================================================================
display:
default_format: table # table, json, csv
table_width: 78
show_all_holdings: false # Show top 5 by default
default_sort: value # value, allocation, name, change
currency: USD
# =============================================================================
# Allocation Thresholds
# =============================================================================
thresholds:
# Concentration warning threshold (percent)
concentration_warning: 25.0
# Risk levels for reporting
risk_levels:
low: 10 # < 10% is low risk
medium: 25 # 10-25% is medium
high: 50 # 25-50% is high
# > 50% is very high
# =============================================================================
# API Configuration
# =============================================================================
api:
coingecko:
base_url: "https://api.coingecko.com/api/v3"
timeout: 15 # Request timeout in seconds
rate_limit: 30 # Max requests per minute (free tier)
batch_size: 250 # Max coins per request
# =============================================================================
# Caching
# =============================================================================
cache:
enabled: true
ttl: 60 # Time-to-live in seconds
file: ".price_cache.json" # Cache file (relative to scripts/)
# =============================================================================
# Coin Symbol Mappings
# =============================================================================
# Maps common symbols to CoinGecko IDs
# Add custom mappings here for non-standard symbols
symbol_mappings:
# Layer 1
BTC: bitcoin
ETH: ethereum
SOL: solana
ADA: cardano
DOT: polkadot
AVAX: avalanche-2
NEAR: near
APT: aptos
SUI: sui
# Layer 2 / Scaling
MATIC: matic-network
ARB: arbitrum
OP: optimism
# DeFi
LINK: chainlink
UNI: uniswap
AAVE: aave
MKR: maker
CRV: curve-dao-token
LDO: lido-dao
# Stablecoins
USDC: usd-coin
USDT: tether
DAI: dai
FRAX: frax
# Meme / Community
DOGE: dogecoin
SHIB: shiba-inu
PEPE: pepe
BONK: bonk
# Other Major
XRP: ripple
LTC: litecoin
BNB: binancecoin
ATOM: cosmos
# =============================================================================
# Category Definitions
# =============================================================================
# Default category mappings if not specified in portfolio file
default_categories:
# Layer 1
BTC: "Layer 1"
ETH: "Layer 1"
SOL: "Layer 1"
ADA: "Layer 1"
DOT: "Layer 1"
AVAX: "Layer 1"
# DeFi
LINK: "DeFi"
UNI: "DeFi"
AAVE: "DeFi"
MKR: "DeFi"
CRV: "DeFi"
# Stablecoins
USDC: "Stablecoin"
USDT: "Stablecoin"
DAI: "Stablecoin"
# Meme
DOGE: "Meme"
SHIB: "Meme"
PEPE: "Meme"
# =============================================================================
# Portfolio File Schema
# =============================================================================
# Reference for portfolio file structure
portfolio_schema:
required_fields:
- coin # Symbol (BTC, ETH, etc.)
- quantity # Amount held
optional_fields:
- cost_basis # Average purchase price per coin
- acquired # Date acquired (YYYY-MM-DD)
- wallet # Wallet/exchange name
- notes # Free-form notes
portfolio_fields:
- name # Portfolio name
- currency # Display currency (default: USD)
- categories # Custom category mappings
# =============================================================================
# Output Formatting
# =============================================================================
formatting:
decimal_places:
price: 2
quantity: 4
value: 2
percent: 2
allocation: 1
# ANSI colors for terminal output
colors:
positive: "\033[92m" # Green
negative: "\033[91m" # Red
warning: "\033[93m" # Yellow
reset: "\033[0m"
PRD: Tracking Crypto Portfolio
Document Control
| Field | Value |
|---|---|
| Skill Name | tracking-crypto-portfolio |
| Type | Portfolio Management & Analytics |
| Domain | Cryptocurrency Portfolio Tracking |
| Target Users | Traders, Investors, Portfolio Managers |
| Priority | P1 - Core Analytics Skill |
| Version | 2.0.0 |
| Author | Jeremy Longshore <jeremy@intentsolutions.io> |
---
Executive Summary
The tracking-crypto-portfolio skill provides comprehensive cryptocurrency portfolio management with real-time valuations, performance analytics, and allocation insights. It enables users to track holdings across multiple wallets, exchanges, and chains with unified reporting.
Value Proposition: Consolidate multi-chain, multi-exchange crypto holdings into a single view with real-time valuations, P&L tracking, and allocation analysis.
---
Problem Statement
Current Pain Points
1. Fragmented Holdings: Assets spread across exchanges, wallets, and chains 2. Manual Tracking: Spreadsheets can't keep up with real-time prices 3. No Cost Basis: Hard to track acquisition costs and realize P&L 4. Allocation Drift: No visibility into portfolio concentration risks 5. Performance Blind Spots: Can't compare performance across assets
Impact of Not Solving
- Missed rebalancing opportunities
- Tax reporting nightmares
- Overexposure to single assets
- No clear view of actual returns
- Emotional decisions from lack of data
---
Target Users
Persona 1: Active Trader
- Name: Marcus
- Role: Day/swing trader
- Goals: Track positions across exchanges, monitor P&L in real-time
- Pain Points: Holdings on 5 exchanges, needs quick portfolio snapshot
- Usage: Multiple times daily, JSON export for trading bot integration
Persona 2: Long-Term Investor
- Name: Sarah
- Role: HODLer with diverse portfolio
- Goals: Track overall portfolio value, monitor allocation percentages
- Pain Points: Assets in cold wallets and exchanges, no unified view
- Usage: Weekly check-ins, detailed allocation reports
Persona 3: DeFi User
- Name: Alex
- Role: DeFi yield farmer
- Goals: Track LP positions, staking rewards, multi-chain holdings
- Pain Points: Assets across Ethereum, Arbitrum, Polygon, etc.
- Usage: Daily monitoring, export for yield calculations
---
User Stories
US-1: Portfolio Overview (Critical)
As a crypto investor I want to see my total portfolio value across all holdings So that I know my current net worth in crypto
Acceptance Criteria:
- Display total portfolio value in USD
- Show 24h and 7d change (absolute and percentage)
- List top holdings by value
- Complete in under 10 seconds
US-2: Holdings Breakdown (Critical)
As a portfolio manager I want to see each asset with quantity, value, and allocation So that I can monitor concentration risk
Acceptance Criteria:
- List all holdings with current prices
- Show allocation percentage per asset
- Highlight overweighted positions (>25% allocation)
- Sort by value, alphabetical, or allocation
US-3: Performance Tracking (Important)
As a trader I want to track P&L for each position So that I know which trades are profitable
Acceptance Criteria:
- Calculate unrealized P&L per holding
- Show cost basis vs current value
- Display percentage gain/loss
- Support FIFO/LIFO/average cost methods
US-4: Multi-Format Export (Important)
As a quant I want to export portfolio data in JSON format So that I can feed it into my analysis tools
Acceptance Criteria:
- JSON export with all holdings and metadata
- CSV export for spreadsheet analysis
- Include timestamps for time-series tracking
- Support custom field selection
US-5: Allocation Analysis (Nice-to-Have)
As an investor I want to see portfolio allocation by category So that I can ensure proper diversification
Acceptance Criteria:
- Group by asset type (L1, L2, DeFi, stables, etc.)
- Show pie chart or percentage breakdown
- Flag concentration risks
- Compare to target allocation
---
Functional Requirements
REQ-1: Holdings Management
- Add holdings manually (coin, quantity, cost basis)
- Import from JSON portfolio file
- Support for multiple portfolio files
- Track acquisition date for tax purposes
REQ-2: Real-Time Valuations
- Fetch current prices from CoinGecko API
- Calculate total value per holding
- Sum to portfolio total value
- Cache prices with configurable TTL
REQ-3: Performance Metrics
- Calculate 24h, 7d, 30d price changes
- Track unrealized P&L per position
- Support multiple cost basis methods
- Historical performance tracking (if data available)
REQ-4: Allocation Analysis
- Calculate allocation percentages
- Group by configurable categories
- Flag overweight positions
- Compare to benchmark allocations
REQ-5: Output Formatting
- Table format for terminal display
- JSON format for programmatic use
- CSV format for spreadsheet import
- Summary format for quick checks
---
Non-Goals
- Exchange Integration: No direct API connections to exchanges (manual entry only)
- Trading: No buy/sell execution capabilities
- Wallet Monitoring: No on-chain wallet tracking (use dedicated tools)
- Tax Calculations: Basic P&L only, not full tax reporting
- Historical Charts: Current snapshot only, not time-series visualization
---
Success Metrics
| Metric | Target | Measurement |
|---|---|---|
| Load time | < 10s | Script execution time |
| Price accuracy | Real-time within 5 min | API freshness check |
| Export completeness | All holdings included | Field validation |
| User activation | Triggered by portfolio phrases | Plugin analytics |
---
UX Flow
User: "show my crypto portfolio"
│
├─► Load portfolio file (JSON)
│
├─► Fetch current prices (CoinGecko)
│
├─► Calculate valuations
│
├─► Calculate allocations
│
├─► Calculate P&L (if cost basis provided)
│
└─► Display formatted output---
Integration Points
Dependencies
- market-price-tracker: For real-time price data (can work standalone)
- CoinGecko API for price fetching
Consumers
- crypto-tax-calculator: Portfolio data for tax reporting
- trading-strategy-backtester: Portfolio composition input
Data Sources
- CoinGecko API (free tier)
- User-provided portfolio JSON file
---
Constraints & Assumptions
Constraints
- CoinGecko free tier rate limits (~10-30 calls/minute)
- Manual portfolio entry (no exchange API sync)
- Single currency display (USD)
Assumptions
- User maintains portfolio file with accurate holdings
- Cost basis data optional but beneficial
- Network connectivity for price fetching
---
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| CoinGecko rate limiting | Medium | Medium | Caching, batch requests |
| Coin not found | Low | Low | Map common symbols, warn user |
| Stale prices | Low | Medium | Show last updated timestamp |
| Portfolio file format errors | Medium | Medium | Validation with clear errors |
---
Examples
Example 1: Quick Portfolio Check
python portfolio_tracker.py --portfolio holdings.jsonShows portfolio summary with total value and top holdings.
Example 2: Detailed Holdings
python portfolio_tracker.py --portfolio holdings.json --detailedFull breakdown with allocation percentages and P&L.
Example 3: JSON Export
python portfolio_tracker.py --portfolio holdings.json --format json --output portfolio.jsonExport for analysis tools.
---
Portfolio File Format
{
"name": "Main Portfolio",
"holdings": [
{
"coin": "BTC",
"quantity": 0.5,
"cost_basis": 25000,
"acquired": "2024-01-15"
},
{
"coin": "ETH",
"quantity": 10,
"cost_basis": 2000,
"acquired": "2024-02-01"
}
]
}---
Version History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2025-10-16 | Jeremy Longshore | Initial stub |
| 2.0.0 | 2026-01-14 | Jeremy Longshore | Full PRD, implementation |
Error Handling Reference
Comprehensive error handling guide for the Crypto Portfolio Tracker.
---
File Errors
Portfolio File Not Found
Error: Cannot find portfolio file at specified path
Symptoms:
Error: Portfolio file not found: /path/to/holdings.jsonCauses:
- Incorrect file path
- File moved or deleted
- Typo in filename
Solutions: 1. Verify the file path is correct 2. Use absolute path or path relative to current directory 3. Check file exists: ls -la /path/to/holdings.json
---
Invalid JSON Format
Error: Portfolio file contains invalid JSON
Symptoms:
Error: Invalid JSON in portfolio file: Expecting ',' delimiterCauses:
- Missing commas between array items
- Unquoted strings
- Trailing commas (not allowed in JSON)
- UTF-8 encoding issues
Solutions: 1. Validate JSON: python -m json.tool holdings.json 2. Use a JSON linter or formatter 3. Check for common errors: trailing commas, missing quotes
---
Missing Required Fields
Error: Holdings missing required coin or quantity
Symptoms:
Warning: Holding 2 missing coin symbol, skipping
Warning: Holding 3 (ETH) missing quantity, skippingCauses:
- Incomplete portfolio entry
- Wrong field names
Solutions: 1. Ensure each holding has coin and quantity fields 2. Check for typos in field names 3. Valid example:
{"coin": "BTC", "quantity": 0.5}---
API Errors
CoinGecko Rate Limit
Error: Too many API requests
Symptoms:
API request failed: 429 Too Many Requests
Using stale cached prices as fallbackCauses:
- Exceeded free tier limit (~10-30 calls/minute)
- Running multiple queries too quickly
Solutions: 1. Automatic: Uses cached prices with warning 2. Manual: Wait 60 seconds before retry 3. Cache persists between runs for resilience
---
Unknown Coin Symbol
Error: Coin not found in CoinGecko
Symptoms:
Warning: Unknown symbols, trying lowercase: ['MYCOIN']Causes:
- Coin not listed on CoinGecko
- Non-standard symbol
- New coin not yet indexed
Solutions: 1. Use standard symbols (BTC, ETH, SOL, etc.) 2. Check CoinGecko for correct ID 3. Holding will show with $0 price if not found
---
Network Timeout
Error: Cannot connect to price API
Symptoms:
API request failed: Connection timed outCauses:
- Network connectivity issues
- API server downtime
- Firewall blocking requests
Solutions: 1. Check internet connectivity 2. Test API: curl https://api.coingecko.com/api/v3/ping 3. Uses cached prices as fallback
---
Validation Errors
Invalid Quantity
Error: Holding has invalid quantity value
Symptoms:
Warning: Holding 1 (BTC) has non-positive quantity, skippingCauses:
- Quantity is zero or negative
- Non-numeric value
- Empty string
Solutions: 1. Ensure quantity is a positive number 2. Remove or fix the invalid entry 3. Valid example: "quantity": 0.5 (not "quantity": "0.5")
---
Invalid Cost Basis
Note: Cost basis is optional, invalid values are ignored silently
Causes:
- Negative cost basis
- Non-numeric value
Solutions: 1. Remove the field or set to valid positive number 2. Cost basis should be per-coin price, not total cost
---
Graceful Degradation
The tracker implements graceful degradation:
Full Analysis (all prices + P&L)
│
├─► API unavailable → Use cached prices
│ │
│ ▼
│ Show "stale prices" warning
│
├─► Unknown coin → Skip in valuation
│ │
│ ▼
│ Show "coin not found" warning
│
└─► No cost basis → Skip P&L calculation
│
▼
Show allocations only---
Diagnostic Commands
Validate Portfolio File
# Check JSON syntax
python -m json.tool holdings.json
# Test portfolio loading
python portfolio_loader.py holdings.json -vTest Price Fetching
# Fetch prices for specific coins
python price_fetcher.py BTC ETH SOL -v
# Check cache
cat scripts/.price_cache.json | python -m json.toolClear Cache
rm scripts/.price_cache.json---
Common Issues
Issue: All Values Show $0
Causes:
- API failed and no cache available
- All coin symbols unknown
Diagnosis:
python portfolio_tracker.py --portfolio holdings.json -v
# Check for API errors in outputIssue: Missing Coins in Output
Causes:
- Invalid quantity (zero or negative)
- Missing required fields
- Duplicate coins aggregated
Diagnosis:
python portfolio_loader.py holdings.json -v
# Check warnings for skipped holdingsIssue: P&L Not Showing
Causes:
- No cost_basis field in holdings
- Invalid cost_basis values
- Not using --detailed flag
Solutions: 1. Add cost_basis to holdings 2. Use --detailed flag 3. Example: {"coin": "BTC", "quantity": 0.5, "cost_basis": 50000}
---
Portfolio File Troubleshooting
Minimal Valid Portfolio
{
"holdings": [
{"coin": "BTC", "quantity": 0.5}
]
}Full Valid Portfolio
{
"name": "My Portfolio",
"holdings": [
{
"coin": "BTC",
"quantity": 0.5,
"cost_basis": 50000,
"acquired": "2024-01-15",
"wallet": "Ledger"
}
],
"categories": {
"BTC": "Layer 1"
}
}Common JSON Errors
// WRONG: Trailing comma
{"holdings": [{"coin": "BTC", "quantity": 0.5},]}
// WRONG: Unquoted key
{holdings: [{"coin": "BTC", "quantity": 0.5}]}
// WRONG: Single quotes
{'holdings': [{'coin': 'BTC', 'quantity': 0.5}]}
// CORRECT
{"holdings": [{"coin": "BTC", "quantity": 0.5}]}Usage Examples
Comprehensive examples for the Crypto Portfolio Tracker.
---
Quick Start Examples
1. Basic Portfolio Check
python portfolio_tracker.py --portfolio holdings.jsonOutput:
==============================================================================
CRYPTO PORTFOLIO TRACKER 2026-01-14 15:30 UTC
==============================================================================
PORTFOLIO SUMMARY: My Portfolio
------------------------------------------------------------------------------
Total Value: $125,450.00 USD
24h Change: +$2,540.50 (+2.07%)
7d Change: +$8,125.00 (+6.92%)
Holdings: 8 assets
TOP HOLDINGS
------------------------------------------------------------------------------
Coin Quantity Price Value Alloc 24h
BTC 0.5000 $95,000.00 $47,500.00 37.9% +2.5%
ETH 10.0000 $3,200.00 $32,000.00 25.5% +1.8%
SOL 100.0000 $180.00 $18,000.00 14.4% +4.2%
... and 5 more holdings
==============================================================================---
2. Show All Holdings
python portfolio_tracker.py --portfolio holdings.json --holdingsDisplays complete breakdown of all positions instead of just top 5.
---
3. Detailed Analysis with P&L
python portfolio_tracker.py --portfolio holdings.json --detailedShows full analysis including:
- Unrealized P&L per position
- Total portfolio P&L
- Category allocation breakdown
- All concentration warnings
---
Sorting Options
4. Sort by Allocation
python portfolio_tracker.py --portfolio holdings.json --holdings --sort allocationShows holdings sorted by allocation percentage (highest first).
5. Sort by 24h Change
python portfolio_tracker.py --portfolio holdings.json --holdings --sort changeShows holdings sorted by 24h price change (best performers first).
6. Sort Alphabetically
python portfolio_tracker.py --portfolio holdings.json --holdings --sort nameShows holdings sorted alphabetically by coin symbol.
---
Export Examples
7. JSON Export
python portfolio_tracker.py --portfolio holdings.json --format jsonOutput:
{
"portfolio_name": "My Portfolio",
"total_value_usd": 125450.00,
"change_24h": {
"amount": 2540.50,
"percent": 2.07
},
"holdings": [
{
"coin": "BTC",
"quantity": 0.5,
"price_usd": 95000,
"value_usd": 47500,
"allocation_pct": 37.9,
"change_24h_pct": 2.5
}
],
"meta": {
"timestamp": "2026-01-14T15:30:00Z"
}
}---
8. CSV Export
python portfolio_tracker.py --portfolio holdings.json --format csv --output portfolio.csvCreates spreadsheet-compatible file with columns:
- coin, quantity, price_usd, value_usd, allocation_pct, change_24h_pct, etc.
---
9. JSON Export to File
python portfolio_tracker.py --portfolio holdings.json --format json --output portfolio_data.jsonSaves JSON output to file for programmatic use.
---
Threshold Configuration
10. Custom Concentration Threshold
python portfolio_tracker.py --portfolio holdings.json --threshold 15Flags any position > 15% allocation (default is 25%).
11. Strict Threshold
python portfolio_tracker.py --portfolio holdings.json --threshold 10 --holdingsShows concentration warnings for positions > 10%.
---
Portfolio File Examples
12. Minimal Portfolio
{
"holdings": [
{"coin": "BTC", "quantity": 0.5},
{"coin": "ETH", "quantity": 10},
{"coin": "SOL", "quantity": 100}
]
}13. Portfolio with Cost Basis
{
"name": "Main Portfolio",
"holdings": [
{"coin": "BTC", "quantity": 0.5, "cost_basis": 50000},
{"coin": "ETH", "quantity": 10, "cost_basis": 2500},
{"coin": "SOL", "quantity": 100, "cost_basis": 100}
]
}14. Full Portfolio with Categories
{
"name": "Diversified Portfolio",
"holdings": [
{"coin": "BTC", "quantity": 0.5, "cost_basis": 50000, "wallet": "Ledger"},
{"coin": "ETH", "quantity": 10, "cost_basis": 2500, "wallet": "Ledger"},
{"coin": "SOL", "quantity": 100, "cost_basis": 100, "wallet": "Phantom"},
{"coin": "LINK", "quantity": 500, "cost_basis": 15, "wallet": "MetaMask"},
{"coin": "USDC", "quantity": 10000, "wallet": "Coinbase"}
],
"categories": {
"BTC": "Layer 1",
"ETH": "Layer 1",
"SOL": "Layer 1",
"LINK": "DeFi",
"USDC": "Stablecoin"
}
}---
Integration Examples
15. Daily Portfolio Snapshot
# Save daily snapshot with timestamp
DATE=$(date +%Y-%m-%d)
python portfolio_tracker.py --portfolio holdings.json --format json --output "snapshots/${DATE}.json"16. Cron Job for Hourly Tracking
# Add to crontab
0 * * * * cd /path/to/skill && python portfolio_tracker.py --portfolio ~/holdings.json --format json --output ~/snapshots/$(date +\%Y\%m\%d_\%H).json17. Extract Total Value for Scripts
# Get total value for shell script
TOTAL=$(python portfolio_tracker.py --portfolio holdings.json --format json | jq -r '.total_value_usd')
echo "Portfolio value: $${TOTAL}"18. Check for Rebalancing
# Flag if any position > 30%
python portfolio_tracker.py --portfolio holdings.json --threshold 30 --format json | jq '.risk_flags'---
Multiple Portfolios
19. Compare Portfolios
# Run for each portfolio
python portfolio_tracker.py --portfolio main.json --format json > main_value.json
python portfolio_tracker.py --portfolio trading.json --format json > trading_value.json
# Compare totals
jq -s '.[0].total_value_usd + .[1].total_value_usd' main_value.json trading_value.json20. Aggregate Multiple Wallets
Create a single portfolio file with wallet tracking:
{
"name": "All Wallets",
"holdings": [
{"coin": "BTC", "quantity": 0.3, "wallet": "Ledger"},
{"coin": "BTC", "quantity": 0.2, "wallet": "Coinbase"},
{"coin": "ETH", "quantity": 5, "wallet": "Ledger"},
{"coin": "ETH", "quantity": 5, "wallet": "MetaMask"}
]
}The tracker automatically aggregates holdings for the same coin.
---
Debugging
21. Verbose Mode
python portfolio_tracker.py --portfolio holdings.json -vShows detailed progress:
- Loading portfolio file
- Fetching prices for X coins
- API responses and cache status
- Any warnings or errors
22. Test Price Fetching
python price_fetcher.py BTC ETH SOL -vTests price API independently.
23. Test Portfolio Loading
python portfolio_loader.py holdings.json -vValidates portfolio file and shows any skipped entries.
---
Output Interpretation
Reading the Summary
| Field | Meaning |
|---|---|
| Total Value | Sum of all holdings at current prices |
| 24h Change | Portfolio value change in last 24 hours |
| 7d Change | Portfolio value change in last 7 days |
| Holdings | Number of unique assets |
Reading Holdings Table
| Column | Meaning |
|---|---|
| Coin | Asset symbol |
| Quantity | Amount held |
| Price | Current USD price |
| Value | Quantity × Price |
| Alloc | Percentage of total portfolio |
| 24h | Price change in last 24 hours |
| P&L | Unrealized gain/loss (with --detailed) |
Risk Flags
- Exceeds X% threshold: Single asset concentration risk
- Majority of portfolio: One asset > 50%
- Limited diversification: < 3 assets
Implementation Guide
Step 1: Configure Data Sources
Set up connections to crypto data providers: 1. Use Read tool to load API credentials from {baseDir}/config/crypto-apis.env 2. Configure blockchain RPC endpoints for target networks 3. Set up exchange API connections if required 4. Verify rate limits and subscription tiers 5. Test connectivity and authentication
Step 2: Query Crypto Data
Retrieve relevant blockchain and market data: 1. Use Bash(crypto:portfolio-*) to execute crypto data queries 2. Fetch real-time prices, volumes, and market cap data 3. Query blockchain for on-chain metrics and transactions 4. Retrieve exchange order book and trade history 5. Aggregate data from multiple sources for accuracy
Step 3: Analyze and Process
Process crypto data to generate insights:
- Calculate key metrics (returns, volatility, correlation)
- Identify patterns and anomalies in data
- Apply technical indicators or on-chain signals
- Compare across timeframes and assets
- Generate actionable insights and alerts
Step 4: Generate Reports
Document findings in {baseDir}/crypto-reports/:
- Market summary with key price movements
- Detailed analysis with charts and metrics
- Trading signals or opportunity recommendations
- Risk assessment and position sizing guidance
- Historical context and trend analysis
#!/usr/bin/env python3
"""
Portfolio Output Formatters
Formats portfolio data in various output formats:
- Table (default): Terminal-friendly dashboard
- JSON: Machine-readable export
- CSV: Spreadsheet-compatible
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import csv
import io
import json
from datetime import datetime
from typing import Dict, Any, List
class PortfolioFormatter:
"""Formats portfolio data for output."""
def format(
self,
data: Dict[str, Any],
format_type: str = "table",
show_all_holdings: bool = False,
show_pnl: bool = False
) -> str:
"""Format portfolio data for output.
Args:
data: Portfolio valuation data
format_type: Output format ("table", "json", "csv")
show_all_holdings: Show all holdings (not just top 5)
show_pnl: Show P&L information
Returns:
Formatted string output
"""
if format_type == "json":
return self._format_json(data)
elif format_type == "csv":
return self._format_csv(data, show_pnl)
else:
return self._format_table(data, show_all_holdings, show_pnl)
def _format_table(
self,
data: Dict[str, Any],
show_all: bool,
show_pnl: bool
) -> str:
"""Format as terminal table/dashboard."""
lines = []
w = 78 # Width
# Header
timestamp = data.get("meta", {}).get("timestamp", "")
if timestamp:
try:
dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
time_str = dt.strftime("%Y-%m-%d %H:%M UTC")
except ValueError:
time_str = timestamp[:19]
else:
time_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
lines.append("=" * w)
lines.append(f" CRYPTO PORTFOLIO TRACKER{' ' * (w - 41)}{time_str}")
lines.append("=" * w)
lines.append("")
# Portfolio Summary
name = data.get("portfolio_name", "My Portfolio")
total = data.get("total_value_usd", 0)
holdings_count = data.get("holdings_count", 0)
lines.append(f" PORTFOLIO SUMMARY: {name}")
lines.append("-" * w)
lines.append(f" Total Value: ${total:,.2f} USD")
# 24h change
change_24h = data.get("change_24h", {})
if change_24h.get("percent") is not None:
amount = change_24h.get("amount", 0)
pct = change_24h.get("percent", 0)
sign = "+" if amount >= 0 else ""
lines.append(f" 24h Change: {sign}${amount:,.2f} ({sign}{pct:.2f}%)")
# 7d change
change_7d = data.get("change_7d", {})
if change_7d.get("percent") is not None:
amount = change_7d.get("amount", 0)
pct = change_7d.get("percent", 0)
sign = "+" if amount >= 0 else ""
lines.append(f" 7d Change: {sign}${amount:,.2f} ({sign}{pct:.2f}%)")
lines.append(f" Holdings: {holdings_count} assets")
# Total P&L if available
if show_pnl and data.get("total_unrealized_pnl") is not None:
pnl = data.get("total_unrealized_pnl", 0)
pnl_pct = data.get("total_pnl_pct", 0)
sign = "+" if pnl >= 0 else ""
lines.append(f" Total P&L: {sign}${pnl:,.2f} ({sign}{pnl_pct:.2f}%)")
lines.append("")
# Holdings Table
holdings = data.get("holdings", [])
if holdings:
display_count = len(holdings) if show_all else min(5, len(holdings))
if show_all:
lines.append(" ALL HOLDINGS")
else:
lines.append(" TOP HOLDINGS")
lines.append("-" * w)
# Table header
if show_pnl:
lines.append(f" {'Coin':<6} {'Quantity':>12} {'Price':>12} {'Value':>14} {'Alloc':>7} {'P&L':>10}")
else:
lines.append(f" {'Coin':<6} {'Quantity':>12} {'Price':>12} {'Value':>14} {'Alloc':>7} {'24h':>8}")
for holding in holdings[:display_count]:
coin = holding.get("coin", "")
qty = holding.get("quantity", 0)
price = holding.get("price_usd", 0)
value = holding.get("value_usd", 0)
alloc = holding.get("allocation_pct", 0)
if show_pnl and holding.get("pnl_pct") is not None:
pnl_pct = holding.get("pnl_pct", 0)
sign = "+" if pnl_pct >= 0 else ""
change_str = f"{sign}{pnl_pct:.1f}%"
else:
change_24h = holding.get("change_24h_pct")
if change_24h is not None:
sign = "+" if change_24h >= 0 else ""
change_str = f"{sign}{change_24h:.1f}%"
else:
change_str = "N/A"
lines.append(
f" {coin:<6} {qty:>12.4f} ${price:>10,.2f} ${value:>12,.2f} {alloc:>6.1f}% {change_str:>8}"
)
if not show_all and len(holdings) > 5:
remaining = len(holdings) - 5
lines.append(f" ... and {remaining} more holdings")
lines.append("")
# Category Allocation
categories = data.get("allocation_by_category", {})
if categories and show_all:
lines.append(" ALLOCATION BY CATEGORY")
lines.append("-" * w)
for category, pct in categories.items():
bar_len = int(pct / 2) # Scale to fit
bar = "█" * bar_len
lines.append(f" {category:<15} {pct:>5.1f}% {bar}")
lines.append("")
# Risk Flags
risk_flags = data.get("risk_flags", [])
if risk_flags:
lines.append(" ⚠ CONCENTRATION WARNINGS")
lines.append("-" * w)
for flag in risk_flags:
lines.append(f" • {flag}")
lines.append("")
# Footer
lines.append("=" * w)
threshold = data.get("meta", {}).get("threshold", 25)
lines.append(f" Concentration threshold: {threshold}% | Data: CoinGecko")
lines.append("=" * w)
return "\n".join(lines)
def _format_json(self, data: Dict[str, Any]) -> str:
"""Format as JSON."""
return json.dumps(data, indent=2, default=str)
def _format_csv(self, data: Dict[str, Any], include_pnl: bool) -> str:
"""Format as CSV."""
output = io.StringIO()
writer = csv.writer(output)
# Header row
headers = [
"coin",
"quantity",
"price_usd",
"value_usd",
"allocation_pct",
"change_24h_pct",
"change_7d_pct",
"market_cap"
]
if include_pnl:
headers.extend([
"cost_basis",
"total_cost",
"unrealized_pnl",
"pnl_pct"
])
writer.writerow(headers)
# Data rows
for holding in data.get("holdings", []):
row = [
holding.get("coin", ""),
holding.get("quantity", 0),
holding.get("price_usd", 0),
holding.get("value_usd", 0),
holding.get("allocation_pct", 0),
holding.get("change_24h_pct", ""),
holding.get("change_7d_pct", ""),
holding.get("market_cap", "")
]
if include_pnl:
row.extend([
holding.get("cost_basis", ""),
holding.get("total_cost", ""),
holding.get("unrealized_pnl", ""),
holding.get("pnl_pct", "")
])
writer.writerow(row)
# Summary row
writer.writerow([])
writer.writerow(["SUMMARY"])
writer.writerow(["Portfolio Name", data.get("portfolio_name", "")])
writer.writerow(["Total Value USD", data.get("total_value_usd", 0)])
writer.writerow(["Holdings Count", data.get("holdings_count", 0)])
if data.get("change_24h", {}).get("percent") is not None:
writer.writerow(["24h Change %", data["change_24h"]["percent"]])
if include_pnl and data.get("total_unrealized_pnl") is not None:
writer.writerow(["Total Unrealized P&L", data.get("total_unrealized_pnl", 0)])
writer.writerow(["Total P&L %", data.get("total_pnl_pct", 0)])
return output.getvalue()
def main():
"""CLI entry point for testing."""
# Test with sample data
sample_data = {
"portfolio_name": "Test Portfolio",
"total_value_usd": 125450.00,
"change_24h": {"amount": 2540.50, "percent": 2.07},
"change_7d": {"amount": 8125.00, "percent": 6.92},
"holdings_count": 5,
"total_cost": 80000,
"total_unrealized_pnl": 45450,
"total_pnl_pct": 56.8,
"holdings": [
{
"coin": "BTC",
"quantity": 0.5,
"price_usd": 95000,
"value_usd": 47500,
"allocation_pct": 37.9,
"change_24h_pct": 2.5,
"cost_basis": 50000,
"total_cost": 25000,
"unrealized_pnl": 22500,
"pnl_pct": 90.0
},
{
"coin": "ETH",
"quantity": 10,
"price_usd": 3200,
"value_usd": 32000,
"allocation_pct": 25.5,
"change_24h_pct": 1.8,
"cost_basis": 2500,
"total_cost": 25000,
"unrealized_pnl": 7000,
"pnl_pct": 28.0
},
{
"coin": "SOL",
"quantity": 100,
"price_usd": 180,
"value_usd": 18000,
"allocation_pct": 14.4,
"change_24h_pct": 4.2
}
],
"allocation_by_category": {
"Layer 1": 77.8,
"Other": 22.2
},
"risk_flags": [
"BTC allocation (37.9%) exceeds 25% threshold",
"ETH allocation (25.5%) exceeds 25% threshold"
],
"meta": {
"timestamp": "2026-01-14T15:30:00Z",
"threshold": 25
}
}
formatter = PortfolioFormatter()
print("=== TABLE FORMAT ===")
print(formatter.format(sample_data, "table", show_all_holdings=True, show_pnl=True))
print()
print("=== JSON FORMAT ===")
print(formatter.format(sample_data, "json"))
print()
print("=== CSV FORMAT ===")
print(formatter.format(sample_data, "csv", show_pnl=True))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Portfolio Loader
Loads and validates portfolio JSON files.
Supports multiple portfolio formats with graceful handling.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import json
import sys
from pathlib import Path
from typing import Optional, Dict, Any, List
class PortfolioLoader:
"""Loads and validates portfolio files."""
def __init__(self, verbose: bool = False):
"""Initialize loader.
Args:
verbose: Enable verbose output
"""
self.verbose = verbose
def load(self, path: str) -> Optional[Dict[str, Any]]:
"""Load portfolio from JSON file.
Args:
path: Path to portfolio JSON file
Returns:
Validated portfolio dict or None on error
"""
file_path = Path(path).expanduser()
# Check file exists
if not file_path.exists():
print(f"Error: Portfolio file not found: {file_path}", file=sys.stderr)
return None
# Load JSON
try:
with open(file_path, "r") as f:
data = json.load(f)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in portfolio file: {e}", file=sys.stderr)
return None
except Exception as e:
print(f"Error: Failed to read portfolio file: {e}", file=sys.stderr)
return None
# Validate and normalize
return self._validate_portfolio(data)
def _validate_portfolio(self, data: Any) -> Optional[Dict[str, Any]]:
"""Validate and normalize portfolio data."""
# Handle list format (just holdings array)
if isinstance(data, list):
data = {"holdings": data}
if not isinstance(data, dict):
print("Error: Portfolio must be a JSON object or array", file=sys.stderr)
return None
# Check for holdings
holdings = data.get("holdings", [])
if not holdings:
print("Error: Portfolio has no holdings", file=sys.stderr)
return None
# Validate each holding
valid_holdings = []
for i, holding in enumerate(holdings):
validated = self._validate_holding(holding, i)
if validated:
valid_holdings.append(validated)
if not valid_holdings:
print("Error: No valid holdings found in portfolio", file=sys.stderr)
return None
# Aggregate duplicate coins
aggregated = self._aggregate_holdings(valid_holdings)
return {
"name": data.get("name", "My Portfolio"),
"holdings": aggregated,
"categories": data.get("categories", {}),
"currency": data.get("currency", "USD")
}
def _validate_holding(self, holding: Any, index: int) -> Optional[Dict[str, Any]]:
"""Validate a single holding entry."""
if not isinstance(holding, dict):
if self.verbose:
print(f"Warning: Holding {index} is not an object, skipping", file=sys.stderr)
return None
# Required: coin symbol
coin = holding.get("coin") or holding.get("symbol") or holding.get("ticker")
if not coin:
if self.verbose:
print(f"Warning: Holding {index} missing coin symbol, skipping", file=sys.stderr)
return None
coin = str(coin).upper().strip()
# Required: quantity
quantity = holding.get("quantity") or holding.get("amount") or holding.get("qty")
if quantity is None:
if self.verbose:
print(f"Warning: Holding {index} ({coin}) missing quantity, skipping", file=sys.stderr)
return None
try:
quantity = float(quantity)
except (TypeError, ValueError):
if self.verbose:
print(f"Warning: Holding {index} ({coin}) has invalid quantity, skipping", file=sys.stderr)
return None
if quantity <= 0:
if self.verbose:
print(f"Warning: Holding {index} ({coin}) has non-positive quantity, skipping", file=sys.stderr)
return None
# Optional: cost basis
cost_basis = holding.get("cost_basis") or holding.get("cost") or holding.get("avg_cost")
if cost_basis is not None:
try:
cost_basis = float(cost_basis)
if cost_basis < 0:
cost_basis = None
except (TypeError, ValueError):
cost_basis = None
# Optional: acquired date
acquired = holding.get("acquired") or holding.get("date") or holding.get("purchase_date")
# Optional: wallet/location
wallet = holding.get("wallet") or holding.get("location") or holding.get("exchange")
# Optional: notes
notes = holding.get("notes") or holding.get("memo")
return {
"coin": coin,
"quantity": quantity,
"cost_basis": cost_basis,
"acquired": acquired,
"wallet": wallet,
"notes": notes
}
def _aggregate_holdings(self, holdings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Aggregate holdings for the same coin."""
aggregated = {}
for holding in holdings:
coin = holding["coin"]
if coin not in aggregated:
aggregated[coin] = {
"coin": coin,
"quantity": 0,
"total_cost": 0,
"has_cost_basis": False,
"wallets": [],
"notes": []
}
aggregated[coin]["quantity"] += holding["quantity"]
# Track cost basis if available
if holding.get("cost_basis") is not None:
cost = holding["quantity"] * holding["cost_basis"]
aggregated[coin]["total_cost"] += cost
aggregated[coin]["has_cost_basis"] = True
# Track wallets
if holding.get("wallet"):
aggregated[coin]["wallets"].append(holding["wallet"])
# Track notes
if holding.get("notes"):
aggregated[coin]["notes"].append(holding["notes"])
# Convert to list with calculated average cost basis
result = []
for coin, data in aggregated.items():
entry = {
"coin": coin,
"quantity": data["quantity"]
}
# Calculate average cost basis
if data["has_cost_basis"] and data["quantity"] > 0:
entry["cost_basis"] = data["total_cost"] / data["quantity"]
# Include wallets if tracked
if data["wallets"]:
entry["wallets"] = list(set(data["wallets"]))
# Include notes
if data["notes"]:
entry["notes"] = "; ".join(data["notes"])
result.append(entry)
return result
def main():
"""CLI entry point for testing."""
import argparse
parser = argparse.ArgumentParser(description="Load and validate portfolio file")
parser.add_argument("portfolio", type=str, help="Path to portfolio JSON")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
args = parser.parse_args()
loader = PortfolioLoader(verbose=args.verbose)
portfolio = loader.load(args.portfolio)
if portfolio:
print(json.dumps(portfolio, indent=2))
else:
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Crypto Portfolio Tracker - Main CLI Entry Point
Track cryptocurrency portfolio with real-time valuations,
allocation analysis, and P&L tracking.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import argparse
import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List
# Add scripts directory to path for local imports
SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(SCRIPT_DIR))
from portfolio_loader import PortfolioLoader
from price_fetcher import PriceFetcher
from valuation_engine import ValuationEngine
from formatters import PortfolioFormatter
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Track cryptocurrency portfolio with real-time valuations",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --portfolio holdings.json # Portfolio summary
%(prog)s --portfolio holdings.json --holdings # All holdings
%(prog)s --portfolio holdings.json --detailed # Full analysis
%(prog)s --portfolio holdings.json --format json # JSON export
"""
)
# Required
parser.add_argument(
"--portfolio", "-p",
type=str,
required=True,
help="Path to portfolio JSON file"
)
# Display options
parser.add_argument(
"--holdings",
action="store_true",
help="Show all holdings breakdown"
)
parser.add_argument(
"--detailed",
action="store_true",
help="Show detailed analysis with P&L"
)
parser.add_argument(
"--sort",
type=str,
choices=["value", "allocation", "name", "change"],
default="value",
help="Sort holdings by (default: value)"
)
# Thresholds
parser.add_argument(
"--threshold",
type=float,
default=25.0,
help="Allocation warning threshold in percent (default: 25)"
)
# Output options
parser.add_argument(
"--format", "-f",
type=str,
choices=["table", "json", "csv"],
default="table",
help="Output format (default: table)"
)
parser.add_argument(
"--output", "-o",
type=str,
help="Output file path (default: stdout)"
)
# Debug options
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable verbose output"
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 2.0.0"
)
return parser.parse_args()
def main() -> None:
"""Main entry point."""
args = parse_args()
# Initialize components
loader = PortfolioLoader(verbose=args.verbose)
fetcher = PriceFetcher(verbose=args.verbose)
engine = ValuationEngine(verbose=args.verbose)
formatter = PortfolioFormatter()
# Load portfolio
if args.verbose:
print(f"Loading portfolio from {args.portfolio}...", file=sys.stderr)
portfolio = loader.load(args.portfolio)
if not portfolio:
print(f"Error: Failed to load portfolio from {args.portfolio}", file=sys.stderr)
sys.exit(1)
if args.verbose:
print(f"Loaded {len(portfolio.get('holdings', []))} holdings", file=sys.stderr)
# Get unique coins
coins = list(set(h.get("coin", "").upper() for h in portfolio.get("holdings", [])))
coins = [c for c in coins if c] # Remove empty
if not coins:
print("Error: No valid holdings found in portfolio", file=sys.stderr)
sys.exit(1)
# Fetch prices
if args.verbose:
print(f"Fetching prices for {len(coins)} coins...", file=sys.stderr)
prices = fetcher.fetch_prices(coins)
if not prices:
print("Warning: Could not fetch prices, using cached or fallback values", file=sys.stderr)
# Calculate valuations
if args.verbose:
print("Calculating valuations...", file=sys.stderr)
valuations = engine.calculate(
portfolio=portfolio,
prices=prices,
threshold=args.threshold
)
# Sort holdings
if valuations.get("holdings"):
sort_key = {
"value": lambda x: x.get("value_usd", 0),
"allocation": lambda x: x.get("allocation_pct", 0),
"name": lambda x: x.get("coin", ""),
"change": lambda x: x.get("change_24h_pct", 0) or 0,
}.get(args.sort, lambda x: x.get("value_usd", 0))
reverse = args.sort != "name"
valuations["holdings"] = sorted(
valuations["holdings"],
key=sort_key,
reverse=reverse
)
# Add metadata
valuations["meta"] = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"portfolio_file": args.portfolio,
"threshold": args.threshold,
"sort_by": args.sort
}
# Determine display mode
show_all = args.holdings or args.detailed
show_pnl = args.detailed
# Format output
output = formatter.format(
valuations,
format_type=args.format,
show_all_holdings=show_all,
show_pnl=show_pnl
)
# Write output
if args.output:
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w") as f:
f.write(output)
print(f"Output written to {output_path}", file=sys.stderr)
else:
print(output)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Price Fetcher
Fetches cryptocurrency prices from CoinGecko API.
Implements caching and batch requests for efficiency.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import json
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List
try:
import requests
except ImportError:
print("Error: requests library required. Install with: pip install requests", file=sys.stderr)
sys.exit(1)
# Map common symbols to CoinGecko IDs
SYMBOL_TO_ID = {
"BTC": "bitcoin",
"ETH": "ethereum",
"SOL": "solana",
"XRP": "ripple",
"ADA": "cardano",
"DOGE": "dogecoin",
"DOT": "polkadot",
"LINK": "chainlink",
"AVAX": "avalanche-2",
"MATIC": "matic-network",
"BNB": "binancecoin",
"LTC": "litecoin",
"ATOM": "cosmos",
"UNI": "uniswap",
"AAVE": "aave",
"CRV": "curve-dao-token",
"MKR": "maker",
"COMP": "compound-governance-token",
"SNX": "havven",
"SUSHI": "sushi",
"YFI": "yearn-finance",
"1INCH": "1inch",
"ENS": "ethereum-name-service",
"LDO": "lido-dao",
"ARB": "arbitrum",
"OP": "optimism",
"APT": "aptos",
"SUI": "sui",
"SEI": "sei-network",
"TIA": "celestia",
"INJ": "injective-protocol",
"FET": "fetch-ai",
"RNDR": "render-token",
"GRT": "the-graph",
"FIL": "filecoin",
"NEAR": "near",
"ICP": "internet-computer",
"HBAR": "hedera-hashgraph",
"VET": "vechain",
"ALGO": "algorand",
"XLM": "stellar",
"XTZ": "tezos",
"EOS": "eos",
"FLOW": "flow",
"MANA": "decentraland",
"SAND": "the-sandbox",
"AXS": "axie-infinity",
"APE": "apecoin",
"SHIB": "shiba-inu",
"PEPE": "pepe",
"WIF": "dogwifcoin",
"BONK": "bonk",
# Stablecoins
"USDC": "usd-coin",
"USDT": "tether",
"DAI": "dai",
"BUSD": "binance-usd",
"TUSD": "true-usd",
"FRAX": "frax",
"LUSD": "liquity-usd",
"USDD": "usdd",
}
class PriceFetcher:
"""Fetches cryptocurrency prices from CoinGecko."""
COINGECKO_API = "https://api.coingecko.com/api/v3"
CACHE_FILE = Path(__file__).parent / ".price_cache.json"
CACHE_TTL = 60 # 1 minute
def __init__(self, verbose: bool = False):
"""Initialize fetcher.
Args:
verbose: Enable verbose output
"""
self.verbose = verbose
self._cache: Dict[str, Any] = {}
self._cache_time: float = 0
def fetch_prices(self, symbols: List[str]) -> Dict[str, Dict[str, Any]]:
"""Fetch prices for list of symbols.
Args:
symbols: List of coin symbols (e.g., ["BTC", "ETH"])
Returns:
Dict mapping symbol to price data
"""
# Check cache first
if self._is_cache_valid():
if self.verbose:
print("Using cached prices", file=sys.stderr)
return self._get_cached_prices(symbols)
# Map symbols to CoinGecko IDs
id_to_symbol = {}
unknown = []
for symbol in symbols:
symbol = symbol.upper()
coin_id = SYMBOL_TO_ID.get(symbol, symbol.lower())
id_to_symbol[coin_id] = symbol
if symbol not in SYMBOL_TO_ID:
unknown.append(symbol)
if unknown and self.verbose:
print(f"Warning: Unknown symbols, trying lowercase: {unknown}", file=sys.stderr)
# Fetch from API
prices = self._fetch_from_api(list(id_to_symbol.keys()))
# Map back to symbols
result = {}
for coin_id, data in prices.items():
symbol = id_to_symbol.get(coin_id, coin_id.upper())
result[symbol] = data
# Update cache
self._cache = result
self._cache_time = time.time()
self._save_cache()
return result
def _fetch_from_api(self, coin_ids: List[str]) -> Dict[str, Dict[str, Any]]:
"""Fetch prices from CoinGecko API."""
results = {}
# Batch into groups of 250 (API limit)
batches = [coin_ids[i:i+250] for i in range(0, len(coin_ids), 250)]
for batch in batches:
try:
if self.verbose:
print(f"Fetching prices for {len(batch)} coins...", file=sys.stderr)
response = requests.get(
f"{self.COINGECKO_API}/coins/markets",
params={
"vs_currency": "usd",
"ids": ",".join(batch),
"order": "market_cap_desc",
"sparkline": "false",
"price_change_percentage": "24h,7d,30d"
},
timeout=15
)
response.raise_for_status()
data = response.json()
for coin in data:
results[coin["id"]] = {
"price": coin.get("current_price", 0),
"change_24h": coin.get("price_change_percentage_24h"),
"change_7d": coin.get("price_change_percentage_7d_in_currency"),
"change_30d": coin.get("price_change_percentage_30d_in_currency"),
"market_cap": coin.get("market_cap"),
"volume_24h": coin.get("total_volume"),
"last_updated": coin.get("last_updated")
}
# Rate limit protection
if len(batches) > 1:
time.sleep(0.5)
except requests.exceptions.RequestException as e:
if self.verbose:
print(f"API request failed: {e}", file=sys.stderr)
# Try to load from cache
return self._load_cached_prices()
return results
def _is_cache_valid(self) -> bool:
"""Check if cache is still valid."""
if not self._cache:
self._load_cache_file()
return (time.time() - self._cache_time) < self.CACHE_TTL
def _get_cached_prices(self, symbols: List[str]) -> Dict[str, Dict[str, Any]]:
"""Get prices from cache."""
return {s: self._cache.get(s.upper(), {}) for s in symbols}
def _load_cache_file(self) -> None:
"""Load cache from file."""
try:
if self.CACHE_FILE.exists():
with open(self.CACHE_FILE, "r") as f:
data = json.load(f)
self._cache = data.get("prices", {})
self._cache_time = data.get("timestamp", 0)
except Exception:
self._cache = {}
self._cache_time = 0
def _save_cache(self) -> None:
"""Save cache to file."""
try:
with open(self.CACHE_FILE, "w") as f:
json.dump({
"prices": self._cache,
"timestamp": self._cache_time
}, f)
except Exception:
pass # Cache save failure is non-fatal
def _load_cached_prices(self) -> Dict[str, Dict[str, Any]]:
"""Load prices from cache file (fallback)."""
self._load_cache_file()
if self._cache:
if self.verbose:
print("Using stale cached prices as fallback", file=sys.stderr)
return self._cache
def main():
"""CLI entry point for testing."""
import argparse
parser = argparse.ArgumentParser(description="Fetch cryptocurrency prices")
parser.add_argument("symbols", nargs="+", help="Coin symbols (BTC, ETH, etc.)")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
args = parser.parse_args()
fetcher = PriceFetcher(verbose=args.verbose)
prices = fetcher.fetch_prices(args.symbols)
print(json.dumps(prices, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Valuation Engine
Calculates portfolio valuations, allocations, and P&L.
Generates risk flags for concentration warnings.
Author: Jeremy Longshore <jeremy@intentsolutions.io>
Version: 2.0.0
License: MIT
"""
import sys
from datetime import datetime
from typing import Optional, Dict, Any, List
class ValuationEngine:
"""Calculates portfolio valuations and analytics."""
def __init__(self, verbose: bool = False):
"""Initialize engine.
Args:
verbose: Enable verbose output
"""
self.verbose = verbose
def calculate(
self,
portfolio: Dict[str, Any],
prices: Dict[str, Dict[str, Any]],
threshold: float = 25.0
) -> Dict[str, Any]:
"""Calculate portfolio valuations.
Args:
portfolio: Validated portfolio dict
prices: Price data keyed by symbol
threshold: Allocation warning threshold (percent)
Returns:
Valuation result with holdings and analytics
"""
holdings = portfolio.get("holdings", [])
categories = portfolio.get("categories", {})
# Calculate individual holdings
valued_holdings = []
total_value = 0
total_cost = 0
has_cost_basis = False
for holding in holdings:
coin = holding.get("coin", "").upper()
quantity = holding.get("quantity", 0)
cost_basis = holding.get("cost_basis")
# Get price data
price_data = prices.get(coin, {})
price = price_data.get("price", 0)
# Calculate value
value = quantity * price
entry = {
"coin": coin,
"quantity": quantity,
"price_usd": price,
"value_usd": value,
"change_24h_pct": price_data.get("change_24h"),
"change_7d_pct": price_data.get("change_7d"),
"change_30d_pct": price_data.get("change_30d"),
"market_cap": price_data.get("market_cap"),
}
# Add cost basis and P&L if available
if cost_basis is not None:
has_cost_basis = True
cost = quantity * cost_basis
pnl = value - cost
pnl_pct = ((price - cost_basis) / cost_basis * 100) if cost_basis > 0 else 0
entry["cost_basis"] = cost_basis
entry["total_cost"] = cost
entry["unrealized_pnl"] = pnl
entry["pnl_pct"] = pnl_pct
total_cost += cost
# Track category
if coin in categories:
entry["category"] = categories[coin]
# Add wallets if tracked
if holding.get("wallets"):
entry["wallets"] = holding["wallets"]
valued_holdings.append(entry)
total_value += value
# Calculate allocations
for holding in valued_holdings:
if total_value > 0:
holding["allocation_pct"] = (holding["value_usd"] / total_value) * 100
else:
holding["allocation_pct"] = 0
# Calculate totals
total_change_24h = self._calculate_total_change(valued_holdings, "change_24h_pct", total_value)
total_change_7d = self._calculate_total_change(valued_holdings, "change_7d_pct", total_value)
# Generate risk flags
risk_flags = self._generate_risk_flags(valued_holdings, threshold)
# Calculate category allocation
category_allocation = self._calculate_category_allocation(valued_holdings, total_value)
# Build result
result = {
"portfolio_name": portfolio.get("name", "My Portfolio"),
"total_value_usd": round(total_value, 2),
"change_24h": {
"amount": round(total_change_24h["amount"], 2) if total_change_24h else None,
"percent": round(total_change_24h["percent"], 2) if total_change_24h else None
},
"change_7d": {
"amount": round(total_change_7d["amount"], 2) if total_change_7d else None,
"percent": round(total_change_7d["percent"], 2) if total_change_7d else None
},
"holdings": valued_holdings,
"holdings_count": len(valued_holdings),
"risk_flags": risk_flags,
"allocation_by_category": category_allocation
}
# Add total P&L if cost basis available
if has_cost_basis and total_cost > 0:
total_pnl = total_value - total_cost
total_pnl_pct = (total_pnl / total_cost) * 100
result["total_cost"] = round(total_cost, 2)
result["total_unrealized_pnl"] = round(total_pnl, 2)
result["total_pnl_pct"] = round(total_pnl_pct, 2)
return result
def _calculate_total_change(
self,
holdings: List[Dict[str, Any]],
change_key: str,
total_value: float
) -> Optional[Dict[str, float]]:
"""Calculate weighted total change."""
if total_value <= 0:
return None
weighted_change = 0
valid_weight = 0
for holding in holdings:
change = holding.get(change_key)
value = holding.get("value_usd", 0)
if change is not None and value > 0:
weighted_change += (value / total_value) * change
valid_weight += value / total_value
if valid_weight < 0.5: # Less than 50% of portfolio has change data
return None
# Calculate dollar amount
# If current value is V and it changed by P%, then previous value was V/(1 + P/100)
if weighted_change != -100:
previous_value = total_value / (1 + weighted_change / 100)
change_amount = total_value - previous_value
else:
change_amount = -total_value
return {
"amount": change_amount,
"percent": weighted_change
}
def _generate_risk_flags(
self,
holdings: List[Dict[str, Any]],
threshold: float
) -> List[str]:
"""Generate risk flags for portfolio."""
flags = []
# Check concentration
for holding in holdings:
allocation = holding.get("allocation_pct", 0)
coin = holding.get("coin", "")
if allocation > threshold:
flags.append(f"{coin} allocation ({allocation:.1f}%) exceeds {threshold:.0f}% threshold")
# Check for single asset dominance
if holdings and holdings[0].get("allocation_pct", 0) > 50:
coin = holdings[0].get("coin", "")
flags.append(f"{coin} represents majority of portfolio (>50%)")
# Check for lack of diversification
if len(holdings) < 3:
flags.append("Portfolio has limited diversification (<3 assets)")
return flags
def _calculate_category_allocation(
self,
holdings: List[Dict[str, Any]],
total_value: float
) -> Dict[str, float]:
"""Calculate allocation by category."""
if total_value <= 0:
return {}
categories = {}
uncategorized = 0
for holding in holdings:
category = holding.get("category", "Other")
value = holding.get("value_usd", 0)
if category not in categories:
categories[category] = 0
categories[category] += value
# Convert to percentages
return {
cat: round((val / total_value) * 100, 1)
for cat, val in sorted(categories.items(), key=lambda x: -x[1])
}
def main():
"""CLI entry point for testing."""
import json
# Test with sample data
portfolio = {
"name": "Test Portfolio",
"holdings": [
{"coin": "BTC", "quantity": 0.5, "cost_basis": 50000},
{"coin": "ETH", "quantity": 10, "cost_basis": 2500},
{"coin": "SOL", "quantity": 100, "cost_basis": 100}
],
"categories": {
"BTC": "Layer 1",
"ETH": "Layer 1",
"SOL": "Layer 1"
}
}
prices = {
"BTC": {"price": 95000, "change_24h": 2.5, "change_7d": 5.0},
"ETH": {"price": 3200, "change_24h": 1.8, "change_7d": 3.5},
"SOL": {"price": 180, "change_24h": 4.2, "change_7d": 8.0}
}
engine = ValuationEngine(verbose=True)
result = engine.calculate(portfolio, prices, threshold=25.0)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
Related skills
FAQ
Where does it get prices?
It fetches real-time prices from the CoinGecko API using the requests library.
How does it flag risk?
By default it flags any position above 25% allocation as a concentration risk, with configurable thresholds.