Positions & PnL
Position Representation
Each open position is represented as the following set of fields:
| Field | Type | Description |
|---|---|---|
symbol | string | Market identifier. |
side | enum | LONG or SHORT, derived from the sign of signed_size. |
signed_size | decimal | Position size. Positive for long, negative for short. Zero means no open position. |
avg_entry_price | decimal | Running quantity-weighted-average entry price. |
unrealized_pnl | decimal | Mark-to-market PnL using current mark price. Computed on demand. |
realized_pnl_lifetime | decimal | Cumulative realized PnL since the position was opened. Resets when position fully closes. |
funding_paid_lifetime | decimal | Cumulative funding received (positive) or paid (negative) since the position was opened. |
margin_mode | enum | CROSS or ISOLATED. |
allocated_margin | decimal | Only present when margin_mode = ISOLATED. The collateral allocated specifically to this position. |
A position is closed when signed_size = 0. When a closed position is re-opened, all lifetime fields reset.
Average Entry Price
Origin uses average entry price (running quantity-weighted average) for position cost basis. There is no FIFO, LIFO, or per-fill lot tracking: every unit of an open position shares the same cost basis, recomputed each time the position is increased.
Let pos_old be the signed size before a fill, trade_size the signed quantity of the fill (positive for buys, negative for sells), and trade_price the fill price. The new position size is:
pos_new = pos_old + trade_size
The update to avg_entry_price and realized_pnl depends on whether the fill increases, reduces, or reverses the position.
Position Increase (same direction)
A fill increases the position when sign(pos_old) == sign(trade_size), or when pos_old == 0. The average entry price updates as a quantity-weighted average:
avg_entry_price_new
= ( avg_entry_price_old × |pos_old| + trade_price × |trade_size| )
/ |pos_new|
Realized PnL on this fill: zero. Trading fees are charged separately to collateral (Section 6.4).
Position Reduction (opposite direction, partial close)
A fill reduces the position when sign(trade_size) == -sign(pos_old) and |trade_size| ≤ |pos_old|. The average entry price does not change:
avg_entry_price_new = avg_entry_price_old
Realized PnL on the closed portion:
realized_pnl_delta
= sign(pos_old) × ( trade_price - avg_entry_price_old ) × |trade_size|
This generalizes both directions: a long being reduced (pos_old > 0, trade_size < 0) realizes positive PnL when sold above entry; a short being reduced (pos_old < 0, trade_size > 0) realizes positive PnL when bought below entry. The realized PnL is added to the user's collateral balance immediately on settlement.
Position Reversal (opposite direction, exceeds existing size)
A fill reverses the position when sign(trade_size) == -sign(pos_old) and |trade_size| > |pos_old|. The fill is accounted as two atomic operations:
- Close. Realize PnL on the entire existing
|pos_old|quantity attrade_priceper Section 6.2.2. - Open new. Open a new position in the opposite direction with size
|trade_size| - |pos_old|attrade_price. Setavg_entry_price = trade_price. Lifetime fields reset.
Unrealized & Realized PnL
Unrealized PnL
Unrealized PnL is mark-to-market against the current mark price (Section 8.2):
unrealized_pnl = signed_size × ( mark_price - avg_entry_price )
Unrealized PnL is included in account_equity for the purpose of margin ratio (Section 7.2). It is not credited to free collateral until a fill realizes it.
Worked Example: Increase, Reduce, Reverse
Starting state: no position. All fills assumed to be on BTC-PERP.
Trade 1: BUY 2 BTC @ 80,000
pos_old = 0
pos_new = 0 + 2 = +2 (LONG)
avg_entry_price_new = ( 0 × 0 + 80,000 × 2 ) / 2 = 80,000
realized_pnl_delta = 0
Trade 2: BUY 1 BTC @ 82,000 # increase
pos_old = +2
pos_new = +2 + 1 = +3
avg_entry_price_new = ( 80,000 × 2 + 82,000 × 1 ) / 3 = 80,666.67
realized_pnl_delta = 0
Trade 3: SELL 1 BTC @ 85,000 # reduce
pos_old = +3
pos_new = +3 - 1 = +2
avg_entry_price_new = 80,666.67 (unchanged)
realized_pnl_delta = +1 × ( 85,000 - 80,666.67 ) × 1 = +4,333.33
Trade 4: SELL 3 BTC @ 86,000 # reversal: closes 2 long, opens 1 short
Step 1, close existing long:
realized_pnl_delta = +1 × ( 86,000 - 80,666.67 ) × 2 = +10,666.67
pos after step 1 = 0
(lifetime fields preserved through close)
Step 2, open new short:
pos_new = 0 + (-1) = -1
avg_entry_price = 86,000
realized_pnl_delta = 0
(lifetime fields reset)
Total realized price-PnL across these trades: 4,333.33 + 10,666.67 = 15,000.
Funding & Fees
Funding Payments
At each funding interval (Section 8.4), funding payments are applied to each open position in proportion to its size:
funding_payment = signed_size × mark_price × funding_rate
# positive value: account receives funding
# negative value: account pays funding
Funding payments are recorded directly to realized PnL (no impact on avg_entry_price):
realized_pnl_lifetime += funding_payment
funding_paid_lifetime += funding_payment
Funding settles immediately to the user's collateral balance.
If during Trade 3 from the example above a funding payment is applied (e.g., position is long 2 at mark $84,000 and funding rate is +0.10%):
funding_payment = +2 × 84,000 × 0.001 = +168
(long pays a positive funding rate to shorts)
collateral_balance -= 168
realized_pnl_lifetime += -168
funding_paid_lifetime += -168
Trading Fees
Trading fees are charged at the time of fill and settle directly to collateral. They are tracked separately from realized PnL on the position so that PnL metrics reported to the user reflect price-driven gains and losses, while fee impact is visible on its own line. Total economic PnL for a position is realized_pnl_lifetime + funding_paid_lifetime - cumulative_fees_paid.
Isolated-Mode PnL
For a position in ISOLATED margin mode, all PnL accounting works identically except that account_equity for the position is computed against allocated_margin rather than the global collateral pool:
isolated_account_equity = allocated_margin + unrealized_pnl
Realized PnL on an isolated position settles to allocated_margin, not to global collateral, until the position is closed. Closing an isolated position releases the remaining allocated_margin (plus accumulated realized PnL minus realized losses) back to the global collateral pool.

