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

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
| 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` |

# `arb_status`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

split_order/4 validation error

# `split_plan`

```elixir
@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`

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

Unallocated remainder after planning

# `split_target`

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

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

# `best_for_side`

```elixir
@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

```elixir
%{exchange: "binance", price: 83512.0}
```

```elixir
# 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`

```elixir
@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

```elixir
%{
  best_bid: %{exchange: "bybit", price: 83510.0},
  best_ask: %{exchange: "binance", price: 83512.0},
  spread: 2.0,
  venues: [%{exchange: "bybit", ask: 83512.0, bid: 83510.0}],
  venue_count: 2,
  mid: 83511.0,
  arb_status: :none,
  spread_bps: 0.24,
  arb_opportunity: false
}
```

## Composes With

  * `best_for_side`

```elixir
# 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: %{
    best_bid: %{exchange: "bybit", price: 83510.0},
    best_ask: %{exchange: "binance", price: 83512.0},
    spread: 2.0,
    venues: [%{exchange: "bybit", ask: 83512.0, bid: 83510.0}],
    venue_count: 2,
    mid: 83511.0,
    arb_status: :none,
    spread_bps: 0.24,
    arb_opportunity: false
  },
  composes_with: [:best_for_side]
}
```

# `from_multi`

```elixir
@spec from_multi(%{required(map() | module()) =&gt; 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

```elixir
[1.0, 0.9]
```

## Composes With

  * `best_price`

```elixir
# 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`

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

---

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