Skip to main content
⚠️ DCA and portfolio tracking are educational automation examples. This is not financial advice. Crypto investments carry extreme risk including total loss of principal.
CRYPTO & DEFI · PART 5 OF 5

Portfolio & DCA Automation

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.

CoinGecko Free
YAML Config
DCA Scheduler
No Exchange API Needed

1. Portfolio YAML Configuration

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:

YAML
# 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

2. Unrealized P&L Calculator

Load your YAML portfolio and fetch live prices from CoinGecko to calculate unrealized gains/losses for each position and your total portfolio.

Python
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)

3. Rebalancing Drift Detection

Compare your current allocation against target weights. Automatically flag positions that have drifted beyond your threshold, showing whether to trim or add.

Python
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.")

4. Dollar-Cost Averaging Scheduler

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.

Python
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)

5. Tax Lot Awareness

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.

Python
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)

6. Daily Portfolio Brief

Combine all tools into one morning report: P&L snapshot, rebalancing status, upcoming DCA purchases, tax lot classification, and market sentiment.

Python
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()

7. YAML Configuration Reference

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

Key Takeaways

🎓 You've Completed the Crypto & DeFi Series

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