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

> Learn how to track and manage positions across multiple venues in Nautilus Trader. Utilize its in-memory cache and venue indexes for efficient exchange reconciliation and a unified portfolio view.

- Repository: [Nautech Systems/nautilus_trader](https://github.com/nautechsystems/nautilus_trader)
- Tags: how-to-guide
- Published: 2026-02-19

---

**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 `PositionId`s 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:

```python

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

```python
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:

```python
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`](https://github.com/nautechsystems/nautilus_trader/blob/main/nautilus_trader/execution/reports.py) carries the broker's position ID and venue information:

```python

# 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`](https://github.com/nautechsystems/nautilus_trader/blob/main/nautilus_trader/live/execution_engine.py), the engine queries external position status and processes reports:

```python

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

```python

# 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`](https://github.com/nautechsystems/nautilus_trader/blob/main/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.