DEV Community

Paarthurnax
Paarthurnax

Posted on

How to Build a Crypto DCA Bot with OpenClaw (Paper Trading)

Dollar-Cost Averaging (DCA) is one of the most battle-tested investment strategies in crypto. The idea is simple: instead of trying to time the market, you buy a fixed dollar amount of an asset at regular intervals — regardless of price. Over time, you accumulate more coins when prices are low and fewer when prices are high, averaging out your cost basis.

The problem? Doing DCA manually is tedious. You have to remember to buy, fight the temptation to skip when markets are red, and track your average cost basis across dozens of purchases.

The solution: a DCA bot. But before you run one with real money, you should validate it in paper trading mode. That's exactly what we're going to build today with OpenClaw.


What You'll Build

By the end of this tutorial, you'll have:

  • A configurable DCA bot that buys on a schedule
  • Paper trading mode so you can test without real money
  • Performance tracking to measure your average cost basis
  • Basic risk controls (max buy price, emergency stop)

Why OpenClaw for DCA?

OpenClaw is a local AI agent framework that runs entirely on your machine. For a DCA bot, this has specific advantages:

  1. No subscription fees. A DCA bot runs 24/7 — paying $30-100/month to a cloud platform to run it defeats the purpose of accumulation.
  2. Full control. You decide the exact logic: frequency, amounts, which assets, maximum price thresholds.
  3. Privacy. Your accumulation strategy stays local. Nobody else can see when you're buying or how much.
  4. Paper trading first. Test your DCA parameters before deploying real capital.

Get the complete guide at dragonwhisper36.gumroad.com/l/homeaiagent.


Prerequisites

  • Python 3.10+
  • OpenClaw installed and configured
  • Free CoinGecko API access (no key needed for basic data)
  • Basic Python knowledge (if you can read Python, you can follow this)

Step 1: Set Up Your DCA Configuration

First, create your DCA config file. This is where you control all the parameters.

# dca_config.py
DCA_CONFIG = {
    "assets": {
        "BTC": {
            "buy_amount_usd": 25,      # Buy $25 of BTC each cycle
            "frequency_hours": 24,     # Buy every 24 hours
            "max_price": 120000,       # Don't buy above $120k
            "enabled": True,
        },
        "ETH": {
            "buy_amount_usd": 10,      # Buy $10 of ETH each cycle
            "frequency_hours": 48,     # Buy every 48 hours
            "max_price": 5000,         # Don't buy above $5k
            "enabled": True,
        },
    },
    "paper_trading": True,             # ALWAYS start with True
    "starting_balance_usd": 1000,      # Virtual USD balance for paper trading
    "log_file": "dca_log.json",
}
Enter fullscreen mode Exit fullscreen mode

Note: paper_trading: True is non-negotiable when starting out. We'll keep this flag front and center throughout the tutorial.


Step 2: Build the Price Fetcher

We'll use CoinGecko's free API — no key required for basic price data.

# price_fetcher.py
import urllib.request
import json
import time

COINGECKO_BASE = "https://api.coingecko.com/api/v3"

COIN_IDS = {
    "BTC": "bitcoin",
    "ETH": "ethereum",
    "SOL": "solana",
    "BNB": "binancecoin",
}

def get_prices(symbols: list[str]) -> dict:
    """
    Fetch current USD prices for given symbols.
    Returns dict like {"BTC": 95000.50, "ETH": 3200.75}
    """
    coin_ids = [COIN_IDS.get(s, s.lower()) for s in symbols]
    ids_str = ",".join(coin_ids)

    url = f"{COINGECKO_BASE}/simple/price?ids={ids_str}&vs_currencies=usd"

    try:
        req = urllib.request.Request(url, headers={"User-Agent": "DCABot/1.0"})
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read())

        # Map back from coin_id to symbol
        result = {}
        for symbol in symbols:
            coin_id = COIN_IDS.get(symbol, symbol.lower())
            if coin_id in data:
                result[symbol] = data[coin_id]["usd"]

        return result

    except Exception as e:
        print(f"[price_fetcher] Error: {e}")
        return {}


def get_price(symbol: str) -> float:
    """Get single asset price."""
    prices = get_prices([symbol])
    return prices.get(symbol, 0.0)


if __name__ == "__main__":
    prices = get_prices(["BTC", "ETH", "SOL"])
    for symbol, price in prices.items():
        print(f"{symbol}: ${price:,.2f}")
Enter fullscreen mode Exit fullscreen mode

Test this before proceeding:

python price_fetcher.py
# BTC: $94,523.00
# ETH: $3,187.50
# SOL: $142.30
Enter fullscreen mode Exit fullscreen mode

Step 3: Build the DCA Engine

The core of our bot: the DCA execution logic with paper trading support.

# dca_engine.py
import json
import time
from pathlib import Path
from datetime import datetime, timedelta
from price_fetcher import get_price
from dca_config import DCA_CONFIG

