Pure calculation functions for leverage, liquidation, allocation, and safety analysis. All functions use Decimal arithmetic exclusively for precision.
Important: Simplified Modeling
This module provides analytical approximations for planning purposes, not exact exchange-specific calculations. Real exchanges include additional factors:
- Stepped margin tier structures (not constant MMR)
- Fee buffers and insurance fund contributions
- Mark price vs last price differences
- Partial liquidation bands and incremental liquidations
- Funding rate impacts over time
- Cross-margin vs isolated margin specifics
For exact liquidation prices, always refer to the specific exchange's API or risk engine. This module is designed for position planning and risk assessment rather than precise liquidation modeling.
API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
quantize | 1 | Round a Decimal to standard output precision (8 places). | value: value |
dca_ladder | 8 | Calculate DCA ladder steps using reserve allocation. | position: value, reserve: value, entry_price: value, ui_lev: value, ladder_preset: value, side: value, mmr_rate: value, mark_buffer: value |
convert_ladder_for_short | 1 | Convert a long DCA ladder preset to a short preset. | long_preset: value |
compare_dca_safety | 8 | Compare safety metrics before and after adding a DCA leg. | single_leg: value, dca_leg: value, current_price: value, initial_equity: value, mmr: value, side: value, swan_pct: value, safety_cfg: value |
safety | 5 | Evaluate position safety and compute risk metrics. | liq: value, entry: value, swan_pct: value, side: value, cfg: value |
multi_leg_position | 3 | Calculate multi-leg cross-margin position aggregates. | legs: value, current_price: value, initial_equity: value |
position | 5 | Calculate position notional and effective leverage. | sub_eq: value, init_margin_pct: value, ui_lev: value, entry: value, side: value |
allocate | 5 | Compute subaccount equity envelope from mode configuration. | aum: value, mode_cfg: value, assets: value, weights: value, per_sub_cap_pct: value |
liquidation | 4 | Calculate liquidation price using simplified analytical model. | entry: value, leff: value, mmr_total: value, side: value |
leverage_to_aum | 2 | Calculate position notional as a fraction of total AUM. | notional: value, total_aum: value |
effective_leverage | 2 | Calculate effective leverage from notional and wallet equity. | notional: value, wallet_equity: value |
Summary
Functions
Computes the subaccount equity envelope based on mode configuration.
Compares safety metrics before and after DCA for cross-margin positions.
Converts a long DCA ladder preset to a short preset.
Calculates DCA ladder steps for a position with reserve allocation.
Calculates the effective leverage based on notional and wallet equity.
Returns {:error, :non_positive_wallet_equity} for zero or negative equity.
Uses absolute notional value for consistent leverage calculation.
Calculates the position size as a percentage of total AUM (Assets Under Management). This shows what portion of the total portfolio is at risk in this position.
Calculates the liquidation price for a position using simplified analytical model.
Calculates multi-leg position with cross-margin dynamics.
Calculates position size and effective leverage.
Quantizes a Decimal value to the standard output precision.
Evaluates position safety and calculates risk metrics.
Types
Functions
Computes the subaccount equity envelope based on mode configuration.
This function calculates the total subaccount allocation without distributing funds across individual assets. Asset-specific allocation is handled separately.
Parameters
- aum: Assets under management
- mode_cfg: Mode configuration with %{pct: Decimal, cap: Decimal}
- assets: List of asset symbols (for interface compatibility, not used)
- weights: Map of asset => weight percentage (for interface compatibility, not used)
- per_sub_cap_pct: Per-subaccount capital percentage (0-1)
Returns
Map with:
- sub_eq: Subaccount equity envelope
- init_margin: Initial margin available within the envelope
- reserve: Reserve amount within the envelope
- leftover: Unallocated amount remaining outside the envelope
Example
iex> allocate(Decimal.new(10000), %{pct: Decimal.new("0.01"), cap: Decimal.new("0.01")},
...> [:ETH], %{ETH: Decimal.new(100)}, Decimal.new("0.5"))
%{
sub_eq: #Decimal<100.00000000>,
init_margin: #Decimal<50.00000000>,
reserve: #Decimal<50.00000000>,
leftover: #Decimal<9900.00000000>
}
@spec compare_dca_safety( map(), map(), Decimal.t(), Decimal.t(), Decimal.t(), :long | :short, Decimal.t(), map() ) :: map() | {:error, atom()}
Compares safety metrics before and after DCA for cross-margin positions.
Useful for LiveView to show how adding legs affects liquidation distance. In cross-margin, DCA while underwater typically worsens safety metrics.
Parameters
- single_leg: %{entry: Decimal.t(), notional: Decimal.t()}
- dca_leg: %{entry: Decimal.t(), notional: Decimal.t()}
- current_price: Market price when adding DCA leg
- initial_equity: Starting subaccount equity
- mmr: Minimum margin requirement
- side: :long or :short
- swan_pct: Black swan threshold percentage
- safety_cfg: Safety configuration (optional)
Returns
Map with:
- pre_dca: Safety metrics before adding DCA leg
- post_dca: Safety metrics after adding DCA leg
- leverage_change: Difference in effective leverage
- liquidation_change: Change in liquidation price (+ means worse for longs)
Example
iex> single = %{entry: Decimal.new(3000), notional: Decimal.new(125)}
iex> dca = %{entry: Decimal.new(2800), notional: Decimal.new(125)}
iex> compare_dca_safety(single, dca, Decimal.new(2800), Decimal.new(50),
...> Decimal.new("0.005"), :long, Decimal.new(25))
%{
pre_dca: %{verdict: :safe, distance_to_liq_pct: #Decimal<...>, ...},
post_dca: %{verdict: :tight, distance_to_liq_pct: #Decimal<...>, ...},
leverage_change: #Decimal<3.50000000>, # 6.0x - 2.5x
liquidation_change: #Decimal<610.00000000> # $2416 - $1806
}
Converts a long DCA ladder preset to a short preset.
For shorts, we want to add to positions at higher prices (when price moves against us). This function converts long ladder multipliers (< 1.0) to short multipliers (> 1.0).
Parameters
- long_preset: List of {price_mult, reserve_pct} tuples for longs
Returns
List of {price_mult, reserve_pct} tuples for shorts
Example
iex> long_preset = [{Decimal.new("0.95"), Decimal.new("0.3")},
...> {Decimal.new("0.90"), Decimal.new("0.3")}]
iex> convert_ladder_for_short(long_preset)
[{#Decimal<1.05>, #Decimal<0.3>}, {#Decimal<1.10>, #Decimal<0.3>}]
@spec dca_ladder( map(), Decimal.t(), Decimal.t(), Decimal.t(), list(), :long | :short, Decimal.t(), Decimal.t() ) :: map()
Calculates DCA ladder steps for a position with reserve allocation.
This function models adding to a position at different price levels using reserve funds. Each step recalculates the average entry price and liquidation level based on the cumulative position. The function ensures reserve is never overspent.
Parameters
- position: Initial position map with :notional and :eff_lev
- reserve: Reserve amount available for DCA
- entry_price: Initial entry price
- ui_lev: UI leverage for new positions
- ladder_preset: List of {price_mult, reserve_pct} tuples
- side: :long or :short
- mmr_rate: Minimum margin requirement rate
- mark_buffer: Mark price buffer (optional, defaults to 0)
Returns
Map with:
- steps: List of DCA steps with details
- final_avg_entry: Final average entry price
- final_notional: Total notional after all DCA steps
- final_liq: Final liquidation price
- final_eff_lev: Final effective leverage
Example
iex> position = %{notional: Decimal.new(1500), eff_lev: Decimal.new("1.5")}
iex> ladder_preset = [{Decimal.new("0.95"), Decimal.new("0.3")},
...> {Decimal.new("0.90"), Decimal.new("0.3")}]
iex> dca_ladder(position, Decimal.new(500), Decimal.new(3000), Decimal.new(3),
...> ladder_preset, :long, Decimal.new("0.005"), Decimal.new(0))
%{
steps: [...],
final_avg_entry: #Decimal<...>,
final_notional: #Decimal<...>,
final_liq: #Decimal<...>,
final_eff_lev: #Decimal<...>
}
@spec effective_leverage(Decimal.t(), Decimal.t()) :: decimal_result()
Calculates the effective leverage based on notional and wallet equity.
Returns {:error, :non_positive_wallet_equity} for zero or negative equity.
Uses absolute notional value for consistent leverage calculation.
Examples
iex> effective_leverage(Decimal.new(10000), Decimal.new(5000))
#Decimal<2.00000000>
iex> effective_leverage(Decimal.new(-10000), Decimal.new(5000))
#Decimal<2.00000000>
iex> effective_leverage(Decimal.new(10000), Decimal.new(0))
{:error, :non_positive_wallet_equity}
@spec leverage_to_aum(Decimal.t(), Decimal.t()) :: decimal_result()
Calculates the position size as a percentage of total AUM (Assets Under Management). This shows what portion of the total portfolio is at risk in this position.
Formula: notional_position_size / total_aum
Note: The notional position size already includes leverage (position_size = margin * leverage), so this directly gives the exposure ratio without additional leverage multiplication.
Examples
iex> leverage_to_aum(Decimal.new(10000), Decimal.new(100000))
#Decimal<0.10000000> # 10% of AUM at risk
iex> leverage_to_aum(Decimal.new(10000), Decimal.new(0))
{:error, :non_positive_total_aum}
@spec liquidation(Decimal.t(), Decimal.t(), Decimal.t(), :long | :short) :: decimal_result()
Calculates the liquidation price for a position using simplified analytical model.
Parameters
- entry: Entry price (must be > 0, returns
{:error, :non_positive_entry}if ≤ 0) - leff: Effective leverage (must be ≥ 0, returns 0 if 0 and error if < 0)
- mmr_total: Total minimum margin requirement (decimal 0-1, automatically clamped)
- side: :long or :short
Input Domains
- entry: > 0 (positive price)
- leff: ≥ 0 (non-negative leverage)
- mmr_total: [0, 1] (percentage as decimal, automatically clamped to [0, 0.99999999])
Formula
- Long: entry * (1 - (1 - mmr_total) / leff)
- Short: entry * (1 + (1 - mmr_total) / leff)
Important Limitations
This is a simplified model that assumes:
- Constant MMR (no tier structures)
- MMR includes trading fees (hence no separate fee parameter)
- No funding or insurance buffers
- Cross margin with equity = subaccount equity
- Mark price = last price
Real exchange liquidation prices will differ due to additional risk factors. Use this for planning and risk assessment, not exact liquidation prediction.
Safety Guards
- MMR is clamped to [0, 0.99999999] to prevent invalid values
- Long liquidation prices are clamped to non-negative values
- Returns 0 for zero leverage (no position)
- Returns
{:error, :negative_effective_leverage}for negative leverage - Returns
{:error, :non_positive_entry}for non-positive entry
Examples
iex> liquidation(Decimal.new(3000), Decimal.new(2), Decimal.new("0.005"), :long)
#Decimal<1507.50000000>
iex> liquidation(Decimal.new(3000), Decimal.new(2), Decimal.new("0.005"), :short)
#Decimal<4492.50000000>
Calculates multi-leg position with cross-margin dynamics.
In cross-margin, adding legs while price moves against you:
- Unrealized PnL reduces wallet equity
- New notional is added to existing notional
- Effective leverage typically increases (worse liquidation)
Parameters
- legs: List of %{entry: Decimal.t(), notional: Decimal.t()}
- current_price: Current market price
- initial_equity: Starting subaccount equity
Returns
Map with:
- total_notional: Combined notional across all legs
- avg_entry: Volume-weighted average entry price
- unrealized_pnl: Total unrealized profit/loss
- current_equity: Remaining equity after unrealized PnL
- effective_leverage: total_notional / current_equity
Example
iex> legs = [
...> %{entry: Decimal.new(3000), notional: Decimal.new(125)},
...> %{entry: Decimal.new(2800), notional: Decimal.new(125)}
...> ]
iex> multi_leg_position(legs, Decimal.new(2800), Decimal.new(50))
%{
total_notional: #Decimal<250.00000000>,
avg_entry: #Decimal<2896.55172414>,
unrealized_pnl: #Decimal<-8.33333333>,
current_equity: #Decimal<41.66666667>,
effective_leverage: #Decimal<6.00000000>
}
Calculates position size and effective leverage.
Parameters
- sub_eq: Subaccount equity
- init_margin_pct: Initial margin percentage (0-1)
- ui_lev: UI leverage (1-125)
- entry: Entry price
- side: :long or :short
Returns
Map with:
- notional: Position notional value
- eff_lev: Effective leverage
For shorts, returns notional only (no token calculation).
Example
iex> position(Decimal.new(1000), Decimal.new("0.5"), Decimal.new(3),
...> Decimal.new(3000), :long)
%{
notional: #Decimal<1500.00000000>,
eff_lev: #Decimal<1.50000000>
}
Quantizes a Decimal value to the standard output precision.
All financial outputs should be quantized to 8 decimal places for consistency.
@spec safety(Decimal.t(), Decimal.t(), Decimal.t(), :long | :short, map()) :: safety_result()
Evaluates position safety and calculates risk metrics.
Parameters
- liq: Liquidation price (any value)
- entry: Entry price (must be > 0, returns
{:error, :non_positive_entry}if ≤ 0) - swan_pct: Black swan percentage threshold (≥ 0, typically 0-100)
- side: :long or :short
- cfg: Safety configuration with thresholds
Input Domains
- entry: > 0 (positive price, guarded against division by zero)
- swan_pct: ≥ 0 (non-negative percentage)
- liq: any value (compared against entry)
- cfg: optional map with threshold_multiplier and safe_multiplier
Returns
Map with:
- verdict: :safe, :tight, or :unsafe
- distance_to_liq_pct: Percentage distance to liquidation
- distance_to_liq_usd: Dollar distance to liquidation
- distance_to_swan_pct: Percentage distance to black swan
- distance_to_swan_usd: Dollar distance to black swan
- composite_score: Overall safety score (0-100)
Safety Guards
- Returns
{:error, :non_positive_entry}if entry ≤ 0 - Handles swan_pct = 0 without division errors
- Caps composite score at 100 for distances beyond swan threshold
Example
iex> safety(Decimal.new(2850), Decimal.new(3000), Decimal.new(25), :long,
...> %{threshold_multiplier: Decimal.new("1.5")})
%{
verdict: :safe,
distance_to_liq_pct: #Decimal<5.00000000>,
distance_to_liq_usd: #Decimal<150.00000000>,
distance_to_swan_pct: #Decimal<20.00000000>,
distance_to_swan_usd: #Decimal<600.00000000>,
composite_score: #Decimal<75.00000000>
}