|
| 1 | +// ------------------------------------------------------------------------------------------------- |
| 2 | +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. |
| 3 | +// https://nautechsystems.io |
| 4 | +// |
| 5 | +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); |
| 6 | +// You may not use this file except in compliance with the License. |
| 7 | +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | +// ------------------------------------------------------------------------------------------------- |
| 15 | + |
| 16 | +use std::{cell::RefCell, rc::Rc}; |
| 17 | + |
| 18 | +use nautilus_common::cache::Cache; |
| 19 | +use nautilus_model::{ |
| 20 | + enums::OmsType, |
| 21 | + identifiers::{PositionId, TradeId, Venue, VenueOrderId}, |
| 22 | + orders::OrderAny, |
| 23 | +}; |
| 24 | +use uuid::Uuid; |
| 25 | + |
| 26 | +pub struct IdsGenerator { |
| 27 | + venue: Venue, |
| 28 | + raw_id: u32, |
| 29 | + oms_type: OmsType, |
| 30 | + use_random_ids: bool, |
| 31 | + use_position_ids: bool, |
| 32 | + cache: Rc<RefCell<Cache>>, |
| 33 | + position_count: usize, |
| 34 | + order_count: usize, |
| 35 | + execution_count: usize, |
| 36 | +} |
| 37 | + |
| 38 | +impl IdsGenerator { |
| 39 | + pub fn new( |
| 40 | + venue: Venue, |
| 41 | + oms_type: OmsType, |
| 42 | + raw_id: u32, |
| 43 | + use_random_ids: bool, |
| 44 | + use_position_ids: bool, |
| 45 | + cache: Rc<RefCell<Cache>>, |
| 46 | + ) -> Self { |
| 47 | + Self { |
| 48 | + venue, |
| 49 | + raw_id, |
| 50 | + oms_type, |
| 51 | + cache, |
| 52 | + use_random_ids, |
| 53 | + use_position_ids, |
| 54 | + position_count: 0, |
| 55 | + order_count: 0, |
| 56 | + execution_count: 0, |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + pub fn reset(&mut self) { |
| 61 | + self.position_count = 0; |
| 62 | + self.order_count = 0; |
| 63 | + self.execution_count = 0; |
| 64 | + } |
| 65 | + |
| 66 | + fn get_venue_order_id(&mut self, order: &OrderAny) -> anyhow::Result<VenueOrderId> { |
| 67 | + // check existing on order |
| 68 | + if let Some(venue_order_id) = order.venue_order_id() { |
| 69 | + return Ok(venue_order_id); |
| 70 | + } |
| 71 | + |
| 72 | + // check existing in cache |
| 73 | + if let Some(venue_order_id) = self.cache.borrow().venue_order_id(&order.client_order_id()) { |
| 74 | + return Ok(venue_order_id.to_owned()); |
| 75 | + } |
| 76 | + |
| 77 | + let venue_order_id = self.generate_venue_order_id(); |
| 78 | + self.cache.borrow_mut().add_venue_order_id( |
| 79 | + &order.client_order_id(), |
| 80 | + &venue_order_id, |
| 81 | + false, |
| 82 | + )?; |
| 83 | + Ok(venue_order_id) |
| 84 | + } |
| 85 | + |
| 86 | + fn get_position_id(&mut self, order: &OrderAny, generate: Option<bool>) -> Option<PositionId> { |
| 87 | + let generate = generate.unwrap_or(true); |
| 88 | + if self.oms_type == OmsType::Hedging { |
| 89 | + { |
| 90 | + let cache = self.cache.as_ref().borrow(); |
| 91 | + let position_id_result = cache.position_id(&order.client_order_id()); |
| 92 | + if let Some(position_id) = position_id_result { |
| 93 | + return Some(position_id.to_owned()); |
| 94 | + } |
| 95 | + } |
| 96 | + if generate { |
| 97 | + self.generate_venue_position_id() |
| 98 | + } else { |
| 99 | + panic!("Position id should be generated. Hedging Oms type order matching engine doesnt exists in cache.") |
| 100 | + } |
| 101 | + } else { |
| 102 | + // Netting OMS (position id will be derived from instrument and strategy) |
| 103 | + let cache = self.cache.as_ref().borrow(); |
| 104 | + let positions_open = |
| 105 | + cache.positions_open(None, Some(&order.instrument_id()), None, None); |
| 106 | + if positions_open.is_empty() { |
| 107 | + None |
| 108 | + } else { |
| 109 | + Some(positions_open[0].id) |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + pub fn generate_trade_id(&mut self) -> TradeId { |
| 115 | + self.execution_count += 1; |
| 116 | + let trade_id = if self.use_random_ids { |
| 117 | + Uuid::new_v4().to_string() |
| 118 | + } else { |
| 119 | + format!("{}-{}-{}", self.venue, self.raw_id, self.execution_count) |
| 120 | + }; |
| 121 | + TradeId::from(trade_id.as_str()) |
| 122 | + } |
| 123 | + |
| 124 | + pub fn generate_venue_position_id(&mut self) -> Option<PositionId> { |
| 125 | + if !self.use_position_ids { |
| 126 | + return None; |
| 127 | + } |
| 128 | + |
| 129 | + self.position_count += 1; |
| 130 | + if self.use_random_ids { |
| 131 | + Some(PositionId::new(Uuid::new_v4().to_string())) |
| 132 | + } else { |
| 133 | + Some(PositionId::new( |
| 134 | + format!("{}-{}-{}", self.venue, self.raw_id, self.position_count).as_str(), |
| 135 | + )) |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + pub fn generate_venue_order_id(&mut self) -> VenueOrderId { |
| 140 | + self.order_count += 1; |
| 141 | + if self.use_random_ids { |
| 142 | + VenueOrderId::new(Uuid::new_v4().to_string()) |
| 143 | + } else { |
| 144 | + VenueOrderId::new( |
| 145 | + format!("{}-{}-{}", self.venue, self.raw_id, self.order_count).as_str(), |
| 146 | + ) |
| 147 | + } |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +#[cfg(test)] |
| 152 | +mod tests { |
| 153 | + use std::{cell::RefCell, rc::Rc}; |
| 154 | + |
| 155 | + use nautilus_common::cache::Cache; |
| 156 | + use nautilus_core::time::AtomicTime; |
| 157 | + use nautilus_model::{ |
| 158 | + enums::OmsType, |
| 159 | + events::OrderFilled, |
| 160 | + identifiers::{stubs::account_id, AccountId, PositionId, Venue, VenueOrderId}, |
| 161 | + instruments::InstrumentAny, |
| 162 | + orders::OrderAny, |
| 163 | + position::Position, |
| 164 | + }; |
| 165 | + use rstest::rstest; |
| 166 | + |
| 167 | + use crate::matching_engine::{ |
| 168 | + ids_generator::IdsGenerator, |
| 169 | + tests::{ |
| 170 | + instrument_eth_usdt, market_order_buy, market_order_fill, market_order_sell, time, |
| 171 | + }, |
| 172 | + }; |
| 173 | + |
| 174 | + fn get_ids_generator( |
| 175 | + cache: Rc<RefCell<Cache>>, |
| 176 | + use_position_ids: bool, |
| 177 | + oms_type: OmsType, |
| 178 | + ) -> IdsGenerator { |
| 179 | + IdsGenerator::new( |
| 180 | + Venue::from("BINANCE"), |
| 181 | + oms_type, |
| 182 | + 1, |
| 183 | + false, |
| 184 | + use_position_ids, |
| 185 | + cache, |
| 186 | + ) |
| 187 | + } |
| 188 | + |
| 189 | + #[rstest] |
| 190 | + fn test_get_position_id_hedging_with_existing_position( |
| 191 | + account_id: AccountId, |
| 192 | + time: AtomicTime, |
| 193 | + instrument_eth_usdt: InstrumentAny, |
| 194 | + market_order_buy: OrderAny, |
| 195 | + market_order_fill: OrderFilled, |
| 196 | + ) { |
| 197 | + let cache = Rc::new(RefCell::new(Cache::default())); |
| 198 | + let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Hedging); |
| 199 | + |
| 200 | + let position = Position::new(&instrument_eth_usdt, market_order_fill); |
| 201 | + |
| 202 | + // Add position to cache |
| 203 | + cache |
| 204 | + .borrow_mut() |
| 205 | + .add_position(position.clone(), OmsType::Hedging) |
| 206 | + .unwrap(); |
| 207 | + |
| 208 | + let position_id = ids_generator.get_position_id(&market_order_buy, None); |
| 209 | + assert_eq!(position_id, Some(position.id)); |
| 210 | + } |
| 211 | + |
| 212 | + #[rstest] |
| 213 | + fn test_get_position_id_hedging_with_generated_position( |
| 214 | + instrument_eth_usdt: InstrumentAny, |
| 215 | + account_id: AccountId, |
| 216 | + market_order_buy: OrderAny, |
| 217 | + ) { |
| 218 | + let cache = Rc::new(RefCell::new(Cache::default())); |
| 219 | + let mut ids_generator = get_ids_generator(cache.clone(), true, OmsType::Hedging); |
| 220 | + |
| 221 | + let position_id = ids_generator.get_position_id(&market_order_buy, None); |
| 222 | + assert_eq!(position_id, Some(PositionId::new("BINANCE-1-1"))); |
| 223 | + } |
| 224 | + |
| 225 | + #[rstest] |
| 226 | + fn test_get_position_id_netting( |
| 227 | + instrument_eth_usdt: InstrumentAny, |
| 228 | + account_id: AccountId, |
| 229 | + market_order_buy: OrderAny, |
| 230 | + market_order_fill: OrderFilled, |
| 231 | + ) { |
| 232 | + let cache = Rc::new(RefCell::new(Cache::default())); |
| 233 | + let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Netting); |
| 234 | + |
| 235 | + // position id should be none in non-initialized position id for this instrument |
| 236 | + let position_id = ids_generator.get_position_id(&market_order_buy, None); |
| 237 | + assert_eq!(position_id, None); |
| 238 | + |
| 239 | + // create and add position in cache |
| 240 | + let position = Position::new(&instrument_eth_usdt, market_order_fill); |
| 241 | + cache |
| 242 | + .as_ref() |
| 243 | + .borrow_mut() |
| 244 | + .add_position(position.clone(), OmsType::Netting) |
| 245 | + .unwrap(); |
| 246 | + |
| 247 | + // position id should be returned for the existing position |
| 248 | + let position_id = ids_generator.get_position_id(&market_order_buy, None); |
| 249 | + assert_eq!(position_id, Some(position.id)); |
| 250 | + } |
| 251 | + |
| 252 | + #[rstest] |
| 253 | + fn test_generate_venue_position_id( |
| 254 | + account_id: AccountId, |
| 255 | + time: AtomicTime, |
| 256 | + instrument_eth_usdt: InstrumentAny, |
| 257 | + ) { |
| 258 | + let cache = Rc::new(RefCell::new(Cache::default())); |
| 259 | + let mut ids_generator_with_position_ids = |
| 260 | + get_ids_generator(cache.clone(), true, OmsType::Netting); |
| 261 | + let mut ids_generator_no_position_ids = |
| 262 | + get_ids_generator(cache.clone(), false, OmsType::Netting); |
| 263 | + |
| 264 | + assert_eq!( |
| 265 | + ids_generator_no_position_ids.generate_venue_position_id(), |
| 266 | + None |
| 267 | + ); |
| 268 | + |
| 269 | + let position_id_1 = ids_generator_with_position_ids.generate_venue_position_id(); |
| 270 | + let position_id_2 = ids_generator_with_position_ids.generate_venue_position_id(); |
| 271 | + assert_eq!(position_id_1, Some(PositionId::new("BINANCE-1-1"))); |
| 272 | + assert_eq!(position_id_2, Some(PositionId::new("BINANCE-1-2"))); |
| 273 | + } |
| 274 | + |
| 275 | + #[rstest] |
| 276 | + fn get_venue_position_id( |
| 277 | + instrument_eth_usdt: InstrumentAny, |
| 278 | + account_id: AccountId, |
| 279 | + market_order_buy: OrderAny, |
| 280 | + market_order_sell: OrderAny, |
| 281 | + market_order_fill: OrderFilled, |
| 282 | + ) { |
| 283 | + let cache = Rc::new(RefCell::new(Cache::default())); |
| 284 | + let mut ids_generator = get_ids_generator(cache.clone(), true, OmsType::Netting); |
| 285 | + |
| 286 | + let venue_order_id1 = ids_generator.get_venue_order_id(&market_order_buy).unwrap(); |
| 287 | + let venue_order_id2 = ids_generator |
| 288 | + .get_venue_order_id(&market_order_sell) |
| 289 | + .unwrap(); |
| 290 | + assert_eq!(venue_order_id1, VenueOrderId::from("BINANCE-1-1")); |
| 291 | + assert_eq!(venue_order_id2, VenueOrderId::from("BINANCE-1-2")); |
| 292 | + |
| 293 | + // check if venue order id is cached again |
| 294 | + let venue_order_id3 = ids_generator.get_venue_order_id(&market_order_buy).unwrap(); |
| 295 | + assert_eq!(venue_order_id3, VenueOrderId::from("BINANCE-1-1")); |
| 296 | + } |
| 297 | +} |
0 commit comments