LOG_FILE = Path(DCA_CONFIG["log_file"])

def load_state() -> dict:
    """Load DCA state from file."""
    if LOG_FILE.exists():
        try:
            return json.loads(LOG_FILE.read_text())
        except Exception:
            pass

    return {
        "paper_balance_usd": DCA_CONFIG["starting_balance_usd"],
        "holdings": {},      # {"BTC": {"units": 0.5, "avg_cost": 85000}}
        "purchases": [],
        "last_buy": {},      # {"BTC": "2026-03-20T10:00:00"}
    }

def save_state(state: dict):
    LOG_FILE.write_text(json.dumps(state, indent=2, default=str))

def should_buy(symbol: str, state: dict) -> tuple[bool, str]:
    """
    Check if it's time to buy this asset.
    Returns (should_buy: bool, reason: str)
    """
    config = DCA_CONFIG["assets"].get(symbol)
    if not config or not config.get("enabled"):
        return False, "disabled"

    # Check frequency
    last_buy_str = state["last_buy"].get(symbol)
    if last_buy_str:
        last_buy = datetime.fromisoformat(last_buy_str)
        next_buy = last_buy + timedelta(hours=config["frequency_hours"])
        if datetime.now() < next_buy:
            remaining = next_buy - datetime.now()
            return False, f"next buy in {remaining.total_seconds()/3600:.1f}h"

    return True, "scheduled buy"

def execute_buy(symbol: str, state: dict, dry_run_price: float = None) -> dict:
    """
    Execute (or simulate) a DCA buy.
    In paper trading mode, this updates virtual balances only.
    """
    config = DCA_CONFIG["assets"][symbol]
    amount_usd = config["buy_amount_usd"]
    max_price = config.get("max_price", float("inf"))

    # Get current price
    price = dry_run_price or get_price(symbol)
    if not price:
        return {"success": False, "reason": "price fetch failed"}

    # Price check
    if price > max_price:
        return {
            "success": False,
            "reason": f"price ${price:,.2f} exceeds max ${max_price:,.2f}"
        }

    units = amount_usd / price

    if DCA_CONFIG["paper_trading"]:
        # PAPER TRADING: update virtual balance
        if state["paper_balance_usd"] < amount_usd:
            return {"success": False, "reason": "insufficient paper balance"}

        state["paper_balance_usd"] -= amount_usd

        # Update holdings and average cost
        holdings = state["holdings"].get(symbol, {"units": 0, "avg_cost": 0})
        old_units = holdings["units"]
        old_avg = holdings["avg_cost"]

        new_units = old_units + units
        new_avg = ((old_units * old_avg) + (units * price)) / new_units if new_units > 0 else price

        state["holdings"][symbol] = {"units": new_units, "avg_cost": new_avg}

        mode = "PAPER"
    else:
        # LIVE TRADING: implement exchange integration here
        # This is where you'd call exchange API
        raise NotImplementedError("Live trading not implemented — run paper first!")
        mode = "LIVE"

    # Log the purchase
    purchase = {
        "timestamp": datetime.now().isoformat(),
        "symbol": symbol,
        "amount_usd": amount_usd,
        "price": price,
        "units": units,
        "mode": mode,
    }
    state["purchases"].append(purchase)
    state["last_buy"][symbol] = datetime.now().isoformat()

    save_state(state)

    return {
        "success": True,
        "symbol": symbol,
        "amount_usd": amount_usd,
        "price": price,
        "units": units,
        "mode": mode,
    }

def print_portfolio_summary(state: dict):
    """Print current paper portfolio status."""
    print("\n=== DCA Portfolio Summary ===")
    print(f"Paper cash balance: ${state['paper_balance_usd']:,.2f}")
    print(f"Total purchases: {len(state['purchases'])}")

    if state["holdings"]:
        print("\nHoldings:")
        for symbol, data in state["holdings"].items():
            current_price = get_price(symbol)
            if current_price:
                current_value = data["units"] * current_price
                avg_cost = data["avg_cost"]
                pnl_pct = ((current_price - avg_cost) / avg_cost) * 100
                print(f"  {symbol}:")
                print(f"    Units: {data['units']:.6f}")
                print(f"    Avg Cost: ${avg_cost:,.2f}")
                print(f"    Current Price: ${current_price:,.2f}")
                print(f"    Current Value: ${current_value:,.2f}")
                print(f"    P&L: {pnl_pct:+.1f}%")

    print("=" * 30)
Enter fullscreen mode Exit fullscreen mode

Step 4: The Main Loop

# dca_bot.py
import time
from dca_engine import load_state, save_state, should_buy, execute_buy, print_portfolio_summary
from dca_config import DCA_CONFIG

