Docs · Bot Examples

The Bot Examples Library

Five complete, runnable starter bots covering the most common patterns. Copy any of them into WatchDog Bot, paste your API keys (or skip — they all default to demo mode), and click Start. Each one is intentionally short so you can read it end-to-end in a minute.

In this library

  1. 1. Hello Fleet (5 lines)
  2. 2. Simple Momentum Bot (crypto)
  3. 3. DCA Bot (any exchange)
  4. 4. Kalshi Market Maker
  5. 5. News-Driven Bot (with LLM)
  6. 6. Cross-Exchange Arbitrage Scanner

1. Hello Fleet

Easy · 1 min Any No API keys

The simplest possible bot — verifies your environment works, prints a heartbeat, exits cleanly. Use it to confirm the platform is wired up before you write anything real.

import time
import wd

wd.log.info("Hello fleet — bot %s starting", wd.bot_name())

for i in range(10):
    wd.log.info("Heartbeat %d/10 at %s", i + 1, time.strftime("%H:%M:%S"))
    time.sleep(2)

wd.log.info("Bot finished cleanly")

2. Simple Momentum Bot

Easy · 5 min Crypto (Binance) Demo-safe

Buys when the 5-minute moving average crosses above the 20-minute, sells when it crosses back. Classic trend-following starter — easy to understand, easy to tweak.

import time
from collections import deque
import wd
import ccxt

conn = wd.connection("Binance")
exchange = ccxt.binance({
    "apiKey": conn.api_key,
    "secret": conn.api_secret,
    "enableRateLimit": True,
})

SYMBOL = "BTC/USDT"
SHORT_WINDOW = 5
LONG_WINDOW = 20
POSITION_USDT = 100  # in demo mode this is symbolic

short_ma = deque(maxlen=SHORT_WINDOW)
long_ma  = deque(maxlen=LONG_WINDOW)
in_position = False

wd.log.info("Momentum bot starting on %s", SYMBOL)

while True:
    try:
        ticker = exchange.fetch_ticker(SYMBOL)
        price = float(ticker["last"])
        short_ma.append(price)
        long_ma.append(price)

        if len(long_ma) < LONG_WINDOW:
            wd.log.info("Warming up — %d/%d ticks", len(long_ma), LONG_WINDOW)
            time.sleep(60)
            continue

        s = sum(short_ma) / len(short_ma)
        l = sum(long_ma)  / len(long_ma)
        wd.log.info("price=%.2f  short=%.2f  long=%.2f", price, s, l)

        if s > l and not in_position:
            wd.log.info("BUY signal — short MA crossed above long")
            if not wd.is_demo():
                exchange.create_market_buy_order(SYMBOL, POSITION_USDT / price)
            in_position = True
        elif s < l and in_position:
            wd.log.info("SELL signal — short MA crossed below long")
            if not wd.is_demo():
                exchange.create_market_sell_order(SYMBOL, POSITION_USDT / price)
            in_position = False

        time.sleep(60)
    except Exception as e:
        wd.log.error("tick failed: %s", e)
        time.sleep(60)

Warning: A simple MA crossover is a teaching example, not a profitable strategy. Real momentum strategies need volatility filters, position sizing, and stop losses. Use this to learn the pattern, not as live capital.

3. DCA Bot (Dollar-Cost Average)

Easy · 3 min Crypto (any) Demo-safe

Buys a fixed dollar amount on a schedule. The boring strategy that beats most active traders. Configure the symbol, amount, and interval at the top.

import time, json
import wd
import ccxt

SYMBOL = "BTC/USDT"
AMOUNT_USDT = 25
INTERVAL_HOURS = 24

conn = wd.connection("Binance")
exchange = ccxt.binance({"apiKey": conn.api_key, "secret": conn.api_secret, "enableRateLimit": True})

state_file = wd.bot_dir() / "dca_state.json"
state = json.loads(state_file.read_text()) if state_file.exists() else {"total_spent": 0, "total_btc": 0.0}

wd.log.info("DCA bot starting — %s $%d every %dh", SYMBOL, AMOUNT_USDT, INTERVAL_HOURS)

while True:
    try:
        price = float(exchange.fetch_ticker(SYMBOL)["last"])
        qty = AMOUNT_USDT / price
        wd.log.info("Buying $%d of %s at $%.2f (=%f BTC)", AMOUNT_USDT, SYMBOL, price, qty)

        if not wd.is_demo():
            exchange.create_market_buy_order(SYMBOL, qty)

        state["total_spent"] += AMOUNT_USDT
        state["total_btc"]   += qty
        avg = state["total_spent"] / state["total_btc"]
        wd.log.info("Lifetime: $%.2f → %.6f BTC → avg cost $%.2f",
                    state["total_spent"], state["total_btc"], avg)
        state_file.write_text(json.dumps(state))
    except Exception as e:
        wd.log.error("DCA buy failed: %s", e)

    time.sleep(INTERVAL_HOURS * 3600)

4. Kalshi Market Maker

Medium · 30 min Kalshi Read tutorial first

Quotes both YES and NO on a single Kalshi market, capturing the bid-ask spread. Works best on illiquid markets where the spread is wide.

Prerequisites: Set up a Kalshi connection in WatchDog Bot. See the Kalshi tutorial for the auth client referenced as KalshiClient below.

import time
import wd
from kalshi_client import KalshiClient  # from the Kalshi tutorial

conn = wd.connection("Kalshi")
client = KalshiClient(conn.api_key, conn.api_secret_path)  # api_secret holds PEM path

TICKER = "KXHIGHNY-26MAY20-T75"
SPREAD = 2          # quote 2 cents inside the bid/ask
QUOTE_SIZE = 5      # number of contracts on each side

