{"record":{"id":"860491a0b9a605aa","repo":"nautechsystems/nautilus_trader","slug":"duplicate-fill-event-for-position-position-id-860491","errorCode":null,"errorMessage":"Duplicate fill event for position {position_id}: {}","messagePattern":"Duplicate fill event for position (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/infrastructure/src/sql/queries.rs","lineNumber":960,"sourceCode":"                .map_err(|e| anyhow::anyhow!(\"Failed to decode position replay state: {e}\"));\n        }\n\n        let fills = Self::load_position_events(pool, position_id).await?;\n        let Some((first_fill, remaining_fills)) = fills.split_first() else {\n            return Ok(None);\n        };\n        let Some(instrument) = Self::load_instrument(pool, &first_fill.instrument_id).await? else {\n            log::error!(\n                \"Instrument not found for position {position_id}: {}\",\n                first_fill.instrument_id\n            );\n            return Ok(None);\n        };\n\n        let mut position = Position::new(&instrument, first_fill.clone());\n        for fill in remaining_fills {\n            if position.trade_ids().contains(&fill.trade_id) {\n                anyhow::bail!(\n                    \"Duplicate fill event for position {position_id}: {}\",\n                    fill.trade_id\n                );\n            }\n            position.apply(fill);\n        }\n\n        Ok(Some(position))\n    }\n\n    /// Loads and replays all `Position` entries via the provided `pool`.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if loading position IDs or replaying any position fails.\n    pub async fn load_positions(pool: &PgPool) -> anyhow::Result<Vec<Position>> {\n        let position_ids: Vec<PositionId> = sqlx::query(\n            r#\"","sourceCodeStart":942,"sourceCodeEnd":978,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/infrastructure/src/sql/queries.rs#L942-L978","documentation":"This error is thrown by `load_position` in the SQL cache layer when rebuilding a `Position` from persisted fill events. After constructing the position from the first fill, each remaining fill's `trade_id` is checked against `position.trade_ids()`; if a fill with a trade_id already applied to the position appears again, the load aborts. It indicates the persisted position event stream contains duplicate fills, so the position cannot be reconstructed deterministically.","triggerScenarios":"Calling `PositionQueries::load_position(pool, position_id)` where the rows fetched for that position (beyond the first fill) contain a `trade_id` already present in the position's trade_ids — i.e. the `position` events table has a duplicate fill/`position_opened`/`position_modified` event with the same trade_id.","commonSituations":"Double-writing the same fill event to Postgres (e.g. a retried INSERT without deduplication, or two writers persisting the same trade), a partially-failed batch insert that was re-run, or a corrupted/manual copy of rows between environments.","solutions":["Inspect the persisted position events for the given position_id and delete the duplicate fill rows (dedupe on trade_id).","Find the writer that inserted the fill twice (usually a retry after timeout without idempotency) and make the INSERT idempotent, e.g. a UNIQUE constraint on (account_id, instrument_id, trade_id) with ON CONFLICT DO NOTHING.","If the duplicate came from an adapter re-emitting fills, guard upstream where fills are persisted (skip events whose trade_id already exists)."],"exampleFix":"// before: blindly inserting each fill event\nsqlx::query(\"INSERT INTO position_events (...) VALUES (...)\")\n    .execute(pool).await?;\n\n// after: make persistence idempotent on trade_id\nsqlx::query(\n    \"INSERT INTO position_events (...) VALUES (...) ON CONFLICT (trade_id) DO NOTHING\",\n)\n.execute(pool).await?;","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match PositionQueries::load_position(&pool, position_id).await {\n    Ok(Some(pos)) => pos,\n    Ok(None) => return /* position absent */,\n    Err(e) if e.to_string().contains(\"Duplicate fill event\") => {\n        // quarantine the position rows and re-ingest from source of truth\n        log::error!(\"corrupt position stream: {e}\");\n        /* re-ingest */\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Add a UNIQUE constraint on trade_id in persisted fill/position events and use ON CONFLICT DO NOTHING.","Make all event-persistence writes idempotent so retries cannot double-insert.","Monitor for duplicate trade_ids when copying or migrating cache data."],"tags":["database","data-integrity","duplicate-data","rust"],"backgroundTag":"database-query-failed","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}