Mark Price
The mark price is the price at which all PnL, margin, and liquidation computations are performed. It is not the oracle price, and it is not the latest trade — it is a composite, smoothed, manipulation-resistant blend of both.
Why not just use the oracle price?
The oracle publishes every 5 minutes from off-chain event-data inputs. Using it directly as the mark price would mean:
- Step jumps every 5 minutes as oracle pushes land, causing phantom liquidations at the discontinuity.
- Single point of failure. A manipulated oracle → immediate manipulated PnL and liquidations.
- No market-consensus signal. Traders pricing more aggressively than the oracle’s slow cadence couldn’t move the mark at all.
And why not just use the vAMM?
- Oracle drift. A thin, imbalanced vAMM could drift far from the OBV-derived fair value, opening arbitrage against the protocol itself.
- Funding becomes meaningless. The premium/discount vs the index is what drives the funding rate; if mark = vAMM, funding always gravitates to zero.
The composite design solves both problems.
The formula
vamm_mid = oracle + (long_oi − short_oi) / (long_oi + short_oi)
× impact_factor × oracle
composite = oracle_weight × oracle + (1 − oracle_weight) × vamm_mid
mark_ema = prev_ema + α(dt) × (composite − prev_ema)
where α(dt) = EMA alpha from 150-second half-lifeThe three stages — vAMM mid from OI imbalance, weighted composite, then EMA smoothing — are implemented in programs/obv-perps/src/math/mark.rs.
Oracle weight: live vs between matches
The oracle weight is not a single constant — it toggles based on whether the oracle is currently receiving live-match updates:
| State | Oracle weight | vAMM weight | Rationale |
|---|---|---|---|
| Live match (oracle is pushing updates mid-match) | 50% | 50% | Fresh event-driven data from the live feed, but vAMM also expresses real-time trader consensus during the high-volume live window. Equal weighting. |
| Between matches (oracle is stable, no live data) | 30% | 70% | Oracle numbers barely change day-to-day. vAMM-dominant weighting lets market consensus set the price. |
Stored per-market as oracle_weight_live_bps and oracle_weight_between_bps on the MarketConfig account, with defaults of 5000 and 3000 bps respectively. Admin-adjustable via update_market_params.
Note: earlier documentation references a flat “70% oracle / 30% vAMM” split — that was the pre-launch design. The deployed program uses the live-vs-between split described here. See the constants in mark.rs for authoritative values.
vAMM impact factor
The vAMM adjustment from OI imbalance is small by design:
vamm_mid = oracle × (1 + imbalance_fraction × impact_factor)
where impact_factor = 0.001 (= 0.1%, DEFAULT_VAMM_IMPACT_FACTOR = 1000 at 10^6 scale)A 100% imbalance (all longs, no shorts) shifts the vAMM mid by 0.1% above the oracle. A 50% imbalance shifts it by 0.05%. Balanced OI (equal longs and shorts) means vamm_mid = oracle.
This keeps the vAMM from swinging the mark dramatically based on OI — it’s a gentle nudge that prevents blatant dislocations, not a free-market pricing engine. Combined with the 30–50% weight and the 150-second EMA, the vAMM’s influence is well-damped.
EMA smoothing
The raw composite is smoothed with a 150-second exponential moving average:
α(dt) = 1 − exp(−dt / 150) (half-life = 150s)
new_ema = prev_ema × (1 − α) + new_composite × αAt dt = 30s, α ≈ 0.18 — a new composite is 18% incorporated into the EMA. At dt = 150s (one half-life), α = 0.5 — half the weight goes to the new value. At dt > 600s (four half-lives), α = 1.0 — the EMA effectively snaps to the current composite.
For on-chain efficiency, the program uses a piecewise-linear approximation of the exponential rather than computing exp directly. The approximation matches the true EMA within 2% at every tested dt, verified in mark.rs tests.
The 150-second half-life was chosen to:
- Smooth out burst vAMM activity. A single large order shouldn’t move the mark by much; a sustained trend should move it fully.
- Stay fast enough for live matches. A 10-minute half-life would be unresponsive to a mid-match goal; 150s reaches full convergence in ~10 minutes.
What each instruction prices against
A common misconception is that the mark EMA is used everywhere. In the deployed program it is not. The on-chain code uses two distinct prices, deliberately:
| Instruction path | Price used | Why |
|---|---|---|
open_position, close_position, partial_close_position | oracle_price | Oracle is the canonical external-data anchor; opens and closes settle against a manipulation-resistant external price, not the locally-smoothed mark. |
execute_trigger_close (SL/TP), execute_trigger_open (LimitOpen) — both condition and fill | oracle_price | Triggers must fire on a price every keeper can observe; the EMA is path-dependent and not deterministic from a single account read. |
apply_funding — premium sample | oracle_price vs mark_price_ema (delta) | The funding rate signal is exactly the gap between the on-chain mark and the oracle. |
partial_liquidate, backstop_liquidate, liquidate (legacy) — eligibility | mark_price_ema | Liquidation eligibility must resist single-push manipulation. The EMA dampens flash dislocations so a momentarily-spiked oracle cannot force an otherwise-healthy position into the cascade. |
auto_deleverage — eligibility | mark_price_ema | Same reasoning as Layer 1/2. |
auto_deleverage — settlement (transfer to ADL target) | oracle_price | ADL targets are paid against the external-data anchor so they don’t get a worse fill from a transiently dislocated mark EMA. |
settle_position (post-sunset) | market.settlement_price (the last oracle_price at sunset) | Settlement is a one-shot snapshot, not an ongoing mark. |
So the mark EMA’s job is narrow but critical: it is the liquidation-eligibility price. Everything else uses the raw oracle.
This split is deliberate. The mark EMA is designed to be slow and manipulation-resistant — perfect for deciding whether to forcibly close someone’s position. But you don’t want SL/TP triggers firing on a 150-second-lagged price, and you don’t want opens/closes settling at a price that’s slightly drifted from what every other off-chain consumer of the same oracle is seeing.
The raw mark_price (composite before EMA) is also stored but is only used for computing the EMA update itself.
Further reading
- Funding Rate — uses the mark–oracle premium as its input signal.
- Entry Price & PnL — how the mark feeds PnL realization.
- Oracle Design — the oracle side of the composite.