> ## 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.

# Showing a trader's open and closed positions

> Render a trader's portfolio (held vs exited) using the trader outcome PnL endpoint, then keep it live via the trader positions room.

`client.trader.getTraderOutcomePnl` returns one row per outcome a wallet has ever traded, with `current_shares_balance`, `realized_pnl_usd`, and supporting fields. That single response covers both halves of a portfolio view: open positions (still held) and closed positions (fully exited). The same row shape is pushed live by `polymarket_trader_positions`, so a portfolio UI seeded from REST can stay in sync with one merge key.

## When to use this

* Wallet portfolio pages with separate "Open" and "Closed" sections.
* Leaderboard drilldowns where clicking a trader expands to their bets.
* "My open bets" UI for an authenticated user, with realized PnL surfaced for closed exits.

## Open vs closed model

A position is **open** when `current_shares_balance > 0`: the wallet still holds outcome tokens, so the row has both unrealised exposure and realized PnL from any partial sells. A position is **closed** when `current_shares_balance === 0`: the wallet has fully exited (sold or redeemed), and only `realized_pnl_usd` remains relevant.

The same row carries both halves of the lifecycle, which means you do not need two endpoints. Filter once in client code.

## Step 1: fetch the position book

Use `min_shares: 0` to include closed rows; bump it to drop dust positions.

<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 open = rows.filter((r) => (r.current_shares_balance ?? 0) > 0);
  const closed = rows.filter((r) => (r.current_shares_balance ?? 0) === 0);
  ```

  ```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"]

  open_positions = [r for r in rows if (r.get("current_shares_balance") or 0) > 0]
  closed_positions = [r for r in rows if (r.get("current_shares_balance") or 0) == 0]
  ```
</CodeGroup>

## Step 2: split and sort

Open positions usually rank by current mark-to-market value. Closed positions rank by realized PnL.

```typescript theme={null}
const sortedOpen = [...open].sort(
  (a, b) =>
    (b.current_shares_balance ?? 0) * (b.latest_price ?? 0) -
    (a.current_shares_balance ?? 0) * (a.latest_price ?? 0),
);

const sortedClosed = [...closed].sort(
  (a, b) => (b.realized_pnl_usd ?? 0) - (a.realized_pnl_usd ?? 0),
);
```

## Step 3: scope to a timeframe

Pass `timeframe` to limit `realized_pnl_usd` (and any windowed counters) to a window: `1d`, `7d`, `30d`, or `lifetime`. Use this to drive period switchers on the portfolio header.

```typescript theme={null}
const { data: last7d } = await client.trader.getTraderOutcomePnl({
  address: "0x...",
  timeframe: "7d",
});
```

You can also narrow the response to a specific event or market via `event_slug` or `condition_id`, useful for "this trader's bets on the election" drilldowns.

## Step 4: keep it live

Subscribe to `polymarket_trader_positions` with the same address. Every `trader_position_update` carries `(trader, position_id)` plus `current_shares_balance` and `realized_pnl_usd`, so merging into the REST-seeded map is one-to-one.

<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"],
  });

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

  ws.on("trader_position_update", (event) => {
    const row = event.data;
    positions.set(row.position_id, row);
    if (row.current_shares_balance === 0) movePositionToClosed(row.position_id);
    else upsertOpenPosition(row.position_id, row);
  });
  ```

  ```bash cURL theme={null}
  > {"type":"room_message","payload":{"room_id":"polymarket_trader_positions","message":{"action":"subscribe","traders":["0xd8da6bf26964af9d7eed9e03e53415d37aa96045"]}}}
  ```

  ```python Python theme={null}
  await ws.send(json.dumps({
      "type": "room_message",
      "payload": {
          "room_id": "polymarket_trader_positions",
          "message": {"action": "subscribe", "traders": ["0xd8da6bf26964af9d7eed9e03e53415d37aa96045"]},
      },
  }))
  ```
</CodeGroup>

When `current_shares_balance` flips to 0, move that row from the Open list to the Closed list in your UI. When it ticks up from 0, the trader has re-entered an old position; move the row back.

## Common combinations

| Goal                               | Parameters                                      |
| ---------------------------------- | ----------------------------------------------- |
| Lifetime portfolio (open + closed) | `address=0x...&min_shares=0&timeframe=lifetime` |
| Weekly performance                 | `address=0x...&timeframe=7d`                    |
| Drop dust                          | `address=0x...&min_shares=10`                   |
| Bets on one event                  | `address=0x...&event_slug=us-election-2028`     |
| Sort by realized PnL               | `address=0x...&sort_by=realized_pnl_usd`        |

## Pagination

For traders with long histories, the SDK's `paginate` helper walks every page without manual cursor handling.

```typescript theme={null}
import { paginate } from "@structbuild/sdk";

for await (const row of paginate(
  (params) => client.trader.getTraderOutcomePnl(params),
  { address: "0x...", min_shares: 0 },
)) {
  ingest(row);
}
```

See [Pagination](/sdk/pagination) for how `offset` and `pagination_key` interact.

## Follow-on

For aggregated PnL summary cards next to the position list (global, per-event, per-market roll-ups), see the [Trader PnL room](/websockets/rooms/trader-pnl). It pairs cleanly with this recipe: positions on the left, summary numbers on the right, both fed from the same wallet address.
