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

# Trader PnL

> Stream global, market, and category PnL for tracked traders across rolling windows.

<Note>
  **Room ID:** `polymarket_trader_pnl` \
  **Endpoint:** `wss://api.struct.to/ws` \
  **Rate:** 0.1 credits per message
</Note>

Stream profit and loss for tracked traders. PnL is computed at three grains and maintained over four rolling windows, so you can drive a full portfolio view from a single subscription.

The three grains are `global` (the whole portfolio), `market` (per condition), and `category` (per market category). Each grain is tracked over four windows: `1d`, `7d`, `30d`, and `lifetime`.

<Tip>
  **Related guide:** [Live trader PnL dashboard](/guides/live-trader-pnl-dashboard) builds a complete realized PnL view from this room.
</Tip>

## Event families

Each grain emits three families of events. A given dirty row goes to exactly one family per block, so the families are disjoint.

| Family         | Events                                                                                                 | Trigger                                           | Timeframe                       |
| -------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | ------------------------------- |
| Full-row batch | `trader_global_pnl_batch`, `trader_market_pnl_batch`, `trader_category_pnl_batch`                      | Trade landed or a rolling window boundary crossed | Carries a top-level `timeframe` |
| Price tick     | `trader_global_tick_batch`, `trader_market_tick_batch`, `trader_category_tick_batch`                   | Mark-to-market refresh (outcome price moved)      | Window-agnostic                 |
| Resolution     | `trader_global_resolution_batch`, `trader_market_resolution_batch`, `trader_category_resolution_batch` | The owning market resolved with no trade          | Window-agnostic                 |

Updates are batched per block. Each envelope is shaped `{ type, room_id, block, [timeframe,] data: [ ...rows ] }`, with one envelope per grain, family, and (for full-row batches) timeframe per block. Empty batches are not sent. Every row carries a `dirty_kinds` array naming what triggered it.

Tick and resolution rows are window-agnostic and carry boundary snapshots, so any per-window delta is recoverable client-side without waiting for a full-row batch.

### dirty\_kinds

| Value             | Meaning                                                          |
| ----------------- | ---------------------------------------------------------------- |
| `trade`           | A buy, sell, merge, split, redemption, or NegRisk convert landed |
| `price`           | An outcome price moved (mark-to-market refresh)                  |
| `window`          | A 1d, 7d, or 30d rolling window boundary crossed                 |
| `market_resolved` | The owning market resolved                                       |

## Subscribe

The `traders` filter is required. The remaining filters are optional and narrow the stream.

### Filters

| Filter         | Type       | Required | Description                                                                                                                                       |
| -------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `traders`      | `string[]` | Yes      | EVM wallet addresses (0x-prefixed), non-empty. Invalid addresses are returned in `rejected`.                                                      |
| `update_types` | `string[]` | No       | Subset of `["global","market","category"]`. Omit or pass empty for all grains.                                                                    |
| `timeframes`   | `string[]` | No       | Subset of `["1d","7d","30d","lifetime"]`. Omit or pass empty for all windows. Ignored by tick and resolution families, which are window-agnostic. |
| `dirty_kinds`  | `string[]` | No       | Subset of `["trade","price","window","market_resolved"]`, or `["all"]`. A row is delivered only if its `dirty_kinds` intersects this set.         |

An unknown value in `update_types`, `timeframes`, or `dirty_kinds` rejects the whole subscription with an `error`.

### Example

```json theme={null}
{
  "type": "join_room",
  "payload": {
    "room_id": "polymarket_trader_pnl"
  }
}
```

```json theme={null}
{
  "type": "room_message",
  "payload": {
    "room_id": "polymarket_trader_pnl",
    "message": {
      "action": "subscribe",
      "traders": ["0x1234567890abcdef1234567890abcdef12345678"]
    }
  }
}
```

### Response

The subscribe response (`trader_pnl_stream_subscribe_response`) echoes the resolved configuration.

```json theme={null}
{
  "type": "trader_pnl_stream_subscribe_response",
  "room_id": "polymarket_trader_pnl",
  "data": {
    "traders": ["0x1234567890abcdef1234567890abcdef12345678"],
    "update_types": ["global", "market", "category"],
    "timeframes": ["1d", "7d", "30d", "lifetime"],
    "rejected": [],
    "error": null
  }
}
```

## Events

### `trader_global_pnl_batch`

Full-row PnL across the whole portfolio. Carries a top-level `timeframe`.

