Skip to content

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 cheap

With 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

PriceWhat It IsSourceUsed For
Last PriceMost recent tradeBackpack order bookCharts, display
Index PriceAggregated external price15+ exchangesReference, funding rate
Mark PriceFair price for calculationsIndex + premiumMargin, 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

ActionFrequency
Fetch from external exchangesEvery 2 seconds
Update index price to engineEvery 5 seconds
Market refreshEvery 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 Premium

Where:

Premium = Backpack Mid Price - Index Price
Mid Price = (Best Bid + Best Ask) / 2

What 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 ≈ $495

Final calculation:

Mark Price = Index Price + Average Premium
Mark Price = $100,000 + $495 = $100,495

Why 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 price

Update Frequency

ActionFrequency
Calculate mark priceEvery 1 second
Add data point to moving averageMax once per second
Moving average window60 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 reqwest HTTP client with 5-second timeout
  • All exchanges fetched in parallel using async futures
  • Each exchange has its own module implementing the PriceSource trait

Where Prices Are Stored

Primarily in-memory - NOT in a database for the hot path:

StorageWhat's StoredPurpose
In-Memory (primary)Current prices, 60s moving averages, price stateAll real-time calculations
RedisMessage bus between servicesCommunication only
Postgres (optional)Configuration (which exchanges, thresholds)Settings only
ClickHouse (optional)Historical price dataAnalytics/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

rust
// 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 engine

Crate 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 structure

Performance Characteristics

MetricValue
Index fetch interval2 seconds
Mark price calculation1 second
Exchange fetch parallelismAll 14+ concurrent
HTTP timeout5 seconds
Moving average window60 seconds
Price band window5 minutes
Storage latency~1 microsecond (in-memory)

Deviation Thresholds by Market Tier

Different markets have different outlier thresholds:

TierMarketsMax Deviation
Tier 1BTC, ETH, SOL, USDT3%
Tier 2BNB, DOGE, LINK, etc.4%
Established (4+ hrs old)Most markets6%
New/SmallRecent listings50% (permissive)

Staleness Protection

Prices can become "stale" if not updated:

DataStale Threshold
Last traded price60 seconds
Moving average dataMinimum 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

ComponentLocation
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

QuestionAnswer
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