use nautilus_core::UnixNanos;
use nautilus_model::{
enums::{OrderSide, OrderType},
events::{OrderEventAny, OrderFilled},
identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId},
orders::{Order, OrderAny},
types::{Price, Quantity},
};
use uuid::Uuid;
use super::types::FillSnapshot;
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0100_0000_01b3;
const DETERMINISTIC_UUID_VERSION: u8 = 5;
#[must_use]
pub(super) fn create_synthetic_venue_order_id(
fill: &FillSnapshot,
instrument_id: InstrumentId,
) -> VenueOrderId {
let hash_suffix = synthetic_fill_id_suffix("venue_order", fill, Some(instrument_id));
let venue_order_id_value = format!("S-{:x}-{hash_suffix:08x}", fill.ts_event);
VenueOrderId::new(&venue_order_id_value)
}
#[must_use]
pub(super) fn create_synthetic_trade_id(fill: &FillSnapshot) -> TradeId {
let hash_suffix = synthetic_fill_id_suffix("trade", fill, None);
let trade_id_value = format!("S-{:x}-{hash_suffix:08x}", fill.ts_event);
TradeId::new(&trade_id_value)
}
#[must_use]
#[expect(clippy::too_many_arguments)]
pub fn create_inferred_reconciliation_trade_id(
account_id: AccountId,
instrument_id: InstrumentId,
client_order_id: ClientOrderId,
venue_order_id: Option<VenueOrderId>,
order_side: OrderSide,
order_type: OrderType,
filled_qty: Quantity,
last_qty: Quantity,
last_px: Price,
position_id: PositionId,
ts_last: UnixNanos,
) -> TradeId {
let mut seed = String::from("reconciliation-fill");
append_seed_part(&mut seed, account_id.as_str());
append_seed_part(&mut seed, &instrument_id.to_string());
append_seed_part(&mut seed, client_order_id.as_str());
append_seed_part(
&mut seed,
venue_order_id.as_ref().map_or("", |value| value.as_ref()),
);
append_seed_part(&mut seed, order_side.as_ref());
append_seed_part(&mut seed, order_type.as_ref());
append_seed_part(&mut seed, &filled_qty.to_string());
append_seed_part(&mut seed, &last_qty.to_string());
append_seed_part(&mut seed, &last_px.to_string());
append_seed_part(&mut seed, position_id.as_str());
append_seed_part(&mut seed, &ts_last.as_u64().to_string());
TradeId::new(deterministic_uuid_from_seed("reconciliation-fill", &seed))
}
#[must_use]
pub fn is_inferred_reconciliation_trade_id_format(trade_id: &TradeId) -> bool {
Uuid::parse_str(trade_id.as_str())
.is_ok_and(|uuid| uuid.get_version_num() == usize::from(DETERMINISTIC_UUID_VERSION))
}
#[must_use]
#[expect(clippy::too_many_arguments)]
pub fn create_position_reconciliation_venue_order_id(
account_id: AccountId,
instrument_id: InstrumentId,
order_side: OrderSide,
order_type: OrderType,
quantity: Quantity,
price: Option<Price>,
venue_position_id: Option<PositionId>,
tag: Option<&str>,
ts_last: UnixNanos,
) -> VenueOrderId {
let mut seed = String::from("position-reconciliation-order");
append_seed_part(&mut seed, account_id.as_str());
append_seed_part(&mut seed, &instrument_id.to_string());
append_seed_part(&mut seed, order_side.as_ref());
append_seed_part(&mut seed, order_type.as_ref());
append_seed_part(&mut seed, &quantity.to_string());
append_seed_part(
&mut seed,
&price.map_or_else(String::new, |value| value.to_string()),
);
append_seed_part(
&mut seed,
&venue_position_id.map_or_else(String::new, |value| value.to_string()),
);
append_seed_part(&mut seed, tag.unwrap_or(""));
append_seed_part(&mut seed, &ts_last.as_u64().to_string());
VenueOrderId::new(deterministic_uuid_from_seed(
"position-reconciliation-order",
&seed,
))
}
pub fn inferred_reconciliation_trade_ids(order: &OrderAny) -> anyhow::Result<Vec<TradeId>> {
let events = order.events();
let trade_ids = order.trade_ids();
let is_candidate = |fill: &OrderFilled| {
fill.reconciliation
&& is_inferred_reconciliation_trade_id_format(&fill.trade_id)
&& trade_ids.contains(&&fill.trade_id)
};
if !events
.iter()
.any(|event| matches!(event, OrderEventAny::Filled(fill) if is_candidate(fill)))
{
return Ok(Vec::new());
}
let Some((first, remaining)) = events.split_first() else {
return Ok(Vec::new());
};
let mut projected = OrderAny::from_events(vec![(*first).clone()]).map_err(|e| {
anyhow::anyhow!(
"cannot replay order {} for inferred fill detection: {e}",
order.client_order_id(),
)
})?;
let mut inferred_trade_ids = Vec::new();
for event in remaining {
projected.apply((*event).clone()).map_err(|e| {
anyhow::anyhow!(
"cannot replay order {} for inferred fill detection: {e}",
order.client_order_id(),
)
})?;
let OrderEventAny::Filled(fill) = event else {
continue;
};
if !is_candidate(fill) {
continue;
}
let external_position_id = PositionId::new(format!("{}-EXTERNAL", fill.instrument_id));
let position_ids = [fill.position_id, Some(external_position_id)];
let inferred = position_ids.into_iter().flatten().any(|position_id| {
create_inferred_reconciliation_trade_id(
fill.account_id,
fill.instrument_id,
fill.client_order_id,
Some(fill.venue_order_id),
fill.order_side,
fill.order_type,
projected.filled_qty(),
fill.last_qty,
fill.last_px,
position_id,
fill.ts_event,
) == fill.trade_id
});
if inferred {
inferred_trade_ids.push(fill.trade_id);
}
}
Ok(inferred_trade_ids)
}
fn synthetic_fill_id_suffix(
namespace: &str,
fill: &FillSnapshot,
instrument_id: Option<InstrumentId>,
) -> u32 {
let mut hash: u64 = FNV_OFFSET_BASIS;
update_synthetic_fill_hash(&mut hash, namespace.as_bytes());
if let Some(instrument_id) = instrument_id {
update_synthetic_fill_hash(&mut hash, instrument_id.to_string().as_bytes());
}
update_synthetic_fill_hash(&mut hash, &fill.ts_event.to_le_bytes());
update_synthetic_fill_hash(&mut hash, fill.venue_order_id.as_str().as_bytes());
update_synthetic_fill_hash(&mut hash, order_side_tag(fill.side).as_bytes());
update_synthetic_fill_hash(&mut hash, fill.qty.to_string().as_bytes());
update_synthetic_fill_hash(&mut hash, fill.px.to_string().as_bytes());
hash as u32
}
fn deterministic_uuid_from_seed(namespace: &str, seed: &str) -> String {
let primary = stable_hash64(namespace.as_bytes(), &[seed.as_bytes()]);
let secondary = stable_hash64(b"uuid-alt", &[namespace.as_bytes(), seed.as_bytes()]);
let mut bytes = [0_u8; 16];
bytes[..8].copy_from_slice(&primary.to_be_bytes());
bytes[8..].copy_from_slice(&secondary.to_be_bytes());
bytes[6] = (bytes[6] & 0x0f) | (DETERMINISTIC_UUID_VERSION << 4);
bytes[8] = (bytes[8] & 0x3f) | 0x80;
Uuid::from_bytes(bytes).to_string()
}
fn stable_hash64(namespace: &[u8], parts: &[&[u8]]) -> u64 {
let mut hash: u64 = FNV_OFFSET_BASIS;
update_synthetic_fill_hash(&mut hash, namespace);
for part in parts {
update_synthetic_fill_hash(&mut hash, part);
}
hash
}
fn append_seed_part(seed: &mut String, value: &str) {
seed.push('|');
seed.push_str(value);
}
fn update_synthetic_fill_hash(hash: &mut u64, bytes: &[u8]) {
for &byte in bytes {
*hash ^= byte as u64;
*hash = hash.wrapping_mul(FNV_PRIME);
}
*hash ^= 0xff;
*hash = hash.wrapping_mul(FNV_PRIME);
}
fn order_side_tag(side: OrderSide) -> &'static str {
match side {
OrderSide::Buy => "BUY",
OrderSide::Sell => "SELL",
}
}