Track your multi-coin portfolio, automate dollar-cost averaging logic, monitor unrealized P&L, and detect rebalancing drift — all with free APIs and local YAML config.
Define all your holdings in a local YAML file — no exchange API required. This becomes the source of truth for your portfolio analysis.
Each position tracks:
# crypto-portfolio.yaml
portfolio:
name: "My Crypto Portfolio"
currency: USD
positions:
- coin_id: bitcoin # CoinGecko ID
symbol: BTC
quantity: 0.5
avg_cost_usd: 42500.00
account: cold_wallet
tax_lot_date: "2023-06-15"
- coin_id: ethereum
symbol: ETH
quantity: 4.2
avg_cost_usd: 2800.00
account: hardware_wallet
tax_lot_date: "2023-09-01"
- coin_id: solana
symbol: SOL
quantity: 50
avg_cost_usd: 95.00
account: phantom_wallet
tax_lot_date: "2024-01-10"
- coin_id: chainlink
symbol: LINK
quantity: 200
avg_cost_usd: 14.50
account: metamask
tax_lot_date: "2024-03-20"
target_allocation:
bitcoin: 50 # percent
ethereum: 30
solana: 15
chainlink: 5
dca:
enabled: true
amount_usd: 100
frequency: weekly # weekly, biweekly, monthly
coins:
bitcoin: 60 # percent of DCA amount
ethereum: 40
Load your YAML portfolio and fetch live prices from CoinGecko to calculate unrealized gains/losses for each position and your total portfolio.
import yaml
import requests
from datetime import datetime
def load_portfolio(path: str = "crypto-portfolio.yaml") -> dict:
with open(path) as f:
return yaml.safe_load(f)["portfolio"]
def get_live_prices(coin_ids: list) -> dict:
"""Fetch current prices from CoinGecko (no key needed)."""
ids = ",".join(coin_ids)
resp = requests.get(
"https://api.coingecko.com/api/v3/simple/price",
params={"ids": ids, "vs_currencies": "usd", "include_24hr_change": "true"},
timeout=10
)
return resp.json()
def calculate_pnl(portfolio: dict) -> list:
"""Calculate unrealized P&L for each position."""
positions = portfolio["positions"]
coin_ids = [p["coin_id"] for p in positions]
prices = get_live_prices(coin_ids)
results = []
total_cost = 0
total_value = 0
for pos in positions:
cid = pos["coin_id"]
price_data = prices.get(cid, {})
current_price = price_data.get("usd", 0)
change_24h = price_data.get("usd_24h_change", 0)
cost_basis = pos["quantity"] * pos["avg_cost_usd"]
current_value = pos["quantity"] * current_price
pnl = current_value - cost_basis
pnl_pct = (pnl / cost_basis * 100) if cost_basis > 0 else 0
results.append({
"symbol": pos["symbol"],
"quantity": pos["quantity"],
"avg_cost": pos["avg_cost_usd"],
"current_price": current_price,
"change_24h": change_24h,
"cost_basis": cost_basis,
"current_value": current_value,
"pnl_usd": pnl,
"pnl_pct": pnl_pct,
"account": pos.get("account", ""),
})
total_cost += cost_basis
total_value += current_value
total_pnl = total_value - total_cost
total_pnl_pct = (total_pnl / total_cost * 100) if total_cost > 0 else 0
# Print report
print(f"{'':=<70}")
print(f" OpenClaw Crypto Portfolio | {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print(f"{'':=<70}")
print(f"{'Coin':<8} {'Qty':>8} {'Avg Cost':>10} {'Price':>10} {'24h':>7} {'P&L':>12} {'P&L%':>7}")
print(f"{'':-<70}")
for r in results:
arrow = "▲" if r["pnl_pct"] > 0 else "▼"
arrow24 = "▲" if r["change_24h"] > 0 else "▼"
print(f"{r['symbol']:<8} {r['quantity']:>8.4f} ${r['avg_cost']:>9,.2f} "
f"${r['current_price']:>9,.2f} {arrow24}{abs(r['change_24h']):>5.1f}% "
f"${r['pnl_usd']:>+10,.2f} {arrow}{abs(r['pnl_pct']):>5.1f}%")
print(f"{'':-<70}")
print(f"{'TOTAL':<8} {'':>8} {'':>10} {'':>10} {'':>7} "
f"${total_pnl:>+10,.2f} {'+' if total_pnl_pct>0 else ''}{total_pnl_pct:.1f}%")
print(f"\n Cost Basis: ${total_cost:,.2f} | Current Value: ${total_value:,.2f}")
return results
portfolio = load_portfolio()
pnl_data = calculate_pnl(portfolio)
Compare your current allocation against target weights. Automatically flag positions that have drifted beyond your threshold, showing whether to trim or add.
def check_drift(portfolio: dict, pnl_data: list, drift_threshold: float = 5.0) -> list:
"""
Compare current allocation vs target.
Flag any position drifted more than drift_threshold percentage points.
"""
targets = portfolio.get("target_allocation", {})
if not targets:
print("No target allocation defined in YAML.")
return []
total_value = sum(r["current_value"] for r in pnl_data)
print(f"\n{'':=<55}")
print(f" Rebalancing Check (threshold: ±{drift_threshold}%)")
print(f"{'':=<55}")
print(f"{'Coin':<10} {'Target':>8} {'Current':>8} {'Drift':>8} {'Action':>15}")
print(f"{'':-<55}")
alerts = []
for r in pnl_data:
cid = next((p["coin_id"] for p in portfolio["positions"]
if p["symbol"] == r["symbol"]), None)
target_pct = targets.get(cid, 0)
current_pct = (r["current_value"] / total_value * 100) if total_value > 0 else 0
drift = current_pct - target_pct
if abs(drift) >= drift_threshold:
action = "⬇ TRIM" if drift > 0 else "⬆ ADD"
alerts.append({"symbol": r["symbol"], "drift": drift, "action": action})
else:
action = "✓ OK"
print(f"{r['symbol']:<10} {target_pct:>7.1f}% {current_pct:>7.1f}% "
f"{drift:>+7.1f}% {action:>15}")
return alerts
drift_alerts = check_drift(portfolio, pnl_data, drift_threshold=5.0)
if drift_alerts:
print(f"\n⚠️ {len(drift_alerts)} positions need rebalancing attention.")
Automatically calculate upcoming DCA purchase dates and amounts based on your YAML config. This scheduler is educational—use it to remind yourself when to buy, or integrate with an exchange API for execution.
from datetime import datetime, timedelta
import calendar
def next_dca_dates(frequency: str, count: int = 4) -> list:
"""Calculate upcoming DCA dates."""
now = datetime.now()
dates = []
current = now
for _ in range(count):
if frequency == "weekly":
# Next Monday
days_ahead = 7 - current.weekday()
if days_ahead == 0:
days_ahead = 7
current = current + timedelta(days=days_ahead)
elif frequency == "biweekly":
current = current + timedelta(weeks=2)
elif frequency == "monthly":
# First of next month
if current.month == 12:
current = current.replace(year=current.year+1, month=1, day=1)
else:
current = current.replace(month=current.month+1, day=1)
dates.append(current)
return dates
def dca_plan(portfolio: dict) -> None:
"""Print upcoming DCA schedule."""
dca_cfg = portfolio.get("dca", {})
if not dca_cfg.get("enabled"):
print("DCA not enabled in config.")
return
amount = dca_cfg["amount_usd"]
frequency = dca_cfg["frequency"]
coins = dca_cfg["coins"]
upcoming = next_dca_dates(frequency, count=4)
print(f"\n{'':=<50}")
print(f" DCA Schedule — ${amount}/period ({frequency})")
print(f"{'':=<50}")
for date in upcoming:
print(f"\n 📅 {date.strftime('%Y-%m-%d (%A)')}")
for coin, pct in coins.items():
buy_usd = amount * (pct / 100)
print(f" {coin.upper():<10} ${buy_usd:.2f}")
dca_plan(portfolio)
Classify positions as short-term (<1 year) or long-term (≥1 year). Long-term capital gains typically receive favorable tax treatment. Always consult a tax professional for your jurisdiction.
from datetime import date
def tax_lot_summary(portfolio: dict, pnl_data: list) -> None:
"""
Classify each position as short-term (<1 year) or long-term (≥1 year).
Long-term lots qualify for lower capital gains rates in most jurisdictions.
NOTE: This is informational only — consult a tax professional.
"""
today = date.today()
print(f"\n{'':=<65}")
print(f" Tax Lot Summary (as of {today}) — Educational Only")
print(f"{'':=<65}")
print(f"{'Coin':<8} {'Acquired':>12} {'Holding':>10} {'Type':>14} {'Unrlzd P&L':>12}")
print(f"{'':-<65}")
pnl_map = {r["symbol"]: r for r in pnl_data}
for pos in portfolio["positions"]:
sym = pos["symbol"]
lot_date = date.fromisoformat(pos["tax_lot_date"])
days_held = (today - lot_date).days
holding_type = "Long-Term ✓" if days_held >= 365 else "Short-Term ⚠"
pnl = pnl_map.get(sym, {}).get("pnl_usd", 0)
print(f"{sym:<8} {str(lot_date):>12} {days_held:>8}d {holding_type:>14} ${pnl:>+10,.2f}")
print(f"\n ⚠️ Tax laws vary by jurisdiction. Consult a qualified tax advisor.")
tax_lot_summary(portfolio, pnl_data)
Combine all tools into one morning report: P&L snapshot, rebalancing status, upcoming DCA purchases, tax lot classification, and market sentiment.
def daily_brief(portfolio_path: str = "crypto-portfolio.yaml") -> None:
"""Generate a complete daily portfolio brief."""
portfolio = load_portfolio(portfolio_path)
print(f"\n{'🦞 OpenClaw Crypto Daily Brief':=^70}")
print(f"{'Generated: ' + datetime.now().strftime('%Y-%m-%d %H:%M'):^70}")
print()
# 1. P&L snapshot
pnl_data = calculate_pnl(portfolio)
# 2. Drift check
drift_alerts = check_drift(portfolio, pnl_data)
# 3. DCA schedule
dca_plan(portfolio)
# 4. Tax summary
tax_lot_summary(portfolio, pnl_data)
# 5. Quick sentiment check
fg = requests.get("https://api.alternative.me/fng/?limit=1").json()
fg_val = int(fg["data"][0]["value"])
fg_label = fg["data"][0]["value_classification"]
print(f"\n{'':=<55}")
print(f" Market Sentiment: {fg_val}/100 — {fg_label}")
print(f"{'':=<55}")
# Run as cron or scheduled task
daily_brief()
Quick lookup table for all available YAML fields and their meanings:
| Field | Description | Example |
|---|---|---|
coin_id |
CoinGecko identifier (check /coins/list) | bitcoin, ethereum |
symbol |
Ticker symbol for display | BTC, ETH |
quantity |
Number of coins currently held | 0.5, 4.2 |
avg_cost_usd |
Average purchase price per coin | 42500.00, 2800.00 |
account |
Storage location (informational) | cold_wallet, hardware_wallet |
tax_lot_date |
Acquisition date (YYYY-MM-DD) for tax reporting | 2023-06-15 |
target_allocation |
Desired portfolio weight by coin_id (%) | bitcoin: 50, ethereum: 30 |
dca.amount_usd |
Total USD to invest per DCA period | 100, 500 |
dca.frequency |
How often to DCA: weekly, biweekly, or monthly |
weekly |
dca.coins |
Allocation of DCA amount by coin (%) | bitcoin: 60, ethereum: 40 |
You've mastered sentiment analysis, API integration, strategy backtesting, automated alerts, and portfolio tracking. Return to the Claw Street Terminal to explore all 7 modules.
Return to Terminal