```json theme={null}
{
  "type": "trader_global_pnl_batch",
  "room_id": "polymarket_trader_pnl",
  "block": 68420001,
  "timeframe": "lifetime",
  "data": [
    {
      "trader": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
      "dirty_kinds": ["trade"],
      "timeframe": "lifetime",
      "realized_pnl_usd": 12500.75,
      "events_traded": 15,
      "markets_traded": 42,
      "total_buys": 120,
      "total_sells": 85,
      "total_redemptions": 12,
      "total_merges": 0,
      "total_splits": 0,
      "total_volume_usd": 450000.0,
      "buy_volume_usd": 230000.0,
      "sell_volume_usd": 180000.0,
      "redemption_volume_usd": 40000.0,
      "merge_volume_usd": 0.0,
      "split_volume_usd": 0.0,
      "maker_rebate_count": 4,
      "maker_rebate_usd": 12.5,
      "reward_count": 2,
      "reward_usd": 30.0,
      "yield_count": 0,
      "yield_usd": 0.0,
      "total_credit_count": 6,
      "total_credit_usd": 42.5,
      "markets_won": 28,
      "markets_lost": 14,
      "market_win_rate_pct": 66.67,
      "total_wins_usd": 18000.0,
      "total_losses_usd": 5499.25,
      "avg_win_usd": 642.86,
      "avg_loss_usd": 392.8,
      "profit_factor": 3.27,
      "avg_hold_time_seconds": 172800.0,
      "total_fees": 900.0,
      "best_trade_pnl_usd": 5200.0,
      "best_trade_condition_id": "0x1234...cdef",
      "worst_trade_pnl_usd": -1200.0,
      "worst_trade_condition_id": null,
      "first_trade_at": 1740000000,
      "last_trade_at": 1743500000
    }
  ]
}
```

| Field                      | Type             | Description                           |
| -------------------------- | ---------------- | ------------------------------------- |
| `trader`                   | `string`         | Wallet address                        |
| `dirty_kinds`              | `string[]`       | What triggered this row               |
| `timeframe`                | `string \| null` | Window this row covers                |
| `realized_pnl_usd`         | `number`         | Realized PnL in USD                   |
| `events_traded`            | `int64`          | Distinct events traded                |
| `markets_traded`           | `int64`          | Distinct markets traded               |
| `total_buys`               | `int64`          | Buy count                             |
| `total_sells`              | `int64`          | Sell count                            |
| `total_redemptions`        | `int64`          | Redemption count                      |
| `total_merges`             | `int64`          | Merge count                           |
| `total_splits`             | `int64`          | Split count                           |
| `total_volume_usd`         | `number`         | Total traded volume in USD            |
| `buy_volume_usd`           | `number`         | Buy volume in USD                     |
| `sell_volume_usd`          | `number`         | Sell volume in USD                    |
| `redemption_volume_usd`    | `number`         | Redemption volume in USD              |
| `merge_volume_usd`         | `number`         | Merge volume in USD                   |
| `split_volume_usd`         | `number`         | Split volume in USD                   |
| `maker_rebate_count`       | `int64`          | Maker rebate count                    |
| `maker_rebate_usd`         | `number`         | Maker rebate total in USD             |
| `reward_count`             | `int64`          | Reward count                          |
| `reward_usd`               | `number`         | Reward total in USD                   |
| `yield_count`              | `int64`          | Yield credit count                    |
| `yield_usd`                | `number`         | Yield total in USD                    |
| `total_credit_count`       | `int64`          | Combined credit count                 |
| `total_credit_usd`         | `number`         | Combined credit total in USD          |
| `markets_won`              | `int64`          | Markets won                           |
| `markets_lost`             | `int64`          | Markets lost                          |
| `market_win_rate_pct`      | `number`         | Win rate as a percentage              |
| `total_wins_usd`           | `number`         | Sum of winning PnL                    |
| `total_losses_usd`         | `number`         | Sum of losing PnL                     |
| `avg_win_usd`              | `number`         | Average winning PnL                   |
| `avg_loss_usd`             | `number`         | Average losing PnL                    |
| `profit_factor`            | `number \| null` | Gross wins divided by gross losses    |
| `avg_hold_time_seconds`    | `number`         | Average position hold time in seconds |
| `total_fees`               | `number`         | Total fees in USD                     |
| `best_trade_pnl_usd`       | `number \| null` | Best single-trade PnL                 |
| `best_trade_condition_id`  | `string \| null` | Condition of the best trade           |
| `worst_trade_pnl_usd`      | `number \| null` | Worst single-trade PnL                |
| `worst_trade_condition_id` | `string \| null` | Condition of the worst trade          |
| `first_trade_at`           | `int64 \| null`  | First trade (Unix seconds)            |
| `last_trade_at`            | `int64 \| null`  | Last trade (Unix seconds)             |

### `trader_market_pnl_batch`

Full-row PnL for a single market (condition). Carries a top-level `timeframe`.

