Skip to main content
Mark price is the single source of truth for:
  • Unrealized PnL
  • Maintenance margin checks
  • Funding rate TWAP inputs
It is not directly the oracle price, and it is not directly the last trade price. It’s the last trade price clamped to the oracle within ±10%.

Formula

mark_price=clamp ⁣(last_trade_price,  oracle×0.9,  oracle×1.1)\text{mark\_price} = \text{clamp}\!\left(\text{last\_trade\_price},\; \text{oracle} \times 0.9,\; \text{oracle} \times 1.1\right) If there has been no trade yet, last_trade_price defaults to the oracle price.

Why clamped?

Without the clamp, a thinly-traded market could be manipulated, one fill at +50% would make every long position look wildly profitable, passing margin checks they shouldn’t. The 10% clamp window:
  • Tight enough that adversarial trades can’t push users into insolvency via mark alone.
  • Wide enough that legitimate price discovery during a volatile hour still moves mark organically (versus just pinning it to oracle).
Liquidations bypass mark entirely, they run at oracle price, which has its own confidence and staleness bounds.

What updates mark

  • Every fill updates orderbook.last_trade_price and last_trade_ts.
  • update_funding_rate reads the current mark into the mark TWAP.
  • Admin param changes never rewrite mark directly.

TWAPs

Two 1-hour exponential moving averages are maintained on the Market account:
  • mark_twap, 1-hour EMA of mark price
  • oracle_twap, 1-hour EMA of oracle price
They feed the funding rate formula. See funding rate for how they’re combined.

Reading mark price off-chain

const orderbook = await fetchOrderbook(rpc, orderbookPda);
const oracle = await fetchOracle(rpc, market.oracle);

const mark = clamp(
  orderbook.lastTradePrice,
  oracle.price * 0.9,
  oracle.price * 1.1,
);
Or, simpler: the frontend renders math::get_mark_price(market, oracle) server-side via a Codama-generated helper.