Skip to content

Collateral System - Technical Deep Dive

TL;DR

Collateral Valuation:

  • Haircut: Discount applied to non-USDC collateral for safety (USDC = 100%, BTC ~95%, alts ~70-90%)
  • Collateral Value = balance × mark_price × haircut
  • Haircut uses InverseSqrt function: larger holdings → slightly lower haircut (more lenient)

Order Limits:

  • Max Order Quantity calculated via binary search (10 iterations) to find maximum passing margin check
  • Position Limit: Account-wide notional cap (configurable per user)
  • Open Order Quantity Limit: Per-market limit on total open order size

Validation Flow:

Order Request → Account Status → Risk Controls → Notional Limits
    → Open Order Limits → Reduce-Only Check → Position Limits → Margin Check → Accept/Reject

Key Insight: Collateral value is always conservative - open orders are assumed to execute unfavorably, and non-USDC assets get haircuts to account for liquidation slippage.


Table of Contents

  1. Collateral Valuation
  2. Order Limits & Constraints
  3. Collateral Validation Flow
  4. Margin System Integration
  5. API Endpoints
  6. Key File Reference

1. Collateral Valuation

Core Concept: Why Haircuts?

When you get liquidated, the exchange must sell your collateral to cover debt. But:

  • Price might drop while selling
  • Large orders have slippage
  • Markets can be volatile

The haircut is a safety buffer to ensure the exchange doesn't get stuck with bad debt.

Haircut Function Types

CollateralFunction has two variants:

TypeUse CaseBehavior
IdentityUSDC (stablecoin)Returns 1.0 (full value)
InverseSqrtAll other assetsDynamic haircut based on size

InverseSqrt Formula

haircut = min(base, 1.1 / (positive_curve_penalty × sqrt(notional) + 1))

Parameters:

  • base: Maximum haircut value (e.g., 0.95 for 95%)
  • positive_curve_penalty: Controls how haircut scales with size

Key property: Larger positions get a slightly lower haircut (more lenient), because the sqrt function grows slowly.

rust
// core/types/src/math/collateral.rs:86-104
impl InverseSqrtFunction {
    pub fn calculate(&self, notional: Decimal) -> Decimal {
        let sqrt_notional = notional.to_f64().map(f64::sqrt)...;
        Decimal::min(
            self.base,
            dec!(1.1) / (self.positive_curve_penalty * sqrt_notional + Decimal::ONE),
        )
    }
}

Collateral Value Calculation

┌──────────────────────────────────────────────────────────────┐
│                    For Each Asset                            │
├──────────────────────────────────────────────────────────────┤
│  1. Start with available balance                             │
│  2. Add lend positions (you can redeem them)                 │
│  3. Adjust for open orders (conservative estimate)           │
│  4. Apply haircut function                                   │
│  5. Multiply by mark price                                   │
│                                                              │
│  collateral_value = adjusted_balance × mark_price × haircut  │
└──────────────────────────────────────────────────────────────┘

Conservative Order Adjustments:

For open orders, the system takes the worst-case outcome:

  • Bid orders: Assumes they fill (reduces quote currency)
  • Ask orders: Assumes they fill (reduces base currency)

This ensures collateral calculations are never overstated.

rust
// engine/src/models/account/mod.rs:962-976
// For each asset we make the following adjustments:
// - Start with your available balances
// - Add in your lends
// - If any order occurs, take the most conservative outcome
//   (is it filled or is it cancelled?)
// - Apply haircut and mark price

CollateralParameters Structure

Each collateral asset has parameters stored in the engine:

rust
// core/types/src/models/position/borrow_lend.rs:135-150
pub struct CollateralParameters {
    pub imf_function: PositionImfFunction,  // Initial margin for borrows
    pub mmf_function: PositionMmfFunction,  // Maintenance margin for borrows
    pub haircut_function: CollateralFunction,  // Haircut calculation
    pub symbol: CustodyAsset,  // The asset (SOL, BTC, etc.)
}

Example: Collateral Calculation

Account holds: 10 BTC, 5,000 USDC
BTC price: $50,000
BTC haircut: 95% (base)

