Positions
This guide explains how NautilusTrader creates and updates positions from order fills and calculates profit
and loss (PnL). It also explains how NautilusTrader preserves closed cycles under NETTING order management
system (OMS) configurations.
Overview
A position records exposure to an instrument during an open-close cycle. It aggregates the fills assigned to one position ID and tracks its quantity, average prices, realized PnL, commissions, and related identifiers. Use a market price with the position's methods to calculate unrealized PnL and notional value.
The execution engine creates positions when orders fill and tracks them from open to close. OMS configuration determines whether fills share a net position or stay in separate hedged positions.
Position lifecycle
Creation
The system opens a position on the first fill:
- NETTING OMS: Opens on the first fill for an instrument and strategy. The position uses the
deterministic ID
{instrument_id}-{strategy_id}. - HEDGING OMS: Opens on first fill for a new
position_id(multiple positions per instrument).
A position tracks:
- Opening order and fill details.
- Entry side (
BUYorSELL). - Quantity and average entry price after applying the opening fill.
- Timestamps for initialization and opening.
You can access positions through the Cache using self.cache.position(position_id) or
self.cache.positions(instrument_id=instrument_id) from within your actors/strategies.
Updates
As additional fills occur, the position:
- Aggregates quantities from buy and sell fills.
- Recalculates average entry and exit prices.
- Updates peak quantity for the current cycle.
- Tracks the current cycle's order IDs and trade IDs.
- Accumulates commissions by currency.
Closure
A position closes when the net quantity becomes zero (FLAT). At closure:
- The closing order ID is recorded. A settlement at contract expiration leaves it empty.
- Duration is calculated from open to close.
- Final realized PnL is computed.
- In either OMS type, when the position later reopens under the same ID, the engine snapshots the closed state to preserve historical PnL (see Position snapshotting).
Settlement at contract expiration
In a live node, an InstrumentClose of type CONTRACT_EXPIRED settles binary option positions only on venues
served by the node's execution clients. Under that condition, it settles each open position in that instrument
at the close price (such as 1 for a winning outcome and 0 for a losing one). For each position, the
execution engine:
- Closes the current cycle at the close price without creating an order or fill.
- Books realized PnL for the settled quantity. Fill quantities and commissions stay as filled, so opening fees stay in realized PnL.
- Emits one
PositionClosedwith no closing order ID.
The first close applied to an instrument is authoritative. A repeated or conflicting close, or a restart that restores the settled position, does not settle it again. After settlement, a fill or fill void for the instrument still updates its order but no longer changes positions. The engine logs a warning and emits no position event.
Settlement does not redeem venue assets or change account balances. Venue account reports stay authoritative for cash, so an unredeemed payout counts toward account balances only once the venue reports the redeemed funds.
Backtests and sandbox paper trading keep the simulated venue's expiration handling instead. The matching engine closes positions with expiration fills, and those fills credit the simulated account. The live node does not apply engine settlement on a venue whose execution client settles expiring contracts itself, as the sandbox client does.
Order fill aggregation
Positions aggregate order fills to maintain an accurate view of market exposure. The aggregation process handles both sides of trading activity:
Buy fills
When a BUY order fills:
- Increases long exposure or reduces short exposure.
- Updates average entry price for opening trades.
- Updates average exit price for closing trades.
- Calculates realized PnL for any closed portion.
Sell fills
When a SELL order fills:
- Increases short exposure or reduces long exposure.
- Updates average entry price for opening trades.
- Updates average exit price for closing trades.
- Calculates realized PnL for any closed portion.
Net position calculation
The position maintains a signed_qty field representing the net exposure:
- Positive values indicate
LONGpositions. - Negative values indicate
SHORTpositions. - Zero indicates a
FLAT(closed) position.
# Example: Position aggregation
# Initial BUY 100 units at $50
signed_qty = +100 # LONG position
# Subsequent SELL 150 units at $55
signed_qty = -50 # Closes the LONG cycle and opens a SHORT cycle
# Final BUY 50 units at $52
signed_qty = 0 # Position FLAT (closed)Reversal accounting
An opposite-side fill larger than the open quantity closes the existing exposure and opens the residual in the other direction. The execution engine splits this fill into a close and a new opening, with closed-state retention governed by Position snapshotting.
An unsplit reversal fill applied directly to one Position starts a new accounting episode for the residual
exposure. The episode has these resets, including during fill-void replay:
avg_px_openbecomes the reversal fill price.avg_px_closebecomesNone, andrealized_returnbecomes zero until a subsequent closing fill.buy_qtyandsell_qtyrestart with only the opening residual on the new entry side and zero on the other side. Later close averages thus exclude volume from the previous direction.
Only the closing portion realizes PnL against the previous entry price. The object's realized_pnl and
commission totals stay cumulative across this reversal, with the fill's commission counted once. The reset
does not clear fill history, opening timestamps, or peak quantity. If the position instead reaches FLAT and
a later fill reopens it, the full cycle resets, including realized PnL and commissions.
These episode resets apply to fill-driven reversals. A quantity adjustment that changes the position's side does not perform the same reset.
Position adjustments
Position adjustments record quantity or PnL changes that occur outside normal order fills. The
system represents these changes as PositionAdjusted events.
Base-currency commissions
When trading spot currency pairs (for example, BTC/USDT) or FX spot, commissions paid in the base currency directly affect the net quantity received or delivered:
- Opening fills: Commission is deducted from the traded quantity. A buy of 1.0 BTC with 0.001 BTC commission results in a net long position of 0.999 BTC.
- Closing fills: Commission is applied to
signed_qtybecause it affects actual inventory. A sale of a 0.999 BTC LONG position with 0.000999 BTC commission leaves you SHORT 0.000999 BTC, not FLAT. You gave up 0.999999 BTC total. - Flips: Commission affects the final position size on both sides of the flip.
Base-currency commissions only apply to spot currency pairs and FX spot instruments where the
commission currency matches instrument.base_currency. For other instruments, commissions are
tracked separately and do not affect position quantity.
Funding payments
Funding adjustments track periodic payments for perpetual futures without affecting position
quantity. They use quantity_change = None and can include a PnL change.
Adjustment tracking
The position exposes its retained adjustments:
position.adjustments()returns the list of allPositionAdjustedevents.- Each adjustment includes its type (
COMMISSIONorFUNDING), quantity or PnL change, reason, event ID, and timestamps. - The current adjustment history is cleared when a closed position reopens.
- After
purge_events_for_order(), a position with fills regenerates commission adjustments from the surviving fills and reapplies non-commission adjustments. With no fills, the position becomes an emptyFLATshell and clears its adjustment history.
OMS types and position management
NautilusTrader supports two position management modes. A strategy configured with
OmsType.UNSPECIFIED uses the owning execution client's OMS type. For ownership resolution and position ID rules,
see the Execution guide.
NETTING
In NETTING mode, fills for each instrument and strategy are aggregated into a single position:
- One position per instrument and strategy.
- All fills contribute to the same position.
- A fill that crosses zero closes the current cycle and opens a new cycle on the opposite side.
- Historical snapshots preserve closed position states.
HEDGING
In HEDGING mode, multiple positions can exist for the same instrument:
- Multiple simultaneous
LONGandSHORTpositions. - Each position has a unique position ID.
- Positions are tracked independently.
- No automatic netting across positions.
- A fill with a new position ID creates a separate position. If a later fill reuses a closed position ID, the engine archives the closed cycle before replacing the cached state.
- A virtual position flip creates a new ID and keeps the original closed position in the cache. Thus, that path does not need a closed-cycle snapshot.
After a virtual flip, later fills for the same order follow the destination position even if they carry the original position ID. The engine derives the destination from retained order and position history, including after cache recovery and fill-void corrections. It rejects fills when that history is incomplete or ambiguous.
Virtual HEDGING positions always keep replay history across reopens, even when
carry_replay_events_on_reopen=False, to preserve this routing when a position ID is reused.
HEDGING can increase margin requirements when a venue maintains long and short positions
independently. A venue with a NETTING OMS exposes only its net position, even when NautilusTrader
tracks multiple virtual positions. Check the venue's position mode and margin rules.
Strategy vs venue OMS
Strategy and venue OMS types can differ:
| Strategy OMS | Venue OMS | Result |
|---|---|---|
NETTING | NETTING | One position per instrument and strategy. |
HEDGING | HEDGING | Multiple positions per instrument and strategy. |
NETTING | HEDGING | One virtual position across the venue positions. |
HEDGING | NETTING | Multiple virtual positions against the venue's single net position. |
Align the strategy and venue OMS types unless the strategy requires virtual positions. See the integration guide for the venue's position-mode configuration.
Position snapshotting
Position snapshotting preserves closed cycles for PnL tracking and reporting when a later fill reopens a closed position.
Why snapshotting matters
When a position closes (becomes FLAT) and then reopens under the same ID with a new trade, its object
resets. This reset tracks the new exposure. Without snapshotting, the historical realized PnL from the
previous position cycle would be lost.
How it works
When a fill reopens a closed position under the same ID, the execution engine archives the closed
state before opening the next cycle. This applies to both NETTING and HEDGING OMS.
A HEDGING flip using a non-virtual ID follows a separate path: it reuses the ID without
archiving the closed cycle.
The snapshot preserves:
- Final quantities and prices.
- Realized PnL.
- All fill events.
- Commission totals.
The cache stores snapshots by position ID. The active cache entry then represents the new cycle, while previous snapshots stay accessible. The Portfolio includes their realized PnL in instrument totals.
A fill void that corrects a fill from an earlier cycle is the one exception. The correction moves the cycle boundaries the stored snapshots describe. Thus, the engine replaces them with the cycles the corrected history actually closes, keeping each counted once. See Position replay across NETTING cycles.
This closed-cycle archive differs from optional position state snapshots:
snapshot_positions=Truepublishes state when a position opens, changes, or closes.snapshot_positions_interval_secsperiodically publishes all open positions.
A cache with a Redis or Postgres backing also persists these snapshots. Without cache backing, both paths
publish snapshots on the in-process message bus without persisting them. See
LiveExecutionEngineConfig
for these settings.
Example scenario
# NETTING OMS Example
# Cycle 1: Open LONG position
BUY 100 units at $50 # Position opens
SELL 100 units at $55 # Position closes, PnL = $500
# Snapshot taken preserving $500 realized PnL
# Cycle 2: Open SHORT position
SELL 50 units at $54 # Position reopens (SHORT)
BUY 50 units at $52 # Position closes, PnL = $100
# Snapshot taken preserving $100 realized PnL
# Total realized PnL = $500 + $100 = $600 (from snapshots)PnL calculations
Position PnL calculations account for instrument specifications and market conventions.
Realized PnL
The price component of realized PnL is calculated when fills partially or fully close a position, or when a settlement at contract expiration closes it. Commissions in the position's cost currency affect realized PnL as each fill arrives.
# For standard instruments
# LONG: realized_pnl = (exit_price - entry_price) * closed_quantity * multiplier
# SHORT: realized_pnl = (entry_price - exit_price) * closed_quantity * multiplier
# For inverse instruments (side-aware)
# LONG: realized_pnl = closed_quantity * multiplier * (1/entry_price - 1/exit_price)
# SHORT: realized_pnl = closed_quantity * multiplier * (1/exit_price - 1/entry_price)The position side selects the formula. Premium-based inverse instruments (such as coin-settled options) quote the premium in the base currency. Thus, they use the standard formula and report PnL and notional value in the base currency.
Unrealized PnL
unrealized_pnl() calculates PnL for an open position from the supplied price. You can pass a
bid, ask, mid, last, or mark price:
position.unrealized_pnl(last_price) # Using last traded price
position.unrealized_pnl(bid_price) # Conservative for LONG positions
position.unrealized_pnl(ask_price) # Conservative for SHORT positionsFor a FLAT position, it returns Money(0, cost_currency) regardless of the supplied price.
Total PnL
total_pnl() combines the realized and unrealized components:
total_pnl = position.total_pnl(current_price)
# Returns realized_pnl + unrealized_pnlCurrency considerations
- PnL is calculated in the instrument's cost currency: quote for linear contracts, base for inverse contracts, and settlement for quanto contracts.
- For Forex, the cost currency is typically the quote currency.
- Portfolio aggregates realized PnL per instrument in cost currency.
- Multi-currency totals require conversion outside the Position class.
Commissions and costs
Positions track fill commissions:
- Commissions are accumulated by currency.
- Each fill's commission is added to the running total.
- Multiple commission currencies are supported.
- Realized PnL includes commissions only when denominated in the position's cost currency.
- Other commissions are tracked separately and may require conversion.
commissions = position.commissions()
# Returns list[Money] with aggregated commission totals per currency
notional = position.notional_value(current_price)
# Returns Money in quote (linear), base (inverse), or settlement currency (quanto)In Python, notional_value() raises ValueError under these conditions:
- An inverse position lacks a base currency.
- The supplied price is not positive for a non-premium inverse position.
- The result cannot be represented as
Money.
Rust callers can use try_notional_value() to handle these calculation errors. notional_value() panics if
the calculation fails.
Position properties and state
Identifiers
id: Unique position identifier.instrument_id: The traded instrument.account_id: Account where position is held.trader_id: The trader who owns the position.strategy_id: The strategy managing the position.opening_order_id: Client order ID that opened the position.closing_order_id: Client order ID that closed the position, if a fill closed it.
Position state
side: Current position side (LONG,SHORT, orFLAT).entry: Opening side for the current cycle (BUYforLONG,SELLforSHORT). Updates when the position reverses direction.quantity: Current absolute position size.signed_qty: Signed position size (positive forLONG, negative forSHORT).peak_qty: Maximum quantity reached during the current open-close cycle.is_open: Whether position is open.is_closed: Whether position is closed (FLAT).is_long: Whether position side isLONG.is_short: Whether position side isSHORT.
Pricing and valuation
avg_px_open: Average entry price.avg_px_close: Average exit price when closing.realized_pnl: Realized profit/loss.realized_return: Realized return as decimal (for example, 0.05 for 5%).quote_currency: Quote currency of the instrument.base_currency: Base currency if applicable.settlement_currency: Currency for PnL settlement.
See Reversal accounting for how price averages and returns reset while realized PnL stays cumulative when an unsplit fill reverses one position.
Instrument specifications
multiplier: Contract multiplier.price_precision: Decimal precision for prices.size_precision: Decimal precision for quantities.is_inverse: Whether instrument is inverse.
Timestamps
ts_init: When position was initialized.ts_opened: When position was opened.ts_last: Last update timestamp.ts_closed: When the position was closed, if closed.duration_ns: Duration from open to close in nanoseconds, or zero while open.
Associated data
symbol: The instrument's ticker symbol.venue: The trading venue.client_order_ids: Unique client order IDs for retained fills in the current cycle.venue_order_ids: Unique venue order IDs for retained fills in the current cycle.trade_ids: Unique trade IDs for retained fills in the current cycle.events: Retained order fill events in the current cycle.adjustments: Retained position adjustments in the current cycle.event_count: Number of retained fill events in the current cycle.last_event: Most recently retained fill event.last_trade_id: Trade ID of the most recently retained fill.
For complete type information and detailed property documentation, see the Position API Reference.
Events and tracking
Each Position object records the fills and adjustments for its current open-close cycle:
- Fill events stay in application order.
- Client order, venue order, and trade ID accessors return sorted, unique values.
event_countreports the number of retained fill events.- Closed
NETTINGcycles keep their event history in the cache snapshots described above.
This data supports:
- Detailed position analysis.
- Trade reconciliation.
- Performance attribution.
- Audit trails.
Use position.events() to access the current cycle's retained fills for reconciliation.
The position.trade_ids() result helps match against broker statements.
See the Execution guide for reconciliation best practices.
Numerical precision
Position uses f64 for signed quantity, average prices, realized returns, and PnL intermediates. Price,
Quantity, and Money keep their fixed-point representations at the API boundary, but conversions between
these types and f64 can introduce rounding.
f64 represents each integer exactly only through 2^53. Above that boundary, conversion can lose low-order
bits. It provides roughly 15 to 16 significant decimal digits rather than a fixed number of exact decimal
places.
The design avoids the higher computational cost of arbitrary-precision arithmetic. The
average-price calculation also avoids multiplying raw fixed-point values because those products can
overflow their integer representation. Average prices reuse the prior f64 average, and realized
PnL is converted between Money and f64 as fills accumulate. The resulting precision depends on
the values, settlement-currency precision, and sequence of fills.
Numerical limits affect quantity, inverse PnL, and high-precision fills:
- Quantity:
quantityis derived fromsigned_qtyat the instrument'ssize_precision. If that conversion rounds a residual quantity to zero, the position becomesFLATand normalizessigned_qtyto zero. - Inverse PnL: calculations for non-premium instruments reject nonpositive open or close prices and
positive prices below
1e-15. - DeFi precision: with the
defifeature, conversion of aPriceorQuantitywith more than 16 decimal places tof64panics. Thus,Positiondoes not support 17- or 18-decimal fill values.
Tests in crates/model/src/position.rs cover a 0.01 USD commission, nine-decimal price inputs, 100
sequential fills, prices from 0.00001 to 99999.99999, and same-price round trips. These cases do
not establish a universal precision bound.
For exact decimal arithmetic in regulatory reporting or audit records, perform a separate decimal calculation
from the original fills and adjustments. Keep that calculation. Converting Position float outputs back to
decimal, including through signed_decimal_qty(), cannot restore discarded precision. Position does not
provide an exact-decimal guarantee. Validate the instruments, currencies, amount ranges, and fill sequences
used by the application.
Integration with other components
Positions interact with several key components:
- Portfolio: Aggregates position exposure and PnL across instruments and strategies.
- ExecutionEngine: Creates and updates positions from fills.
- Cache: Stores current position state and closed-cycle snapshots.
- RiskEngine: Reads open positions when it checks whether an order reduces exposure.
Positions are not created for spread instruments. Contingent orders can still trigger for spreads, but they operate without position linkage. The engine handles spread instruments separately from regular positions.