def run_dca_bot(check_interval_minutes: int = 60):
    """
    Main DCA bot loop. Checks for scheduled buys every N minutes.
    """
    mode = "PAPER" if DCA_CONFIG["paper_trading"] else "LIVE"
    print(f"Starting DCA Bot — Mode: {mode}")
    print(f"Check interval: every {check_interval_minutes} minutes")
    print("Press Ctrl+C to stop\n")

    state = load_state()

    while True:
        print(f"\n[{__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M')}] Checking DCA schedules...")

        for symbol, config in DCA_CONFIG["assets"].items():
            if not config.get("enabled"):
                continue

            ready, reason = should_buy(symbol, state)

            if ready:
                print(f"  {symbol}: Executing buy...")
                result = execute_buy(symbol, state)

                if result["success"]:
                    print(f"{result['mode']} BUY: ${result['amount_usd']:.0f} of {symbol} "
                          f"@ ${result['price']:,.2f} = {result['units']:.6f} units")
                else:
                    print(f"{symbol}: {result['reason']}")
            else:
                print(f"  {symbol}: Skipping — {reason}")

        # Show portfolio every 6 hours (roughly)
        if int(time.time()) % (6 * 3600) < check_interval_minutes * 60:
            print_portfolio_summary(state)

        print(f"  Sleeping {check_interval_minutes}m until next check...")
        time.sleep(check_interval_minutes * 60)


if __name__ == "__main__":
    run_dca_bot(check_interval_minutes=60)
Enter fullscreen mode Exit fullscreen mode

Step 5: Run in Paper Trading Mode

python dca_bot.py
Enter fullscreen mode Exit fullscreen mode

You should see output like:

Starting DCA Bot — Mode: PAPER
Check interval: every 60 minutes
Press Ctrl+C to stop

[2026-03-23 10:00] Checking DCA schedules...
  BTC: Executing buy...
  ✓ PAPER BUY: $25 of BTC @ $94,523.00 = 0.000265 units
  ETH: Executing buy...
  ✓ PAPER BUY: $10 of ETH @ $3,187.50 = 0.003137 units
  Sleeping 60m until next check...
Enter fullscreen mode Exit fullscreen mode

Step 6: Analyzing Your Paper Trading Results

After running for a few days or weeks, check your results:

# analyze.py
import json
from pathlib import Path

state = json.loads(Path("dca_log.json").read_text())

purchases = state["purchases"]
print(f"Total purchases: {len(purchases)}")
print(f"Total spent: ${sum(p['amount_usd'] for p in purchases):,.2f}")

# Group by asset
from collections import defaultdict
by_asset = defaultdict(list)
for p in purchases:
    by_asset[p["symbol"]].append(p)

for symbol, buys in by_asset.items():
    total_usd = sum(b["amount_usd"] for b in buys)
    total_units = sum(b["units"] for b in buys)
    avg_price = total_usd / total_units if total_units else 0
    print(f"\n{symbol}:")
    print(f"  Purchases: {len(buys)}")
    print(f"  Total invested: ${total_usd:,.2f}")
    print(f"  Average buy price: ${avg_price:,.2f}")
    print(f"  Total units: {total_units:.6f}")
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls to Avoid

1. Going live too fast. The whole point of paper trading is to surface problems before real money is at risk. Run at least 2-4 weeks in paper mode before considering live trading.

2. Setting max_price too low. If your max price ceiling is too restrictive, the bot won't execute many buys. DCA works best when it buys consistently — including at "high" prices.

3. Over-configuring. Start simple: one asset, one frequency, one fixed amount. Complexity compounds errors.

4. Ignoring the logs. The log file is your paper trail. Review it regularly to understand when and why buys are happening.

5. Confusing paper performance with live performance. Paper trading doesn't account for slippage, fees, or order book dynamics. Real performance will differ.


Key Takeaways

  • DCA is a proven accumulation strategy — consistency beats timing
  • Paper trading first is non-negotiable — validate before you risk real capital
  • Local AI means zero ongoing cost — your DCA bot runs for free on your own machine
  • Keep it simple to start — one asset, fixed intervals, conservative amounts
  • Analyze your logs — data tells you whether your DCA parameters are working

Next Steps

Once you're comfortable with this basic DCA bot, OpenClaw's AI skills can extend it significantly:

  • Market regime detection: Only DCA during bearish/sideways phases, not into vertical rallies
  • Fear & Greed integration: Increase buy amounts when fear is extreme
  • Portfolio rebalancing: Automatically adjust allocations as values shift
  • Multi-exchange support: DCA across multiple exchanges with unified portfolio view

Browse the full library at the CryptoClaw Skills Hub and get the base setup guide at Gumroad.


Disclaimer: This is an educational tutorial about paper trading only. Nothing here constitutes financial advice. Crypto trading involves significant risk. All examples use paper trading mode — never deploy capital without understanding the risks involved.

Top comments (0)