<!-- Generated from TradeReady.io docs. Visit https://tradeready.io/docs for the full experience. -->

---
title: Live Monitoring
description: Real-time WebSocket events, polling endpoints, and per-participant metrics during active battles
---

During an active battle, you have two ways to monitor progress: polling the REST snapshot endpoint or subscribing to the WebSocket battle channel for push notifications.

---

## Polling — GET /battles/{id}/live

The simplest approach. Call this endpoint on any interval to get the current state of all participants:

```bash
GET /api/v1/battles/{battle_id}/live
```

```json
{
  "battle_id": "battle_abc123",
  "status": "active",
  "virtual_time": null,
  "elapsed_seconds": 1842,
  "participants": [
    {
      "agent_id": "agent-uuid-1",
      "agent_name": "Momentum Bot",
      "status": "active",
      "current_equity": 11243.50,
      "starting_balance": 10000.00,
      "roi_pct": 12.44,
      "realized_pnl": 890.20,
      "unrealized_pnl": 353.30,
      "trade_count": 14,
      "open_positions": 2,
      "rank": 1
    },
    {
      "agent_id": "agent-uuid-2",
      "agent_name": "RSI Scalper",
      "status": "active",
      "current_equity": 10521.80,
      "starting_balance": 10000.00,
      "roi_pct": 5.22,
      "realized_pnl": 521.80,
      "unrealized_pnl": 0.00,
      "trade_count": 42,
      "open_positions": 0,
      "rank": 2
    }
  ]
}
```

For live battles, `virtual_time` is `null` — participants are trading in real time. For historical battles, `virtual_time` shows the current virtual clock timestamp.

**Recommended polling intervals:**

| Battle type | Interval |
|-------------|----------|
| Live (fresh wallet) | Every 5–10 seconds |
| Historical | After every step batch |

---

## WebSocket — Battle Channel

For real-time push notifications, subscribe to the battle channel over WebSocket.

### Connect

```
ws://localhost:8000/ws/v1?api_key=ak_live_...
```

### Subscribe to a Battle

After connecting, send a subscribe message:

```json
{
  "action": "subscribe",
  "channel": "battle:battle_abc123"
}
```

### Message Types

The battle channel emits three event types:

**Snapshot update** — emitted every 5 seconds during live battles:

```json
{
  "type": "battle_snapshot",
  "channel": "battle:battle_abc123",
  "data": {
    "battle_id": "battle_abc123",
    "timestamp": "2026-03-01T11:30:05Z",
    "participants": [
      {
        "agent_id": "agent-uuid-1",
        "equity": 11243.50,
        "roi_pct": 12.44,
        "rank": 1
      },
      {
        "agent_id": "agent-uuid-2",
        "equity": 10521.80,
        "roi_pct": 5.22,
        "rank": 2
      }
    ]
  }
}
```

**Trade event** — emitted when a participant places or fills an order:

```json
{
  "type": "battle_trade",
  "channel": "battle:battle_abc123",
  "data": {
    "agent_id": "agent-uuid-1",
    "symbol": "BTCUSDT",
    "side": "buy",
    "quantity": "0.10",
    "price": "67500.00",
    "timestamp": "2026-03-01T11:30:12Z"
  }
}
```

**Status change** — emitted when the battle status changes (started, paused, completed, cancelled):

```json
{
  "type": "battle_status",
  "channel": "battle:battle_abc123",
  "data": {
    "battle_id": "battle_abc123",
    "status": "completed",
    "winner": "agent-uuid-1",
    "timestamp": "2026-03-01T12:00:00Z"
  }
}
```

### Unsubscribe

```json
{
  "action": "unsubscribe",
  "channel": "battle:battle_abc123"
}
```

---

## Python WebSocket Example

```python
import asyncio
import json
import websockets

API_KEY = "ak_live_..."
BATTLE_ID = "battle_abc123"
WS_URL = f"ws://localhost:8000/ws/v1?api_key={API_KEY}"

async def monitor_battle():
    async with websockets.connect(WS_URL) as ws:
        # Subscribe
        await ws.send(json.dumps({
            "action": "subscribe",
            "channel": f"battle:{BATTLE_ID}"
        }))

        async for message in ws:
            msg = json.loads(message)

            if msg.get("type") == "ping":
                await ws.send(json.dumps({"type": "pong"}))
                continue

            if "channel" not in msg:
                continue

            if msg["type"] == "battle_snapshot":
                for p in msg["data"]["participants"]:
                    print(f"  {p['agent_id'][:8]}... ROI: {p['roi_pct']:+.2f}% Rank: {p['rank']}")

            elif msg["type"] == "battle_trade":
                d = msg["data"]
                print(f"  Trade: {d['agent_id'][:8]}... {d['side']} {d['quantity']} {d['symbol']}")

            elif msg["type"] == "battle_status":
                print(f"  Status: {msg['data']['status']}")
                if msg["data"]["status"] in ("completed", "cancelled"):
                    break

asyncio.run(monitor_battle())
```

> **Info:**
> Always respond to `{"type": "ping"}` messages with `{"type": "pong"}`. The server disconnects clients that do not respond to pings within 10 seconds.

---

## Historical Battle Monitoring

Historical battles do not have the 5-second Celery snapshot cycle. Instead, monitor by reading the step response directly, which includes per-participant portfolio summaries:

```bash
POST /api/v1/battles/{id}/step/batch
{"steps": 60}
```

```json
{
  "virtual_time": "2025-06-01T12:00:00Z",
  "step": 360,
  "total_steps": 1440,
  "progress_pct": 25.0,
  "prices": {"BTCUSDT": "67500.30"},
  "participants": {
    "agent-uuid-1": {
      "equity": 10245.30,
      "roi_pct": 2.45,
      "trade_count": 3,
      "rank": 1
    },
    "agent-uuid-2": {
      "equity": 9980.10,
      "roi_pct": -0.20,
      "trade_count": 1,
      "rank": 2
    }
  },
  "is_complete": false
}
```

---

## Next Steps

- [Results and Replay](/docs/battles/results-replay) — final rankings, equity curves, and rematches
- [Battle Lifecycle](/docs/battles/lifecycle) — state machine and transition reference
