nautilus_execution/reconciliation/
ids.rs1use nautilus_core::UnixNanos;
24use nautilus_model::{
25 enums::{OrderSide, OrderType},
26 identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId},
27 types::{Price, Quantity},
28};
29use uuid::Uuid;
30
31use super::types::FillSnapshot;
32
33const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
35const FNV_PRIME: u64 = 0x0100_0000_01b3;
36
37#[must_use]
44pub fn create_synthetic_venue_order_id(
45 fill: &FillSnapshot,
46 instrument_id: InstrumentId,
47) -> VenueOrderId {
48 let hash_suffix = synthetic_fill_id_suffix("venue_order", fill, Some(instrument_id));
49 let venue_order_id_value = format!("S-{:x}-{hash_suffix:08x}", fill.ts_event);
50 VenueOrderId::new(&venue_order_id_value)
51}
52
53#[must_use]
57pub fn create_synthetic_trade_id(fill: &FillSnapshot) -> TradeId {
58 let hash_suffix = synthetic_fill_id_suffix("trade", fill, None);
59 let trade_id_value = format!("S-{:x}-{hash_suffix:08x}", fill.ts_event);
60 TradeId::new(&trade_id_value)
61}
62
63#[must_use]
70#[expect(clippy::too_many_arguments)]
71pub fn create_inferred_reconciliation_trade_id(
72 account_id: AccountId,
73 instrument_id: InstrumentId,
74 client_order_id: ClientOrderId,
75 venue_order_id: Option<VenueOrderId>,
76 order_side: OrderSide,
77 order_type: OrderType,
78 filled_qty: Quantity,
79 last_qty: Quantity,
80 last_px: Price,
81 position_id: PositionId,
82 ts_last: UnixNanos,
83) -> TradeId {
84 let mut seed = String::from("reconciliation-fill");
85 append_seed_part(&mut seed, account_id.as_str());
86 append_seed_part(&mut seed, &instrument_id.to_string());
87 append_seed_part(&mut seed, client_order_id.as_str());
88 append_seed_part(
89 &mut seed,
90 venue_order_id.as_ref().map_or("", |value| value.as_ref()),
91 );
92 append_seed_part(&mut seed, order_side.as_ref());
93 append_seed_part(&mut seed, order_type.as_ref());
94 append_seed_part(&mut seed, &filled_qty.to_string());
95 append_seed_part(&mut seed, &last_qty.to_string());
96 append_seed_part(&mut seed, &last_px.to_string());
97 append_seed_part(&mut seed, position_id.as_str());
98 append_seed_part(&mut seed, &ts_last.as_u64().to_string());
99
100 TradeId::new(deterministic_uuid_from_seed("reconciliation-fill", &seed))
101}
102
103#[must_use]
109#[expect(clippy::too_many_arguments)]
110pub fn create_position_reconciliation_venue_order_id(
111 account_id: AccountId,
112 instrument_id: InstrumentId,
113 order_side: OrderSide,
114 order_type: OrderType,
115 quantity: Quantity,
116 price: Option<Price>,
117 venue_position_id: Option<PositionId>,
118 tag: Option<&str>,
119 ts_last: UnixNanos,
120) -> VenueOrderId {
121 let mut seed = String::from("position-reconciliation-order");
122 append_seed_part(&mut seed, account_id.as_str());
123 append_seed_part(&mut seed, &instrument_id.to_string());
124 append_seed_part(&mut seed, order_side.as_ref());
125 append_seed_part(&mut seed, order_type.as_ref());
126 append_seed_part(&mut seed, &quantity.to_string());
127 append_seed_part(
128 &mut seed,
129 &price.map_or_else(String::new, |value| value.to_string()),
130 );
131 append_seed_part(
132 &mut seed,
133 &venue_position_id.map_or_else(String::new, |value| value.to_string()),
134 );
135 append_seed_part(&mut seed, tag.unwrap_or(""));
136 append_seed_part(&mut seed, &ts_last.as_u64().to_string());
137
138 VenueOrderId::new(deterministic_uuid_from_seed(
139 "position-reconciliation-order",
140 &seed,
141 ))
142}
143
144fn synthetic_fill_id_suffix(
145 namespace: &str,
146 fill: &FillSnapshot,
147 instrument_id: Option<InstrumentId>,
148) -> u32 {
149 let mut hash: u64 = FNV_OFFSET_BASIS;
150
151 update_synthetic_fill_hash(&mut hash, namespace.as_bytes());
152 if let Some(instrument_id) = instrument_id {
153 update_synthetic_fill_hash(&mut hash, instrument_id.to_string().as_bytes());
154 }
155 update_synthetic_fill_hash(&mut hash, &fill.ts_event.to_le_bytes());
156 update_synthetic_fill_hash(&mut hash, fill.venue_order_id.as_str().as_bytes());
157 update_synthetic_fill_hash(&mut hash, order_side_tag(fill.side).as_bytes());
158 update_synthetic_fill_hash(&mut hash, fill.qty.to_string().as_bytes());
159 update_synthetic_fill_hash(&mut hash, fill.px.to_string().as_bytes());
160
161 hash as u32
162}
163
164fn deterministic_uuid_from_seed(namespace: &str, seed: &str) -> String {
165 let primary = stable_hash64(namespace.as_bytes(), &[seed.as_bytes()]);
166 let secondary = stable_hash64(b"uuid-alt", &[namespace.as_bytes(), seed.as_bytes()]);
167 let mut bytes = [0_u8; 16];
168 bytes[..8].copy_from_slice(&primary.to_be_bytes());
169 bytes[8..].copy_from_slice(&secondary.to_be_bytes());
170 bytes[6] = (bytes[6] & 0x0f) | 0x50;
171 bytes[8] = (bytes[8] & 0x3f) | 0x80;
172
173 Uuid::from_bytes(bytes).to_string()
174}
175
176fn stable_hash64(namespace: &[u8], parts: &[&[u8]]) -> u64 {
177 let mut hash: u64 = FNV_OFFSET_BASIS;
178
179 update_synthetic_fill_hash(&mut hash, namespace);
180
181 for part in parts {
182 update_synthetic_fill_hash(&mut hash, part);
183 }
184
185 hash
186}
187
188fn append_seed_part(seed: &mut String, value: &str) {
189 seed.push('|');
190 seed.push_str(value);
191}
192
193fn update_synthetic_fill_hash(hash: &mut u64, bytes: &[u8]) {
194 for &byte in bytes {
195 *hash ^= byte as u64;
196 *hash = hash.wrapping_mul(FNV_PRIME);
197 }
198
199 *hash ^= 0xff;
200 *hash = hash.wrapping_mul(FNV_PRIME);
201}
202
203fn order_side_tag(side: OrderSide) -> &'static str {
204 match side {
205 OrderSide::Buy => "BUY",
206 OrderSide::Sell => "SELL",
207 _ => "UNSPECIFIED",
208 }
209}