```json theme={null}
{
  "type": "trader_market_pnl_batch",
  "room_id": "polymarket_trader_pnl",
  "block": 68420001,
  "timeframe": "lifetime",
  "data": [
    {
      "trader": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
      "dirty_kinds": ["trade"],
      "timeframe": "lifetime",
      "condition_id": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
      "realized_pnl_usd": 350.25,
      "current_shares_balance": 1000.0,
      "category": "Crypto",
      "event_slug": "bitcoin-price-markets",
      "outcomes_traded": 2,
      "total_buys": 5,
      "total_sells": 3,
      "total_redemptions": 0,
      "total_merges": 0,
      "total_splits": 0,
      "buy_usd": 2000.0,
      "sell_usd": 2350.25,
      "redemption_usd": 0.0,
      "merge_usd": 0.0,
      "split_volume_usd": 0.0,
      "total_fees": 8.7,
      "total_shares_bought": 3000.0,
      "first_trade_at": 1742000000,
      "last_trade_at": 1743500000
    }
  ]
}
```

| Field                    | Type             | Description                |
| ------------------------ | ---------------- | -------------------------- |
| `trader`                 | `string`         | Wallet address             |
| `dirty_kinds`            | `string[]`       | What triggered this row    |
| `timeframe`              | `string \| null` | Window this row covers     |
| `condition_id`           | `string`         | 64-char hex condition ID   |
| `realized_pnl_usd`       | `number`         | Realized PnL in USD        |
| `current_shares_balance` | `number`         | Current shares held        |
| `category`               | `string \| null` | Market category            |
| `event_slug`             | `string \| null` | Owning event slug          |
| `outcomes_traded`        | `int64`          | Distinct outcomes traded   |
| `total_buys`             | `int64`          | Buy count                  |
| `total_sells`            | `int64`          | Sell count                 |
| `total_redemptions`      | `int64`          | Redemption count           |
| `total_merges`           | `int64`          | Merge count                |
| `total_splits`           | `int64`          | Split count                |
| `buy_usd`                | `number`         | Buy volume in USD          |
| `sell_usd`               | `number`         | Sell volume in USD         |
| `redemption_usd`         | `number`         | Redemption volume in USD   |
| `merge_usd`              | `number`         | Merge volume in USD        |
| `split_volume_usd`       | `number`         | Split volume in USD        |
| `total_fees`             | `number`         | Total fees in USD          |
| `total_shares_bought`    | `number`         | Cumulative shares bought   |
| `first_trade_at`         | `int64 \| null`  | First trade (Unix seconds) |
| `last_trade_at`          | `int64 \| null`  | Last trade (Unix seconds)  |

### `trader_category_pnl_batch`

Full-row PnL for a market category. Carries a top-level `timeframe`.

```json theme={null}
{
  "type": "trader_category_pnl_batch",
  "room_id": "polymarket_trader_pnl",
  "block": 68420001,
  "timeframe": "lifetime",
  "data": [
    {
      "trader": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
      "dirty_kinds": ["trade"],
      "timeframe": "lifetime",
      "category": "Crypto",
      "realized_pnl_usd": 4200.0,
      "markets_in_category": 18,
      "markets_traded": 12,
      "outcomes_traded": 20,
      "total_buys": 40,
      "total_sells": 28,
      "total_redemptions": 4,
      "total_merges": 0,
      "total_splits": 0,
      "total_volume_usd": 90000.0,
      "buy_usd": 50000.0,
      "sell_usd": 35000.0,
      "redemption_usd": 5000.0,
      "merge_usd": 0.0,
      "split_volume_usd": 0.0,
      "total_fees": 180.0,
      "total_shares_bought": 60000.0,
      "markets_won": 8,
      "markets_lost": 4,
      "market_win_rate_pct": 66.67,
      "avg_hold_time_seconds": 150000.0,
      "best_trade_pnl_usd": 2100.0,
      "best_trade_condition_id": "0x1234...cdef",
      "worst_trade_pnl_usd": -600.0,
      "worst_trade_condition_id": null,
      "total_wins_usd": 6000.0,
      "total_losses_usd": 1800.0,
      "avg_win_usd": 750.0,
      "avg_loss_usd": 450.0,
      "profit_factor": 3.33,
      "first_trade_at": 1741000000,
      "last_trade_at": 1743500000
    }
  ]
}
```

