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

# Estimating VWAP

> Work out what a given size would really cost from streamed depth - no firm quote needed.

Before committing to a firm RFQ quote, it's often worth knowing roughly what a specific size would execute at. The depth carried on the price stream is enough to compute a **volume-weighted average price (VWAP)** on your side - a dependable indicative number for any trade size, calculated locally and instantly.

<Info>
  **Typical scenario:** you're deciding whether an intent is worth bidding on. Rather than burning a rate-limited firm quote that expires in seconds, you derive the likely execution price straight from the live stream and only go firm when the trade clears your bar.
</Info>

## The Idea

Stream levels arrive as `(price, size)` pairs, best price first. Pricing a target size means consuming those levels in order - best to worst - until the target is covered. The VWAP is then simply total quote spent over total base received: the blended price of everything you'd have eaten through.

## Walking the Book

### The Four Moves

<Steps>
  <Step title="Order the levels">
    Buying? Consume asks, cheapest first. Selling? Consume bids, richest first.
  </Step>

  <Step title="Consume level by level">
    A level's capacity is `price × size`. Fill from it up to whatever target remains - the final level usually gets consumed only partially.
  </Step>

  <Step title="Blend the totals">
    Keep running sums of base filled and quote spent; dividing one by the other yields the VWAP.
  </Step>

  <Step title="Notice when depth runs out">
    A positive remainder after the last level means the stream can't absorb your size - go straight to a firm quote, or split the trade across sources.
  </Step>
</Steps>

```python theme={null} theme={null}
def estimate_vwap(
    levels: list[tuple[float, float]],
    target_notional: float,
    intent: str,  # "buy" or "sell"
) -> tuple[float, float]:
    """
    Blend streamed depth levels into an execution estimate for one target size.

    levels          -- (price, size) tuples from the stream, best price first.
    target_notional -- how much (in quote terms) you intend to trade.
    intent          -- "buy" eats asks; "sell" eats bids.

    Gives back (vwap, leftover): the blended price, and any notional the
    book couldn't absorb (0 when the size fits).
    """
    # Buys want ascending prices; sells want descending.
    sorted_levels = sorted(levels, key=lambda l: l[0], reverse=(intent == "sell"))

    remaining = target_notional
    total_base = 0.0
    total_quote = 0.0

    for price, size in sorted_levels:
        if remaining <= 0:
            break

        level_notional = price * size
        fill_notional = min(level_notional, remaining)
        fill_base = fill_notional / price

        total_quote += fill_notional
        total_base += fill_base
        remaining -= fill_notional

    if total_base == 0:
        return 0.0, target_notional

    vwap = total_quote / total_base
    return vwap, remaining
```

## Putting It on the Stream

Wire the estimator into the WebSocket connection from the [Quickstart](/price-api/quickstart) and it re-prices on every snapshot:

```python theme={null} theme={null}
import asyncio

import httpx
import websockets

from tetrafi_pb2 import TetraFiPricingUpdate  # type: ignore

NETWORK = "ethereum"
CHAIN_ID = 1
API_KEY = "tfk_live_..."

WSS_URL = (
    f"wss://api.tetrafi.io/api/v1/ws"
    f"?token={API_KEY}"
    f"&format=protobuf"
    f"&network={NETWORK}"
)

PAIR = "WETH/USDC"
TARGET_NOTIONAL = 250_000  # price a $250k clip

def address_to_hex(b: bytes) -> str:
    return "0x" + b.hex()

def to_levels(flat: list[float]) -> list[tuple[float, float]]:
    it = iter(flat)
    return list(zip(it, it, strict=True))

def estimate_vwap(
    levels: list[tuple[float, float]], target_notional: float, intent: str
) -> tuple[float, float]:
    sorted_levels = sorted(levels, key=lambda lv: lv[0], reverse=(intent == "sell"))

    remaining = target_notional
    total_base = 0.0
    total_quote = 0.0

    for price, size in sorted_levels:
        if remaining <= 0:
            break

        level_notional = price * size
        fill_notional = min(level_notional, remaining)
        fill_base = fill_notional / price

        total_quote += fill_notional
        total_base += fill_base
        remaining -= fill_notional

    if total_base == 0:
        return 0.0, target_notional

    return total_quote / total_base, remaining

# Map symbols to addresses via the tradable-pair list
resp = httpx.get("https://api.tetrafi.io/api/v1/pairs",
                 headers={"X-API-Key": API_KEY}, timeout=10.0)
tokens = {}
for p in resp.json()["pairs"]:
    for side in (p["input"], p["output"]):
        if side["chainId"] == CHAIN_ID:
            tokens[side["symbol"]] = side
base_symbol, quote_symbol = PAIR.split("/")
base_addr = tokens[base_symbol]["address"].lower()
quote_addr = tokens[quote_symbol]["address"].lower()

async def main():
    async with websockets.connect(
        WSS_URL,
        ping_interval=20,
        ping_timeout=10,
        max_size=2**21,
    ) as ws:
        print(f"Streaming - pricing {PAIR} at ${TARGET_NOTIONAL:,}\n")

        async for blob in ws:
            update = TetraFiPricingUpdate()
            update.ParseFromString(blob)

            for pair in update.pairs:
                if (
                    address_to_hex(pair.base).lower() != base_addr
                    or address_to_hex(pair.quote).lower() != quote_addr
                ):
                    continue

                bids = to_levels(list(pair.bids))
                asks = to_levels(list(pair.asks))

                if not bids or not asks:
                    continue

                # What would a $250k buy cost right now?
                buy_vwap, buy_unfilled = estimate_vwap(asks, TARGET_NOTIONAL, "buy")

                # And a $250k sell?
                sell_vwap, sell_unfilled = estimate_vwap(bids, TARGET_NOTIONAL, "sell")

                print(
                    f"  BUY  ${TARGET_NOTIONAL:>8,}  "
                    f"vwap: {buy_vwap:.2f}"
                    f'{"  ⚠ unfilled: " + f"${buy_unfilled:,.0f}" if buy_unfilled > 0 else ""}\n'
                    f"  SELL ${TARGET_NOTIONAL:>8,}  "
                    f"vwap: {sell_vwap:.2f}"
                    f'{"  ⚠ unfilled: " + f"${sell_unfilled:,.0f}" if sell_unfilled > 0 else ""}\n'
                )
asyncio.run(main())
```

A \$250,000 WETH/USDC run prints something like:

```json theme={null} theme={null}
{
  "pair": "WETH/USDC",
  "target_notional": 250000,
  "buy": {
    "vwap": 2412.85,
    "unfilled": 0.0
  },
  "sell": {
    "vwap": 2409.12,
    "unfilled": 0.0
  }
}
```

<Warning>
  A streamed VWAP is an **estimate**, nothing more. The firm quote you eventually request can land elsewhere - LP inventory shifts, time passes, and quote-level parameters apply. Treat it as a filter, not a promise.
</Warning>

## Watch-outs

* **A leftover means the book is too thin.** When `unfilled > 0`, the streamed depth can't take your size - request a firm quote or break the trade up instead of trusting the partial number.
* **Size is what separates VWAP from top-of-book.** Tiny clips track the best level closely; real size digs into worse levels, and quantifying exactly how much worse is why you're computing this at all.
* **Depth profiles differ pair to pair.** Identical top-of-book prices can hide wildly different books underneath - always estimate at your true size instead of extrapolating.
* **Never reuse an old estimate.** The stream refreshes constantly; recompute on every message rather than holding onto a number that's already stale.
