> ## Documentation Index
> Fetch the complete documentation index at: https://docs.predexon.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Portfolio monitor

> Live dashboard of positions, P&L, and fills across every venue an account trades on. The pattern behind most prediction-market UIs.

If you're building any kind of trading UI or risk dashboard, this is the loop you need. Pull positions, mark them to market, layer live updates via WebSocket.

**You'll build**:

1. Initial state load (positions + balances + P\&L) from REST
2. Live deltas (fills, splits, redemptions) from WebSocket
3. A reconciliation step that runs periodically

**Endpoints used**: 4 REST + 2 WebSocket channels. Dev plan and up.

***

## Architecture

```mermaid theme={null}
flowchart LR
  BOOT[Boot:<br/>load positions + balance + P&L]
  STATE[(In-memory<br/>portfolio state)]
  WS_TRADES[WS trades<br/>filters.users=wallets]
  WS_ACTIVITY[WS activity<br/>filters.users=wallets]
  APPLY[Apply delta<br/>to state]
  RECONCILE[Every 60s:<br/>re-pull positions]

  BOOT --> STATE
  WS_TRADES --> APPLY
  WS_ACTIVITY --> APPLY
  APPLY --> STATE
  RECONCILE --> STATE
  STATE -.-> UI[Your UI<br/>or alerts]
```

***

## Step 1 — Boot the state

For any account, gather positions on every enabled venue + cash balance + P\&L from the Data API.

```python theme={null}
import os, requests, json, time
from websockets.sync.client import connect

DATA = "https://api.predexon.com"
TRADE = "https://trade.predexon.com"
WSS = "wss://wss.predexon.com/v1"
KEY = os.environ["PREDEXON_API_KEY"]
H = {"x-api-key": KEY}
ACCOUNT_ID = "your-account-id"

def boot_state():
    # 1. Account info — gives us per-venue wallets
    acct = requests.get(f"{TRADE}/api/accounts/{ACCOUNT_ID}", headers=H).json()
    venues = {v: info for v, info in acct["venues"].items() if info["status"] == "active"}

    # 2. Aggregated positions across every enabled venue
    positions = requests.get(
        f"{TRADE}/api/accounts/{ACCOUNT_ID}/positions",
        headers=H,
        params={"aggregated": "true"},
    ).json()["positions"]

    # 3. Cash balance per venue + USD-equivalent total
    balance = requests.get(
        f"{TRADE}/api/accounts/{ACCOUNT_ID}/balance",
        headers=H,
        params={"aggregated": "true"},
    ).json()

    # 4. Polymarket P&L history (other venues TBD)
    poly_wallet = venues.get("polymarket", {}).get("address")
    pnl = {}
    if poly_wallet:
        pnl = requests.get(
            f"{DATA}/v2/polymarket/wallet/pnl/{poly_wallet}",
            headers=H,
        ).json()

    return {
        "venues": venues,
        "positions": positions,
        "balance": balance,
        "pnl": pnl,
        "loaded_at": time.time(),
    }

state = boot_state()
print(f"Loaded {len(state['positions'])} positions across {len(state['venues'])} venues")
```

***

## Step 2 — Subscribe to live deltas

Two channels matter for portfolio updates: `trades` (fills) and `activity` (splits / merges / redemptions). Filter both to your account's venue wallets.

```python theme={null}
def subscribe_msgs(wallets):
    return [
        json.dumps({
            "action": "subscribe",
            "platform": "polymarket",
            "version": 1,
            "type": "orders",
            "filters": {"users": wallets},
        }),
        json.dumps({
            "action": "subscribe",
            "platform": "polymarket",
            "version": 1,
            "type": "activity",
            "filters": {"users": wallets},
        }),
    ]
```

***

## Step 3 — Apply each delta to state

A fill changes a position's size and cost basis. An activity event (split/merge/redeem) restructures positions. Worth modeling both — but at minimum, the trades handler is the must-have.

