🚧 SportsPerp is currently live on devnet. Mainnet target: before Jun 12, 2026 (World Cup kickoff).
TradingMark Price

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-life

The 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:

StateOracle weightvAMM weightRationale
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 pathPrice usedWhy
open_position, close_position, partial_close_positionoracle_priceOracle 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 filloracle_priceTriggers 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 sampleoracle_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) — eligibilitymark_price_emaLiquidation 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 — eligibilitymark_price_emaSame reasoning as Layer 1/2.
auto_deleverage — settlement (transfer to ADL target)oracle_priceADL 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