Entry Price & PnL
Your position’s profit or loss is computed continuously from its entry price and a reference price — oracle_price for normal open/close and trigger-fill paths, mark_price_ema for liquidation eligibility — adjusted for cumulative funding paid or received since entry. This page documents exactly how.
Which price is “the price” here? On-chain, opens, closes, partial closes, and trigger fills all settle against
market.oracle_price. Only liquidation eligibility is evaluated againstmark_price_ema. The audit table in Mark Price → What each instruction prices against is the authoritative map.
Entry price
When a position opens, the program records:
entry_price— the oracle price at the moment of open (not the EMA, not the trigger price for limit orders)cumulative_funding_at_entry— a snapshot of the market’s cumulative funding counter at open timecollateral— the USDC posted as initial marginsize— the notional position value (=collateral × leverage)opened_at— Unix timestamplast_margin_transfer_collateral— same as collateral at open (used for the 18.3% anti-manipulation check)
These fields are immutable after open. Adding or withdrawing collateral changes collateral and last_margin_transfer_collateral but leaves entry_price untouched. This means a position’s PnL is always measured against its original entry — subsequent margin ops don’t rewrite history.
Unrealized PnL formula
At any moment, the position’s unrealized PnL is computed against oracle_price (for opens, closes, and trigger fills) or mark_price_ema (for liquidation eligibility) — the formula is the same; only the reference price differs.
Long PnL = size × (ref − entry) / entry
Short PnL = size × (entry − ref) / entryWhere ref is oracle_price in trade-path computations (open, close, partial close, trigger fill) and mark_price_ema in liquidation-eligibility checks.
Implementation: calculate_pnl in programs/obv-perps/src/math/pnl.rs.
Worked examples
Long on BUR (Brighton, market ID 3), entry at index 520, size 1,000 USDC:
- Oracle rises to 540 → PnL =
1000 × (540 − 520) / 520 = +38.46USDC - Oracle falls to 500 → PnL =
1000 × (500 − 520) / 520 = −38.46USDC
Short on MUN (Manchester United, market ID 11), entry at 620, size 500 USDC:
- Oracle falls to 580 → PnL =
500 × (620 − 580) / 620 = +32.26USDC - Oracle rises to 650 → PnL =
500 × (620 − 650) / 620 = −24.19USDC
PnL scales linearly with size (notional) and hyperbolically with entry price. A 10-point move on an entry of 100 is a 10% return; on an entry of 500 it’s only 2%.
Funding payment
Funding continuously adjusts the position’s PnL between intervals. At any moment:
funding_payment = size × (cumulative_funding_now − cumulative_funding_at_entry) / 10^6- Positive payment → the position owes funding (subtracted from effective collateral).
- Negative payment → the position is owed funding (added to effective collateral).
Longs pay positive funding; shorts pay negative. So:
net_effective_collateral = collateral + unrealized_pnl − funding_payment (for longs)
= collateral + unrealized_pnl + funding_payment (for shorts)The effective_collateral figure used in margin checks already factors this in — funding is settled against the position on every significant interaction (close, margin op, liquidation attempt).
See Funding Rate for how the cumulative counter is computed.
PnL at close
When a position closes (via close_position, trigger execution, or liquidation), the program:
- Applies pending funding. The difference between
cumulative_funding_nowandcumulative_funding_at_entryis settled as a final funding payment. - Computes realized PnL against the current
oracle_price. - Deducts the taker fee (10 bps of notional) from the realized proceeds.
- Transfers
collateral + realized_pnl − funding_payment − feefrom the globalpool_tokenSPL account to the trader’s wallet (gated only onpool_token.amount ≥ payout— closes always work as long as the pool can pay). - Closes the
UserPositionPDA, refunding rent to the trader.
The full accounting is visible in the transaction’s event log — every close emits a PositionClosed event with realized_pnl, funding_paid, fee, and final settlement amount. See sdk/src/events/ for event parsing.
Partial close
A close that specifies a size smaller than the full position size performs a proportional close:
close_collateral = collateral × close_size / sizeclose_pnl = unrealized_pnl × close_size / sizeclose_funding = funding_payment × close_size / size- Position is updated:
size -= close_size,collateral -= close_collateral
entry_price and cumulative_funding_at_entry are preserved on the remaining portion. This is important: a partial close doesn’t rebase your cost basis. Your remaining 80% of the position still bears its original entry price, so future PnL reports stay comparable.
Fees
| Action | Fee | Basis |
|---|---|---|
| Market open | 10 bps (0.1%) | Notional size |
| Market close | 10 bps | Notional size |
| Trigger execution (SL/TP/Limit Open) | 10 bps | Notional size |
| Liquidation (Layer 1, 2, 3) | — | Liquidation reward covers — no separate fee |
| Add/withdraw collateral | 0 | — |
| Place/cancel trigger order | 0 | (rent only) |
Maker fees (5 bps) are reserved for a future limit-book implementation. At v1, all opens and closes are takers.
Displaying PnL in UIs
When an integrator reads a position account and wants to display its current PnL, the computation is:
import { calculatePnl, calculateFundingPayment } from "@sportsperp/sdk";
const position = await client.getPosition(marketId, user);
const market = await client.getMarket(marketId);
// For UI display, show PnL against the oracle (same price the program
// uses for opens/closes). If you want to show "liquidation eligibility"
// PnL specifically, pass market.markPriceEma instead.
const pnl = calculatePnl({
direction: position.direction,
size: position.size,
entryPrice: position.entryPrice,
referencePrice: market.oraclePrice,
});
const funding = calculateFundingPayment({
size: position.size,
cumulativeCurrent: market.cumulativeFunding[position.direction],
cumulativeAtEntry: position.cumulativeFundingAtEntry,
isLong: position.direction === Direction.Long,
});
const effective = position.collateral + pnl - funding;
const marginRatio = (effective * 10_000n) / position.size; // bpsAll four computations mirror the Rust program exactly (sdk/src/math/). Unit tests cross-check the two implementations to ensure the TS result matches an on-chain compute to the last integer.
Fixed-point precision
All PnL math is integer arithmetic at 10^6 scale. Division is performed last to minimize rounding error:
// Correct: multiplies first, divides last
pnl = size * (ref - entry) / entry
// Wrong: would accumulate rounding error
pnl = size * ((ref - entry) / entry)The pnl return is an i64 — positive for profit, negative for loss — so a maximum absolute PnL of ~9.2 × 10^18 at the 10^6 scale (≈ 9.2 × 10^12 USDC) is representable without overflow. Nothing realistic can hit that ceiling.
Further reading
- Mark Price — what PnL is actually computed against.
- Funding Rate — the per-interval payment mechanics.
- Margin — how PnL feeds the maintenance-margin check.