use std::cell::Cell;
use nautilus_core::UUID4;
use rstest::fixture;
use rust_decimal::prelude::ToPrimitive;
use crate::{
data::order::BookOrder,
enums::{BookType, LiquiditySide, OrderSide, OrderType},
identifiers::InstrumentId,
instruments::{CurrencyPair, Instrument, InstrumentAny, stubs::audusd_sim},
orderbook::OrderBook,
orders::{builder::OrderTestBuilder, stubs::TestOrderEventStubs},
position::Position,
types::{Money, Price, Quantity},
};
pub(crate) const TEST_UUID_SEED: u64 = 42;
thread_local! {
static TEST_UUID_STATE: Cell<u64> = const { Cell::new(TEST_UUID_SEED) };
}
fn splitmix64(state: &mut u64) -> u64 {
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
#[must_use]
pub fn test_uuid() -> UUID4 {
TEST_UUID_STATE.with(|cell| {
let mut state = cell.get();
let hi = splitmix64(&mut state).to_be_bytes();
let lo = splitmix64(&mut state).to_be_bytes();
cell.set(state);
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&hi);
bytes[8..].copy_from_slice(&lo);
UUID4::from_bytes(bytes)
})
}
pub fn reset_test_uuid_rng() {
TEST_UUID_STATE.with(|cell| cell.set(TEST_UUID_SEED));
}
pub trait TestDefault {
fn test_default() -> Self;
}
#[must_use]
pub fn calculate_commission(
instrument: &InstrumentAny,
last_qty: Quantity,
last_px: Price,
use_quote_for_inverse: Option<bool>,
) -> Money {
let liquidity_side = LiquiditySide::Taker;
assert_ne!(
liquidity_side,
LiquiditySide::NoLiquiditySide,
"Invalid liquidity side"
);
let notional = instrument
.calculate_notional_value(last_qty, last_px, use_quote_for_inverse)
.as_f64();
let commission = if liquidity_side == LiquiditySide::Maker {
notional * instrument.maker_fee().to_f64().unwrap()
} else if liquidity_side == LiquiditySide::Taker {
notional * instrument.taker_fee().to_f64().unwrap()
} else {
panic!("Invalid liquidity side {liquidity_side}")
};
if instrument.is_inverse() && !use_quote_for_inverse.unwrap_or(false) {
Money::new(commission, instrument.base_currency().unwrap())
} else {
Money::new(commission, instrument.quote_currency())
}
}
#[fixture]
pub fn stub_position_long(audusd_sim: CurrencyPair) -> Position {
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(1))
.build();
let filled = TestOrderEventStubs::filled(
&order,
&audusd_sim,
None,
None,
Some(Price::from("1.0002")),
None,
None,
None,
None,
None,
);
Position::new(&audusd_sim, filled.into())
}
#[fixture]
pub fn stub_position_short(audusd_sim: CurrencyPair) -> Position {
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Sell)
.quantity(Quantity::from(1))
.build();
let filled = TestOrderEventStubs::filled(
&order,
&audusd_sim,
None,
None,
Some(Price::from("22000.0")),
None,
None,
None,
None,
None,
);
Position::new(&audusd_sim, filled.into())
}
#[must_use]
pub fn stub_order_book_mbp_appl_xnas() -> OrderBook {
stub_order_book_mbp(
InstrumentId::from("AAPL.XNAS"),
101.0,
100.0,
100.0,
100.0,
2,
0.01,
0,
100.0,
10,
)
}
#[expect(clippy::too_many_arguments)]
#[must_use]
pub fn stub_order_book_mbp(
instrument_id: InstrumentId,
top_ask_price: f64,
top_bid_price: f64,
top_ask_size: f64,
top_bid_size: f64,
price_precision: u8,
price_increment: f64,
size_precision: u8,
size_increment: f64,
num_levels: usize,
) -> OrderBook {
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
for i in 0..num_levels {
let price = Price::new(
price_increment.mul_add(-(i as f64), top_bid_price),
price_precision,
);
let size = Quantity::new(
size_increment.mul_add(i as f64, top_bid_size),
size_precision,
);
let order = BookOrder::new(
OrderSide::Buy,
price,
size,
0, );
book.add(order, 0, 1, 2.into());
}
for i in 0..num_levels {
let price = Price::new(
price_increment.mul_add(i as f64, top_ask_price),
price_precision,
);
let size = Quantity::new(
size_increment.mul_add(i as f64, top_ask_size),
size_precision,
);
let order = BookOrder::new(
OrderSide::Sell,
price,
size,
0, );
book.add(order, 0, 1, 2.into());
}
book
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_uuid_is_valid_v4_rfc4122() {
reset_test_uuid_rng();
let s = test_uuid().to_string();
assert_eq!(s.len(), 36);
assert_eq!(&s[14..15], "4", "version digit must be 4, was {s}");
let variant = s.chars().nth(19).unwrap();
assert!(
matches!(variant, '8' | '9' | 'a' | 'b'),
"variant nibble must be one of 8/9/a/b, was {variant} in {s}",
);
}
}