```python theme={null}
def apply_trade(state, t):
    """Update a position from a fill event."""
    token_id = t["token_id"]
    size = float(t["shares_normalized"]) * (1 if t["side"] == "BUY" else -1)
    price = float(t["price"])

    pos = next((p for p in state["positions"] if p.get("token_id") == token_id), None)
    if pos is None:
        # new position
        state["positions"].append({
            "token_id": token_id,
            "condition_id": t["condition_id"],
            "size": size,
            "avg_price": price,
            "venue": "polymarket",
        })
    else:
        # weighted-average cost basis
        new_size = pos["size"] + size
        if new_size == 0:
            state["positions"].remove(pos)
        else:
            pos["avg_price"] = (
                (pos["avg_price"] * pos["size"] + price * size) / new_size
            )
            pos["size"] = new_size

def apply_activity(state, a):
    """Activity events restructure positions (splits, merges, redemptions)."""
    if a["event_type"] == "redemption":
        # winning side pays $1/share, losing side pays $0; remove resolved positions
        state["positions"] = [
            p for p in state["positions"] if p["condition_id"] != a["condition_id"]
        ]
    # Splits / merges / NegRisk conversions: re-pull positions in step 4
```

***

## Step 4 — The main loop

```python theme={null}
def run():
    state = boot_state()
    wallets = [v["address"] for v in state["venues"].values()]

    last_reconcile = time.time()

    with connect(f"{WSS}/{KEY}") as ws:
        for msg in subscribe_msgs(wallets):
            ws.send(msg)

        while True:
            try:
                ws.settimeout(60)
                raw = ws.recv()
                msg = json.loads(raw)
                if msg.get("type") != "event":
                    continue
                if msg["subscription_id"].endswith("trades"):
                    apply_trade(state, msg["data"])
                elif msg["subscription_id"].endswith("activity"):
                    apply_activity(state, msg["data"])
                publish(state)   # send to UI / dashboard
            except TimeoutError:
                pass

            # Reconcile every 60s — defends against missed events
            if time.time() - last_reconcile > 60:
                state = boot_state()
                last_reconcile = time.time()
                publish(state)

def publish(state):
    # your UI delivery — websocket to frontend, Redis pub/sub, etc.
    print(f"[{time.strftime('%H:%M:%S')}] {len(state['positions'])} positions, "
          f"${state['balance']['total_usd']:.2f} total")
```

***

## What the UI does with this state

| Display                 | Source                                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| Open positions table    | `state["positions"]`                                                                                          |
| Mark-to-market          | `position.size * latest_price` — fetch latest\_price from [Market Price](/api-reference/markets/market-price) |
| Unrealized P\&L         | `(latest_price - avg_price) * size`                                                                           |
| Realized P\&L over time | `state["pnl"]["history"]` (Polymarket); compute from fills for other venues                                   |
| Recent fills            | Append the trades event stream to a feed                                                                      |
| Withdrawable balance    | `state["balance"]["deposit"]` (the deposit-wallet USDC)                                                       |

For periodic mark-to-market, the cheapest pattern is batch the latest prices for all unique `token_id`s in the positions list and refresh every few seconds.

***

## Variations

* **Multi-account dashboard**: loop over `GET /api/accounts` to enumerate every account this API key owns; boot state for each.
* **Alerting**: add thresholds — alert on unrealized loss > X, on a new position above \$Y, on a redemption event.
* **Historical P\&L**: pair with [Wallet Volume Chart](/api-reference/wallet/volume-chart) for cumulative-equity curves.
* **Cross-venue**: positions endpoint already returns aggregated cross-venue rows with `?aggregated=true`. Group by canonical `predexon_id` for cleaner UX.

***

## Reference

* [Account Get](/trading-api/accounts/get-account) — per-venue addresses and status
* [Account Get Positions](/trading-api/accounts/get-positions) — `?aggregated=true` collapses across venues
* [Account Get Balance](/trading-api/accounts/get-balance) — `?aggregated=true` for USD-equivalent total
* [Wallet P\&L](/api-reference/wallet/pnl) — for Polymarket realized P\&L history
* [WebSocket trades](/websocket/trades), [activity](/websocket/activity)
* [Best Practices — WebSocket reconnect](/start-here/best-practices)
