Cross-venue execution primitives — best price discovery, arbitrage detection, and deterministic non-executing order split planning.
Pure functions for comparing prices and building allocation plans across multiple exchanges using pre-fetched ticker/liquidity data. No exchange calls, no order placement, no state.
Terminology
- BBO - Best Bid and Offer across venues
- Spread - Difference between best ask and best bid
- Arb - Arbitrage opportunity when best bid > best ask across venues
declared comparable by the caller (a shared
:group). A cross between venues quoting/settling different assets (no shared:group) is reported asarb_status: :incomparable, not a riskless edge — ZenQuant holds no venue metadata and never infers comparability from names or symbols. - Split plan - Deterministic child allocations across venue liquidity snapshots (plan only — never places, times, or cancels orders)
Example
venues = [
%{exchange: "binance", bid: 67800.0, ask: 67801.5},
%{exchange: "bybit", bid: 67801.0, ask: 67800.5}
]
ZenQuant.Execution.best_price(venues)
# => %{best_bid: %{price: 67801.0, exchange: "bybit"}, ...}Bourse.Multi Integration
# bourse 0.6.x: build `%Bourse.Exchange{}` handles, then fan out.
exchanges = for id <- [:binance, :bybit], {:ok, ex} <- [Bourse.exchange(id)], do: ex
multi_result = Bourse.Multi.fetch_tickers(exchanges, "BTC/USDT:USDT")
venues = ZenQuant.Execution.from_multi(Bourse.Multi.successes(multi_result))
result = ZenQuant.Execution.best_price(venues)API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
split_order | 4 | Build a deterministic non-executing split plan across venue liquidity snapshots. | side: value, target: value, venues: exchange_data, opts: value |
best_for_side | 2 | Extract the best venue for a given side from a best_price result. | result: value, side: value |
best_price | 2 | Find best bid/ask across venues from pre-fetched ticker data. | venues: exchange_data, opts: value |
from_multi | 1 | Convert Bourse.Multi success map to venue list for best_price/2. | successes: exchange_data |
Summary
Types
Comparability of the crossing venues behind an arb signal.
Best price analysis result
Single child allocation within a split plan
split_order/4 validation error
Deterministic non-executing split plan
Unallocated remainder after planning
Target for split_order/4 — quantity (base) or notional (quote), exclusive
Functions
Extract the best venue for a given side from a best_price result.
Find best bid/ask across venues from pre-fetched ticker data.
Convert Bourse.Multi success map to venue list for best_price/2.
Build a deterministic, non-executing split plan across venue liquidity snapshots.
Types
@type arb_status() :: :none | :comparable | :incomparable
Comparability of the crossing venues behind an arb signal.
:none— best bid does not cross best ask; no arbitrage math applies:comparable— bid crosses ask and the two venues are declared comparable (same venue snapshot or a shared caller-supplied:group); a real cross:incomparable— bid crosses ask but the venues quote/settle different assets (no shared:group); the number is USDT-vs-USD basis or index composition, not a riskless edge
@type best_price_result() :: %{ best_bid: %{price: float(), exchange: atom() | String.t()}, best_ask: %{price: float(), exchange: atom() | String.t()}, spread: float(), spread_bps: float(), mid: float(), arb_opportunity: boolean(), arb_status: arb_status(), venue_count: non_neg_integer(), venues: [map()] }
Best price analysis result
@type split_allocation() :: %{ exchange: atom() | String.t(), price: float(), quantity: float(), notional: float(), fee: float(), rationale: map() }
Single child allocation within a split plan
@type split_error() :: :invalid_side | :invalid_target | :invalid_venues | :invalid_limit | :invalid_opts | {:invalid_orderbook_level, term()}
split_order/4 validation error
@type split_plan() :: %{ side: :buy | :sell, target: split_target(), allocations: [split_allocation()], allocated_quantity: float(), allocated_notional: float(), unallocated: split_remainder(), expected_gross_cost: float(), expected_net_cost: float(), expected_fees: float(), average_price: float() | nil, status: :complete | :partial | :unfillable, assumptions: [String.t()] }
Deterministic non-executing split plan
Unallocated remainder after planning
Target for split_order/4 — quantity (base) or notional (quote), exclusive
Functions
@spec best_for_side(best_price_result(), :buy | :sell) :: %{ price: float(), exchange: atom() | String.t() }
Extract the best venue for a given side from a best_price result.
Parameters
result- Result map from best_price/2 (value)side- :buy (best ask) or :sell (best bid) (value)
Returns
Map with :price and :exchange for the requested side (map)
Example
%{exchange: "binance", price: 83512.0}# descripex:contract
%{
params: %{
result: %{description: "Result map from best_price/2", kind: :value},
side: %{description: ":buy (best ask) or :sell (best bid)", kind: :value}
},
returns: %{
type: :map,
description: "Map with :price and :exchange for the requested side"
},
returns_example: %{exchange: "binance", price: 83512.0}
}
@spec best_price( [map()], keyword() ) :: best_price_result() | nil
Find best bid/ask across venues from pre-fetched ticker data.
Parameters
venues- List of maps with :exchange, :bid, and :ask fields. Optional :group is a caller-declared comparability key (any term): venues sharing a :group are declared comparable for arbitrage. A venue omitting :group is comparable only with itself — never inferred from the exchange name or symbol. (exchange_data)opts- Options: min_venues (minimum valid venues, default 1) (default:[], value)
Returns
Map with :best_bid, :best_ask, :spread, :spread_bps, :mid, :arb_opportunity, :arb_status (:none | :comparable | :incomparable), :venue_count, :venues — or nil if insufficient valid venues. arb_opportunity is true only when the crossing venues are declared comparable; a cross between incomparable venues sets arb_status :incomparable with arb_opportunity false. (map)
Example
%{
mid: 83511.0,
venues: [%{exchange: "bybit", ask: 83512.0, bid: 83510.0}],
best_bid: %{exchange: "bybit", price: 83510.0},
best_ask: %{exchange: "binance", price: 83512.0},
spread: 2.0,
venue_count: 2,
arb_status: :none,
spread_bps: 0.24,
arb_opportunity: false
}Composes With
best_for_side
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: min_venues (minimum valid venues, default 1)",
kind: :value
},
venues: %{
description: "List of maps with :exchange, :bid, and :ask fields. Optional :group is a caller-declared comparability key (any term): venues sharing a :group are declared comparable for arbitrage. A venue omitting :group is comparable only with itself — never inferred from the exchange name or symbol.",
source: "multiple fetch_ticker calls or from_multi/1",
kind: :exchange_data
}
},
returns: %{
type: :map,
description: "Map with :best_bid, :best_ask, :spread, :spread_bps, :mid, :arb_opportunity, :arb_status (:none | :comparable | :incomparable), :venue_count, :venues — or nil if insufficient valid venues. arb_opportunity is true only when the crossing venues are declared comparable; a cross between incomparable venues sets arb_status :incomparable with arb_opportunity false."
},
returns_example: %{
mid: 83511.0,
venues: [%{exchange: "bybit", ask: 83512.0, bid: 83510.0}],
best_bid: %{exchange: "bybit", price: 83510.0},
best_ask: %{exchange: "binance", price: 83512.0},
spread: 2.0,
venue_count: 2,
arb_status: :none,
spread_bps: 0.24,
arb_opportunity: false
},
composes_with: [:best_for_side]
}
Convert Bourse.Multi success map to venue list for best_price/2.
Parameters
successes- Map of %{%Bourse.Exchange{} => %Bourse.Ticker{}} from Bourse.Multi.successes/1. A ticker value carrying a caller-supplied :group key has it carried through to the venue so best_price/2 can judge cross-venue comparability. (exchange_data)
Returns
List of maps with :exchange (string), :bid, :ask, and optional :group fields (list)
Example
[1.0, 0.9]Composes With
best_price
# descripex:contract
%{
params: %{
successes: %{
description: "Map of %{%Bourse.Exchange{} => %Bourse.Ticker{}} from Bourse.Multi.successes/1. A ticker value carrying a caller-supplied :group key has it carried through to the venue so best_price/2 can judge cross-venue comparability.",
source: "Bourse.Multi.successes(Bourse.Multi.fetch_tickers(...))",
kind: :exchange_data
}
},
returns: %{
type: :list,
description: "List of maps with :exchange (string), :bid, :ask, and optional :group fields"
},
returns_example: [1.0, 0.9],
composes_with: [:best_price]
}
@spec split_order(:buy | :sell | String.t(), map(), [map()], keyword()) :: {:ok, split_plan()} | {:error, split_error()}
Build a deterministic, non-executing split plan across venue liquidity snapshots.
Walks available liquidity in fee-adjusted price-priority order (lowest net unit cost for buys, highest net unit proceeds for sells). Ties break by exchange name ascending, then input index. Each child respects liquidity, optional price limit, lot size (floor), minimum amount/notional, fee rate, and venue quantity/notional caps.
Returns a plan only — never places, times, cancels, or tracks live orders.