Packages
Pure-function trading analytics for Elixir — Black-Scholes pricing and IV, options chain analytics (max pain, GEX, skew, surfaces), funding rates, basis, volatility estimators, risk metrics, position sizing, orderflow, and portfolio analytics. Self-describing for AI agents.
Current section
Files
Jump to
Current section
Files
SKILLS.md
# ZenQuant Skills Guide
For agents and developers using ZenQuant as a library. Whether you're a Claude instance building a dashboard, a trading bot making decisions, or a human exploring the API — start here.
## The 10-Second Discovery
```elixir
ZenQuant.describe()
```
That's it. You'll get all discoverable modules with their purpose, namespace, function count, and whether they have full API annotations. Everything else flows from here.
## Progressive Discovery
Three levels of detail, each building on the last:
```elixir
# Level 1: What modules exist?
ZenQuant.describe()
# => [%{module: ZenQuant.Funding, short_name: :funding, function_count: ..., ...}, ...]
# Level 2: What functions does a module have?
ZenQuant.describe(:funding)
# => [%{name: :annualize, arity: 2, description: "Annualize a per-period...", spec: "..."}, ...]
# Level 3: Full detail for one function
ZenQuant.describe(:funding, :annualize)
# => %{name: :annualize, params: [%{name: :rate, kind: :value, ...}], returns: %{type: :float, ...}, returns_example: 0.1095, ...}
```
**Short names** are the last segment of the module name, lowercased: `ZenQuant.PowerLaw` → `:power_law`, `ZenQuant.Options.Deribit` → `:deribit`.
### Machine-Readable Access
For runtime agents that need structured data (not formatted docs):
```elixir
# Per-module hints (all functions)
ZenQuant.Funding.__api__()
# Per-function hints
ZenQuant.Funding.__api__(:annualize)
# Full manifest as map (all modules, all functions)
ZenQuant.Manifest.build()
```
Static export for non-BEAM consumers: `mix zen_quant.manifest` → `api_manifest.json`
---
## What Can ZenQuant Do?
Navigate by intent — what question are you trying to answer?
### "What's the market doing right now?"
| Module | Short Name | Key Functions |
|--------|------------|---------------|
| `ZenQuant.Execution` | `:execution` | `best_price/2` (cross-venue BBO), `from_multi/1` (Bourse.Multi bridge), `best_for_side/2` |
| `ZenQuant.Orderflow` | `:orderflow` | `cvd/1` (total CVD), `cvd_delta/1` (per-trade), `vwap/1`, `imbalance/2` (orderbook), `footprint_cells/3` |
| `ZenQuant.MM` | `:mm` | `fair_value/3` (micro-price from depth), `spread_calculator/2` (Avellaneda-Stoikov) |
### "What are options telling us?"
| Module | Short Name | Key Functions |
|--------|------------|---------------|
| `ZenQuant.Options` | `:options` | `max_pain/1`, `put_call_ratio/1`, `oi_by_strike/1`, `gex_by_strike/2`, `atm_iv/2`, `atm_iv_term_structure/3`, `moneyness_skew/3`, `pin_risk/3` |
| `ZenQuant.Options.Pricing` | `:pricing` | `price/2`, `greeks/2`, `implied_volatility/3` (Black-Scholes-Merton — this is where pricing lives) |
| `ZenQuant.Options.Surface` | `:surface` | `build/1` (sparse IV surface from a chain) |
| `ZenQuant.Options.Skew` | `:skew` | `term_structure/1` (from normalized skew observations) |
| `ZenQuant.Options.Probability` | `:probability` | `estimate/1` (terminal risk-neutral probability from a vertical) |
| `ZenQuant.Options.Snapshot` | `:snapshot` | `to_report/2` (structured briefing; sections fail independently) |
| `ZenQuant.Options.BlockTrades` | `:block_trades` | `summarize/2` (aggressor side preserved, never inferred from size) |
| `ZenQuant.Options.Deribit` | `:deribit` | `chain/3` (fetch + enrich), `gamma_walls/3`, `dvol/3`, `parse_option/1` |
| `ZenQuant.Options.GammaWalls` | `:gamma_walls` | `aggregate/3` (GEX walls with dealer/customer side) |
| `ZenQuant.Options.ZeroDTE` | `:zero_dte` | `near_strikes/3`, `theta_acceleration/3`, `gamma_exposure/3` |
| `ZenQuant.Greeks` | `:greeks` | `position_greeks/1`, `dollar_delta/2`, `delta_neutral?/2`, `hedge_ratio/2` (aggregation only — for pricing use `:pricing`) |
### "How risky is this?"
| Module | Short Name | Key Functions |
|--------|------------|---------------|
| `ZenQuant.Risk` | `:risk` | `sharpe_ratio/2`, `sortino_ratio/2`, `max_drawdown/1`, `var/2`, `portfolio_delta/1`, `check_limits/2` |
| `ZenQuant.Sizing` | `:sizing` | `fixed_fractional/3`, `kelly/2`, `volatility_scaled/4` |
| `ZenQuant.Portfolio` | `:portfolio` | `total_exposure/1`, `unrealized_pnl/1`, `realized_pnl/1` |
### "What's the yield?"
| Module | Short Name | Key Functions |
|--------|------------|---------------|
| `ZenQuant.Funding` | `:funding` | `annualize/2`, `trend/2`, `mean_reversion_signal/2`, `rank/2`, `compare/1` |
| `ZenQuant.Basis` | `:basis` | `spot_perp/2`, `annualized/3`, `futures_curve/2`, `implied_funding/3` |
### "What's BTC's valuation?"
| Module | Short Name | Key Functions |
|--------|------------|---------------|
| `ZenQuant.PowerLaw` | `:power_law` | `fair_value/1`, `z_score/2`, `fair_value_range/1`, `forecast/2`, `classify/2` |
### "Record and replay?"
| Module | Short Name | Key Functions |
|--------|------------|---------------|
| `ZenQuant.Recorder.JSONL` | `:jsonl` | `append/3`, `stream/1`, `read_all/1`, `info/1` |
| `ZenQuant.Recorder.Replay` | `:replay` | `stream/2`, `run/3`, `reduce/4` |
| `ZenQuant.WS` | `:ws` | `stale_check/2`, `reconnect_metrics/2` |
### Order lifecycle
| Module | Short Name | Key Functions |
|--------|------------|---------------|
| `ZenQuant.OrderState` | `:order_state` | `new/2`, `on_fill/3`, `cancel/2`, `summary/1` |
| `ZenQuant.Volatility` | `:volatility` | `realized/2`, `parkinson/1`, `garman_klass/1`, `yang_zhang/1`, `iv_percentile/2` |
---
## Module Short Name Reference
Discoverable modules, sorted alphabetically:
All 26 discoverable modules, sorted alphabetically. The function count is what
`ZenQuant.describe()` reports.
| Short Name | Module | Fns | Purpose |
|------------|--------|-----|---------|
| `:basis` | `ZenQuant.Basis` | 6 | Futures basis/premium, annualized carry |
| `:block_trades` | `ZenQuant.Options.BlockTrades` | 1 | Block-trade aggregation (side never inferred) |
| `:deribit` | `ZenQuant.Options.Deribit` | 11 | Deribit symbol parsing, chain fetch, DVOL |
| `:execution` | `ZenQuant.Execution` | 4 | Cross-venue BBO, arb detection, split plans |
| `:funding` | `ZenQuant.Funding` | 11 | Funding rates, APR/APY, trend, ranking |
| `:gamma_walls` | `ZenQuant.Options.GammaWalls` | 1 | GEX wall computation |
| `:greeks` | `ZenQuant.Greeks` | 9 | Portfolio greeks aggregation/exposure (no pricing — see `:pricing`) |
| `:jsonl` | `ZenQuant.Recorder.JSONL` | 7 | JSONL snapshot recording |
| `:mean_reversion` | `ZenQuant.MeanReversion` | 1 | Ornstein-Uhlenbeck half-life estimation |
| `:mm` | `ZenQuant.MM` | 5 | Micro-price, optimal spread, fill rate |
| `:options` | `ZenQuant.Options` | 21 | Options chain analytics |
| `:order_state` | `ZenQuant.OrderState` | 4 | Immutable order lifecycle |
| `:orderflow` | `ZenQuant.Orderflow` | 8 | CVD, VWAP, imbalance, footprint |
| `:portfolio` | `ZenQuant.Portfolio` | 4 | Portfolio exposure, PnL |
| `:power_law` | `ZenQuant.PowerLaw` | 9 | Bitcoin power law model |
| `:pricing` | `ZenQuant.Options.Pricing` | 3 | Black-Scholes-Merton price, greeks, implied volatility |
| `:probability` | `ZenQuant.Options.Probability` | 1 | Risk-neutral probability from verticals |
| `:replay` | `ZenQuant.Recorder.Replay` | 3 | Filtered replay with speed control |
| `:risk` | `ZenQuant.Risk` | 13 | Sharpe, Sortino, VaR, drawdown, stress test |
| `:sizing` | `ZenQuant.Sizing` | 6 | Position sizing strategies |
| `:skew` | `ZenQuant.Options.Skew` | 1 | Skew term structures |
| `:snapshot` | `ZenQuant.Options.Snapshot` | 1 | Structured options briefing |
| `:surface` | `ZenQuant.Options.Surface` | 1 | Sparse implied-volatility surfaces |
| `:volatility` | `ZenQuant.Volatility` | 11 | Realized, Parkinson, Garman-Klass, Yang-Zhang |
| `:ws` | `ZenQuant.WS` | 2 | WebSocket health monitoring |
| `:zero_dte` | `ZenQuant.Options.ZeroDTE` | 3 | Near-expiry (0DTE) analytics |
Not in `describe()` (public but not Descripex-annotated): `ZenQuant.Backtest`,
`ZenQuant.OrderBook`, `ZenQuant.Manifest`, and the three `ZenQuant.Helpers.*`
formatting modules.
| `:zero_dte` | `ZenQuant.Options.ZeroDTE` | 0DTE analytics |
Need live counts? Query runtime:
```elixir
modules = ZenQuant.describe()
module_count = length(modules)
function_count = Enum.reduce(modules, 0, &(&1.function_count + &2))
```
---
## Param Kinds
Every function parameter has a `kind` — this tells you where the data comes from:
- **`:value`** — You provide this directly (a number, a date, a config option)
- **`:exchange_data`** — Must be fetched from an exchange first
Exchange data params include a `source:` field telling you what to fetch:
```elixir
ZenQuant.describe(:orderflow, :cvd_delta)
# => params: [%{name: :trades, kind: :exchange_data, source: "fetch_trades(symbol)", ...}]
ZenQuant.describe(:funding, :annualize)
# => params: [%{name: :rate, kind: :value, ...}, %{name: :period_hours, kind: :value, default: 8, ...}]
```
---
## Common Composition Chains
### Live Market Overview
Tickers from multiple exchanges → best price + funding + orderflow:
```elixir
# 1. Build exchange handles, then fetch tickers from all of them
{:ok, bybit} = Bourse.exchange(:bybit)
{:ok, okx} = Bourse.exchange(:okx)
multi = Bourse.Multi.fetch_tickers([bybit, okx], "BTC/USDT:USDT")
# 2. Cross-venue price comparison
venues = ZenQuant.Execution.from_multi(Bourse.Multi.successes(multi))
bbo = ZenQuant.Execution.best_price(venues)
# => %{best_bid: %{price: 67801.0, exchange: "bybit"}, spread_bps: 0.74, ...}
# 3. Funding analysis
rates = [%{funding_rate: 0.0001}, %{funding_rate: 0.00015}]
ZenQuant.Funding.annualize(0.0001) # => 10.95% APR
ZenQuant.Funding.trend(rates) # => :rising | :falling | :stable
# 4. Orderflow from recent trades
{:ok, trades} = Bourse.fetch_trades(bybit, "BTC/USDT:USDT", limit: 100)
ZenQuant.Orderflow.vwap(trades) # => volume-weighted average price
```
### Options Analysis Pipeline
Chain → OI distribution → max pain → gamma walls:
```elixir
# 1. Build the exchange handle, then fetch an enriched chain.
# bourse: dispatcher module + %Bourse.Exchange{} handle, not a
# per-exchange module. `chain/3` returns %{chain: ..., spot: ..., ...}.
{:ok, ex} = Bourse.exchange(:deribit)
{:ok, %{chain: chain, spot: spot}} =
ZenQuant.Options.Deribit.chain(Bourse, ex, currency: "BTC", enrich: :greeks)
# 2. Open interest distribution
oi = ZenQuant.Options.oi_by_strike(chain)
{:ok, pain} = ZenQuant.Options.max_pain(chain)
# 3. Gamma walls (pure — from enriched chain)
walls = ZenQuant.Options.GammaWalls.aggregate(chain, spot)
# 4. Or use the composition shortcut
{:ok, result} = ZenQuant.Options.Deribit.gamma_walls(Bourse, ex, currency: "BTC")
# => %{walls: [...], spot: 67800.0, chain_size: 880, enriched_count: 50}
```
### Pricing an Option Yourself
`Options.Deribit` reads greeks *from* the exchange. When you need to compute
them — or back out implied volatility from a mark price — use
`ZenQuant.Options.Pricing`, which is fully pure and needs no exchange at all:
```elixir
inputs = %{
spot: 67_800.0, strike: 70_000.0,
time_to_expiry_years: 21 / 365,
risk_free_rate: 0.05, dividend_yield: 0.0,
volatility: 0.55
}
{:ok, price} = ZenQuant.Options.Pricing.price(:call, inputs)
{:ok, greeks} = ZenQuant.Options.Pricing.greeks(:call, inputs)
# Back out IV from an observed mark price (drop :volatility from the inputs)
{:ok, %{volatility: iv, converged: true}} =
ZenQuant.Options.Pricing.implied_volatility(:call, 1_240.0, Map.delete(inputs, :volatility))
```
Time is a year fraction you compute — the module imposes no calendar or
trading-day count.
### Orderflow from Trades
Fetch → CVD + VWAP + footprint:
```elixir
# Fetch trades (Bourse normalized — works directly)
{:ok, ex} = Bourse.exchange(:bybit)
{:ok, trades} = Bourse.fetch_trades(ex, "BTC/USDT:USDT", limit: 100)
# Total CVD for the list (single float)
total_cvd = ZenQuant.Orderflow.cvd(trades)
# Per-trade CVD contribution (single trade → single float)
delta = ZenQuant.Orderflow.cvd_delta(hd(trades))
# Aggregate metrics
vwap = ZenQuant.Orderflow.vwap(trades)
# Footprint: bucketed buy/sell volume (positional args: price_step, time_window_ms)
cells = ZenQuant.Orderflow.footprint_cells(trades, 100, 60_000)
# => [%{price_bucket: 67800.0, buy_volume: ..., sell_volume: ..., delta: ...}]
```
---
## Gotchas
Things that trip up first-time users:
1. **`cvd/1` vs `cvd_delta/1`** — `cvd/1` takes a trade list and returns total CVD as a single float. `cvd_delta/1` takes ONE trade and returns ONE float (its contribution). To sum a list, use `cvd/1`.
2. **`dom_level/2` accepts orderbook directly** — Pass either a wrapped map `%{orderbook: ob, trades: trades}` or an orderbook directly `%{bids: [...], asks: [...]}`. Both work. The bids/asks entries must be `[price, size]` tuples — which is exactly what a Bourse normalized OrderBook provides.
3. **IV is in percentage format** — Deribit returns `mark_iv: 48.6` (not `0.486`). All ZenQuant IV functions expect and return percentages. `expected_range/3` divides by 100 internally.
4. **`fetch_trades` takes keyword options** — in bourse, `fetch_trades(exchange, symbol, opts)`; `since` and `limit` are keywords, e.g. `Bourse.fetch_trades(ex, "BTC/USDT", limit: 100)`. Only the symbol is positional.
5. **Networked functions take a dispatcher module AND an exchange handle** — `dvol/3`, `chain/3`, and `gamma_walls/3` are `(dispatcher, exchange, opts)`: the top-level dispatcher module (`Bourse`) followed by a built `%Bourse.Exchange{}` handle from `Bourse.exchange(:deribit)`. Per-exchange modules like `Bourse.Deribit` do not exist — passing one raises. These are the only functions in the whole library that touch the network; everything else is pure.
6. **`Greeks` aggregates, `Options.Pricing` computes** — `ZenQuant.Greeks` takes greeks supplied by the exchange and sums/scales them. It contains no Black-Scholes. If you need a price, analytic greeks, or implied volatility, that's `ZenQuant.Options.Pricing`.
7. **All functions accept plain maps** — ZenQuant has zero struct dependencies. Bourse normalized structs work because they're maps underneath. Raw exchange data works too (dual atom/string key access).
8. **Deribit `fetch_ohlcv` returns very few candles** — With `"1d"` timeframe, Deribit returns only 1 candle. Use Bybit or Binance for historical OHLCV data.
9. **`Bourse.Multi.fetch_tickers` takes a single symbol string** — Each exchange must support that exact symbol format. For cross-exchange with different conventions (e.g. Deribit `"BTC-PERPETUAL"` vs Bybit `"BTC/USDT:USDT"`), use the map form keyed by exchange handles: `Bourse.Multi.fetch_tickers(%{bybit => "BTC/USDT:USDT", deribit => "BTC-PERPETUAL"})`.
10. **Call `Bourse.Multi.successes/1` before `ZenQuant.Execution.from_multi/1`** — `Bourse.Multi` returns `%{%Bourse.Exchange{} => {:ok, ticker} | {:error, reason}}`. Unwrap and filter errors first: `multi |> Bourse.Multi.successes() |> ZenQuant.Execution.from_multi()`.
---
## The bourse Connection
ZenQuant is the computation layer. bourse provides the data. Here's what feeds what:
| bourse Function | ZenQuant Domain | Example |
|-----------------|----------------|---------|
| `fetch_tickers` / `Multi.fetch_tickers` | Execution, MM | `from_multi → best_price` |
| `fetch_trades` | Orderflow | `cvd_delta`, `vwap`, `footprint_cells` |
| `fetch_order_book` | Orderflow, MM | `imbalance`, `heatmap_points`, `fair_value` |
| `fetch_funding_rates` | Funding | `annualize`, `trend`, `rank` |
| `fetch_option_chain` | Options | `oi_by_strike`, `max_pain`, `gex_by_strike` |
| `fetch_greeks` | Options, Greeks | `chain/2` enrichment, `position_greeks` |
| `fetch_volatility_history` | Options.Deribit | `dvol/2` |
| `fetch_ticker` (spot + futures) | Basis | `spot_perp`, `annualized` |
| Historical closes | Volatility | `realized`, `parkinson`, `yang_zhang` |
| Position data | Risk, Portfolio, Sizing | `portfolio_delta`, `total_exposure`, `kelly` |
---
## Static Manifest
For non-BEAM consumers (Python, TypeScript, Rust, HTTP agents):
```bash
mix zen_quant.manifest
# => Writes api_manifest.json
```
The manifest includes every module, function, param (with kinds and descriptions), return type, `returns_example`, composition hints (`composes_with`), and error cases. It's the same data as `ZenQuant.Manifest.build()` serialized to JSON.
Use this for:
- HTTP API endpoint generation
- Agent capability registration (EIP-8004)
- SDK generation for other languages
- CI validation that the API contract hasn't changed unexpectedly