Portfolio-level aggregation helpers for positions.
Pure functions for calculating total exposure, unrealized PnL, and multi-venue position exposure summaries. Works with plain maps containing position data — no account fetch or retained state.
Example
positions = [
%{symbol: "BTC/USDT:USDT", notional: 50_000.0, unrealized_pnl: 1000.0},
%{symbol: "BTC/USDT:USDT", notional: 25_000.0, unrealized_pnl: -500.0},
%{symbol: "ETH/USDT:USDT", notional: 10_000.0, unrealized_pnl: 200.0}
]
ZenQuant.Portfolio.total_exposure(positions)
# => %{"BTC" => 75_000.0, "ETH" => 10_000.0}
ZenQuant.Portfolio.unrealized_pnl(positions)
# => 700.0API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
position_exposure_summary | 2 | Aggregate normalized multi-venue positions into portfolio, per-venue, and per-asset exposure. | positions: value |
realized_pnl | 1 | Calculate total realized PnL across all positions. | positions: exchange_data |
unrealized_pnl | 1 | Calculate total unrealized PnL across all positions. | positions: exchange_data |
total_exposure | 1 | Calculate total notional exposure grouped by base asset. | positions: exchange_data |
Summary
Types
Per-asset slice including contract-type labels present in the group
Concentration metrics (max weight, HHI, top-3) over asset gross notionals
Contract type label for linear vs inverse products
Structured error reasons from position_exposure_summary/2
Normalized position for multi-venue exposure aggregation.
Gross/net exposure slice with quantity, notional, P&L, and margin
Full multi-venue position exposure summary
Leverage inputs derived from portfolio equity when supplied
Venue identity (exchange name or account label)
Functions
Aggregate normalized, pre-fetched positions into one exposure summary.
Calculate total realized PnL across all positions.
Calculate total notional exposure grouped by base asset.
Calculate total unrealized PnL across all positions.
Types
@type asset_slice() :: %{ gross_quantity: float(), net_quantity: float(), gross_notional: float(), net_notional: float(), unrealized_pnl: float(), margin_used: float(), contract_types: [contract_type()], quantity_unit: String.t() }
Per-asset slice including contract-type labels present in the group
Concentration metrics (max weight, HHI, top-3) over asset gross notionals
@type contract_type() :: :linear | :inverse
Contract type label for linear vs inverse products
@type exposure_error() :: {:missing_field, atom()} | :missing_mark | :missing_notional | :invalid_mark | :invalid_quantity | :invalid_quantity_unit | :invalid_notional | :invalid_contract_type | :invalid_unrealized_pnl | :invalid_margin | :missing_reporting_currency | {:missing_fx_rate, String.t()} | {:incompatible_units, String.t(), [String.t()]} | :invalid_equity | :invalid_fx_rate
Structured error reasons from position_exposure_summary/2
@type exposure_position() :: %{ :venue => venue(), :asset => String.t(), :quantity => number(), :settlement_currency => String.t(), optional(:mark) => number(), optional(:notional) => number(), optional(:contract_type) => contract_type(), optional(:unrealized_pnl) => number(), optional(:margin) => number(), optional(:quantity_unit) => String.t(), optional(:symbol) => String.t() }
Normalized position for multi-venue exposure aggregation.
Required fields
:venue— venue identity (atom or string); kept in venue breakdowns:asset— base asset id (string), e.g."BTC"; aggregation key for per-asset nets:quantity— signed quantity in base-asset units (positive = long, negative = short):settlement_currency— currency of notional / unrealized P&L / margin for this row
Valuation
:mark— positive mark price insettlement_currencyperquantity_unit. A linear row may omit:notional, in which case notional isabs(quantity) * mark.:notional— absolute notional magnitude insettlement_currency. Required for inverse rows because normalized base-asset quantity does not retain the contract face value or multiplier needed to derive inverse settlement notional.
Optional fields
:contract_type—:linear(default) or:inverselabel:unrealized_pnl— unrealized P&L in settlement currency (default0.0):margin— margin used in settlement currency (default0.0):quantity_unit— unit label for quantity; defaults toasset. Positions for the same asset must share one unit or aggregation returns{:incompatible_units, asset, units}:symbol— optional display symbol; not used for aggregation keys
@type exposure_slice() :: %{ gross_quantity: float(), net_quantity: float(), gross_notional: float(), net_notional: float(), unrealized_pnl: float(), margin_used: float() }
Gross/net exposure slice with quantity, notional, P&L, and margin
@type exposure_summary() :: %{ reporting_currency: String.t() | nil, portfolio: exposure_slice() | %{ gross_notional: float(), net_notional: float(), unrealized_pnl: float(), margin_used: float() }, by_venue: %{required(String.t()) => map()}, by_asset: %{required(String.t()) => asset_slice()}, leverage: leverage_inputs(), concentration: concentration_metrics() | nil }
Full multi-venue position exposure summary
@type leverage_inputs() :: %{ equity: float() | nil, gross_leverage: float() | nil, net_leverage: float() | nil }
Leverage inputs derived from portfolio equity when supplied
Venue identity (exchange name or account label)
Functions
@spec position_exposure_summary( [map()], keyword() ) :: {:ok, exposure_summary()} | {:error, exposure_error()}
Aggregate normalized, pre-fetched positions into one exposure summary.
Does not fetch or retain account state. Opposing positions offset net exposure (quantity and signed notional) but remain fully visible in gross exposure and in per-venue breakdowns.
See the module @type exposure_position for the full input contract
(units, settlement currency, marks, venue identity).
Calculate total realized PnL across all positions.
Parameters
positions- List of maps with :realized_pnl field (exchange_data)
Returns
Sum of realized PnL across all positions (float)
Example
0.1095# descripex:contract
%{
params: %{
positions: %{
description: "List of maps with :realized_pnl field",
source: "fetch_positions()",
kind: :exchange_data
}
},
returns: %{
type: :float,
description: "Sum of realized PnL across all positions"
},
returns_example: 0.1095
}
Calculate total notional exposure grouped by base asset.
Parameters
positions- List of maps with :symbol and :notional fields (exchange_data)
Returns
Map of %{base_asset => total_notional} (map)
Example
%{"BTC" => 75000.0, "ETH" => 10000.0}# descripex:contract
%{
params: %{
positions: %{
description: "List of maps with :symbol and :notional fields",
source: "fetch_positions()",
kind: :exchange_data
}
},
returns: %{type: :map, description: "Map of %{base_asset => total_notional}"},
returns_example: %{"BTC" => 75000.0, "ETH" => 10000.0}
}
Calculate total unrealized PnL across all positions.
Parameters
positions- List of maps with :unrealized_pnl field (exchange_data)
Returns
Sum of unrealized PnL across all positions (float)
Example
0.1095# descripex:contract
%{
params: %{
positions: %{
description: "List of maps with :unrealized_pnl field",
source: "fetch_positions()",
kind: :exchange_data
}
},
returns: %{
type: :float,
description: "Sum of unrealized PnL across all positions"
},
returns_example: 0.1095
}