DEV Community

Cover image for Perpetual Engine Series Part 1: The Liquidation Logic
Sumana
Sumana

Posted on

Perpetual Engine Series Part 1: The Liquidation Logic

When building a high-leverage trading engine, the most critical component isn't just speed—it’s the Risk Engine. If your liquidation logic fails, the exchange carries the debt, and the system becomes insolvent.

In this first post of my series, I’m diving deep into how I built the liquidation safety net for my Rust-based engine.


1. What is Liquidation?

Liquidation is the automatic closure of a position when a trader's collateral (Margin) is exhausted.

In perpetual trading, leverage is a double-edged sword. For example:

  • Margin: $100
  • Leverage: 10x
  • Position Size: $1,000

If the price moves against you, losses mount quickly. Once your $100 margin can no longer cover the loss, the engine must force-close the position to protect the exchange from losses exceeding your collateral.


2. The Liquidation Price (The Threshold)

The moment a position is opened, the engine calculates a Liquidation Price. This provides the trader with a predictable "point of no return."

The Formula

We use a 5% Maintenance Margin Rate ($0.05$). This means we trigger the close when you have 5% of your buffer remaining.

For Longs:
$$Liquidation Price = Entry Price \times (1 - \frac{0.95}{Leverage})$$

For Shorts:
$$Liquidation Price = Entry Price \times (1 + \frac{0.95}{Leverage})$$

Rust Implementation

Using rust_decimal ensures we avoid the dangerous rounding errors found in standard floating-point math.

let maintenance_buffer = dec!(1.0) - self.maintenance_margin_rate; // 0.95

let liquidation_price = match position_type {
    PositionType::Long => {
        entry_price * (dec!(1.0) - (maintenance_buffer / leverage))
    }
    PositionType::Short => {
        entry_price * (dec!(1.0) + (maintenance_buffer / leverage))
    }
};
Enter fullscreen mode Exit fullscreen mode

3. Real-Time Maintenance Checks

While the Liquidation Price is set at the start, liquidations happen in real-time. On every price update, the engine performs a "Health Check":

Rule: Current Equity (Margin + PnL) <= Maintenance Threshold (Margin * 0.05)

The Logic in Action

fn should_liquidate(&self, position: &Position) -> bool {
    let current_equity = position.margin + position.pnl;
    let maintenance_threshold = position.margin * self.maintenance_margin_rate;
    current_equity <= maintenance_threshold
}

pub fn update_price(&mut self, new_price: Decimal) -> Result<UpdateResult, String> {
    // ... update PnL for all positions ...

    let to_liquidate: Vec<Uuid> = self
        .positions
        .values()
        .filter(|p| self.should_liquidate(p)) // Check every position
        .map(|p| p.id)
        .collect();

    for id in to_liquidate {
        self.close_position(id)?; // Force close
    }

    Ok(UpdateResult { ... })
}
Enter fullscreen mode Exit fullscreen mode

4. Edge Cases: Beyond the Price

A robust engine must account for more than just price swings.

Edge Case 1: Funding Liquidation

In perpetuals, funding is applied periodically (every hour in my engine). If funding costs are large enough, they can erode a trader's margin and trigger liquidation even if the market price hasn't moved.

pub fn apply_funding(&mut self) -> Result<FundingResult, String> {
    // ... apply funding payments ...

    // Check for liquidations post-funding
    let to_liquidate: Vec<Uuid> = self
        .positions
        .values()
        .filter(|p| self.should_liquidate(p))
        .map(|p| p.id)
        .collect();

    for id in to_liquidate {
        self.close_position(id)?;
    }

    Ok(FundingResult { ... })
}
Enter fullscreen mode Exit fullscreen mode

Edge Case 2: Double-Liquidation Prevention

Race conditions are real. If a price update and a funding payment hit simultaneously, you might try to close the same position twice. Using Result<T, E> and atomic-style state removal handles this gracefully.

pub fn close_position(&mut self, position_id: Uuid) -> Result<Decimal, String> {
    let position = self.positions.remove(&position_id)
        .ok_or_else(|| format!("Position not found"))?; 

    // If it's already closed, we return an Error rather than panicking.
    // This ensures our engine stays online and consistent.
}
Enter fullscreen mode Exit fullscreen mode

5. Summary Table

Concept What When Why
Liq Price Pre-calculated threshold Position Open User transparency & risk awareness
Maintenance Check Real-time equity check Every price update Catching volatility/flash moves
Close Position Force-close the trade When check triggers Preventing negative equity/systemic loss
Error Handling Result<T, E> logic On liquidation Preventing double-closes & race conditions

Conclusion

Proper liquidation logic ensures that losses are capped at the user's margin, the exchange stays solvent, and the system remains fair for all traders.

Top comments (0)