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

# Tracking positions in real time

> Seed a portfolio from REST positions, then keep every open and closed position live with the trader positions room: trades, mark-to-market price ticks, and resolutions.

<Info>
  **What you'll build:** a live "my positions" view that stays in sync as a wallet trades, as outcome prices move, and as markets resolve. You seed it once from REST, then keep it warm over a websocket so the open and closed sections never go stale.
</Info>

`polymarket_trader_positions` pushes per-position updates for a set of wallets as they happen, batched per block. This recipe seeds a portfolio map from REST, then keeps it warm with three envelope types so the UI reflects new trades, moving prices, and market resolutions without polling.

## When to use this

* A live "my positions" view with open and closed sections.
* A portfolio value ticker that moves with outcome prices between trades.
* Surfacing redeemable and mergeable flags so a user knows when they can claim or convert.

## The three envelope types

| Event                              | Fires on                                                               | Shape                                                        |
| ---------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------ |
| `trader_position_batch`            | A trade landed (buy, sell, merge, split, redemption, NegRisk convert). | Full position rows.                                          |
| `trader_position_price_batch`      | Outcome prices moved.                                                  | Compact price ticks (`dirty_kinds` is `["price"]`).          |
| `trader_position_resolution_batch` | The position's market resolved.                                        | Resolution ticks (`dirty_kinds` is `["position_resolved"]`). |

Full rows are the complete record of a position. Price ticks and resolution ticks are light updates that patch the live fields of a row you already hold, so you keep a single map keyed by `position_id` and merge each envelope in place.

Note that timestamps in this room (`first_trade_at`, `last_trade_at`) are Unix **milliseconds**, unlike the trader PnL room where they are seconds.

## Step 1: seed from REST

Pull the current position book so the portfolio is painted before the first block arrives. Use a trader outcome PnL call with `min_shares: 0` to include fully exited positions, then build a map keyed by `position_id`.

<CodeGroup>
  ```typescript SDK theme={null}
  import { StructClient } from "@structbuild/sdk";

  const client = new StructClient({ apiKey: "sk_live_xxx" });

  const { data: rows } = await client.trader.getTraderOutcomePnl({
    address: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
    min_shares: 0,
    timeframe: "lifetime",
  });

  const positions = new Map<string, TraderPositionRow>();
  for (const row of rows) positions.set(row.position_id, row);
  ```

  ```bash cURL theme={null}
  curl "https://api.struct.to/v1/polymarket/trader/outcome-pnl/0xd8da6bf26964af9d7eed9e03e53415d37aa96045?min_shares=0&timeframe=lifetime" \
    -H "X-API-Key: YOUR_API_KEY"
  ```

  ```python Python theme={null}
  import requests

  rows = requests.get(
      "https://api.struct.to/v1/polymarket/trader/outcome-pnl/0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
      headers={"X-API-Key": "YOUR_API_KEY"},
      params={"min_shares": 0, "timeframe": "lifetime"},
  ).json()["data"]

  positions = {r["position_id"]: r for r in rows}
  ```
</CodeGroup>

See [Showing a trader's open and closed positions](/guides/trader-open-and-closed-positions) for more on the REST shape and the open versus closed split.

## Step 2: subscribe and merge full rows

`traders` is the only required filter. Each `trader_position_batch` carries full rows; upsert them by `position_id`.

<CodeGroup>
  ```typescript SDK theme={null}
  import { StructWebSocket } from "@structbuild/sdk";

  const ws = new StructWebSocket({ apiKey: "sk_live_xxx" });
  await ws.connect();

  await ws.subscribe("polymarket_trader_positions", {
    traders: ["0xd8da6bf26964af9d7eed9e03e53415d37aa96045"],
  });

  ws.on("trader_position_batch", (event) => {
    for (const row of event.data) {
      positions.set(row.position_id, row);
      classify(row);
    }
  });
  ```

  ```json Subscribe message theme={null}
  {
    "type": "room_message",
    "payload": {
      "room_id": "polymarket_trader_positions",
      "message": {
        "action": "subscribe",
        "traders": ["0xd8da6bf26964af9d7eed9e03e53415d37aa96045"]
      }
    }
  }
  ```
</CodeGroup>

A position is **open** when `current_shares_balance > 0` and **closed** when it reaches `0`. The full row also carries `realized_pnl_usd`, `realized_pnl_pct`, `avg_entry_price`, and the descriptive fields (`title`, `question`, `outcome`, `image_url`) you need to render a card.

## Step 3: patch the mark-to-market value from price ticks

Between trades, prices move, and `trader_position_price_batch` is how the live value tracks them. Each tick carries `current_price`, `current_value`, and the refreshed `realized_pnl_usd` for one `position_id`. The unrealized value of an open position is `current_price` times `current_shares_balance`, and the tick's `current_value` is exactly that product, so you can use it directly.

```typescript theme={null}
ws.on("trader_position_price_batch", (event) => {
  for (const tick of event.data) {
    const pos = positions.get(tick.position_id);
    if (!pos) continue;
    pos.current_price = tick.current_price;
    pos.current_value = tick.current_value;
    pos.realized_pnl_usd = tick.realized_pnl_usd;
    pos.realized_pnl_pct = tick.realized_pnl_pct;
  }
  renderPortfolioValue();
});
```

Recompute the portfolio total as the sum of `current_value` across open positions plus realized PnL on closed ones, and the ticker moves smoothly with the market.

## Step 4: handle resolutions

When a position's market resolves, `trader_position_resolution_batch` delivers the verdict: `resolved`, `won`, and the final `realized_pnl_usd`. Apply it, then re-check the redeemable flag.

```typescript theme={null}
ws.on("trader_position_resolution_batch", (event) => {
  for (const res of event.data) {
    const pos = positions.get(res.position_id);
    if (!pos) continue;
    pos.open = false;
    pos.won = res.won ?? undefined;
    pos.realized_pnl_usd = res.realized_pnl_usd;
    classify(pos);
  }
});
```

## Surfacing redeemable and mergeable flags

The full row carries two action flags worth surfacing in the UI:

* `redeemable` is true when the market has resolved and the wallet still holds shares, meaning there is a payout to claim.
* `mergeable` is true for an unresolved NegRisk market where the wallet holds shares, meaning positions can be merged back to collateral.

```typescript theme={null}
function classify(pos: TraderPositionRow) {
  if (pos.redeemable) flagAsRedeemable(pos);
  if (pos.mergeable) flagAsMergeable(pos);
  if ((pos.current_shares_balance ?? 0) > 0) moveToOpen(pos.position_id);
  else moveToClosed(pos.position_id);
}
```

Both flags live on full rows, so they update on the next `trader_position_batch` after a trade or, for resolution-driven redeemability, after you apply a resolution tick and re-evaluate.

## Cutting message volume

This room bills per message. The `dirty_kinds` filter (a subset of `["trade","price","position_resolved"]` or `["all"]`) lets you drop families you do not render. A portfolio view that only redraws on trades and resolutions, not on every price move, can omit `price`:

```typescript theme={null}
await ws.subscribe("polymarket_trader_positions", {
  traders: ["0xd8da6bf26964af9d7eed9e03e53415d37aa96045"],
  dirty_kinds: ["trade", "position_resolved"],
});
```

## Follow-on

* For aggregate realized PnL summary cards alongside the position list, see [Building a live trader PnL dashboard](/guides/live-trader-pnl-dashboard).
* To annotate a PnL chart with the moment each position closed, see [Charting PnL with exit markers](/guides/pnl-chart-with-exit-markers).
* Full payload schemas live on the [Trader positions room](/websockets/rooms/trader-positions) page.
