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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.