## Live streams
`SahiFeed` connects to the
[tick stream](/api/v1/reference/market-data/#wss-v1streammarketdata--tick-stream)
and the [order-update stream](/api/v1/reference/orders/#wss-v1streamorders--order-updates).

- **Callbacks run on the thread that calls `consume()`**, one at a time. If
  ticks queue up, only the newest per instrument is delivered; order updates
  all arrive in order.
- **Ticks have every field**, shaped as the REST endpoints return them: an
  unset field is `None`, an empty side of the book `[]`, an unset count `0`.
- **Reconnects are automatic** after a drop, and subscriptions are re-sent.
- **Close codes `4401`, `4403` and `4429` are final**: `consume()` raises. After
  a new token, build a new feed.
- **Logging** goes to the `sahiapi.sahi.feed` and `sahiapi.sahi.ws_client`
  loggers.

Examples use `api` and `instrument_id` from [Getting started](/sdk/python/),
and `feed` from below.

### SahiFeed(...) — Build a feed

Takes a `SahiAPI` and copies its token. Nothing connects until the first
subscribe.

#### Signature

```python
SahiFeed(api: SahiAPI)
```

#### Example

```python
from sahiapi import SahiFeed

feed = SahiFeed(api)
```

### feed.subscribe_ltp(...) — Subscribe for last price

Subscribes instruments at `LTP` mode. If any id is refused, the whole batch is
refused and your callback gets an `error` event.

#### Signature

```python
feed.subscribe_ltp(instrument_ids: list[int], on_data_received: Callable[[dict, dict], None] | None = None) -> None
```

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `instrument_ids` | list[int] | yes | Up to 1000 per connection. |
| `on_data_received` | callable | no | `callback(data, meta)`. `meta["type"]` is `ltp_tick`, `quote_tick`, `depth_tick`, `ack` or `error`. One callback serves all three modes. |

#### Example

```python
def on_tick(data, meta):
    if meta["type"] == "ltp_tick":
        print(data["instrument_id"], data["last_trade_price"] / 100)

feed.subscribe_ltp([instrument_id], on_data_received=on_tick)
feed.consume()  # blocks; stop with Ctrl+C
```

### feed.subscribe_quote(...) — Subscribe for quotes

As `subscribe_ltp`, at `QUOTE` mode: adds `volume_today`, the price band and
open interest.

#### Signature

```python
feed.subscribe_quote(instrument_ids: list[int], on_data_received: Callable[[dict, dict], None] | None = None) -> None
```

#### Example

```python
feed.subscribe_quote([instrument_id], on_data_received=on_tick)
```

### feed.subscribe_depth(...) — Subscribe for depth

As `subscribe_ltp`, at `DEPTH` mode: adds `best_buys` and `best_sells`.

#### Signature

```python
feed.subscribe_depth(instrument_ids: list[int], on_data_received: Callable[[dict, dict], None] | None = None) -> None
```

#### Example

```python
feed.subscribe_depth([instrument_id], on_data_received=on_tick)
```

### feed.change_mode(...) — Change mode

Moves instruments to another mode.

#### Signature

```python
feed.change_mode(instrument_ids: list[int], mode: str) -> None
```

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `instrument_ids` | list[int] | yes | Ids to move. |
| `mode` | str | yes | `SahiAPI.MODE_LTP`, `MODE_QUOTE` or `MODE_DEPTH`. |

#### Raises

- `ValueError`: unknown `mode`.

#### Example

```python
feed.change_mode([instrument_id], SahiAPI.MODE_DEPTH)
```

### feed.unsubscribe(...) — Unsubscribe

Stops ticks for these instruments and clears their cached values.

#### Signature

```python
feed.unsubscribe(instrument_ids: list[int]) -> None
```

#### Example

```python
feed.unsubscribe([instrument_id])
```

### feed.last_ltp(...) — Latest cached tick

The newest `LTP` tick held for an instrument, or `None` until one arrives. The
cache fills even without `consume()`, so you can poll it.

#### Signature

```python
feed.last_ltp(instrument_id: int) -> dict | None
```

#### Raises

- [`SahiFeedNotSubscribedException`](/sdk/python/errors/#sahifeednotsubscribedexception): not subscribed.
- The [`SahiFeedException`](/sdk/python/errors/#sahifeedexception) that closed the stream, once it has.

#### Example

```python
tick = feed.last_ltp(instrument_id)
print(tick["last_trade_price"] / 100 if tick else "no tick yet")
```

### feed.last_quote(...) — Latest cached quote

As `feed.last_ltp`, for `QUOTE` ticks.

#### Signature

```python
feed.last_quote(instrument_id: int) -> dict | None
```

#### Example

```python
quote = feed.last_quote(instrument_id)
```

### feed.last_depth(...) — Latest cached depth

As `feed.last_ltp`, for `DEPTH` ticks.

#### Signature

```python
feed.last_depth(instrument_id: int) -> dict | None
```

#### Example

```python
depth = feed.last_depth(instrument_id)
```

### feed.subscribe_order_updates(...) — Order updates

Streams every order event on the account. Updates can repeat and are not
replayed after a gap, so take a snapshot with `get_order_list()` in
`on_connect` and keep the newer `last_modified_at` per order: see
[Stream + Snapshot](/api/v1/reference/orders/#stream--snapshot).

#### Signature

```python
feed.subscribe_order_updates(on_data_received: Callable[[dict, dict], None], *, on_connect: Callable[[], None] | None = None) -> None
```

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `on_data_received` | callable | yes | `callback(order, meta)`, one order object per update. |
| `on_connect` | callable | no | Called after every connect and reconnect. |

#### Example

```python
orders = {}

def merge(o):
    seen = orders.get(o["order_id"])
    if seen is None or o["last_modified_at"] >= seen["last_modified_at"]:
        orders[o["order_id"]] = o

def snapshot():
    for o in api.get_order_list()["orders"]:
        merge(o)

feed.subscribe_order_updates(lambda o, meta: merge(o), on_connect=snapshot)
feed.consume()
```

### feed.consume(...) — Run the callbacks

Blocks and runs your callbacks until `stop()` is called. However it ends, both
streams are stopped.

#### Signature

```python
feed.consume() -> None
```

#### Raises

- [`SahiFeedAuthExpiredException`](/sdk/python/errors/#sahifeedauthexpiredexception): the token expired (`4401`).
- [`SahiFeedForbiddenException`](/sdk/python/errors/#sahifeedforbiddenexception): the token lacks the scope (`4403`).
- [`SahiFeedConnectionException`](/sdk/python/errors/#sahifeedconnectionexception): too many connections (`4429`).
- Anything your callback raised.

#### Example

```python
feed.consume()
```

### feed.stop(...) — Stop both streams

Closes both connections and unblocks `consume()`. Safe from any thread.

#### Signature

```python
feed.stop() -> None
```

#### Example

```python
feed.stop()
```
