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

# Stream Prediction Positions

> Streams one account's prediction-market positions as server-sent events: its open positions first, then every change to its positions as it happens.

**Use Case:** Keep a positions table current, and show positions opening, growing, and closing as orders fill, without polling.


<Warning>
  This endpoint is in **closed beta**. It is available only to accounts enrolled in the beta, and only on staging (`https://api.tradearies.dev`). Request and response shapes may change before general availability.
</Warning>

<Note>
  **Booked values only.** The stream carries what fills have booked: quantity, cost, average price, and realized P\&L. It has no mark price and no unrealized P\&L, so it sends an update when a fill changes a position and not on every price tick. Compute unrealized P\&L on the client from the [live price stream](/api-reference/predictions/stream-live-prices); see [Unrealized P\&L](#unrealized-pl).
</Note>

## Request

```text theme={null}
GET /v1/predictions/positions/stream?accountId=ACCOUNT-0001
```

| Parameter | Description |
| - | - |
| `accountId` | Required. The account whose positions to stream. |

Authenticate the same way as [Stream Prediction Orders](/api-reference/predictions/stream-orders). The stream carries only the authenticated user's own positions: an account you have no positions on streams an empty snapshot and nothing after it.

The first line of the response is `retry: 2000`, which tells an `EventSource` to wait two seconds before it reconnects.

## Events

| Event | When | Payload |
| - | - | - |
| `snapshot` | First, once | `accountId` and `positions`: the account's open positions |
| `position` | Each time one of the account's positions changes | One position |
| `resync` | When the stream is about to end | `reason`: `slow_client` or `shutdown` |

A `: ping` comment is sent every 15 seconds to keep idle connections open.

### snapshot

```json theme={null}
{
  "accountId": "ACCOUNT-0001",
  "positions": [
    {
      "accountId": "ACCOUNT-0001",
      "symbol": "HORC_1126_Republican",
      "outcome": "YES",
      "qty": "10",
      "cost": "5.40",
      "avgPx": "0.54",
      "realizedPnl": "0",
      "version": 3,
      "createdAt": "2026-10-08T14:02:15.103Z",
      "updatedAt": "2026-10-08T14:02:15.103Z"
    }
  ]
}
```

`positions` holds only open positions, those with a non-zero `qty`. A position that is flat is not in the snapshot.

### position

```json theme={null}
{
  "accountId": "ACCOUNT-0001",
  "symbol": "HORC_1126_Republican",
  "outcome": "YES",
  "qty": "15",
  "cost": "8.15",
  "avgPx": "0.5433",
  "realizedPnl": "0",
  "version": 4,
  "createdAt": "2026-10-08T14:02:15.103Z",
  "updatedAt": "2026-10-08T14:20:41.870Z"
}
```

Each `position` is the whole row as it stands after the change, not a delta. Replace the row you hold for that `symbol` when the event's `version` is higher than the one you have.

When `qty` is `"0"` the position has closed: remove the row.

```json theme={null}
{
  "accountId": "ACCOUNT-0001",
  "symbol": "HORC_1126_Republican",
  "outcome": "",
  "qty": "0",
  "cost": "0",
  "avgPx": "0",
  "realizedPnl": "1.85",
  "version": 5,
  "createdAt": "2026-10-08T14:02:15.103Z",
  "updatedAt": "2026-10-08T15:47:09.226Z"
}
```

### resync

```json theme={null}
{ "reason": "slow_client" }
```

The stream ends right after a `resync`. Reconnect, and the new `snapshot` brings your view up to date. `slow_client` means you stopped reading events fast enough to keep up; `shutdown` means the server is restarting.

## Position fields

Decimals are strings: `"0.54"`, not `0.54`.

| Field | Description |
| - | - |
| `accountId` | The account holding the position. |
| `symbol` | The contract. A position is keyed by `symbol`: an account has at most one position per contract. |
| `outcome` | `YES` or `NO`, the outcome held. Empty (`""`) when the position is flat. |
| `qty` | Contracts held. `"0"` means the position has closed. |
| `cost` | The cost booked for the contracts held. |
| `avgPx` | The average price of the contracts held. |
| `realizedPnl` | Profit and loss realized so far. |
| `version` | An `int64` that rises with every change to the position. It is the only ordering; see [Ordering](#ordering). |
| `createdAt` | When the position was first opened. |
| `updatedAt` | When the position last changed. |

ForecastEx takes buy orders only, so a position holds one outcome at a time. Buying the other outcome of a contract you hold does not open a second position: it nets against the one you have.

## Unrealized P\&L

Positions carry no mark price and no unrealized P\&L. Take the last price for the held outcome from the [live price stream](/api-reference/predictions/stream-live-prices), where each contract's `snapshot` has a `yes.last` and a `no.last`, and compute:

```text theme={null}
unrealized P&L = (last − avgPx) × qty
```

Use `yes.last` when `outcome` is `YES` and `no.last` when it is `NO`. Do the arithmetic with a decimal type. If that outcome has not traded, `last` is `null`: show no unrealized P\&L rather than zero.

## Ordering

* Every position carries a `version` that rises with each change. **Keep the highest `version` you have seen for each `symbol`, and ignore a position at or below it.** The stream already drops older versions, but applying this rule makes your client correct across reconnects too.
* **Never order by time.** `createdAt` and `updatedAt` are for display. Only `version` says which row is newer.
* `snapshot` comes first. Every `position` after it is newer than the snapshot's copy of that position.
* Changes to one position arrive in the order they happened. Changes to different positions may interleave.

## Reconnecting

Events have no IDs and are not replayed, and there is no `Last-Event-ID` resume. Every connection starts with a fresh `snapshot`, so after a disconnect or a `resync`, reconnect and replace all your positions with the new snapshot. A change that happened while you were disconnected is reflected in it, and a position that closed while you were away is no longer in it.

`EventSource` reconnects on its own after the `retry: 2000` delay. A client that reads the stream with `fetch` has to reconnect itself, as the example below does.

## Examples

```bash cURL theme={null}
curl -N "https://api.tradearies.dev/v1/predictions/positions/stream?accountId=ACCOUNT-0001" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

A browser's `EventSource` cannot send an `Authorization` header, so read the stream with `fetch`:

```javascript JavaScript theme={null}
const positions = new Map(); // symbol -> position

function apply(position) {
  if (position.qty === "0") {
    positions.delete(position.symbol);
    return;
  }
  const held = positions.get(position.symbol);
  if (!held || position.version > held.version) {
    positions.set(position.symbol, position);
  }
}

async function streamPositions(accountId, accessToken) {
  const res = await fetch(
    `https://api.tradearies.dev/v1/predictions/positions/stream?accountId=${encodeURIComponent(accountId)}`,
    { headers: { Authorization: `Bearer ${accessToken}` } }
  );
  if (!res.ok) throw new Error(`stream refused: ${res.status}`);

  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) return; // the stream ended: reconnect
    buffer += value;
    let end;
    while ((end = buffer.indexOf("\n\n")) >= 0) {
      const frame = buffer.slice(0, end);
      buffer = buffer.slice(end + 2);
      const event = frame.match(/^event: (.*)$/m)?.[1];
      const data = frame.match(/^data: (.*)$/m)?.[1];
      if (event === "snapshot") {
        positions.clear();
        JSON.parse(data).positions.forEach(apply);
      } else if (event === "position") {
        apply(JSON.parse(data));
      }
    }
  }
}

