1use std::cell::Cell;
19
20use nautilus_core::UUID4;
21use rstest::fixture;
22use rust_decimal::prelude::ToPrimitive;
23
24use crate::{
25 data::order::BookOrder,
26 enums::{BookType, LiquiditySide, OrderSide, OrderType},
27 identifiers::InstrumentId,
28 instruments::{CurrencyPair, Instrument, InstrumentAny, stubs::audusd_sim},
29 orderbook::OrderBook,
30 orders::{builder::OrderTestBuilder, stubs::TestOrderEventStubs},
31 position::Position,
32 types::{Money, Price, Quantity},
33};
34
35pub(crate) const TEST_UUID_SEED: u64 = 42;
37
38thread_local! {
39 static TEST_UUID_STATE: Cell<u64> = const { Cell::new(TEST_UUID_SEED) };
40}
41
42fn splitmix64(state: &mut u64) -> u64 {
45 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
46 let mut z = *state;
47 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
48 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
49 z ^ (z >> 31)
50}
51
52#[must_use]
61pub fn test_uuid() -> UUID4 {
62 TEST_UUID_STATE.with(|cell| {
63 let mut state = cell.get();
64 let hi = splitmix64(&mut state).to_be_bytes();
65 let lo = splitmix64(&mut state).to_be_bytes();
66 cell.set(state);
67
68 let mut bytes = [0u8; 16];
69 bytes[..8].copy_from_slice(&hi);
70 bytes[8..].copy_from_slice(&lo);
71 UUID4::from_bytes(bytes)
72 })
73}
74
75pub fn reset_test_uuid_rng() {
80 TEST_UUID_STATE.with(|cell| cell.set(TEST_UUID_SEED));
81}
82
83pub trait TestDefault {
89 fn test_default() -> Self;
91}
92
93#[must_use]
101pub fn calculate_commission(
102 instrument: &InstrumentAny,
103 last_qty: Quantity,
104 last_px: Price,
105 use_quote_for_inverse: Option<bool>,
106) -> Money {
107 let liquidity_side = LiquiditySide::Taker;
108 assert_ne!(
109 liquidity_side,
110 LiquiditySide::NoLiquiditySide,
111 "Invalid liquidity side"
112 );
113 let notional = instrument
114 .calculate_notional_value(last_qty, last_px, use_quote_for_inverse)
115 .as_f64();
116 let commission = if liquidity_side == LiquiditySide::Maker {
117 notional * instrument.maker_fee().to_f64().unwrap()
118 } else if liquidity_side == LiquiditySide::Taker {
119 notional * instrument.taker_fee().to_f64().unwrap()
120 } else {
121 panic!("Invalid liquidity side {liquidity_side}")
122 };
123
124 if instrument.is_inverse() && !use_quote_for_inverse.unwrap_or(false) {
125 Money::new(commission, instrument.base_currency().unwrap())
126 } else {
127 Money::new(commission, instrument.quote_currency())
128 }
129}
130
131#[fixture]
132pub fn stub_position_long(audusd_sim: CurrencyPair) -> Position {
133 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
134 let order = OrderTestBuilder::new(OrderType::Market)
135 .instrument_id(audusd_sim.id())
136 .side(OrderSide::Buy)
137 .quantity(Quantity::from(1))
138 .build();
139 let filled = TestOrderEventStubs::filled(
140 &order,
141 &audusd_sim,
142 None,
143 None,
144 Some(Price::from("1.0002")),
145 None,
146 None,
147 None,
148 None,
149 None,
150 );
151 Position::new(&audusd_sim, filled.into())
152}
153
154#[fixture]
155pub fn stub_position_short(audusd_sim: CurrencyPair) -> Position {
156 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
157 let order = OrderTestBuilder::new(OrderType::Market)
158 .instrument_id(audusd_sim.id())
159 .side(OrderSide::Sell)
160 .quantity(Quantity::from(1))
161 .build();
162 let filled = TestOrderEventStubs::filled(
163 &order,
164 &audusd_sim,
165 None,
166 None,
167 Some(Price::from("22000.0")),
168 None,
169 None,
170 None,
171 None,
172 None,
173 );
174 Position::new(&audusd_sim, filled.into())
175}
176
177#[must_use]
178pub fn stub_order_book_mbp_appl_xnas() -> OrderBook {
179 stub_order_book_mbp(
180 InstrumentId::from("AAPL.XNAS"),
181 101.0,
182 100.0,
183 100.0,
184 100.0,
185 2,
186 0.01,
187 0,
188 100.0,
189 10,
190 )
191}
192
193#[expect(clippy::too_many_arguments)]
194#[must_use]
195pub fn stub_order_book_mbp(
196 instrument_id: InstrumentId,
197 top_ask_price: f64,
198 top_bid_price: f64,
199 top_ask_size: f64,
200 top_bid_size: f64,
201 price_precision: u8,
202 price_increment: f64,
203 size_precision: u8,
204 size_increment: f64,
205 num_levels: usize,
206) -> OrderBook {
207 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
208
209 for i in 0..num_levels {
211 let price = Price::new(
212 price_increment.mul_add(-(i as f64), top_bid_price),
213 price_precision,
214 );
215 let size = Quantity::new(
216 size_increment.mul_add(i as f64, top_bid_size),
217 size_precision,
218 );
219 let order = BookOrder::new(
220 OrderSide::Buy,
221 price,
222 size,
223 0, );
225 book.add(order, 0, 1, 2.into());
226 }
227
228 for i in 0..num_levels {
230 let price = Price::new(
231 price_increment.mul_add(i as f64, top_ask_price),
232 price_precision,
233 );
234 let size = Quantity::new(
235 size_increment.mul_add(i as f64, top_ask_size),
236 size_precision,
237 );
238 let order = BookOrder::new(
239 OrderSide::Sell,
240 price,
241 size,
242 0, );
244 book.add(order, 0, 1, 2.into());
245 }
246
247 book
248}
249
250#[cfg(test)]
251mod tests {
252 use rstest::rstest;
253
254 use super::*;
255
256 #[rstest]
257 fn test_uuid_is_valid_v4_rfc4122() {
258 reset_test_uuid_rng();
259 let s = test_uuid().to_string();
260 assert_eq!(s.len(), 36);
262 assert_eq!(&s[14..15], "4", "version digit must be 4, was {s}");
263 let variant = s.chars().nth(19).unwrap();
264 assert!(
265 matches!(variant, '8' | '9' | 'a' | 'b'),
266 "variant nibble must be one of 8/9/a/b, was {variant} in {s}",
267 );
268 }
269}