BTC collateral:  10 × $50,000 × 0.95 = $475,000
USDC collateral: 5,000 × $1 × 1.00   = $5,000
─────────────────────────────────────────────────
Total collateral value:               $480,000

2. Order Limits & Constraints

Max Order Quantity Calculation

The system uses binary search to find the maximum order quantity that passes all margin checks.

┌────────────────────────────────────────────────────────────┐
│               Max Order Quantity Algorithm                  │
├────────────────────────────────────────────────────────────┤
│  1. Calculate initial estimate:                            │
│     - Futures: net_equity_available / account_IMF / price  │
│     - Spot: available balance (with/without margin)        │
│                                                            │
│  2. Find bounds (up to 15 iterations):                     │
│     - Double/halve estimate until we find pass/fail        │
│                                                            │
│  3. Binary search (up to 10 iterations):                   │
│     - Converge to max quantity within 1% accuracy          │
│                                                            │
│  4. Round to step size and return                          │
└────────────────────────────────────────────────────────────┘
rust
// engine/src/models/account/limits.rs:24-30
const MAX_QUANTITY_SEARCH_DEPTH: usize = 10;
const MAX_QUANTITY_BOUND_ATTEMPTS: usize = 15;
const ACCURACY_PERCENTAGE: Decimal = dec!(0.01);  // 1%

Futures Max Quantity Estimate

rust
// engine/src/models/account/limits.rs:698-757
fn estimate_max_futures_quantity(...) -> Result<Decimal, Error> {
    // For reduce-only: max is existing position size
    if reduce_only && is_opposite_side {
        return Ok(existing_position_net_quantity.abs());
    }

    // For new positions:
    // Additional capacity from closing existing opposite position
    let closing_position_addition = if is_opposite_side {
        existing_position.abs() * price * 2  // Released + reusable equity
    } else {
        Decimal::ZERO
    };

    // net_equity_available × (1/account_imf) + closing_addition
    // ─────────────────────────────────────────────────────────
    //                        price
}

Max Borrow Quantity

rust
// engine/src/models/account/limits.rs:933-1001
pub fn estimate_max_borrow_quantity(...) -> Result<Decimal, Error> {
    // Formula: net_equity_available / (1 - haircut + borrow_imf) / mark_price
    net_equity_available
        .checked_div(Decimal::ONE - haircut + borrow_imf)?
        .checked_div(mark_price)
}

Position Limits

Limit TypeScopeSource
Position LimitAccount-wide (all markets)account.settings.position_limit or control_panel.margin_limits.position_limit_notional
Open Order Quantity LimitPer-marketPassed to clear_futures_order()
Leverage LimitAccount-wideaccount.settings.leverage_limit

Position limit check:

rust
// engine/src/clearing_house/clearing/future.rs:156-179
if !is_risk_reducing {
    let position_notional_weighted = self.account.positions
        .net_exposure_notional_weighted(...);
    let position_notional_limit = self.account.settings.position_limit
        .unwrap_or(self.control_panel.margin_limits.position_limit_notional);

    if position_notional_limit < position_notional_weighted {
        return Err(Error::PositionLimit);
    }
}

Open Order Quantity Limit

rust
// engine/src/clearing_house/clearing/future.rs:81-92
// Sum of existing open orders + new order quantity
let open_order_quantity = match (market_exposure, side) {
    (Some(exposure), Side::Ask) => exposure.ask_total_quantity() + quantity,
    (Some(exposure), Side::Bid) => exposure.bid_total_quantity() + quantity,
    (None, _) => quantity,
};

if open_order_quantity_limit < open_order_quantity {
    return Err(Error::OpenOrdersPositionQuantityLimit);
}

3. Collateral Validation Flow

Pre-Trade Validation (Futures)

