nautechsystems/nautilus_trader · error

Cannot persist position event with no position_id: {}

Error message

Cannot persist position event with no position_id: {}

What it means

event_position_id extracts the Option<PositionId> from an OrderFilled event and errors when it is None, because a position_event row cannot be written without its position_id foreign reference. This is a data-integrity guard: only fills that are part of an opened position carry a position_id.

Source

Thrown at crates/infrastructure/src/sql/queries.rs:1079

        .bind(event.commission.map(|commission| commission.to_string()))
        .bind(event.reconciliation)
        .bind(position_event_info)
        .bind(
            event
                .causation_id
                .map(|causation_id| causation_id.to_string()),
        )
        .bind(event.ts_event.to_string())
        .bind(event.ts_init.to_string())
        .execute(&mut **transaction)
        .await
        .map(|_| ())
        .map_err(|e| anyhow::anyhow!("Failed to insert into position_event table: {e}"))
    }

    fn event_position_id(event: &OrderFilled) -> anyhow::Result<PositionId> {
        event.position_id.ok_or_else(|| {
            anyhow::anyhow!(
                "Cannot persist position event with no position_id: {}",
                event.event_id
            )
        })
    }

    /// Inserts or updates an `AccountState` event via the provided `pool`.
    ///
    /// # Errors
    ///
    /// Returns an error if the SQL INSERT or UPDATE operation fails.
    pub async fn add_account(
        pool: &PgPool,
        updated: bool,
        account_event: AccountState,
    ) -> anyhow::Result<()> {
        if updated {
            let exists =

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check why the fill has no position: confirm the order opened/maintains a position and that the execution engine set position_id on the fill.
  2. Filter out fills with position_id None before handing them to the database cache if they are not meant to be persisted as position events.
  3. If constructing OrderFilled manually (tests/fixtures), set position_id to a valid PositionId.
  4. Log and skip such events instead of failing the whole persistence batch if they are expected.

Example fix

// before: cache errors on fills without a position
cache.insert_position_event(&event).await?;

// after: guard before persisting
if let Some(_pos) = event.position_id {
    cache.insert_position_event(&event).await?;
} else {
    log::warn!("Skipping persist, no position_id for event {}", event.event_id);
}
Defensive patterns

Strategy: type-guard

Validate before calling

// caller-side check before persisting
if event.position_id.is_none() {
    log::warn!("fill {} has no position_id; not a position event", event.event_id);
}

Type guard

fn has_position(event: &OrderFilled) -> bool {
    event.position_id.is_some()
}

Try / catch

match cache.insert_position_event(&event).await {
    Err(e) if e.to_string().contains("no position_id") => {
        log::warn!("skipping non-position fill {}: {e}", event.event_id);
    }
    other => other?,
}

Prevention

When it happens

Trigger: insert_position_event is called with an OrderFilled whose position_id field is None — e.g. fills on orders that never opened a position (position_id was not set by the emulation/exec engine), or an OrderFilled reconstructed from a stream/fixture that dropped the field.

Common situations: Persisting fill events from venues or custom adapters that do not populate position_id; replaying events through a path that lost the position linkage; testing with hand-constructed OrderFilled fixtures where position_id was left as None.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/ed398fb70942225f. Report an issue: GitHub.