ZenQuant.Orderflow (zen_quant v0.2.0)

Copy Markdown View Source

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

FunctionArityDescriptionParam Kinds
dom_level2Calculate depth-of-market level combining resting orders and executed trades.price: value, data: exchange_data
footprint_cells3Build footprint chart cells from trades grouped by price/time buckets.trades: exchange_data, price_step: value, time_window_ms: value
heatmap_points2Generate heatmap visualization points from orderbook.orderbook: exchange_data, depth: value
imbalance!2Calculate orderbook bid/ask imbalance (raises on error).orderbook: exchange_data, depth: value
imbalance2Calculate orderbook bid/ask imbalance at given depth.orderbook: exchange_data, depth: value
vwap1Calculate volume-weighted average price for a list of trades.trades: exchange_data
cvd_delta1Calculate CVD delta for a single trade.trade: exchange_data
cvd1Calculate 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

cvd_delta()

@type cvd_delta() :: float()

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

dom_level()

@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()

@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()

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

Heatmap visualization point at a price level

imbalance()

@type imbalance() :: float()

Orderbook imbalance normalized to [-1.0, 1.0]

Functions

cvd(trades)

@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

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
}

cvd_delta(trade)

@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

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
}

dom_level(price, data)

@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
  }
}

footprint_cells(trades, price_step, time_window_ms)

@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
    }
  ]
}

heatmap_points(orderbook, depth)

@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}]
}

imbalance(orderbook, depth)

@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}
}

imbalance!(orderbook, depth)

@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(trades)

@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

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
}