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 |
Summary
Types
Single-trade CVD contribution (positive = buy aggression, negative = sell)
Depth-of-market level: resting liquidity + executed volume at a price
Footprint chart cell: buy/sell volume in a price/time bucket
Heatmap visualization point at a price level
Orderbook imbalance normalized to [-1.0, 1.0]
Functions
Calculate total cumulative volume delta for a list of trades.
Calculate CVD delta for a single trade.
Calculate depth-of-market level combining resting orders and executed trades.
Build footprint chart cells from trades grouped by price/time buckets.
Generate heatmap visualization points from orderbook.
Calculate orderbook bid/ask imbalance at given depth.
Bang variant of imbalance/2. Returns the float directly or raises on error.
Calculate volume-weighted average price for a list of trades.
Types
@type cvd_delta() :: float()
Single-trade CVD contribution (positive = buy aggression, negative = sell)
@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
@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 visualization point at a price level
@type imbalance() :: float()
Orderbook imbalance normalized to [-1.0, 1.0]
Functions
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
12.5# 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
}
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
0.1095# 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
}
@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
%{
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
# 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
}
}
@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
[
%{
delta: 4.0,
price_bucket: 84000.0,
time_bucket: 1708862400000,
buy_volume: 12.0,
sell_volume: 8.0
}
]# 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
}
]
}
@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
[%{size: 5.0, price: 84000.0, side: :bid}]Errors
:invalid_orderbook_level
# 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}]
}
@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
{:ok, 0.25}Errors
:invalid_orderbook:invalid_orderbook_level
# 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}
}
@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
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
0.1095# 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
}