┌─────────────────────────────────────────────────────────────────────────┐
│                     clear_futures_order() Validation                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  1. LIQUIDATION CHECK                                                    │
│     ├─ If liquidation order: must be reduce-only + IOC                  │
│     └─ If normal order: account cannot be in liquidation mode           │
│                                                                          │
│  2. RISK CONTROL CHECK                                                   │
│     └─ If risk_taking disabled: only reduce-only orders allowed         │
│                                                                          │
│  3. MARK PRICE CHECK                                                     │
│     └─ Mark price must exist and be > 0                                 │
│                                                                          │
│  4. ORDER NOTIONAL LIMIT                                                 │
│     └─ Order notional vs market limits                                  │
│                                                                          │
│  5. OPEN ORDER QUANTITY LIMIT                                           │
│     └─ existing_open + new_quantity ≤ limit                             │
│                                                                          │
│  6. REDUCE-ONLY VALIDATION (if reduce_only=true)                        │
│     ├─ Position must exist                                              │
│     ├─ Order must be opposite side to position                          │
│     ├─ Quantity cannot exceed position size                             │
│     └─ For limits: check if existing orders already cover position      │
│                                                                          │
│  7. POSITION LIMIT CHECK (if not risk-reducing)                         │
│     └─ Total weighted exposure ≤ account position limit                 │
│                                                                          │
│  8. MARGIN CHECK                                                         │
│     ├─ Calculate MF after order                                         │
│     ├─ Risk-reducing: MF > MMF                                          │
│     └─ Risk-increasing: MF > IMF                                        │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Margin Check Logic

rust
// engine/src/clearing_house/clearing/future.rs:201-238
pub fn check_margin(&self, order: &OrderType, is_risk_reducing: bool) -> Result<(), Error> {
    // Skip check for on-book liquidation orders
    if system_order_type == Some(&SystemOrderType::LiquidatePositionOnBook) {
        return Ok(());
    }

    // Calculate margin fraction with this order
    let mf = self.account.margin_fraction(margin_parameters, Some(&action))?;

    // Choose threshold based on risk direction
    let mf_bound = if is_risk_reducing {
        self.account.mmf(...)  // More lenient
    } else {
        self.account.imf(...)  // More restrictive
    };

    if mf <= mf_bound {
        return Err(Error::InsufficientMargin);
    }
    Ok(())
}

Risk-Reducing Detection

An order is risk-reducing if it reduces overall exposure:

rust
// engine/src/clearing_house/clearing/future.rs:150-154
let is_risk_reducing = match position {
    None => false,
    Some(_) if is_reduce_only => true,
    Some(position) => position.is_risk_reducing(order_type, market_exposure)?,
};

4. Margin System Integration

Net Equity Calculation

┌─────────────────────────────────────────────────────────────┐
│                  Net Equity Formula                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  net_equity = collateral_value                               │
│             + unrealized_pnl (futures)                       │
│             + unsettled_equity                               │
│             - borrow_liability                               │
│                                                              │
└─────────────────────────────────────────────────────────────┘
rust
// engine/src/models/account/mod.rs:774-784
pub fn net_equity_notional_with_action(...) -> Decimal {
    self.collateral_value(margin_parameters, action)
        + self.positions.total_unrealized_pnl(margin_parameters)
        + Self::additional_action_unrealized_pnl(margin_parameters, action)
        + self.unsettled_equity
        - self.borrow_liability(margin_parameters, action)
}

Margin Fraction

MF = Net Equity / Net Margin Exposure
rust
// engine/src/models/account/mod.rs:485-500
pub fn margin_fraction_and_net_equity_notional(...) -> (Option<Decimal>, Decimal) {
    let net_margin_exposure_notional = self.net_margin_exposure_notional(...);
    let net_equity_notional = self.net_equity_notional_with_action(...);

    if net_margin_exposure_notional.is_zero() {
        return (None, net_equity_notional);  // No exposure = undefined MF
    }

    let margin_fraction = net_equity_notional / net_margin_exposure_notional;
    (Some(margin_fraction), net_equity_notional)
}

IMF/MMF Calculation (Sqrt Scaling)

Both IMF and MMF use the same sqrt function, making margin requirements scale smoothly with position size:

rust
// core/types/src/math/position_imf.rs:47-66
impl SqrtFunction {
    pub fn calculate(&self, notional: Decimal) -> Decimal {
        let sqrt_notional = notional.to_f64().map(f64::sqrt)...;
        Decimal::max(self.base, self.factor * sqrt_notional)
    }
}

Example (SOL market, base=1%, factor=0.0001):

