ZenQuant
Trading analytics for Elixir, as pure functions.
Black-Scholes pricing and implied volatility, options chain analytics (max pain, GEX walls, skew term structures, IV surfaces), funding rates, cash-and-carry basis, volatility estimators, risk metrics, position sizing, orderflow, and portfolio aggregation.
Every function takes plain maps and numbers and returns plain maps and tuples. No processes, no supervision tree, no I/O, no exchange client. You fetch the data however you like; ZenQuant does the math.
The only runtime dependency is Descripex, which powers the self-describing API surface.
Installation
def deps do
[
{:zen_quant, "~> 0.2.0"}
]
end
Requires Elixir ~> 1.18.
Quick Start
Every output below is a real return value, not a sketch.
Options pricing and greeks
inputs = %{
spot: 100.0,
strike: 100.0,
time_to_expiry_years: 0.25,
risk_free_rate: 0.05,
dividend_yield: 0.0,
volatility: 0.6
}
ZenQuant.Options.Pricing.price(:call, inputs)
# => {:ok, 12.480797612835424}
ZenQuant.Options.Pricing.greeks(:call, inputs)
# => {:ok, %{delta: 0.5759983410021886, gamma: 0.0130560458310534,
# vega: 19.5840687465801, theta: -25.756834320265295, rho: 11.27975912184586}}
# Solve for the volatility that reproduces a market price
ZenQuant.Options.Pricing.implied_volatility(:call, 12.5, Map.delete(inputs, :volatility))
# => {:ok, %{volatility: 0.6009805272333324, iterations: 31,
# residual: -4.5e-10, converged: true}}
Time is supplied directly in years — the module applies no calendar or trading-day count, so you choose the convention.
Options chain analytics
chain = %{
"BTC-31JAN26-84000-C" => %{open_interest: 100.0, raw: %{"gamma" => 0.00001}}
}
ZenQuant.Options.oi_by_strike(chain) # => %{84000.0 => 100.0}
ZenQuant.Options.max_pain(chain) # => {:ok, 84000.0}
Plus gex_by_strike/2, pin_magnets/2, gamma_flip/2, put_call_ratio/1,
atm_iv/2, atm_iv_term_structure/3, moneyness_skew/3, expected_range/3,
and ZenQuant.Options.Surface.build/1 for sparse IV surfaces.
Funding and basis
# Per-period funding rate -> annualized APR. The period is a caller argument,
# so venues with non-8h cadence are a value, not a hardcoded constant.
ZenQuant.Funding.annualize(0.0001) # => 0.1095 (10.95% APR at 8h funding)
ZenQuant.Funding.annualize(0.0001, 1) # => 0.876 (hourly funding)
ZenQuant.Basis.spot_perp(100_000.0, 100_250.0)
# => %{absolute: 250.0, percent: 0.25, direction: :contango}
ZenQuant.Basis.annualized(100_000.0, 100_250.0, 30)
# => 0.030416666666666665
Volatility
ZenQuant.Volatility.realized([100.0, 101.5, 99.8, 102.0, 100.5])
# => 0.019963912649687825
ZenQuant.MeanReversion.half_life([100.0, 101.0, 99.5, 100.2, 100.8, 99.9, 100.1])
# => {:ok, 0.448506999188226}
Also parkinson/1, garman_klass/1, yang_zhang/1, cone/2, iv_rank/2,
iv_percentile/2, iv_vs_rv/2.
Portfolio greeks, risk, and sizing
positions = [%{delta: 0.5, gamma: 0.02, theta: -10.0, vega: 25.0, quantity: 10}]
ZenQuant.Greeks.position_greeks(positions)
# => %{delta: 5.0, gamma: 0.2, theta: -100.0, vega: 250.0}
ZenQuant.Risk.sharpe_ratio([0.01, -0.005, 0.02, 0.015, -0.01])
# => 0.4636004455717535
ZenQuant.Risk.max_position_size(100_000, risk_pct: 0.02, stop_distance_pct: 0.05)
# => 10000.0
ZenQuant.Sizing.kelly(0.6, 2.0) # => 0.19999999999999998
Orderflow
ZenQuant.Orderflow.cvd([%{side: "buy", amount: 1.5}, %{side: "sell", amount: 0.5}])
# => 1.0
Also cvd_delta/1 (per-trade series), vwap/1, imbalance/2,
heatmap_points/2, footprint_cells/3, dom_level/2.
API Discovery
ZenQuant describes itself. You do not need to read the docs to find a function:
ZenQuant.describe() # 26 modules with purpose + function count
ZenQuant.describe(:funding) # every function in ZenQuant.Funding
ZenQuant.describe(:funding, :annualize) # params, returns, returns_example, errors, composes_with
Short names are the last module segment, lowercased: ZenQuant.PowerLaw →
:power_law, ZenQuant.Options.Deribit → :deribit.
For runtime agents that want structured data rather than formatted docs:
ZenQuant.Funding.__api__() # all hints for a module
ZenQuant.Funding.__api__(:annualize) # hints for one function
ZenQuant.Manifest.build() # full manifest as a map
Static export for non-BEAM consumers: mix zen_quant.manifest →
api_manifest.json.
SKILLS.md is the full agent guide — composition chains, param kinds, and gotchas.
Modules
Options — pricing & greeks
| Module | Purpose |
|---|---|
ZenQuant.Options.Pricing | Black-Scholes-Merton price, analytic greeks, implied volatility solver |
ZenQuant.Greeks | Portfolio greeks aggregation and exposure — aggregates greeks from exchange data, does not compute them |
ZenQuant.Options.Probability | Terminal risk-neutral probabilities from vertical spreads |
Options — chain analytics
| Module | Purpose |
|---|---|
ZenQuant.Options | Max pain, GEX by strike, pin magnets, gamma flip, OI, ATM IV, expected range |
ZenQuant.Options.GammaWalls | GEX wall computation with explicit dealer/customer side convention |
ZenQuant.Options.Skew | Deterministic term structures from normalized skew observations |
ZenQuant.Options.Surface | Sparse implied-volatility surfaces from a pre-fetched chain |
ZenQuant.Options.ZeroDTE | Near-expiry analytics: near strikes, theta acceleration, gamma exposure |
ZenQuant.Options.BlockTrades | Block-trade aggregation — aggressor side preserved, never inferred from size |
ZenQuant.Options.Snapshot | Structured options briefing over a chain; sections fail independently |
ZenQuant.Options.Deribit | Deribit symbol parsing, chain fetch + enrich, DVOL, gamma walls |
Rates & carry
| Module | Purpose |
|---|---|
ZenQuant.Funding | Funding rates: annualized APR, trend, spikes, ranking, carry candidates |
ZenQuant.Basis | Spot/perp basis, annualized carry, futures curve, implied funding |
Volatility & statistics
| Module | Purpose |
|---|---|
ZenQuant.Volatility | Realized, Parkinson, Garman-Klass, Yang-Zhang, cones, IV rank/percentile |
ZenQuant.MeanReversion | Ornstein-Uhlenbeck half-life estimation |
ZenQuant.PowerLaw | Bitcoin power law: fair value, z-score, support/resistance bands, forecast |
Risk & sizing
| Module | Purpose |
|---|---|
ZenQuant.Risk | Sharpe, Sortino, Calmar, VaR, max drawdown, beta, concentration, stress test, liquidation headroom |
ZenQuant.Sizing | Kelly, fixed fractional, optimal f, anti-martingale, volatility-scaled |
ZenQuant.Portfolio | Exposure aggregation, realized/unrealized PnL, position summaries |
Market microstructure
| Module | Purpose |
|---|---|
ZenQuant.Orderflow | CVD, VWAP, imbalance, heatmap, footprint, DOM level |
ZenQuant.OrderBook | Shared order-book level contract — strict parsing, explicit errors on unknown shapes |
ZenQuant.MM | Market making: fair value, inventory skew, spread calculator, fill rate |
ZenQuant.Execution | Cross-venue best price, arbitrage detection, non-executing order split plans |
ZenQuant.OrderState | Immutable order lifecycle: fills, VWAP accumulation, cancellation |
ZenQuant.WS | WebSocket health: stale check, reconnect metrics |
Recording & backtesting
| Module | Purpose |
|---|---|
ZenQuant.Recorder.JSONL | JSONL snapshot recording — encode, decode, append, stream, read_all |
ZenQuant.Recorder.Replay | Filtered replay with optional speed delays |
ZenQuant.Backtest | Deterministic strategy evaluation over recorded data |
Display helpers
ZenQuant.Helpers.Funding, ZenQuant.Helpers.Greeks, ZenQuant.Helpers.Risk —
formatting for dashboards. Core modules never format for display.
Design Principles
- Pure functions only — no GenServers, no state, no side effects, no started application. Same inputs, same outputs, always.
- Plain maps in, plain maps out — no struct dependencies. Exchange payloads work as-is because Bourse structs are maps.
- Structured returns, never strings — consistent
{:ok, result}/{:error, reason}contracts. Formatting lives inHelpers.*. - No venue constants — anything that varies by exchange, fee tier, or market regime is a caller-supplied parameter, not a baked-in value. Funding cadence is an argument, not an assumption.
- Self-describing — every function carries machine-readable hints via Descripex, so agents discover the API at runtime.
- Determinism as a feature —
ZenQuant.Backtestover recorded JSONL gives same data + same strategy = same result, which is what trustless re-execution (EIP-8004) needs.
For AI Agents
ZenQuant is built agents-first. Start with ZenQuant.describe(), then narrow
with describe/1 and describe/2. All returns are structured data that composes
directly into the next call. See SKILLS.md for composition chains,
the :value vs :exchange_data param distinction, and known gotchas.
License
MIT — see LICENSE.