| Field                      | Type             | Description                        |
| -------------------------- | ---------------- | ---------------------------------- |
| `trader`                   | `string`         | Wallet address                     |
| `dirty_kinds`              | `string[]`       | What triggered this row            |
| `timeframe`                | `string \| null` | Window this row covers             |
| `category`                 | `string`         | Market category                    |
| `realized_pnl_usd`         | `number`         | Realized PnL in USD                |
| `markets_in_category`      | `int64`          | Markets available in the category  |
| `markets_traded`           | `int64`          | Markets traded in the category     |
| `outcomes_traded`          | `int64`          | Distinct outcomes traded           |
| `total_buys`               | `int64`          | Buy count                          |
| `total_sells`              | `int64`          | Sell count                         |
| `total_redemptions`        | `int64`          | Redemption count                   |
| `total_merges`             | `int64`          | Merge count                        |
| `total_splits`             | `int64`          | Split count                        |
| `total_volume_usd`         | `number`         | Total traded volume in USD         |
| `buy_usd`                  | `number`         | Buy volume in USD                  |
| `sell_usd`                 | `number`         | Sell volume in USD                 |
| `redemption_usd`           | `number`         | Redemption volume in USD           |
| `merge_usd`                | `number`         | Merge volume in USD                |
| `split_volume_usd`         | `number`         | Split volume in USD                |
| `total_fees`               | `number`         | Total fees in USD                  |
| `total_shares_bought`      | `number`         | Cumulative shares bought           |
| `markets_won`              | `int64`          | Markets won                        |
| `markets_lost`             | `int64`          | Markets lost                       |
| `market_win_rate_pct`      | `number`         | Win rate as a percentage           |
| `avg_hold_time_seconds`    | `number`         | Average hold time in seconds       |
| `best_trade_pnl_usd`       | `number \| null` | Best single-trade PnL              |
| `best_trade_condition_id`  | `string \| null` | Condition of the best trade        |
| `worst_trade_pnl_usd`      | `number \| null` | Worst single-trade PnL             |
| `worst_trade_condition_id` | `string \| null` | Condition of the worst trade       |
| `total_wins_usd`           | `number`         | Sum of winning PnL                 |
| `total_losses_usd`         | `number`         | Sum of losing PnL                  |
| `avg_win_usd`              | `number`         | Average winning PnL                |
| `avg_loss_usd`             | `number`         | Average losing PnL                 |
| `profit_factor`            | `number \| null` | Gross wins divided by gross losses |
| `first_trade_at`           | `int64 \| null`  | First trade (Unix seconds)         |
| `last_trade_at`            | `int64 \| null`  | Last trade (Unix seconds)          |

### Price tick batches

`trader_global_tick_batch`, `trader_market_tick_batch`, and `trader_category_tick_batch` carry mark-to-market refreshes. They are window-agnostic and their `dirty_kinds` is always `["price"]`.

```json theme={null}
{
  "type": "trader_global_tick_batch",
  "room_id": "polymarket_trader_pnl",
  "block": 68420002,
  "data": [
    {
      "trader": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
      "realized_pnl_usd": 12500.75,
      "open_positions_value": 8400.0,
      "last_block": 68420002,
      "last_trade_at": 1743500000,
      "dirty_kinds": ["price"]
    }
  ]
}
```

| Row                     | Fields                                                                                                                         |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `TraderGlobalTickRow`   | `trader`, `realized_pnl_usd`, `open_positions_value`, `last_block` (`int64`), `last_trade_at` (`int64 \| null`), `dirty_kinds` |
| `TraderMarketTickRow`   | `trader`, `condition_id`, `realized_pnl_usd`, `last_block`, `last_trade_at`, `dirty_kinds`                                     |
| `TraderCategoryTickRow` | `trader`, `category`, `realized_pnl_usd`, `last_block`, `last_trade_at`, `dirty_kinds`                                         |

### Resolution batches

`trader_global_resolution_batch`, `trader_market_resolution_batch`, and `trader_category_resolution_batch` fire when an owning market resolves without a trade. They are window-agnostic and their `dirty_kinds` is always `["market_resolved"]`.

```json theme={null}
{
  "type": "trader_market_resolution_batch",
  "room_id": "polymarket_trader_pnl",
  "block": 68420003,
  "data": [
    {
      "trader": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
      "condition_id": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
      "resolved": true,
      "won": true,
      "realized_pnl_usd": 350.25,
      "last_block": 68420003,
      "last_trade_at": 1743500000,
      "dirty_kinds": ["market_resolved"]
    }
  ]
}
```

| Row                           | Fields                                                                                                                                                                                                     |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TraderGlobalResolutionRow`   | `trader`, `realized_pnl_usd`, `markets_won`, `markets_lost`, `markets_resolved` (`int64`), `total_wins_usd_lifetime`, `total_losses_usd_lifetime` (`number`), `last_block`, `last_trade_at`, `dirty_kinds` |
| `TraderMarketResolutionRow`   | `trader`, `condition_id`, `resolved` (`bool`), `won` (`bool \| null`), `realized_pnl_usd`, `last_block`, `last_trade_at`, `dirty_kinds`                                                                    |
| `TraderCategoryResolutionRow` | `trader`, `category`, `realized_pnl_usd`, `markets_won`, `markets_lost` (`int64`), `total_wins_usd_lifetime`, `total_losses_usd_lifetime`, `last_block`, `last_trade_at`, `dirty_kinds`                    |
