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/RejectKey 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
- Collateral Valuation
- Order Limits & Constraints
- Collateral Validation Flow
- Margin System Integration
- API Endpoints
- 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:
| Type | Use Case | Behavior |
|---|---|---|
| Identity | USDC (stablecoin) | Returns 1.0 (full value) |
| InverseSqrt | All other assets | Dynamic 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.
// 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.
// 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 priceCollateralParameters Structure
Each collateral asset has parameters stored in the engine:
// 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,0002. 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 │
└────────────────────────────────────────────────────────────┘// 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
// 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
// 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 Type | Scope | Source |
|---|---|---|
| Position Limit | Account-wide (all markets) | account.settings.position_limit or control_panel.margin_limits.position_limit_notional |
| Open Order Quantity Limit | Per-market | Passed to clear_futures_order() |
| Leverage Limit | Account-wide | account.settings.leverage_limit |
Position limit check:
// 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
// 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
// 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:
// 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 │
│ │
└─────────────────────────────────────────────────────────────┘// 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// 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:
// 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 Notional | sqrt(notional) | IMF/MMF | Max Leverage |
|---|---|---|---|
| $10,000 | 100 | 1% (base) | 100x |
| $100,000 | 316 | 3.16% | ~31x |
| $1,000,000 | 1000 | 10% | 10x |
Account IMF (Weighted Average)
// 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
// 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)
| Endpoint | Method | Description |
|---|---|---|
/api/v1/account/limits/borrow | GET | Max borrowable quantity |
/api/v1/account/limits/order | GET | Max order quantity |
/api/v1/account/limits/withdrawal | GET | Max 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: 5000Response:
{
"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
| Component | Location |
|---|---|
| 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
| Concept | Formula/Rule |
|---|---|
| Haircut (InverseSqrt) | min(base, 1.1 / (penalty × sqrt(notional) + 1)) |
| Collateral Value | balance × mark_price × haircut |
| Net Equity | collateral + unrealized_pnl + unsettled - borrow_liability |
| Margin Fraction | net_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 Quantity | Binary search (10 iterations, 1% accuracy) |
| Position Limit | weighted_exposure ≤ position_limit_notional |
| Open Order Limit | existing_open + new ≤ open_order_quantity_limit |