# `ZenQuant.Orderflow`
[🔗](https://github.com/ZenHive/zen_quant/blob/v0.8.1/lib/zen_quant/orderflow.ex#L1)

Orderflow analytics for trade-level and orderbook data.

Pure functions for calculating cumulative volume delta (CVD),
volume-weighted average price (VWAP), orderbook imbalance,
heatmap visualization points, footprint chart cells, and
depth-of-market level analysis.

All functions accept plain maps with atom or string keys and values,
following Bourse's unified trade and orderbook formats. Side fields
accept both strings (`"buy"`/`"sell"`) and atoms (`:buy`/`:sell`).

## Terminology

  * **CVD** - Cumulative Volume Delta: net aggressive buying vs selling
  * **VWAP** - Volume-Weighted Average Price
  * **Imbalance** - Bid/ask volume ratio normalized to [-1, 1]
  * **Footprint** - Price/time bucketed buy/sell volume breakdown
  * **DOM** - Depth of Market: resting orders + executed trades at a price

## Example

    trade = %{side: "buy", amount: 1.5, price: 50_000}
    ZenQuant.Orderflow.cvd_delta(trade)
    # => 1.5

    trades = [
      %{side: "buy", amount: 1.0, price: 100},
      %{side: "sell", amount: 2.0, price: 102}
    ]
    ZenQuant.Orderflow.vwap(trades)
    # => 101.333...

## API Functions
| Function | Arity | Description | Param Kinds |
| --- | --- | --- | --- |
| `dom_level` | 2 | Calculate depth-of-market level combining resting orders and executed trades. | `price: value`, `data: exchange_data` |
| `footprint_cells` | 3 | Build footprint chart cells from trades grouped by price/time buckets. | `trades: exchange_data`, `price_step: value`, `time_window_ms: value` |
| `heatmap_points` | 2 | Generate heatmap visualization points from orderbook. | `orderbook: exchange_data`, `depth: value` |
| `imbalance!` | 2 | Calculate orderbook bid/ask imbalance (raises on error). | `orderbook: exchange_data`, `depth: value` |
| `imbalance` | 2 | Calculate orderbook bid/ask imbalance at given depth. | `orderbook: exchange_data`, `depth: value` |
| `vwap` | 1 | Calculate volume-weighted average price for a list of trades. | `trades: exchange_data` |
| `cvd_delta` | 1 | Calculate CVD delta for a single trade. | `trade: exchange_data` |
| `cvd` | 1 | Calculate total cumulative volume delta for a list of trades. | `trades: exchange_data` |

# `cvd_delta`

```elixir
@type cvd_delta() :: float()
```

Single-trade CVD contribution (positive = buy aggression, negative = sell)

# `dom_level`

```elixir
@type dom_level() :: %{
  price: float(),
  bid_size: float(),
  ask_size: float(),
  buy_volume: float(),
  sell_volume: float(),
  delta: float()
}
```

Depth-of-market level: resting liquidity + executed volume at a price

# `footprint_cell`

```elixir
@type footprint_cell() :: %{
  price_bucket: float(),
  time_bucket: integer(),
  buy_volume: float(),
  sell_volume: float(),
  delta: float()
}
```

Footprint chart cell: buy/sell volume in a price/time bucket

# `heatmap_point`

```elixir
@type heatmap_point() :: %{price: float(), size: float(), side: :bid | :ask}
```

Heatmap visualization point at a price level

# `imbalance`

```elixir
@type imbalance() :: float()
```

Orderbook imbalance normalized to [-1.0, 1.0]

# `cvd`

```elixir
@spec cvd([map()]) :: float()
```

Calculate total cumulative volume delta for a list of trades.

## Parameters

  * `trades` - List of trade maps with :side and :amount fields (exchange_data)

## Returns

Net CVD: positive = net buy aggression, negative = net sell (`float`)

### Example

```elixir
12.5
```

```elixir
# descripex:contract
%{
  params: %{
    trades: %{
      description: "List of trade maps with :side and :amount fields",
      source: "fetch_trades(symbol)",
      kind: :exchange_data
    }
  },
  returns: %{
    type: :float,
    description: "Net CVD: positive = net buy aggression, negative = net sell"
  },
  returns_example: 12.5
}
```

# `cvd_delta`

```elixir
@spec cvd_delta(map()) :: cvd_delta()
```

Calculate CVD delta for a single trade.

## Parameters

  * `trade` - Map with :side (buy/sell) and :amount fields (exchange_data)

## Returns

+abs(amount) for buy, -abs(amount) for sell, 0.0 if missing (`float`)

### Example

```elixir
0.1095
```

```elixir
# descripex:contract
%{
  params: %{
    trade: %{
      description: "Map with :side (buy/sell) and :amount fields",
      source: "fetch_trades(symbol)",
      kind: :exchange_data
    }
  },
  returns: %{
    type: :float,
    description: "+abs(amount) for buy, -abs(amount) for sell, 0.0 if missing"
  },
  returns_example: 0.1095
}
```

# `dom_level`

```elixir
@spec dom_level(number(), map()) :: dom_level() | ZenQuant.OrderBook.level_error()
```

Calculate depth-of-market level combining resting orders and executed trades.

## Parameters

  * `price` - The price level to analyze (value)
  * `data` - Two accepted shapes:
(1) Wrapper map: %\{orderbook: %\{bids: [...], asks: [...]\}, trades: [...]\}
(2) Direct orderbook with optional trades: %\{bids: [...], asks: [...], trades: [...]\}
Detected automatically by presence of :bids/:asks keys.
 (exchange_data)

## Returns

Map with :price, :bid_size, :ask_size, :buy_volume, :sell_volume, :delta (`map`)

### Example

```elixir
%{
  price: 84000.0,
  delta: 1.5,
  bid_size: 15.0,
  ask_size: 10.0,
  buy_volume: 3.5,
  sell_volume: 2.0
}
```

## Errors

  * `:invalid_orderbook_level`

```elixir
# descripex:contract
%{
  params: %{
    data: %{
      description: "Two accepted shapes:\n(1) Wrapper map: %{orderbook: %{bids: [...], asks: [...]}, trades: [...]}\n(2) Direct orderbook with optional trades: %{bids: [...], asks: [...], trades: [...]}\nDetected automatically by presence of :bids/:asks keys.\n",
      source: "fetch_order_book(symbol) + fetch_trades(symbol)",
      kind: :exchange_data
    },
    price: %{description: "The price level to analyze", kind: :value}
  },
  errors: [:invalid_orderbook_level],
  returns: %{
    type: :map,
    description: "Map with :price, :bid_size, :ask_size, :buy_volume, :sell_volume, :delta"
  },
  returns_example: %{
    price: 84000.0,
    delta: 1.5,
    bid_size: 15.0,
    ask_size: 10.0,
    buy_volume: 3.5,
    sell_volume: 2.0
  }
}
```

# `footprint_cells`

```elixir
@spec footprint_cells([map()], number(), pos_integer()) :: [footprint_cell()]
```

Build footprint chart cells from trades grouped by price/time buckets.

## Parameters

  * `trades` - List of trade maps with :price, :amount, :timestamp (Unix ms), :side (exchange_data)
  * `price_step` - Price bucket size (e.g., 50 for $50 buckets) (value)
  * `time_window_ms` - Time bucket size in milliseconds (value)

## Returns

List of %\{price_bucket, time_bucket, buy_volume, sell_volume, delta\} (`list`)

### Example

```elixir
[
  %{
    delta: 4.0,
    price_bucket: 84000.0,
    time_bucket: 1708862400000,
    buy_volume: 12.0,
    sell_volume: 8.0
  }
]
```

```elixir
# descripex:contract
%{
  params: %{
    trades: %{
      description: "List of trade maps with :price, :amount, :timestamp (Unix ms), :side",
      source: "fetch_trades(symbol)",
      kind: :exchange_data
    },
    price_step: %{
      description: "Price bucket size (e.g., 50 for $50 buckets)",
      kind: :value
    },
    time_window_ms: %{
      description: "Time bucket size in milliseconds",
      kind: :value
    }
  },
  returns: %{
    type: :list,
    description: "List of %{price_bucket, time_bucket, buy_volume, sell_volume, delta}"
  },
  returns_example: [
    %{
      delta: 4.0,
      price_bucket: 84000.0,
      time_bucket: 1708862400000,
      buy_volume: 12.0,
      sell_volume: 8.0
    }
  ]
}
```

# `heatmap_points`

```elixir
@spec heatmap_points(map(), pos_integer()) ::
  [heatmap_point()] | ZenQuant.OrderBook.level_error()
```

Generate heatmap visualization points from orderbook.

## Parameters

  * `orderbook` - Map with :bids and :asks levels following the ZenQuant.OrderBook contract (exchange_data)
  * `depth` - Number of levels to include from each side (value)

## Returns

List of %\{price, size, side: :bid | :ask\} sorted by price ascending (`list`)

### Example

```elixir
[%{size: 5.0, price: 84000.0, side: :bid}]
```

## Errors

  * `:invalid_orderbook_level`

```elixir
# descripex:contract
%{
  params: %{
    depth: %{
      description: "Number of levels to include from each side",
      kind: :value
    },
    orderbook: %{
      description: "Map with :bids and :asks levels following the ZenQuant.OrderBook contract",
      source: "fetch_order_book(symbol)",
      kind: :exchange_data
    }
  },
  errors: [:invalid_orderbook_level],
  returns: %{
    type: :list,
    description: "List of %{price, size, side: :bid | :ask} sorted by price ascending"
  },
  returns_example: [%{size: 5.0, price: 84000.0, side: :bid}]
}
```

# `imbalance`

```elixir
@spec imbalance(map(), pos_integer()) ::
  {:ok, imbalance()}
  | {:error, :invalid_orderbook}
  | ZenQuant.OrderBook.level_error()
```

Calculate orderbook bid/ask imbalance at given depth.

## Parameters

  * `orderbook` - Map with :bids and :asks levels following the ZenQuant.OrderBook contract (exchange_data)
  * `depth` - Number of levels to include from each side (value)

## Returns

Float from -1.0 (all asks) to 1.0 (all bids) (`float`)

### Example

```elixir
{:ok, 0.25}
```

## Errors

  * `:invalid_orderbook`
  * `:invalid_orderbook_level`

```elixir
# descripex:contract
%{
  params: %{
    depth: %{
      description: "Number of levels to include from each side",
      kind: :value
    },
    orderbook: %{
      description: "Map with :bids and :asks levels following the ZenQuant.OrderBook contract",
      source: "fetch_order_book(symbol)",
      kind: :exchange_data
    }
  },
  errors: [:invalid_orderbook, :invalid_orderbook_level],
  returns: %{
    type: :float,
    description: "Float from -1.0 (all asks) to 1.0 (all bids)"
  },
  returns_example: {:ok, 0.25}
}
```

# `imbalance!`

```elixir
@spec imbalance!(map(), pos_integer()) :: imbalance()
```

Bang variant of `imbalance/2`. Returns the float directly or raises on error.

## Example

    book = %{bids: [[100, 5.0]], asks: [[101, 3.0]]}
    ZenQuant.Orderflow.imbalance!(book, 1)
    # => 0.25

# `vwap`

```elixir
@spec vwap([map()]) :: float() | nil
```

Calculate volume-weighted average price for a list of trades.

## Parameters

  * `trades` - List of trade maps with :price and :amount fields (exchange_data)

## Returns

VWAP as float, or nil if no valid trades (`float`)

### Example

```elixir
0.1095
```

```elixir
# descripex:contract
%{
  params: %{
    trades: %{
      description: "List of trade maps with :price and :amount fields",
      source: "fetch_trades(symbol)",
      kind: :exchange_data
    }
  },
  returns: %{
    type: :float,
    description: "VWAP as float, or nil if no valid trades"
  },
  returns_example: 0.1095
}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