wd.log.info("Market maker starting on %s", TICKER)

while True:
    try:
        market = client.get(f"/markets/{TICKER}")["market"]
        yes_bid = market["yes_bid"]
        yes_ask = market["yes_ask"]

        if yes_bid == 0 or yes_ask == 0 or yes_ask - yes_bid < 2 * SPREAD:
            wd.log.info("Spread too tight (%d/%d) — skipping", yes_bid, yes_ask)
            time.sleep(30)
            continue

        my_yes_bid = yes_bid + SPREAD
        my_no_bid  = (100 - yes_ask) + SPREAD

        # Cancel any old quotes first (out of scope here — see Kalshi tutorial)

        for side, price in [("yes", my_yes_bid), ("no", my_no_bid)]:
            wd.log.info("Quoting %s @ %d × %d", side, price, QUOTE_SIZE)
            if not wd.is_demo():
                client.post("/portfolio/orders", {
                    "ticker": TICKER, "action": "buy", "side": side,
                    "count": QUOTE_SIZE, "type": "limit",
                    f"{side}_price": price,
                    "client_order_id": f"mm-{side}-{int(time.time())}",
                })

        time.sleep(30)
    except Exception as e:
        wd.log.error("MM tick failed: %s", e)
        time.sleep(60)

Market-making risks: If the underlying moves while your quotes are live, you can get filled on both sides and end up with a directional position you didn't want. Real market makers re-quote every few seconds and have inventory limits. This example is a teaching skeleton — add inventory tracking before going live.

5. News-Driven Bot

Medium · 20 min Any Needs OpenAI/Anthropic key

Polls a news API every minute, sends headlines to an LLM with a structured prompt, and adjusts positions based on the LLM's bullish/bearish/neutral classification. The skeleton — replace the LLM call with whatever signal you trust.

import time, json
import wd
import requests
from anthropic import Anthropic

news_conn = wd.connection("NewsAPI")
ai_conn   = wd.connection("Anthropic")
client    = Anthropic(api_key=ai_conn.api_key)

QUERY = "Federal Reserve interest rate decision"
last_seen_url = None

wd.log.info("News-driven bot starting — query: %s", QUERY)

while True:
    try:
        # 1. fetch latest news
        r = requests.get(
            f"{news_conn.base_url}/everything",
            params={"q": QUERY, "sortBy": "publishedAt", "pageSize": 1},
            headers={"X-Api-Key": news_conn.api_key},
            timeout=10,
        )
        articles = r.json().get("articles", [])
        if not articles:
            time.sleep(60); continue

        article = articles[0]
        if article["url"] == last_seen_url:
            time.sleep(60); continue
        last_seen_url = article["url"]

        wd.log.info("New article: %s", article["title"])

        # 2. ask the LLM to classify
        prompt = (
            f"Headline: {article['title']}\n"
            f"Summary: {article.get('description','')}\n\n"
            "Classify market impact in JSON as: "
            "{\"impact\": \"bullish\"|\"bearish\"|\"neutral\", \"confidence\": 0.0-1.0, \"reason\": \"...\"}"
        )
        resp = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=200,
            messages=[{"role": "user", "content": prompt}],
        )
        classification = json.loads(resp.content[0].text)
        wd.log.info("LLM verdict: %s @ %.2f confidence",
                    classification["impact"], classification["confidence"])

        # 3. act on it
        if classification["confidence"] > 0.7:
            if classification["impact"] == "bullish":
                wd.log.info("→ would open LONG position")
                # place_long_order(...)
            elif classification["impact"] == "bearish":
                wd.log.info("→ would open SHORT position")
                # place_short_order(...)

        time.sleep(60)
    except Exception as e:
        wd.log.error("news tick failed: %s", e)
        time.sleep(120)

6. Cross-Exchange Arbitrage Scanner

Advanced · 1+ hour Crypto (Binance + Coinbase) Read-only by default

Watches BTC/USDT on two exchanges and alerts when the spread exceeds your threshold. Doesn't execute by default — arbitrage requires careful order timing, latency analysis, and fee math that's bigger than a starter example. Use this to spot opportunities.

import time
import wd
import ccxt

binance  = ccxt.binance({"enableRateLimit": True})
coinbase = ccxt.coinbase({"enableRateLimit": True})

SYMBOL = "BTC/USDT"
THRESHOLD_BPS = 30   # alert when spread > 0.30%

wd.log.info("Arb scanner starting — symbol %s, threshold %d bps", SYMBOL, THRESHOLD_BPS)

while True:
    try:
        a = float(binance.fetch_ticker(SYMBOL)["last"])
        b = float(coinbase.fetch_ticker("BTC/USD")["last"])
        spread_bps = abs(a - b) / min(a, b) * 10000
        direction = "Binance > Coinbase" if a > b else "Coinbase > Binance"
        wd.log.info("Binance %.2f  Coinbase %.2f  spread %.1f bps  %s",
                    a, b, spread_bps, direction)

        if spread_bps > THRESHOLD_BPS:
            wd.log.warning("⚠ Arb opportunity: %.1f bps — %s", spread_bps, direction)
            # Execution would go here:
            #   buy on the lower, sell on the higher, account for fees + latency.

        time.sleep(5)
    except Exception as e:
        wd.log.error("scan failed: %s", e)
        time.sleep(15)

Why arbitrage is hard: By the time you see the spread, slower traders have already closed it. Real arbitrage requires colocated infrastructure, sub-millisecond latency, and inventory on both venues. Most "easy arb" you'll see in public APIs is already eaten by HFT firms. This scanner is a learning tool, not a money printer.


Pick one. Ship it.

Every example above runs in WatchDog Bot with zero extra setup. Free trial, no credit card.

Start Free Trial →

More reading