Position Notionalsqrt(notional)IMF/MMFMax Leverage
$10,0001001% (base)100x
$100,0003163.16%~31x
$1,000,000100010%10x

Account IMF (Weighted Average)

rust
// engine/src/models/account/mod.rs:533-593
pub fn imf(...) -> Decimal {
    // Sum: position_notional × position_imf for all positions
    let futures_numerator: Decimal = self.positions
        .get_exposure(...)
        .map(|p| p.initial_margin_equity(mark_price, params))
        .sum();

    let spot_margin_numerator: Decimal = self.borrow_lend_positions
        .get_exposure(...)
        .map(|p| p.initial_margin_equity(mark_price, params))
        .sum();

    // Account IMF = sum(margin_equity) / total_exposure
    Decimal::max(
        (futures_numerator + spot_margin_numerator) / total_exposure,
        imf_lower_bound
    )
}

Net Equity Available

rust
// engine/src/models/account/mod.rs:722-743
pub fn net_equity_available(...) -> Decimal {
    net_equity_notional - net_equity_locked
}

pub fn net_equity_locked(...) -> Decimal {
    Decimal::min(
        net_equity_notional,
        imf × net_margin_exposure_notional  // Equity "used" by positions
    )
}

5. API Endpoints

Current API (v1)

EndpointMethodDescription
/api/v1/account/limits/borrowGETMax borrowable quantity
/api/v1/account/limits/orderGETMax order quantity
/api/v1/account/limits/withdrawalGETMax withdrawable quantity

Max Order Quantity Request

GET /api/v1/account/limits/order
  ?symbol=SOL_USDC_PERP
  &side=Bid
  &price=100.00          (optional, omit for market orders)
  &reduceOnly=false
  &autoBorrow=false
  &autoBorrowRepay=false
  &autoLendRedeem=false
  &subaccountId=1        (optional)

Headers:
  X-API-Key: your_api_key
  X-Signature: signature   (optional for signed requests)
  X-Timestamp: 1234567890
  X-Window: 5000

Response:

json
{
  "maxOrderQuantity": "10.5"
}

Max Borrow Quantity Request

GET /api/v1/account/limits/borrow
  ?symbol=SOL
  &subaccountId=1

Response:
{
  "maxBorrowQuantity": "100.0"
}

Max Withdrawal Quantity Request

GET /api/v1/account/limits/withdrawal
  ?symbol=SOL
  &autoBorrow=false
  &autoLendRedeem=true
  &subaccountId=1

Response:
{
  "maxWithdrawalQuantity": "50.0"
}

6. Key File Reference

ComponentLocation
Collateral haircut function/core/types/src/math/collateral.rs
CollateralParameters struct/core/types/src/models/position/borrow_lend.rs:135-150
IMF/MMF sqrt function/core/types/src/math/position_imf.rs
Max order/borrow/withdrawal/engine/src/models/account/limits.rs
Futures order validation/engine/src/clearing_house/clearing/future.rs
Spot order validation/engine/src/clearing_house/clearing/spot.rs
Collateral value calculation/engine/src/models/account/mod.rs:951-1050
Margin fraction calculation/engine/src/models/account/mod.rs:485-520
Account IMF/MMF/engine/src/models/account/mod.rs:533-666
Net equity calculations/engine/src/models/account/mod.rs:722-784
API limits endpoints/api/src/routes/limits.rs
Collateral reconciler/liquidator/src/tasks/collateral_reconciler.rs

Summary

ConceptFormula/Rule
Haircut (InverseSqrt)min(base, 1.1 / (penalty × sqrt(notional) + 1))
Collateral Valuebalance × mark_price × haircut
Net Equitycollateral + unrealized_pnl + unsettled - borrow_liability
Margin Fractionnet_equity / net_margin_exposure
IMF (Sqrt)max(base, factor × sqrt(notional))
Margin Check (risk-increasing)MF > IMF
Margin Check (risk-reducing)MF > MMF
Max Order QuantityBinary search (10 iterations, 1% accuracy)
Position Limitweighted_exposure ≤ position_limit_notional
Open Order Limitexisting_open + new ≤ open_order_quantity_limit