How to Track and Manage Positions Across Multiple Venues in Nautilus Trader

Nautilus Trader maintains an in-memory Cache with bi-directional venue indexes to isolate, query, and reconcile positions per exchange while preserving a unified portfolio view.

Multi-venue trading strategies require precise segregation of exposure per exchange to manage risk limits and reconcile external broker reports. Nautilus Trader solves this through a centralized Cache class that indexes every Position by its originating Venue, enabling O(1) lookups and seamless live reconciliation with execution engines.

Architecture: The Cache as Single Source of Truth

All position state lives in nautilus_trader/cache/cache.pyx, which stores Position objects in an internal _positions dictionary keyed by PositionId. Every position carries a venue attribute linking it to its originating exchange, while the cache maintains separate index sets to distinguish open from closed states.

Bi-Directional Indexing by Venue

The cache builds several lookup tables to enable fast venue-scoped queries without full scans:

  • _index_venue_positions: dict[Venue, set[PositionId]] — Maps each venue to the set of position IDs belonging to it
  • _index_positions_open — Set of IDs for active positions
  • _index_positions_closed — Set of IDs for finalized positions
  • _index_position_strategy — Maps strategies to their position IDs for additional filtering

When a fill arrives via the execution engine, the Position.apply() method in nautilus_trader/model/position.pyx updates the signed_qty, side, and timestamps. The Venue embedded in the OrderFilled event ensures the cache stores the position under the correct venue key in _index_venue_positions.

Querying Positions by Venue

The cache exposes high-level helpers that accept a venue filter and resolve relevant PositionIds from the appropriate indexes before materializing objects.

Filtering Open Positions

The positions_open() method accepts an optional venue parameter and returns only positions matching that exchange:


# nautilus_trader/cache/cache.pyx

def positions_open(
    self,
    Venue venue = None,
    InstrumentId instrument_id = None,
    StrategyId strategy_id = None,
    PositionSide side = PositionSide.NO_POSITION_SIDE,
    AccountId account_id = None,
) -> list[Position]:
    """
    Return all open positions with the given query filters.
    """
    cdef set position_ids = self.position_open_ids(
        venue, instrument_id, strategy_id, account_id)
    return self._get_positions_for_ids(position_ids, side)

Internally, position_open_ids() pulls IDs from _index_positions_open and, when a venue is supplied, narrows the set using _index_venue_positions[venue]. A strategy trading on multiple venues can retrieve isolated exposure instantly:

from nautilus_trader.model.identifiers import Venue

# Retrieve NYSE positions only

nyse_positions = self.cache.positions_open(venue=Venue('NYSE'))

# Retrieve LME positions only  

lme_positions = self.cache.positions_open(venue=Venue('LME'))

Aggregating Exposure Across Venues

To calculate net quantity per venue, iterate through open positions and aggregate by the venue attribute:

from collections import defaultdict
from nautilus_trader.model.identifiers import Venue

def net_quantity_by_venue(cache):
    net = defaultdict(float)
    for pos in cache.positions_open():          # all venues if no filter

        net[pos.venue] += pos.signed_qty        # signed_qty: LONG (+), SHORT (-)

    return net

net_qty = net_quantity_by_venue(trader._cache)
for venue, qty in net_qty.items():
    print(f"Venue {venue.value}: net qty {qty}")

Live Reconciliation with External Venues

When live brokers report their own view of positions, the system must merge external state with the internal cache while preserving venue granularity.

PositionStatusReport Handling

The PositionStatusReport class in nautilus_trader/execution/reports.py carries the broker's position ID and venue information:


# nautilus_trader/execution/reports.py

class PositionStatusReport(ExecutionReport):
    def __init__(self,
                 account_id: AccountId,
                 instrument_id: InstrumentId,
                 position_side: PositionSide,
                 quantity: Quantity,
                 report_id: UUID4,
                 ts_last: int,
                 ts_init: int,
                 venue_position_id: PositionId | None = None,
                 avg_px_open: Decimal | None = None):

The venue_position_id field maps the exchange's native position identifier to Nautilus Trader's internal PositionId.

Execution Engine Reconciliation Flow

In nautilus_trader/live/execution_engine.py, the engine queries external position status and processes reports:


# live/execution_engine.py

venue_positions: dict[InstrumentId, PositionStatusReport] = await self._query_position_status_reports()
await self._process_venue_reported_positions(positions_by_instrument, venue_positions)

The _process_venue_reported_positions() method matches the external venue_position_id to internal cache entries (or creates new Position objects) and updates quantities while maintaining the venue mapping in _index_venue_positions.

Reporting and Analytics

The Trader component aggregates cache data into Pandas DataFrames for risk analysis and PnL attribution per venue:


# nautilus_trader/trading/trader.py

def generate_positions_report(self) -> pd.DataFrame:
    """
    Generate a positions report.
    """
    positions = self._cache.positions()
    snapshots = self._cache.position_snapshots()
    return ReportProvider.generate_positions_report(positions, snapshots)

The resulting DataFrame includes a venue column, enabling downstream analytics to filter and aggregate by exchange.

Summary

  • Venue Isolation: The cache uses _index_venue_positions to maintain O(1) lookups of all positions belonging to a specific exchange.
  • Filtered Queries: Call cache.positions_open(venue=Venue('BINANCE')) to retrieve only that venue's exposure.
  • State Updates: The Position.apply() method processes fills while preserving venue linkage through the OrderFilled event's embedded venue.
  • External Reconciliation: PositionStatusReport objects carry venue_position_id to synchronize broker-reported positions with internal state via the live execution engine.
  • Unified Reporting: Trader.generate_positions_report() outputs a venue-tagged DataFrame for cross-venue analytics.

Frequently Asked Questions

How does Nautilus Trader prevent position ID collisions across different venues?

Each position receives a unique internal PositionId while optionally storing the exchange-native identifier in venue_position_id. The _index_venue_positions dictionary maps Venue objects to sets of position IDs, ensuring complete isolation between exchanges even when trading identical instruments.

Can strategies query historical closed positions for a specific venue?

Yes. The cache provides positions_closed(venue=Venue('NYSE')) which queries the _index_positions_closed set and filters results using the same _index_venue_positions lookup used for open positions, returning only finalized positions for that exchange.

What occurs when a live broker reports a position unknown to the internal cache?

The execution engine in nautilus_trader/live/execution_engine.py detects the missing internal ID during _process_venue_reported_positions() and instantiates a new Position object using the PositionStatusReport data. It assigns the venue from the report and inserts the position into _index_venue_positions and _index_positions_open accordingly.

Is it possible to calculate portfolio-level net exposure across all venues simultaneously?

Yes. Omit the venue parameter when calling cache.positions_open() to retrieve all active positions across every exchange, then aggregate using pos.signed_qty and pos.venue attributes to compute both total portfolio exposure and per-venue breakdowns in a single pass.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →