Price Oracle - How Backpack Determines Asset Prices
TL;DR
A price oracle is a service that determines the "true" fair price of an asset by aggregating data from multiple sources. Backpack uses prices from 15+ external exchanges to prevent manipulation.
Key outputs:
- Index Price = Aggregated price from external exchanges (updated every 2-5 seconds)
- Mark Price = Index + smoothed premium (updated every 1 second) → Used for margin, PnL, liquidations
Why Do You Need a Price Oracle?
Without an oracle, the exchange would only use its own order book price. This creates a manipulation risk:
Normal market: BTC = $100,000
Attacker places huge sell order at $80,000
↓
Last price on Backpack = $80,000
↓
Everyone with BTC collateral gets liquidated!
↓
Attacker cancels order, buys liquidated positions cheapWith an oracle: The mark price stays at ~$100,000 because it's based on 15+ external exchanges. The attacker would need to crash ALL exchanges simultaneously - practically impossible.
The Three Prices
| Price | What It Is | Source | Used For |
|---|---|---|---|
| Last Price | Most recent trade | Backpack order book | Charts, display |
| Index Price | Aggregated external price | 15+ exchanges | Reference, funding rate |
| Mark Price | Fair price for calculations | Index + premium | Margin, PnL, Liquidations |
How Index Price Is Calculated
Data Sources (15+ Exchanges)
Backpack fetches prices from:
- Binance
- Coinbase
- Kraken
- OKX
- Bybit
- Bitfinex
- Kucoin
- Huobi
- Gate.io
- MEXC
- Bitget
- Lighter
- Aster
- Pyth (on-chain oracle)
- Backpack (internal)
Aggregation Algorithm
Step 1: Fetch bid, ask, last_price from each exchange
Step 2: Calculate per-exchange price
price = median(best_bid, best_ask, last_price)
Step 3: Remove outliers
If price is >1% away from median of all sources → excluded
Step 4: Average remaining prices
index_price = average(remaining_prices)Update Frequency
| Action | Frequency |
|---|---|
| Fetch from external exchanges | Every 2 seconds |
| Update index price to engine | Every 5 seconds |
| Market refresh | Every 15 seconds |
Price Matching
For some assets, direct pairs might not exist. The oracle handles:
- Direct match: BTC-USDC price directly available
- Inverse match: Use 1 / (USDC-BTC price)
- Cross-rate: Use BTC-USDT if BTC-USDC unavailable
Key file: /price-oracle/src/index_price/index.rs
How Mark Price Is Calculated
Mark price is what actually matters for your account - it determines:
- Your unrealized PnL
- Your margin fraction (MF)
- Whether you get liquidated
The Formula
Mark Price = Index Price + Moving Average of PremiumWhere:
Premium = Backpack Mid Price - Index Price
Mid Price = (Best Bid + Best Ask) / 2What Is Premium?
Premium = How much Backpack's price differs from external exchanges
Example:
Backpack mid price: $100,500 (more buyers here)
Index price: $100,000 (external average)
Premium: +$500 (or +0.5%)Premium exists because:
- More buyers than sellers on Backpack → positive premium
- More sellers than buyers → negative premium
- Normal market dynamics
Why 60-Second Moving Average?
Instead of using the instant premium (manipulable), Backpack averages over 60 seconds:
Second 1: Premium = +$500
Second 2: Premium = +$480
Second 3: Premium = +$520
Second 4: Premium = +$510
...
Second 60: Premium = +$490
Average Premium ≈ $495Final calculation:
Mark Price = Index Price + Average Premium
Mark Price = $100,000 + $495 = $100,495Why average?
- 2-second price spike barely affects 60-second average
- Smooths out noise and manipulation attempts
- Still reflects genuine sustained premium/discount
Visual Example
Time Index Backpack Mid Premium 60s Avg Premium Mark Price
─────────────────────────────────────────────────────────────────────────
10:00:00 $100,000 $100,200 +$200 +$150 $100,150
10:00:01 $100,000 $100,500 +$500 +$155 $100,155
10:00:02 $100,000 $100,100 +$100 +$154 $100,154 ← Spike absorbed
10:00:03 $100,000 $100,200 +$200 +$155 $100,155
...Even a brief $500 spike only moved the mark price by $5.
Fallback Chain
If preferred method unavailable, oracle falls back:
1. Index + 60-second moving average of premium ← Preferred
↓ (if not enough data points)
2. Just index price
↓ (if no index available)
3. Median(bid, ask, last) from Backpack order book
↓ (if no bid/ask)
4. Mid price = (bid + ask) / 2
↓ (if nothing else)
5. Last traded priceUpdate Frequency
| Action | Frequency |
|---|---|
| Calculate mark price | Every 1 second |
| Add data point to moving average | Max once per second |
| Moving average window | 60 seconds |
Key file: /price-oracle/src/mark_price/price_state.rs
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Price Oracle Service │
│ (Separate microservice) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Index Price │ │ Mark Price │ │
│ │ Module │ │ Module │ │
│ │ │ │ │ │
│ │ • Fetch 15+ │ │ • Get index │ │
│ │ exchanges │─────────→│ • Get Backpack │ │
│ │ • Aggregate │ │ mid price │ │
│ │ • Filter │ │ • Calculate │ │
│ │ outliers │ │ 60s avg │ │
│ │ │ │ premium │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ ↓ ↓ │
│ IndexPricesUpdatedEvent MarkPricesUpdatedEvent │
│ │
└──────────────────────────────┬──────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────────┐
│ Engine │
│ │
│ • Stores current mark/index prices in MarginParameters │
│ • Uses mark price for margin calculations │
│ • Broadcasts to WebSocket for clients │
└──────────────────────────────────────────────────────────────────┘Technical Implementation Details
How Prices Are Fetched (HTTP API)
The price oracle uses HTTP REST API requests (not WebSocket) to fetch prices:
Every 2 seconds:
┌─────────────────┐
│ Price Oracle │
│ Service │
└────────┬────────┘
│
├──→ GET https://api.binance.com/api/v3/ticker/24hr
├──→ GET https://api.coinbase.com/...
├──→ GET https://api.kraken.com/...
├──→ GET https://api.okx.com/...
│ ... (14+ exchanges in parallel)
│
↓
Aggregate prices- Uses
reqwestHTTP client with 5-second timeout - All exchanges fetched in parallel using async futures
- Each exchange has its own module implementing the
PriceSourcetrait
Where Prices Are Stored
Primarily in-memory - NOT in a database for the hot path:
| Storage | What's Stored | Purpose |
|---|---|---|
| In-Memory (primary) | Current prices, 60s moving averages, price state | All real-time calculations |
| Redis | Message bus between services | Communication only |
| Postgres (optional) | Configuration (which exchanges, thresholds) | Settings only |
| ClickHouse (optional) | Historical price data | Analytics/compliance |
Why in-memory?
In-memory lookup: ~1 microsecond
Postgres lookup: ~1-10 milliseconds (1000x slower)For a trading system doing thousands of margin checks per second, this matters.
In-Memory Data Structure
// Per-market price state (all in memory)
struct PriceState {
last_index_price: Option<(Decimal, Timestamp)>,
last_bbo: Option<(BestBid, BestAsk, Timestamp)>,
last_traded_price: Option<(Decimal, Timestamp)>,
// Time series for moving averages
mid_price_index_price_delta_ts: TimeSeries, // 60-second window
mark_price_ts: TimeSeries, // 5-minute window
premium_ts: TimeSeries, // 5-minute window
}
// All markets stored in a thread-safe map
market_symbol_to_price_state: Arc<Mutex<HashMap<MarketSymbol, PriceState>>>It's a Separate Service
The price oracle runs as its own binary/process (bpx_price_oracle):
┌─────────────────────────────────────────────────────────────────┐
│ bpx_price_oracle binary │
│ (Port 9014 health check) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Index Price │ │ Stream │ │ Mark Price │ │
│ │ Oracle Task │ │ Processor Task │ │ Oracle Task │ │
│ │ │ │ │ │ │ │
│ │ • HTTP fetch │ │ • Listens to │ │ • Calculates │ │
│ │ from 14+ │ │ engine events │ │ mark prices │ │
│ │ exchanges │ │ (fills,depth) │ │ every 1s │ │
│ │ • Every 2s │ │ • Updates BBO │ │ • Sends to │ │
│ │ │ │ & last trade │ │ engine │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ ↓ │
│ In-Memory Price State │
│ (Arc<Mutex<HashMap>>) │
│ │ │
└────────────────────────────────┼────────────────────────────────┘
│
↓ (via Redis message bus)
┌────────────────────────────────────────────────────────────────┐
│ Engine │
│ │
│ Receives: IndexPriceUpdate, MarkPriceUpdate, PriceBandUpdate │
│ Stores in: MarginParameters (also in-memory) │
│ Uses for: Margin calculations, liquidations │
└────────────────────────────────────────────────────────────────┘Complete Data Flow
EVERY 2 SECONDS (Index Price):
1. HTTP GET to 14+ exchanges (parallel async requests)
2. Parse JSON responses → extract bid, ask, last price
3. Calculate median per exchange
4. Filter outliers (>1% from median of all)
5. Calculate weighted average → INDEX PRICE
6. Store in memory
CONTINUOUSLY (Stream Processor):
7. Engine emits fill events → update last_traded_price in memory
8. Engine emits depth events → update best bid/ask in memory
EVERY 1 SECOND (Mark Price):
9. Read index price from memory
10. Read Backpack mid price from memory
11. Calculate: mark = index + 60s_avg(mid - index)
12. Send MarkPriceUpdate to engine via Redis message bus
EVERY 5 SECONDS:
13. Send IndexPriceUpdate to engineCrate Structure
price-oracle/src/
├── bin/main.rs # Entry point (bpx_price_oracle binary)
├── lib.rs # Module exports
├── config.rs # Configuration from environment
├── engine.rs # Engine communication via Redis
│
├── index_price/ # Index Price Logic
│ ├── index_price_oracle.rs # Main fetch loop (every 2s)
│ ├── index.rs # PriceSourceIndex aggregation
│ └── source/
│ └── exchanges/
│ ├── binance.rs # HTTP fetch from Binance
│ ├── coinbase.rs # HTTP fetch from Coinbase
│ ├── kraken.rs # ... etc (14 total)
│ └── ...
│
└── mark_price/ # Mark Price Logic
├── mark_price_oracle.rs # Main calculation loop (every 1s)
├── stream_processor.rs # Consumes engine events, maintains state
└── price_state.rs # Per-market price data structurePerformance Characteristics
| Metric | Value |
|---|---|
| Index fetch interval | 2 seconds |
| Mark price calculation | 1 second |
| Exchange fetch parallelism | All 14+ concurrent |
| HTTP timeout | 5 seconds |
| Moving average window | 60 seconds |
| Price band window | 5 minutes |
| Storage latency | ~1 microsecond (in-memory) |
Deviation Thresholds by Market Tier
Different markets have different outlier thresholds:
| Tier | Markets | Max Deviation |
|---|---|---|
| Tier 1 | BTC, ETH, SOL, USDT | 3% |
| Tier 2 | BNB, DOGE, LINK, etc. | 4% |
| Established (4+ hrs old) | Most markets | 6% |
| New/Small | Recent listings | 50% (permissive) |
Staleness Protection
Prices can become "stale" if not updated:
| Data | Stale Threshold |
|---|---|
| Last traded price | 60 seconds |
| Moving average data | Minimum 20 seconds of data required |
If data is stale, the oracle falls back to the next method in the chain.
Protection Against Manipulation
Attack: Spike Backpack Price
Attacker buys aggressively, spikes Backpack to +5%
↓
Premium = +5%
But 60-second average = +0.1% (mostly old data)
Mark price barely moves
Attack fails ✗Attack: Spike Single External Exchange
Attacker spikes Binance price by 10%
↓
Binance price is now >1% from median
Gets filtered out as outlier
Index price unchanged
Attack fails ✗Attack: Spike Multiple Exchanges
Attacker would need to move 8+ exchanges simultaneously
by similar amounts, sustained for 60+ seconds
↓
Practically impossible and extremely expensive
Attack fails ✗Special Cases
Prediction Markets
Prediction markets don't have external index prices. They use fallback methods:
- Median(bid, ask, last)
- Mid price
- Last traded price
Non-Trading Hours
When order book is closed or in "LimitOnly" mode:
- Mid price might be stale
- Oracle uses index price directly instead of mark price formula
Key File Reference
| Component | Location |
|---|---|
| Index price fetching | /price-oracle/src/index_price/index_price_oracle.rs |
| Index price calculation | /price-oracle/src/index_price/index.rs |
| Mark price calculation | /price-oracle/src/mark_price/price_state.rs |
| Mark price oracle | /price-oracle/src/mark_price/mark_price_oracle.rs |
| Price sources enum | /core/types/src/models/prices.rs |
| Engine price storage | /engine/src/models/margin_parameters.rs |
Summary
| Question | Answer |
|---|---|
| What is a price oracle? | Service that determines "true" asset price |
| Why needed? | Prevents price manipulation |
| How many sources? | 15+ external exchanges |
| How is index calculated? | Median per source → filter outliers → average |
| How is mark calculated? | Index + 60-second average premium |
| Update frequency? | Index: 2-5s, Mark: 1s |
| What uses mark price? | Margin, PnL, liquidations |