DeltaCalc.Calc (DeltaCalc v0.1.0)

Copy Markdown View Source

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

FunctionArityDescriptionParam Kinds
quantize1Round a Decimal to standard output precision (8 places).value: value
dca_ladder8Calculate 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_short1Convert a long DCA ladder preset to a short preset.long_preset: value
compare_dca_safety8Compare 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
safety5Evaluate position safety and compute risk metrics.liq: value, entry: value, swan_pct: value, side: value, cfg: value
multi_leg_position3Calculate multi-leg cross-margin position aggregates.legs: value, current_price: value, initial_equity: value
position5Calculate position notional and effective leverage.sub_eq: value, init_margin_pct: value, ui_lev: value, entry: value, side: value
allocate5Compute subaccount equity envelope from mode configuration.aum: value, mode_cfg: value, assets: value, weights: value, per_sub_cap_pct: value
liquidation4Calculate liquidation price using simplified analytical model.entry: value, leff: value, mmr_total: value, side: value
leverage_to_aum2Calculate position notional as a fraction of total AUM.notional: value, total_aum: value
effective_leverage2Calculate 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 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

decimal_result()

@type decimal_result() :: Decimal.t() | {:error, atom()}

safety_result()

@type safety_result() :: map() | {:error, :non_positive_entry}

Functions

allocate(aum, mode_cfg, assets, weights, per_sub_cap_pct)

@spec allocate(Decimal.t(), map(), [atom()], map(), Decimal.t()) :: map()

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

compare_dca_safety(single_leg, dca_leg, current_price, initial_equity, mmr, side, swan_pct, safety_cfg \\ %{})

@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
}

convert_ladder_for_short(long_preset)

@spec convert_ladder_for_short(list()) :: list()

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>}]

dca_ladder(position, reserve, entry_price, ui_lev, ladder_preset, side, mmr_rate, mark_buffer \\ Decimal.new(0))

@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<...>
}

effective_leverage(notional, wallet_equity)

@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}

leverage_to_aum(notional, total_aum)

@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}

liquidation(entry, leff, mmr_total, side)

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

multi_leg_position(legs, current_price, initial_equity)

@spec multi_leg_position([map()], Decimal.t(), Decimal.t()) :: map()

Calculates multi-leg position with cross-margin dynamics.

In cross-margin, adding legs while price moves against you:

  1. Unrealized PnL reduces wallet equity
  2. New notional is added to existing notional
  3. 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>
}

position(sub_eq, init_margin_pct, ui_lev, entry, side)

@spec position(Decimal.t(), Decimal.t(), Decimal.t(), Decimal.t(), :long | :short) ::
  map()

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

quantize(value)

@spec quantize(Decimal.t()) :: Decimal.t()

Quantizes a Decimal value to the standard output precision.

All financial outputs should be quantized to 8 decimal places for consistency.

safety(liq, entry, swan_pct, side, cfg \\ %{})

@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>
}