async function keepStreaming(accountId, accessToken) {
  for (;;) {
    try {
      await streamPositions(accountId, accessToken);
    } catch (err) {
      console.error(err);
    }
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}
```

To show unrealized P\&L, keep each contract's latest `yes.last` and `no.last` from the live price stream and combine them with the held row:

```javascript JavaScript theme={null}
function unrealizedPnl(position, quote) {
  const last = position.outcome === "YES" ? quote.yes?.last : quote.no?.last;
  if (last == null) return null; // the held outcome has not traded
  return (Number(last) - Number(position.avgPx)) * Number(position.qty);
}
```

`Number` keeps the example short. In production, use a decimal library for money.

## Working with the stream

* **Prices, quantities, and P\&L are decimal strings.** Parse them with a decimal type.
* **Do not put the stream behind a buffering proxy.** Events must reach you as they are sent.

## Errors

Errors are returned before the stream opens, in the usual [error envelope](/prediction-api/overview#response-envelope):

| Status | `code` | When |
| - | - | - |
| `400` | `INVALID_STREAM_REQUEST` | `accountId` is missing. |
| `401` | — | Missing or invalid credentials. |
| `500` | `POSITION_STREAM_FAILED` | Unexpected server error. |
| `503` | `POSITION_STREAM_UNAVAILABLE` | The server is restarting or busy; retry shortly. |


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