pub mod any;
pub mod betting;
pub mod binary_option;
pub mod cfd;
pub mod commodity;
pub mod crypto_future;
pub mod crypto_futures_spread;
pub mod crypto_option;
pub mod crypto_option_spread;
pub mod crypto_perpetual;
pub mod currency_pair;
pub mod equity;
pub mod futures_contract;
pub mod futures_spread;
pub mod index_instrument;
pub mod option_contract;
pub mod option_spread;
pub mod perpetual_contract;
pub mod synthetic;
pub mod tick_scheme;
pub mod tokenized_asset;
#[cfg(any(test, feature = "stubs"))]
pub mod stubs;
use std::{fmt::Display, str::FromStr};
use enum_dispatch::enum_dispatch;
use nautilus_core::{
UnixNanos,
correctness::{
CorrectnessError, CorrectnessResult, check_equal_u8, check_positive_decimal,
check_predicate_true,
},
string::parsing::min_increment_precision_from_str,
};
use rust_decimal::{Decimal, RoundingStrategy};
use rust_decimal_macros::dec;
use ustr::Ustr;
pub use crate::instruments::{
any::InstrumentAny,
betting::BettingInstrument,
binary_option::BinaryOption,
cfd::Cfd,
commodity::Commodity,
crypto_future::CryptoFuture,
crypto_futures_spread::CryptoFuturesSpread,
crypto_option::CryptoOption,
crypto_option_spread::CryptoOptionSpread,
crypto_perpetual::CryptoPerpetual,
currency_pair::CurrencyPair,
equity::Equity,
futures_contract::FuturesContract,
futures_spread::FuturesSpread,
index_instrument::IndexInstrument,
option_contract::OptionContract,
option_spread::OptionSpread,
perpetual_contract::PerpetualContract,
synthetic::{SyntheticInstrument, SyntheticInstrumentError},
tick_scheme::{
FixedTickScheme, TickScheme, TickSchemeError, TickSchemeRule, TieredTickScheme,
tick_scheme_rule_from_name,
},
tokenized_asset::TokenizedAsset,
};
use crate::{
enums::{AssetClass, InstrumentClass, OptionKind},
identifiers::{InstrumentId, Symbol, Venue},
types::{
Currency, ERROR_PRICE, Money, PRICE_ERROR, Price, Quantity,
fixed::{FIXED_PRECISION, raw_scales_match},
money::check_positive_money,
price::{PriceRaw, check_positive_price},
quantity::{QuantityRaw, check_positive_quantity},
},
};
#[expect(clippy::missing_errors_doc, clippy::too_many_arguments)]
pub fn validate_instrument_common(
price_precision: u8,
size_precision: u8,
size_increment: Quantity,
multiplier: Quantity,
margin_init: Decimal,
margin_maint: Decimal,
price_increment: Option<Price>,
lot_size: Option<Quantity>,
max_quantity: Option<Quantity>,
min_quantity: Option<Quantity>,
max_notional: Option<Money>,
min_notional: Option<Money>,
max_price: Option<Price>,
min_price: Option<Price>,
) -> CorrectnessResult<()> {
check_positive_quantity(size_increment, "size_increment")?;
check_equal_u8(
size_increment.precision,
size_precision,
"size_increment.precision",
"size_precision",
)?;
check_positive_quantity(multiplier, "multiplier")?;
check_positive_decimal(margin_init, "margin_init")?;
check_positive_decimal(margin_maint, "margin_maint")?;
if let Some(price_increment) = price_increment {
check_positive_price(price_increment, "price_increment")?;
check_equal_u8(
price_increment.precision,
price_precision,
"price_increment.precision",
"price_precision",
)?;
}
if let Some(lot) = lot_size {
check_positive_quantity(lot, "lot_size")?;
}
if let Some(quantity) = max_quantity {
check_positive_quantity(quantity, "max_quantity")?;
}
if let Some(quantity) = min_quantity {
check_positive_quantity(quantity, "min_quantity")?;
}
if let Some(notional) = max_notional {
check_positive_money(notional, "max_notional")?;
}
if let Some(notional) = min_notional {
check_positive_money(notional, "min_notional")?;
}
if let Some(max_price) = max_price {
check_positive_price(max_price, "max_price")?;
check_equal_u8(
max_price.precision,
price_precision,
"max_price.precision",
"price_precision",
)?;
}
if let Some(min_price) = min_price {
check_positive_price(min_price, "min_price")?;
check_equal_u8(
min_price.precision,
price_precision,
"min_price.precision",
"price_precision",
)?;
}
if let (Some(min), Some(max)) = (min_price, max_price) {
check_predicate_true(min.raw <= max.raw, "min_price exceeds max_price")?;
}
Ok(())
}
fn currencies_equivalent_for_quanto(left: Currency, right: Currency) -> bool {
if left == right {
return true;
}
is_usd_equivalent_currency(left) && is_usd_equivalent_currency(right)
}
fn is_usd_equivalent_currency(currency: Currency) -> bool {
matches!(
currency.code.as_str(),
"BUSD" | "FDUSD" | "pUSD" | "TUSD" | "USD" | "USDC" | "USDC.e" | "USDP" | "USDT"
)
}
#[enum_dispatch]
pub trait Instrument: 'static + Send {
fn tick_scheme(&self) -> Option<Ustr> {
None
}
fn tick_scheme_rule(&self) -> Option<&dyn TickSchemeRule> {
self.tick_scheme()
.and_then(|scheme| tick_scheme_rule_from_name(scheme.as_str()))
}
fn into_any(self) -> InstrumentAny
where
Self: Sized,
InstrumentAny: From<Self>,
{
self.into()
}
fn id(&self) -> InstrumentId;
fn symbol(&self) -> Symbol {
self.id().symbol
}
fn venue(&self) -> Venue {
self.id().venue
}
fn raw_symbol(&self) -> Symbol;
fn asset_class(&self) -> AssetClass;
fn instrument_class(&self) -> InstrumentClass;
fn underlying(&self) -> Option<Ustr>;
fn base_currency(&self) -> Option<Currency>;
fn quote_currency(&self) -> Currency;
fn settlement_currency(&self) -> Currency;
fn cost_currency(&self) -> Currency {
if self.is_inverse() {
self.base_currency()
.expect("inverse instrument without base_currency")
} else if self.is_quanto() {
self.settlement_currency()
} else {
self.quote_currency()
}
}
fn isin(&self) -> Option<Ustr>;
fn option_kind(&self) -> Option<OptionKind>;
fn exchange(&self) -> Option<Ustr>;
fn strike_price(&self) -> Option<Price>;
fn strategy_type(&self) -> Option<Ustr> {
None
}
fn activation_ns(&self) -> Option<UnixNanos>;
fn expiration_ns(&self) -> Option<UnixNanos>;
fn has_expiration(&self) -> bool {
self.instrument_class().has_expiration()
}
fn allows_negative_price(&self) -> bool {
self.instrument_class().allows_negative_price()
}
fn is_inverse(&self) -> bool;
fn is_quanto(&self) -> bool {
self.base_currency().is_some_and(|base_currency| {
self.settlement_currency() != base_currency
&& !currencies_equivalent_for_quanto(
self.settlement_currency(),
self.quote_currency(),
)
})
}
fn price_precision(&self) -> u8;
fn size_precision(&self) -> u8;
fn price_increment(&self) -> Price;
fn size_increment(&self) -> Quantity;
fn multiplier(&self) -> Quantity;
fn lot_size(&self) -> Option<Quantity>;
fn max_quantity(&self) -> Option<Quantity>;
fn min_quantity(&self) -> Option<Quantity>;
fn max_notional(&self) -> Option<Money>;
fn min_notional(&self) -> Option<Money>;
fn max_price(&self) -> Option<Price>;
fn min_price(&self) -> Option<Price>;
fn margin_init(&self) -> Decimal {
dec!(0)
}
fn margin_maint(&self) -> Decimal {
dec!(0)
}
fn maker_fee(&self) -> Decimal {
dec!(0)
}
fn taker_fee(&self) -> Decimal {
dec!(0)
}
fn ts_event(&self) -> UnixNanos;
fn ts_init(&self) -> UnixNanos;
fn min_price_increment_precision(&self) -> u8 {
min_increment_precision_from_str(&self.price_increment().to_string())
}
fn min_size_increment_precision(&self) -> u8 {
min_increment_precision_from_str(&self.size_increment().to_string())
}
#[inline(always)]
fn try_make_price(&self, value: f64) -> anyhow::Result<Price> {
let dec_value = Decimal::from_str(&value.to_string())
.map_err(|_| anyhow::anyhow!("invalid `value` for make_price, was {value}"))?;
let precision = u32::from(self.min_price_increment_precision());
let rounded_decimal =
dec_value.round_dp_with_strategy(precision, RoundingStrategy::MidpointNearestEven);
Price::from_decimal_dp(rounded_decimal, self.price_precision()).map_err(Into::into)
}
fn make_price(&self, value: f64) -> Price {
self.try_make_price(value).unwrap()
}
#[inline(always)]
fn try_normalize_price(&self, price: Price) -> CorrectnessResult<Price> {
if price == ERROR_PRICE {
return Err(CorrectnessError::InvalidValue {
param: "price".to_string(),
value: "ERROR_PRICE".to_string(),
type_name: "`Price`",
});
}
if price.raw == PRICE_ERROR {
return Err(CorrectnessError::InvalidValue {
param: "price".to_string(),
value: "PRICE_ERROR".to_string(),
type_name: "`Price`",
});
}
if price.is_undefined() {
return Err(CorrectnessError::InvalidValue {
param: "price".to_string(),
value: "PRICE_UNDEF".to_string(),
type_name: "`Price`",
});
}
let precision = self.price_precision();
let increment = self.price_increment();
if !raw_scales_match(price.precision, precision) {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"`price` raw scale does not match instrument price precision, price precision was {}, instrument price precision was {precision}",
price.precision
),
});
}
if !raw_scales_match(price.precision, increment.precision) {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"`price` raw scale does not match price increment precision, price precision was {}, price increment precision was {}",
price.precision, increment.precision
),
});
}
let precision_diff = FIXED_PRECISION.saturating_sub(precision);
let scale = PriceRaw::pow(10, u32::from(precision_diff));
if price.raw % scale != 0 {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"`price` requires rounding to instrument price precision {precision}, was {price}"
),
});
}
let increment_raw = increment.raw.abs();
if increment_raw != 0 && price.raw % increment_raw != 0 {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"`price` is not aligned to price increment {increment}, was {price}"
),
});
}
Price::from_raw_checked(price.raw, precision)
}
#[inline(always)]
fn try_make_qty(&self, value: f64, round_down: Option<bool>) -> anyhow::Result<Quantity> {
let dec_value = Decimal::from_str(&value.to_string())
.map_err(|_| anyhow::anyhow!("invalid `value` for make_qty, was {value}"))?;
let precision = u32::from(self.min_size_increment_precision());
let strategy = if round_down.unwrap_or(false) {
RoundingStrategy::ToZero
} else {
RoundingStrategy::MidpointNearestEven
};
let rounded = dec_value.round_dp_with_strategy(precision, strategy);
if dec_value > Decimal::ZERO && rounded.is_zero() {
anyhow::bail!("value rounded to zero for quantity");
}
Quantity::from_decimal_dp(rounded, self.size_precision()).map_err(Into::into)
}
fn make_qty(&self, value: f64, round_down: Option<bool>) -> Quantity {
self.try_make_qty(value, round_down).unwrap()
}
#[inline(always)]
fn try_normalize_qty(&self, quantity: Quantity) -> CorrectnessResult<Quantity> {
if quantity.is_undefined() {
return Err(CorrectnessError::InvalidValue {
param: "quantity".to_string(),
value: "QUANTITY_UNDEF".to_string(),
type_name: "`Quantity`",
});
}
let precision = self.size_precision();
let increment = self.size_increment();
if !raw_scales_match(quantity.precision, precision) {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"`quantity` raw scale does not match instrument size precision, quantity precision was {}, instrument size precision was {precision}",
quantity.precision
),
});
}
if !raw_scales_match(quantity.precision, increment.precision) {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"`quantity` raw scale does not match size increment precision, quantity precision was {}, size increment precision was {}",
quantity.precision, increment.precision
),
});
}
let precision_diff = FIXED_PRECISION.saturating_sub(precision);
let scale = QuantityRaw::pow(10, u32::from(precision_diff));
if !quantity.raw.is_multiple_of(scale) {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"`quantity` requires rounding to instrument size precision {precision}, was {quantity}"
),
});
}
if increment.raw != 0 && !quantity.raw.is_multiple_of(increment.raw) {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"`quantity` is not aligned to size increment {increment}, was {quantity}"
),
});
}
Quantity::from_raw_checked(quantity.raw, precision)
}
fn try_calculate_base_quantity(
&self,
quantity: Quantity,
last_price: Price,
) -> anyhow::Result<Quantity> {
let last_px = last_price.as_decimal();
if last_px.is_zero() {
anyhow::bail!("`last_price` was zero when calculating base quantity");
}
let precision = u32::from(self.min_size_increment_precision());
let value = (quantity.as_decimal() / last_px)
.round_dp_with_strategy(precision, RoundingStrategy::MidpointNearestEven);
Quantity::from_decimal_dp(value, self.size_precision()).map_err(Into::into)
}
fn calculate_base_quantity(&self, quantity: Quantity, last_price: Price) -> Quantity {
self.try_calculate_base_quantity(quantity, last_price)
.unwrap()
}
#[inline(always)]
fn calculate_notional_value(
&self,
quantity: Quantity,
price: Price,
use_quote_for_inverse: Option<bool>,
) -> Money {
let use_quote_inverse = use_quote_for_inverse.unwrap_or(false);
let (amount, currency) = if self.is_inverse() {
if use_quote_inverse {
(quantity.as_decimal(), self.quote_currency())
} else {
let amount =
quantity.as_decimal() * self.multiplier().as_decimal() / price.as_decimal();
let currency = self
.base_currency()
.expect("inverse instrument without base_currency");
(amount, currency)
}
} else if self.is_quanto() {
let amount =
quantity.as_decimal() * self.multiplier().as_decimal() * price.as_decimal();
(amount, self.settlement_currency())
} else {
let amount =
quantity.as_decimal() * self.multiplier().as_decimal() * price.as_decimal();
(amount, self.quote_currency())
};
Money::from_decimal(amount, currency).expect("Invalid notional value")
}
#[inline(always)]
fn next_bid_price(&self, value: f64, n: i32) -> Option<Price> {
if n < 0 {
return None;
}
let price = if let Some(scheme) = self.tick_scheme_rule() {
scheme.next_bid_price(value, n, self.price_precision())?
} else {
let value = Decimal::from_str(&value.to_string()).ok()?;
let increment = self.price_increment().as_decimal();
if increment.is_zero() {
return None;
}
let base = (value / increment).floor() * increment;
let result = base - Decimal::from(n) * increment;
Price::from_decimal_dp(result, self.price_precision()).ok()?
};
if self.min_price().is_some_and(|min| price < min)
|| self.max_price().is_some_and(|max| price > max)
{
return None;
}
Some(price)
}
#[inline(always)]
fn next_ask_price(&self, value: f64, n: i32) -> Option<Price> {
if n < 0 {
return None;
}
let price = if let Some(scheme) = self.tick_scheme_rule() {
scheme.next_ask_price(value, n, self.price_precision())?
} else {
let value = Decimal::from_str(&value.to_string()).ok()?;
let increment = self.price_increment().as_decimal();
if increment.is_zero() {
return None;
}
let base = (value / increment).ceil() * increment;
let result = base + Decimal::from(n) * increment;
Price::from_decimal_dp(result, self.price_precision()).ok()?
};
if self.min_price().is_some_and(|min| price < min)
|| self.max_price().is_some_and(|max| price > max)
{
return None;
}
Some(price)
}
#[inline]
fn next_bid_prices(&self, value: f64, n: usize) -> Vec<Price> {
let mut prices = Vec::with_capacity(n);
for i in 0..n {
let Ok(i) = i32::try_from(i) else { break };
if let Some(price) = self.next_bid_price(value, i) {
prices.push(price);
} else {
break;
}
}
prices
}
#[inline]
fn next_ask_prices(&self, value: f64, n: usize) -> Vec<Price> {
let mut prices = Vec::with_capacity(n);
for i in 0..n {
let Ok(i) = i32::try_from(i) else { break };
if let Some(price) = self.next_ask_price(value, i) {
prices.push(price);
} else {
break;
}
}
prices
}
}
impl Display for CurrencyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}(instrument_id='{}', tick_scheme='{}', price_precision={}, size_precision={}, \
price_increment={}, size_increment={}, multiplier={}, margin_init={}, margin_maint={})",
stringify!(CurrencyPair),
self.id,
self.tick_scheme()
.map_or_else(|| "None".into(), |s| s.to_string()),
self.price_precision(),
self.size_precision(),
self.price_increment(),
self.size_increment(),
self.multiplier(),
self.margin_init(),
self.margin_maint(),
)
}
}
#[cfg(test)]
mod tests {
use nautilus_core::correctness::{CorrectnessResultExt, FAILED};
use proptest::prelude::*;
use rstest::rstest;
use rust_decimal::{Decimal, prelude::*};
use super::*;
use crate::{
instruments::stubs::*,
types::{ERROR_PRICE, Money, PRICE_ERROR, PRICE_UNDEF, QUANTITY_UNDEF},
};
pub(super) fn default_price_increment(precision: u8) -> Price {
let step = 10f64.powi(-i32::from(precision));
Price::new(step, precision)
}
#[rstest]
fn default_increment_precision() {
let inc = default_price_increment(2);
assert_eq!(inc, Price::new(0.01, 2));
}
#[rstest]
#[case(Price::new(0.5, 1), 1)] #[case(Price::new(0.50, 2), 1)] #[case(Price::new(0.500, 3), 1)] #[case(Price::new(0.01, 2), 2)] #[case(Price::new(0.010, 3), 2)] #[case(Price::new(0.25, 2), 2)] #[case(Price::new(1.0, 1), 1)] #[case(Price::new(1.00, 2), 2)] #[case(Price::new(100.0, 0), 0)] #[case(Price::new(0.001, 3), 3)] fn test_min_increment_precision(#[case] price: Price, #[case] expected: u8) {
assert_eq!(
nautilus_core::string::parsing::min_increment_precision_from_str(&price.to_string()),
expected
);
}
#[rstest]
#[case(1.5, "1.500000")]
#[case(2.5, "2.500000")]
#[case(1.234_567_8, "1.234568")]
#[case(0.000_123, "0.000123")]
#[case(99_999.999_999, "99999.999999")]
fn make_qty_rounding(
currency_pair_btcusdt: CurrencyPair,
#[case] input: f64,
#[case] expected: &str,
) {
assert_eq!(
currency_pair_btcusdt.make_qty(input, None).to_string(),
expected
);
}
#[rstest]
#[case(1.234_567_8, "1.234567")]
#[case(1.999_999_9, "1.999999")]
#[case(0.000_123_45, "0.000123")]
#[case(10.999_999_9, "10.999999")]
fn make_qty_round_down(
currency_pair_btcusdt: CurrencyPair,
#[case] input: f64,
#[case] expected: &str,
) {
assert_eq!(
currency_pair_btcusdt
.make_qty(input, Some(true))
.to_string(),
expected
);
}
#[rstest]
#[case(1.234_567_8, "1.23457")]
#[case(2.345_678_1, "2.34568")]
#[case(0.00001, "0.00001")]
fn make_qty_precision(
currency_pair_ethusdt: CurrencyPair,
#[case] input: f64,
#[case] expected: &str,
) {
assert_eq!(
currency_pair_ethusdt.make_qty(input, None).to_string(),
expected
);
}
#[rstest]
#[case(1.234_567_5, "1.234568")]
#[case(1.234_566_5, "1.234566")]
fn make_qty_half_even(
currency_pair_btcusdt: CurrencyPair,
#[case] input: f64,
#[case] expected: &str,
) {
assert_eq!(
currency_pair_btcusdt.make_qty(input, None).to_string(),
expected
);
}
#[rstest]
#[case(Price::from("10000"), "10000.00")]
#[case(Price::from("10000.0000"), "10000.00")]
fn try_normalize_price_rewrites_grid_aligned_values(
currency_pair_btcusdt: CurrencyPair,
#[case] input: Price,
#[case] expected: &str,
) {
let normalized = currency_pair_btcusdt.try_normalize_price(input).unwrap();
assert_eq!(normalized.raw, input.raw);
assert_eq!(
normalized.precision,
currency_pair_btcusdt.price_precision()
);
assert_eq!(normalized, Price::from(expected));
}
#[rstest]
fn try_normalize_price_rejects_sub_precision_value(currency_pair_btcusdt: CurrencyPair) {
let error = currency_pair_btcusdt
.try_normalize_price(Price::from("10000.001"))
.unwrap_err();
assert!(matches!(
error,
CorrectnessError::PredicateViolation { ref message }
if message.contains("requires rounding to instrument price precision")
));
}
#[rstest]
#[case(Price::from_raw(PRICE_UNDEF, 0), "PRICE_UNDEF")]
#[case(Price::from_raw(PRICE_ERROR, 0), "PRICE_ERROR")]
#[case(ERROR_PRICE, "ERROR_PRICE")]
fn try_normalize_price_rejects_sentinel_values(
currency_pair_btcusdt: CurrencyPair,
#[case] input: Price,
#[case] expected_value: &str,
) {
let error = currency_pair_btcusdt
.try_normalize_price(input)
.unwrap_err();
match error {
CorrectnessError::InvalidValue {
param,
value,
type_name,
} => {
assert_eq!(param, "price");
assert_eq!(value, expected_value);
assert_eq!(type_name, "`Price`");
}
_ => panic!("expected invalid price error, was {error}"),
}
}
#[rstest]
#[case(Price::from("-10000"), Some(Price::from("-10000.00")))]
#[case(Price::from("-10000.001"), None)]
fn try_normalize_price_handles_negative_values(
currency_pair_btcusdt: CurrencyPair,
#[case] input: Price,
#[case] expected: Option<Price>,
) {
let normalized = currency_pair_btcusdt.try_normalize_price(input).ok();
assert_eq!(normalized, expected);
}
#[rstest]
fn try_normalize_price_rejects_sub_increment_value() {
let instrument = CurrencyPair::new(
InstrumentId::from("TEST.VENUE"),
Symbol::from("TEST"),
Currency::from("BTC"),
Currency::from("USD"),
2,
2,
Price::from("0.50"),
Quantity::from("0.01"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None, UnixNanos::default(),
UnixNanos::default(),
);
assert_eq!(
instrument.try_normalize_price(Price::from("1.500")),
Ok(Price::from("1.50"))
);
let error = instrument
.try_normalize_price(Price::from("1.20"))
.unwrap_err();
assert!(matches!(
error,
CorrectnessError::PredicateViolation { ref message }
if message.contains("not aligned to price increment")
));
}
#[rstest]
#[case(Quantity::from("1"), "1.000000")]
#[case(Quantity::from("1.0000000"), "1.000000")]
fn try_normalize_qty_rewrites_grid_aligned_values(
currency_pair_btcusdt: CurrencyPair,
#[case] input: Quantity,
#[case] expected: &str,
) {
let normalized = currency_pair_btcusdt.try_normalize_qty(input).unwrap();
assert_eq!(normalized.raw, input.raw);
assert_eq!(normalized.precision, currency_pair_btcusdt.size_precision());
assert_eq!(normalized, Quantity::from(expected));
}
#[rstest]
fn try_normalize_qty_rejects_sub_precision_value(currency_pair_btcusdt: CurrencyPair) {
let error = currency_pair_btcusdt
.try_normalize_qty(Quantity::from("1.0000001"))
.unwrap_err();
assert!(matches!(
error,
CorrectnessError::PredicateViolation { ref message }
if message.contains("requires rounding to instrument size precision")
));
}
#[rstest]
fn try_normalize_qty_rejects_undefined_value(currency_pair_btcusdt: CurrencyPair) {
let error = currency_pair_btcusdt
.try_normalize_qty(Quantity::from_raw(QUANTITY_UNDEF, 0))
.unwrap_err();
match error {
CorrectnessError::InvalidValue {
param,
value,
type_name,
} => {
assert_eq!(param, "quantity");
assert_eq!(value, "QUANTITY_UNDEF");
assert_eq!(type_name, "`Quantity`");
}
_ => panic!("expected invalid quantity error, was {error}"),
}
}
#[cfg(feature = "defi")]
#[rstest]
fn try_normalize_values_reject_mixed_raw_scales() {
let defi_precision = 18;
let price_increment = Price::from_raw(PriceRaw::from(5) * PriceRaw::pow(10, 17), 18);
let size_increment =
Quantity::from_raw(QuantityRaw::from(5_u8) * QuantityRaw::pow(10, 17), 18);
let instrument = CurrencyPair::new(
InstrumentId::from("TEST.VENUE"),
Symbol::from("TEST"),
Currency::from("BTC"),
Currency::from("USD"),
defi_precision,
defi_precision,
price_increment,
size_increment,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None, UnixNanos::default(),
UnixNanos::default(),
);
let fixed_scale = u32::from(FIXED_PRECISION);
let fixed_price = Price::from_raw(
PriceRaw::pow(10, fixed_scale) * PriceRaw::from(100),
FIXED_PRECISION,
);
let fixed_qty = Quantity::from_raw(
QuantityRaw::pow(10, fixed_scale) * QuantityRaw::from(100_u8),
FIXED_PRECISION,
);
let price_error = instrument.try_normalize_price(fixed_price).unwrap_err();
let qty_error = instrument.try_normalize_qty(fixed_qty).unwrap_err();
assert!(matches!(
price_error,
CorrectnessError::PredicateViolation { ref message }
if message.contains("raw scale does not match instrument price precision")
));
assert!(matches!(
qty_error,
CorrectnessError::PredicateViolation { ref message }
if message.contains("raw scale does not match instrument size precision")
));
}
#[rstest]
fn try_normalize_qty_rejects_sub_increment_value() {
let instrument = CurrencyPair::new(
InstrumentId::from("TEST.VENUE"),
Symbol::from("TEST"),
Currency::from("BTC"),
Currency::from("USD"),
2,
2,
Price::from("0.01"),
Quantity::from("0.50"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None, UnixNanos::default(),
UnixNanos::default(),
);
assert_eq!(
instrument.try_normalize_qty(Quantity::from("1.500")),
Ok(Quantity::from("1.50"))
);
let error = instrument
.try_normalize_qty(Quantity::from("1.20"))
.unwrap_err();
assert!(matches!(
error,
CorrectnessError::PredicateViolation { ref message }
if message.contains("not aligned to size increment")
));
}
#[rstest]
#[should_panic(expected = "value rounded to zero")]
fn make_qty_rounds_to_zero(currency_pair_btcusdt: CurrencyPair) {
currency_pair_btcusdt.make_qty(1e-12, None);
}
#[rstest]
fn notional_linear(currency_pair_btcusdt: CurrencyPair) {
let quantity = currency_pair_btcusdt.make_qty(2.0, None);
let price = currency_pair_btcusdt.make_price(10_000.0);
let notional = currency_pair_btcusdt.calculate_notional_value(quantity, price, None);
let expected = Money::new(20_000.0, currency_pair_btcusdt.quote_currency());
assert_eq!(notional, expected);
}
#[rstest]
fn currency_pair_is_not_quanto(currency_pair_btcusdt: CurrencyPair) {
assert!(!currency_pair_btcusdt.is_quanto());
assert_eq!(currency_pair_btcusdt.cost_currency(), Currency::USDT());
}
#[rstest]
fn tick_navigation(currency_pair_btcusdt: CurrencyPair) {
let start = 10_000.123_4;
let bid_0 = currency_pair_btcusdt.next_bid_price(start, 0).unwrap();
let bid_1 = currency_pair_btcusdt.next_bid_price(start, 1).unwrap();
assert!(bid_1 < bid_0);
let asks = currency_pair_btcusdt.next_ask_prices(start, 3);
assert_eq!(asks.len(), 3);
assert!(asks[0] > bid_0);
}
#[rstest]
fn tick_navigation_uses_tick_scheme() {
let instrument = CurrencyPair::new(
InstrumentId::from("TEST.VENUE"),
Symbol::from("TEST"),
Currency::from("BTC"),
Currency::from("USD"),
2,
2,
Price::new(0.01, 2),
Quantity::from("0.01"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
Some(Ustr::from("FIXED_PRECISION_1")),
None,
UnixNanos::default(),
UnixNanos::default(),
);
assert_eq!(
instrument.tick_scheme(),
Some(Ustr::from("FIXED_PRECISION_1"))
);
assert_eq!(instrument.next_bid_price(1.23, 0), Some(Price::new(1.2, 2)));
assert_eq!(instrument.next_ask_price(1.23, 0), Some(Price::new(1.3, 2)));
}
#[rstest]
#[case("BOGUS")]
#[case("FIXED_PRECISION_99")]
fn invalid_tick_scheme_returns_error(#[case] tick_scheme: &str) {
let err = CurrencyPair::new_checked(
InstrumentId::from("TEST.VENUE"),
Symbol::from("TEST"),
Currency::from("BTC"),
Currency::from("USD"),
2,
2,
Price::new(0.01, 2),
Quantity::from("0.01"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
Some(Ustr::from(tick_scheme)),
None,
UnixNanos::default(),
UnixNanos::default(),
)
.expect_err("invalid tick scheme must fail");
assert!(
err.to_string()
.contains("tick_scheme not found in tick schemes"),
"{err}"
);
}
#[rstest]
#[should_panic(expected = "'margin_init' not positive")]
fn validate_negative_margin_init() {
let size_increment = Quantity::new(0.01, 2);
let multiplier = Quantity::new(1.0, 0);
validate_instrument_common(
2,
2, size_increment, multiplier, dec!(-0.01), dec!(0.01), None, None, None, None, None, None, None, None, )
.expect_display(FAILED);
}
#[rstest]
#[should_panic(expected = "'margin_maint' not positive")]
fn validate_negative_margin_maint() {
let size_increment = Quantity::new(0.01, 2);
let multiplier = Quantity::new(1.0, 0);
validate_instrument_common(
2,
2, size_increment, multiplier, dec!(0.01), dec!(-0.01), None, None, None, None, None, None, None, None, )
.expect_display(FAILED);
}
#[rstest]
#[should_panic(expected = "'margin_init' not positive")]
fn validate_negative_max_qty() {
let quantity = Quantity::new(0.0, 0);
validate_instrument_common(
2,
2,
Quantity::new(0.01, 2),
Quantity::new(1.0, 0),
dec!(0),
dec!(0),
None,
None,
Some(quantity),
None,
None,
None,
None,
None,
)
.expect_display(FAILED);
}
#[rstest]
fn make_price_negative_rounding(currency_pair_ethusdt: CurrencyPair) {
let price = currency_pair_ethusdt.make_price(-123.456_789);
assert!(price.as_f64() < 0.0);
}
#[rstest]
fn base_quantity_linear(currency_pair_btcusdt: CurrencyPair) {
let quantity = currency_pair_btcusdt.make_qty(2.0, None);
let price = currency_pair_btcusdt.make_price(10_000.0);
let base = currency_pair_btcusdt.calculate_base_quantity(quantity, price);
assert_eq!(base.to_string(), "0.000200");
}
#[rstest]
fn base_quantity_zero_last_price_returns_error(currency_pair_btcusdt: CurrencyPair) {
let quantity = currency_pair_btcusdt.make_qty(2.0, None);
let error = currency_pair_btcusdt
.try_calculate_base_quantity(quantity, Price::new(0.0, 2))
.unwrap_err();
assert!(
error.to_string().contains("`last_price` was zero"),
"{error}"
);
}
#[rstest]
#[case(f64::NAN)]
#[case(f64::INFINITY)]
#[case(1e30)] fn make_price_invalid_value_returns_error(
currency_pair_btcusdt: CurrencyPair,
#[case] value: f64,
) {
let error = currency_pair_btcusdt.try_make_price(value).unwrap_err();
assert!(
error.to_string().contains("invalid `value` for make_price"),
"{error}"
);
}
#[rstest]
fn make_qty_invalid_value_returns_error(currency_pair_btcusdt: CurrencyPair) {
let error = currency_pair_btcusdt
.try_make_qty(f64::NAN, None)
.unwrap_err();
assert!(
error.to_string().contains("invalid `value` for make_qty"),
"{error}"
);
}
#[rstest]
fn next_bid_prices_sequence(currency_pair_btcusdt: CurrencyPair) {
let start = 10_000.0;
let bids = currency_pair_btcusdt.next_bid_prices(start, 5);
assert_eq!(bids.len(), 5);
for i in 1..bids.len() {
assert!(bids[i] < bids[i - 1]);
}
}
#[rstest]
fn next_ask_prices_sequence(currency_pair_btcusdt: CurrencyPair) {
let start = 10_000.0;
let asks = currency_pair_btcusdt.next_ask_prices(start, 5);
assert_eq!(asks.len(), 5);
for i in 1..asks.len() {
assert!(asks[i] > asks[i - 1]);
}
}
#[rstest]
#[should_panic(expected = "'margin_init' not positive")]
fn validate_price_increment_precision_mismatch() {
let size_increment = Quantity::new(0.01, 2);
let multiplier = Quantity::new(1.0, 0);
let price_increment = Price::new(0.001, 3);
validate_instrument_common(
2,
2,
size_increment,
multiplier,
dec!(0),
dec!(0),
Some(price_increment),
None,
None,
None,
None,
None,
None,
None,
)
.expect_display(FAILED);
}
#[rstest]
#[should_panic(expected = "'margin_init' not positive")]
fn validate_min_price_exceeds_max_price() {
let size_increment = Quantity::new(0.01, 2);
let multiplier = Quantity::new(1.0, 0);
let min_price = Price::new(10.0, 2);
let max_price = Price::new(5.0, 2);
validate_instrument_common(
2,
2,
size_increment,
multiplier,
dec!(0),
dec!(0),
None,
None,
None,
None,
None,
None,
Some(max_price),
Some(min_price),
)
.expect_display(FAILED);
}
#[rstest]
fn validate_instrument_common_ok() {
let res = validate_instrument_common(
2,
4,
Quantity::new(0.0001, 4),
Quantity::new(1.0, 0),
dec!(0.02),
dec!(0.01),
Some(Price::new(0.01, 2)),
None,
None,
None,
None,
None,
None,
None,
);
assert!(matches!(res, Ok(())));
}
#[rstest]
#[should_panic(expected = "not in range")]
fn validate_multiple_errors() {
validate_instrument_common(
2,
2,
Quantity::new(-0.01, 2),
Quantity::new(0.0, 0),
dec!(0),
dec!(0),
None,
None,
None,
None,
None,
None,
None,
None,
)
.expect_display(FAILED);
}
#[rstest]
#[case(1.234_999_9, false, "1.235000")]
#[case(1.234_999_9, true, "1.234999")]
fn make_qty_boundary(
currency_pair_btcusdt: CurrencyPair,
#[case] input: f64,
#[case] round_down: bool,
#[case] expected: &str,
) {
let quantity = currency_pair_btcusdt.make_qty(input, Some(round_down));
assert_eq!(quantity.to_string(), expected);
}
#[rstest]
#[case(1.234_999, 1.23)]
#[case(1.235, 1.24)]
#[case(1.235_001, 1.24)]
fn make_price_rounding_parity(
currency_pair_btcusdt: CurrencyPair,
#[case] input: f64,
#[case] expected: f64,
) {
let price = currency_pair_btcusdt.make_price(input);
assert!((price.as_f64() - expected).abs() < 1e-9);
}
#[rstest]
fn make_price_half_even_parity(currency_pair_btcusdt: CurrencyPair) {
let rounding_precision = std::cmp::min(
currency_pair_btcusdt.price_precision(),
currency_pair_btcusdt.min_price_increment_precision(),
);
let step = 10f64.powi(-i32::from(rounding_precision));
let base_even_multiple = 42.0;
let base_value = step * base_even_multiple;
let delta = step / 2000.0;
let value_below = base_value + 0.5 * step - delta;
let value_exact = base_value + 0.5 * step;
let value_above = base_value + 0.5 * step + delta;
let price_below = currency_pair_btcusdt.make_price(value_below);
let price_exact = currency_pair_btcusdt.make_price(value_exact);
let price_above = currency_pair_btcusdt.make_price(value_above);
assert_eq!(price_below, price_exact);
assert_ne!(price_exact, price_above);
}
#[rstest]
fn is_quanto_flag(ethbtc_quanto: CryptoFuture) {
assert!(ethbtc_quanto.is_quanto());
}
#[rstest]
fn notional_quanto(ethbtc_quanto: CryptoFuture) {
let quantity = ethbtc_quanto.make_qty(5.0, None);
let price = ethbtc_quanto.make_price(0.036);
let notional = ethbtc_quanto.calculate_notional_value(quantity, price, None);
let expected = Money::new(0.18, ethbtc_quanto.settlement_currency());
assert_eq!(notional, expected);
}
#[rstest]
#[case("USD", "BUSD")]
#[case("USD", "FDUSD")]
#[case("USD", "pUSD")]
#[case("USD", "TUSD")]
#[case("USD", "USD")]
#[case("USD", "USDC")]
#[case("USD", "USDC.e")]
#[case("USD", "USDP")]
#[case("USD", "USDT")]
#[case("BUSD", "USD")]
#[case("FDUSD", "USD")]
#[case("pUSD", "USD")]
#[case("TUSD", "USD")]
#[case("USDC", "USD")]
#[case("USDC.e", "USD")]
#[case("USDP", "USD")]
#[case("USDT", "USD")]
fn usd_equivalent_settlement_is_not_quanto(
#[case] quote_currency_code: &str,
#[case] settlement_currency_code: &str,
) {
let quote_currency =
Currency::try_from_str(quote_currency_code).expect("quote currency must exist");
let settlement_currency = Currency::try_from_str(settlement_currency_code)
.expect("settlement currency must exist");
let instrument = crypto_future_with_quote_settlement(quote_currency, settlement_currency);
let quantity = instrument.make_qty(5.0, None);
let price = instrument.make_price(1000.0);
let notional = instrument.calculate_notional_value(quantity, price, None);
assert!(!instrument.is_quanto());
assert_eq!(instrument.cost_currency(), quote_currency);
assert_eq!(notional, Money::new(5000.0, quote_currency));
}
#[rstest]
fn notional_inverse_base(xbtusd_inverse_perp: CryptoPerpetual) {
let quantity = xbtusd_inverse_perp.make_qty(100.0, None);
let price = xbtusd_inverse_perp.make_price(50_000.0);
let notional = xbtusd_inverse_perp.calculate_notional_value(quantity, price, Some(false));
let expected = Money::new(
100.0 * xbtusd_inverse_perp.multiplier().as_f64() * (1.0 / 50_000.0),
xbtusd_inverse_perp.base_currency().unwrap(),
);
assert_eq!(notional, expected);
}
#[rstest]
fn notional_inverse_quote_use_quote(xbtusd_inverse_perp: CryptoPerpetual) {
let quantity = xbtusd_inverse_perp.make_qty(100.0, None);
let price = xbtusd_inverse_perp.make_price(50_000.0);
let notional = xbtusd_inverse_perp.calculate_notional_value(quantity, price, Some(true));
let expected = Money::new(100.0, xbtusd_inverse_perp.quote_currency());
assert_eq!(notional, expected);
}
#[rstest]
#[should_panic(expected = "'margin_init' not positive")]
fn validate_non_positive_max_price() {
let size_increment = Quantity::new(0.01, 2);
let multiplier = Quantity::new(1.0, 0);
let max_price = Price::new(0.0, 2);
validate_instrument_common(
2,
2,
size_increment,
multiplier,
dec!(0),
dec!(0),
None,
None,
None,
None,
None,
None,
Some(max_price),
None,
)
.expect_display(FAILED);
}
#[rstest]
#[should_panic(expected = "'margin_init' not positive")]
fn validate_non_positive_max_notional(currency_pair_btcusdt: CurrencyPair) {
let size_increment = Quantity::new(0.01, 2);
let multiplier = Quantity::new(1.0, 0);
let max_notional = Money::new(0.0, currency_pair_btcusdt.quote_currency());
validate_instrument_common(
2,
2,
size_increment,
multiplier,
dec!(0),
dec!(0),
None,
None,
None,
None,
Some(max_notional),
None,
None,
None,
)
.expect_display(FAILED);
}
#[rstest]
#[should_panic(expected = "'margin_init' not positive")]
fn validate_price_increment_min_price_precision_mismatch() {
let size_increment = Quantity::new(0.01, 2);
let multiplier = Quantity::new(1.0, 0);
let price_increment = Price::new(0.01, 2);
let min_price = Price::new(1.0, 3);
validate_instrument_common(
2,
2,
size_increment,
multiplier,
dec!(0),
dec!(0),
Some(price_increment),
None,
None,
None,
None,
None,
None,
Some(min_price),
)
.expect_display(FAILED);
}
#[rstest]
#[should_panic(expected = "'margin_init' not positive")]
fn validate_negative_min_notional(currency_pair_btcusdt: CurrencyPair) {
let size_increment = Quantity::new(0.01, 2);
let multiplier = Quantity::new(1.0, 0);
let min_notional = Money::new(-1.0, currency_pair_btcusdt.quote_currency());
let max_notional = Money::new(1.0, currency_pair_btcusdt.quote_currency());
validate_instrument_common(
2,
2,
size_increment,
multiplier,
dec!(0),
dec!(0),
None,
None,
None,
None,
Some(max_notional),
Some(min_notional),
None,
None,
)
.expect_display(FAILED);
}
#[rstest]
#[case::dp0(Decimal::new(1_000, 0), Decimal::new(2, 0), 500.0)]
#[case::dp1(Decimal::new(10_000, 1), Decimal::new(2, 0), 500.0)]
#[case::dp2(Decimal::new(100_000, 2), Decimal::new(2, 0), 500.0)]
#[case::dp3(Decimal::new(1_000_000, 3), Decimal::new(2, 0), 500.0)]
#[case::dp4(Decimal::new(10_000_000, 4), Decimal::new(2, 0), 500.0)]
#[case::dp5(Decimal::new(100_000_000, 5), Decimal::new(2, 0), 500.0)]
#[case::dp6(Decimal::new(1_000_000_000, 6), Decimal::new(2, 0), 500.0)]
#[case::dp7(Decimal::new(10_000_000_000, 7), Decimal::new(2, 0), 500.0)]
#[case::dp8(Decimal::new(100_000_000_000, 8), Decimal::new(2, 0), 500.0)]
fn base_qty_rounding(
currency_pair_btcusdt: CurrencyPair,
#[case] q: Decimal,
#[case] px: Decimal,
#[case] expected: f64,
) {
let qty = Quantity::new(q.to_f64().unwrap(), 8);
let price = Price::new(px.to_f64().unwrap(), 8);
let base = currency_pair_btcusdt.calculate_base_quantity(qty, price);
assert!((base.as_f64() - expected).abs() < 1e-9);
}
proptest! {
#[rstest]
fn make_price_qty_fuzz(input in 0.0001f64..1e8) {
let instrument = currency_pair_btcusdt();
let price = instrument.make_price(input);
prop_assert!(price.as_f64().is_finite());
let quantity = instrument.make_qty(input, None);
prop_assert!(quantity.as_f64().is_finite());
}
}
#[rstest]
fn tick_walk_limits_btcusdt_ask(currency_pair_btcusdt: CurrencyPair) {
if let Some(max_price) = currency_pair_btcusdt.max_price() {
assert!(
currency_pair_btcusdt
.next_ask_price(max_price.as_f64(), 1)
.is_none()
);
}
}
#[rstest]
fn tick_walk_limits_ethusdt_ask(currency_pair_ethusdt: CurrencyPair) {
if let Some(max_price) = currency_pair_ethusdt.max_price() {
assert!(
currency_pair_ethusdt
.next_ask_price(max_price.as_f64(), 1)
.is_none()
);
}
}
#[rstest]
fn tick_walk_limits_btcusdt_bid(currency_pair_btcusdt: CurrencyPair) {
if let Some(min_price) = currency_pair_btcusdt.min_price() {
assert!(
currency_pair_btcusdt
.next_bid_price(min_price.as_f64(), 1)
.is_none()
);
}
}
#[rstest]
fn tick_walk_limits_ethusdt_bid(currency_pair_ethusdt: CurrencyPair) {
if let Some(min_price) = currency_pair_ethusdt.min_price() {
assert!(
currency_pair_ethusdt
.next_bid_price(min_price.as_f64(), 1)
.is_none()
);
}
}
#[rstest]
fn tick_walk_limits_quanto_ask(ethbtc_quanto: CryptoFuture) {
if let Some(max_price) = ethbtc_quanto.max_price() {
assert!(
ethbtc_quanto
.next_ask_price(max_price.as_f64(), 1)
.is_none()
);
}
}
#[rstest]
#[case(0.999_999, false)]
#[case(0.999_999, true)]
#[case(1.000_000_1, false)]
#[case(1.000_000_1, true)]
#[case(1.234_5, false)]
#[case(1.234_5, true)]
#[case(2.345_5, false)]
#[case(2.345_5, true)]
#[case(0.000_999_999, false)]
#[case(0.000_999_999, true)]
fn quantity_rounding_grid(
currency_pair_btcusdt: CurrencyPair,
#[case] input: f64,
#[case] round_down: bool,
) {
let qty = currency_pair_btcusdt.make_qty(input, Some(round_down));
assert!(qty.as_f64().is_finite());
}
#[rstest]
fn pyo3_failure_validate_price_increment_max_price_precision_mismatch() {
let size_increment = Quantity::new(0.01, 2);
let multiplier = Quantity::new(1.0, 0);
let price_increment = Price::new(0.01, 2);
let max_price = Price::new(1.0, 3);
let res = validate_instrument_common(
2,
2,
size_increment,
multiplier,
dec!(0),
dec!(0),
Some(price_increment),
None,
None,
None,
None,
None,
Some(max_price),
None,
);
assert!(res.is_err());
}
#[rstest]
#[case::dp9(Decimal::new(1_000_000_000_000, 9), Decimal::new(2, 0), 500.0)]
#[case::dp10(Decimal::new(10_000_000_000_000, 10), Decimal::new(2, 0), 500.0)]
#[case::dp11(Decimal::new(100_000_000_000_000, 11), Decimal::new(2, 0), 500.0)]
#[case::dp12(Decimal::new(1_000_000_000_000_000, 12), Decimal::new(2, 0), 500.0)]
#[case::dp13(Decimal::new(10_000_000_000_000_000, 13), Decimal::new(2, 0), 500.0)]
#[case::dp14(Decimal::new(100_000_000_000_000_000, 14), Decimal::new(2, 0), 500.0)]
#[case::dp15(Decimal::new(1_000_000_000_000_000_000, 15), Decimal::new(2, 0), 500.0)]
#[case::dp16(
Decimal::from_i128_with_scale(10_000_000_000_000_000_000i128, 16),
Decimal::new(2, 0),
500.0
)]
#[case::dp17(
Decimal::from_i128_with_scale(100_000_000_000_000_000_000i128, 17),
Decimal::new(2, 0),
500.0
)]
fn base_qty_rounding_high_dp(
currency_pair_btcusdt: CurrencyPair,
#[case] q: Decimal,
#[case] px: Decimal,
#[case] expected: f64,
) {
let qty = Quantity::new(q.to_f64().unwrap(), 8);
let price = Price::new(px.to_f64().unwrap(), 8);
let base = currency_pair_btcusdt.calculate_base_quantity(qty, price);
assert!((base.as_f64() - expected).abs() < 1e-9);
}
#[rstest]
fn check_positive_money_ok(currency_pair_btcusdt: CurrencyPair) {
let money = Money::new(100.0, currency_pair_btcusdt.quote_currency());
assert!(check_positive_money(money, "money").is_ok());
}
#[rstest]
#[should_panic(expected = "NotPositive")]
fn check_positive_money_zero(currency_pair_btcusdt: CurrencyPair) {
let money = Money::new(0.0, currency_pair_btcusdt.quote_currency());
check_positive_money(money, "money").unwrap();
}
#[rstest]
#[should_panic(expected = "NotPositive")]
fn check_positive_money_negative(currency_pair_btcusdt: CurrencyPair) {
let money = Money::new(-0.01, currency_pair_btcusdt.quote_currency());
check_positive_money(money, "money").unwrap();
}
fn crypto_future_with_quote_settlement(
quote_currency: Currency,
settlement_currency: Currency,
) -> CryptoFuture {
CryptoFuture::new(
InstrumentId::from("ETHUSD-QUANTO-TEST.BINANCE"),
Symbol::from("ETHUSD-QUANTO-TEST"),
Currency::ETH(),
quote_currency,
settlement_currency,
false,
0.into(),
0.into(),
2,
0,
Price::from("0.01"),
Quantity::from("1"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
0.into(),
0.into(),
)
}
#[rstest]
fn make_price_with_trailing_zeros_in_increment() {
let instrument = CurrencyPair::new(
InstrumentId::from("TEST.VENUE"),
Symbol::from("TEST"),
Currency::from("BTC"),
Currency::from("USD"),
2, 2, Price::new(0.50, 2), Quantity::from("0.01"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None, UnixNanos::default(),
UnixNanos::default(),
);
assert_eq!(instrument.min_price_increment_precision(), 1);
let price = instrument.make_price(1.234);
assert_eq!(price.as_f64(), 1.2);
let price = instrument.make_price(1.25);
assert_eq!(price.as_f64(), 1.2);
let price = instrument.make_price(1.35);
assert_eq!(price.as_f64(), 1.4);
assert_eq!(price.precision, 2);
}
#[rstest]
fn make_qty_with_trailing_zeros_in_increment() {
let instrument = CurrencyPair::new(
InstrumentId::from("TEST.VENUE"),
Symbol::from("TEST"),
Currency::from("BTC"),
Currency::from("USD"),
2, 2, Price::new(0.01, 2),
Quantity::new(0.50, 2), None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None, UnixNanos::default(),
UnixNanos::default(),
);
assert_eq!(instrument.min_size_increment_precision(), 1);
let qty = instrument.make_qty(1.234, None);
assert_eq!(qty.as_f64(), 1.2);
let qty = instrument.make_qty(1.25, None);
assert_eq!(qty.as_f64(), 1.2);
let qty = instrument.make_qty(1.35, None);
assert_eq!(qty.as_f64(), 1.4);
assert_eq!(qty.precision, 2);
let qty = instrument.make_qty(1.99, Some(true));
assert_eq!(qty.as_f64(), 1.9);
}
#[rstest]
#[case(InstrumentClass::Future, true)]
#[case(InstrumentClass::FuturesSpread, true)]
#[case(InstrumentClass::Option, true)]
#[case(InstrumentClass::OptionSpread, true)]
#[case(InstrumentClass::Spot, false)]
#[case(InstrumentClass::Swap, false)]
#[case(InstrumentClass::Forward, false)]
#[case(InstrumentClass::Cfd, false)]
#[case(InstrumentClass::Bond, false)]
#[case(InstrumentClass::Warrant, false)]
#[case(InstrumentClass::SportsBetting, false)]
#[case(InstrumentClass::BinaryOption, false)]
fn test_instrument_class_has_expiration(
#[case] instrument_class: InstrumentClass,
#[case] expected: bool,
) {
assert_eq!(instrument_class.has_expiration(), expected);
}
}