In Part 1, we explored Liquidation Logic—the safety net that prevents exchange insolvency. But for that safety net to work, the engine needs a precise, real-time "heartbeat."
That heartbeat is PnL (Profit and Loss). Get this calculation wrong, and traders lose money unfairly. Get it right, and the system stays trustworthy at scale. Here’s how I built a PnL system in Rust that handles thousands of updates per second with zero rounding errors.
1. What is PnL? (The Engine's Core Metric)
PnL is simply the measure of how much a position has gained or lost. In a perpetual engine, PnL isn't just for a pretty dashboard; it is a critical input for:
- Liquidations: Triggered when PnL drops and erodes the margin buffer.
- Funding: Periodic payments deducted directly from or added to PnL.
- Risk Management: Monitoring the exchange's total liability and exposure in real-time.
2. Long vs. Short: Directional Profit
Position direction flips the math entirely. If you're Long, you profit as the price climbs. If you're Short, you profit as the market falls.
The Formulas:
For Long Positions:
$$PnL = (Current Price - Entry Price) \times Quantity$$
For Short Positions:
$$PnL = (Entry Price - Current Price) \times Quantity$$
3. The Real-Time Update Loop
Speed is a feature, not an afterthought. Stale prices lead to unreliable liquidations. Every price update received from the WebSocket triggers a recalculation across the entire position book.
Rust Implementation
pub fn update_price(&mut self, new_price: Decimal) -> Result<UpdateResult, String> {
self.current_price = new_price;
// Step 1: Update PnL for EVERY position
for position in self.positions.values_mut() {
let pnl = match position.position_type {
PositionType::Long => {
(self.current_price - position.entry_price) * position.quantity
}
PositionType::Short => {
(position.entry_price - self.current_price) * position.quantity
}
};
position.pnl = pnl; // Real-time update before risk checks
}
// Step 2: Check liquidations immediately after PnL update
let to_liquidate: Vec<Uuid> = self
.positions
.values()
.filter(|p| self.should_liquidate(p))
.map(|p| p.id)
.collect();
// ... Close liquidated positions logic ...
Ok(UpdateResult { .. })
}
4. Decimal vs. Float: Precision is Non-Negotiable
This is the "Senior Dev" separator. Standard f64 floats have rounding errors that compound dangerously when you apply 10x, 50x, or 100x leverage.
-
The Problem (f64):
(110.0 - 100.0) * 10.0might result in100.0000000001. - The Scale: 10,000 calculations per second means $1,000+ in unaccounted losses within an hour.
-
The Solution: Use the
rust_decimalcrate. It ensures that $100.00 is mathematically treated as exactly $100.00.
5. Unrealized vs. Realized PnL
It’s vital to distinguish between "paper gains" and actual account balance.
- PnL/Equity: Unrealized. It fluctuates with the market and stays tied to the open position.
- Balance: Realized. This only updates once the trade is finalized.
pub fn close_position(&mut self, id: Uuid) -> Result<Decimal, String> {
let position = self.positions.remove(&id).ok_or("Position not found")?;
// Equity = Initial Margin + Unrealized PnL
let realized_amount = position.margin + position.pnl;
self.balance += realized_amount; // Profit becomes Realized
Ok(realized_amount)
}
6. Edge Case: Funding-Triggered Liquidation
PnL can drop even if the price stands still. If funding rates are high, the periodic "Funding Fee" can erode a trader's margin until they hit the liquidation threshold. My engine treats Funding as a direct PnL adjustment, ensuring the risk engine catches these "silent" liquidations during the funding window.
7. Summary Table
| Concept | What | When | Why |
|---|---|---|---|
| Formula | (Current - Entry) × Qty | Every Price Tick | Calculate directional profit |
| Precision |
Decimal (not f64) |
Always | Prevent compounding errors |
| Real-Time | <5ms Update Loop | Every price update | Ensure safety checks use fresh data |
| Funding | Deduct from PnL | Hourly | Reflect cost of carry/leverage |
Conclusion
PnL is the heartbeat of the engine. By using Rust’s type safety, Decimal precision, and a strict execution loop, we ensure that every dollar (and every satoshi) is accounted for.
Next in the series: Funding Rates—how we keep the perpetual price anchored to the spot market.
Top comments (2)
I had gained lot of insights from your rust engine blog series.
Thanks Yash! Glad you're finding it useful.