ZenQuant.Execution (zen_quant v0.2.0)

Copy Markdown View Source

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 as arb_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

FunctionArityDescriptionParam Kinds
split_order4Build a deterministic non-executing split plan across venue liquidity snapshots.side: value, target: value, venues: exchange_data, opts: value
best_for_side2Extract the best venue for a given side from a best_price result.result: value, side: value
best_price2Find best bid/ask across venues from pre-fetched ticker data.venues: exchange_data, opts: value
from_multi1Convert 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

arb_status()

@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

best_price_result()

@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

split_allocation()

@type split_allocation() :: %{
  exchange: atom() | String.t(),
  price: float(),
  quantity: float(),
  notional: float(),
  fee: float(),
  rationale: map()
}

Single child allocation within a split plan

split_error()

@type split_error() ::
  :invalid_side
  | :invalid_target
  | :invalid_venues
  | :invalid_limit
  | :invalid_opts
  | {:invalid_orderbook_level, term()}

split_order/4 validation error

split_plan()

@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

split_remainder()

@type split_remainder() :: %{quantity: float()} | %{notional: float()}

Unallocated remainder after planning

split_target()

@type split_target() :: %{quantity: number()} | %{notional: number()}

Target for split_order/4 — quantity (base) or notional (quote), exclusive

Functions

best_for_side(map, atom)

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

best_price(venues, opts \\ [])

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

from_multi(successes)

@spec from_multi(%{required(map() | module()) => map()}) :: [map()]

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

split_order(side, target, venues, opts \\ [])

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