use std::hash::{Hash, Hasher};
use nautilus_core::{
Params, UnixNanos,
correctness::{FAILED, check_equal_u8},
};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use ustr::Ustr;
use super::any::InstrumentAny;
use crate::{
enums::{AssetClass, InstrumentClass, OptionKind},
identifiers::{InstrumentId, Symbol},
instruments::Instrument,
types::{
currency::Currency,
money::Money,
price::{Price, check_positive_price},
quantity::{Quantity, check_positive_quantity},
},
};
#[repr(C)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct CryptoPerpetual {
pub id: InstrumentId,
pub raw_symbol: Symbol,
pub base_currency: Currency,
pub quote_currency: Currency,
pub settlement_currency: Currency,
pub is_inverse: bool,
pub price_precision: u8,
pub size_precision: u8,
pub price_increment: Price,
pub size_increment: Quantity,
pub multiplier: Quantity,
pub lot_size: Quantity,
pub margin_init: Decimal,
pub margin_maint: Decimal,
pub maker_fee: Decimal,
pub taker_fee: Decimal,
pub max_quantity: Option<Quantity>,
pub min_quantity: Option<Quantity>,
pub max_notional: Option<Money>,
pub min_notional: Option<Money>,
pub max_price: Option<Price>,
pub min_price: Option<Price>,
pub info: Option<Params>,
pub ts_event: UnixNanos,
pub ts_init: UnixNanos,
}
impl CryptoPerpetual {
#[allow(clippy::too_many_arguments)]
pub fn new_checked(
instrument_id: InstrumentId,
raw_symbol: Symbol,
base_currency: Currency,
quote_currency: Currency,
settlement_currency: Currency,
is_inverse: bool,
price_precision: u8,
size_precision: u8,
price_increment: Price,
size_increment: Quantity,
multiplier: Option<Quantity>,
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>,
margin_init: Option<Decimal>,
margin_maint: Option<Decimal>,
maker_fee: Option<Decimal>,
taker_fee: Option<Decimal>,
info: Option<Params>,
ts_event: UnixNanos,
ts_init: UnixNanos,
) -> anyhow::Result<Self> {
check_equal_u8(
price_precision,
price_increment.precision,
stringify!(price_precision),
stringify!(price_increment.precision),
)?;
check_equal_u8(
size_precision,
size_increment.precision,
stringify!(size_precision),
stringify!(size_increment.precision),
)?;
check_positive_price(price_increment, stringify!(price_increment))?;
check_positive_quantity(size_increment, stringify!(size_increment))?;
Ok(Self {
id: instrument_id,
raw_symbol,
base_currency,
quote_currency,
settlement_currency,
is_inverse,
price_precision,
size_precision,
price_increment,
size_increment,
multiplier: multiplier.unwrap_or(Quantity::from(1)),
lot_size: lot_size.unwrap_or(Quantity::from(1)),
margin_init: margin_init.unwrap_or_default(),
margin_maint: margin_maint.unwrap_or_default(),
maker_fee: maker_fee.unwrap_or_default(),
taker_fee: taker_fee.unwrap_or_default(),
max_quantity,
min_quantity,
max_notional,
min_notional,
max_price,
min_price,
info,
ts_event,
ts_init,
})
}
#[allow(clippy::too_many_arguments)]
pub fn new(
instrument_id: InstrumentId,
raw_symbol: Symbol,
base_currency: Currency,
quote_currency: Currency,
settlement_currency: Currency,
is_inverse: bool,
price_precision: u8,
size_precision: u8,
price_increment: Price,
size_increment: Quantity,
multiplier: Option<Quantity>,
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>,
margin_init: Option<Decimal>,
margin_maint: Option<Decimal>,
maker_fee: Option<Decimal>,
taker_fee: Option<Decimal>,
info: Option<Params>,
ts_event: UnixNanos,
ts_init: UnixNanos,
) -> Self {
Self::new_checked(
instrument_id,
raw_symbol,
base_currency,
quote_currency,
settlement_currency,
is_inverse,
price_precision,
size_precision,
price_increment,
size_increment,
multiplier,
lot_size,
max_quantity,
min_quantity,
max_notional,
min_notional,
max_price,
min_price,
margin_init,
margin_maint,
maker_fee,
taker_fee,
info,
ts_event,
ts_init,
)
.expect(FAILED)
}
}
impl PartialEq<Self> for CryptoPerpetual {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for CryptoPerpetual {}
impl Hash for CryptoPerpetual {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl Instrument for CryptoPerpetual {
fn into_any(self) -> InstrumentAny {
InstrumentAny::CryptoPerpetual(self)
}
fn id(&self) -> InstrumentId {
self.id
}
fn raw_symbol(&self) -> Symbol {
self.raw_symbol
}
fn asset_class(&self) -> AssetClass {
AssetClass::Cryptocurrency
}
fn instrument_class(&self) -> InstrumentClass {
InstrumentClass::Swap
}
fn underlying(&self) -> Option<Ustr> {
None
}
fn base_currency(&self) -> Option<Currency> {
Some(self.base_currency)
}
fn quote_currency(&self) -> Currency {
self.quote_currency
}
fn settlement_currency(&self) -> Currency {
self.settlement_currency
}
fn isin(&self) -> Option<Ustr> {
None
}
fn option_kind(&self) -> Option<OptionKind> {
None
}
fn exchange(&self) -> Option<Ustr> {
None
}
fn strike_price(&self) -> Option<Price> {
None
}
fn activation_ns(&self) -> Option<UnixNanos> {
None
}
fn expiration_ns(&self) -> Option<UnixNanos> {
None
}
fn is_inverse(&self) -> bool {
self.is_inverse
}
fn price_precision(&self) -> u8 {
self.price_precision
}
fn size_precision(&self) -> u8 {
self.size_precision
}
fn price_increment(&self) -> Price {
self.price_increment
}
fn size_increment(&self) -> Quantity {
self.size_increment
}
fn multiplier(&self) -> Quantity {
self.multiplier
}
fn lot_size(&self) -> Option<Quantity> {
Some(self.lot_size)
}
fn max_quantity(&self) -> Option<Quantity> {
self.max_quantity
}
fn min_quantity(&self) -> Option<Quantity> {
self.min_quantity
}
fn max_notional(&self) -> Option<Money> {
self.max_notional
}
fn min_notional(&self) -> Option<Money> {
self.min_notional
}
fn max_price(&self) -> Option<Price> {
self.max_price
}
fn min_price(&self) -> Option<Price> {
self.min_price
}
fn margin_init(&self) -> Decimal {
self.margin_init
}
fn margin_maint(&self) -> Decimal {
self.margin_maint
}
fn maker_fee(&self) -> Decimal {
self.maker_fee
}
fn taker_fee(&self) -> Decimal {
self.taker_fee
}
fn ts_event(&self) -> UnixNanos {
self.ts_event
}
fn ts_init(&self) -> UnixNanos {
self.ts_init
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use crate::{
enums::{AssetClass, InstrumentClass},
identifiers::{InstrumentId, Symbol},
instruments::{CryptoPerpetual, Instrument, stubs::*},
types::{Currency, Money, Price, Quantity},
};
#[rstest]
fn test_trait_accessors(crypto_perpetual_ethusdt: CryptoPerpetual) {
assert_eq!(
crypto_perpetual_ethusdt.id(),
InstrumentId::from("ETHUSDT-PERP.BINANCE"),
);
assert_eq!(
crypto_perpetual_ethusdt.asset_class(),
AssetClass::Cryptocurrency
);
assert_eq!(
crypto_perpetual_ethusdt.instrument_class(),
InstrumentClass::Swap
);
assert_eq!(
crypto_perpetual_ethusdt.base_currency(),
Some(Currency::ETH())
);
assert_eq!(crypto_perpetual_ethusdt.quote_currency(), Currency::USDT());
assert_eq!(
crypto_perpetual_ethusdt.settlement_currency(),
Currency::USDT()
);
assert!(!crypto_perpetual_ethusdt.is_inverse());
assert_eq!(crypto_perpetual_ethusdt.price_precision(), 2);
assert_eq!(crypto_perpetual_ethusdt.size_precision(), 3);
assert_eq!(
crypto_perpetual_ethusdt.price_increment(),
Price::from("0.01")
);
assert_eq!(
crypto_perpetual_ethusdt.size_increment(),
Quantity::from("0.001")
);
assert_eq!(crypto_perpetual_ethusdt.multiplier(), Quantity::from("1"));
assert_eq!(
crypto_perpetual_ethusdt.lot_size(),
Some(Quantity::from("1"))
);
assert_eq!(
crypto_perpetual_ethusdt.max_quantity(),
Some(Quantity::from("10000.0")),
);
assert_eq!(
crypto_perpetual_ethusdt.min_quantity(),
Some(Quantity::from("0.001")),
);
assert_eq!(
crypto_perpetual_ethusdt.min_notional(),
Some(Money::new(10.00, Currency::USDT())),
);
assert_eq!(crypto_perpetual_ethusdt.underlying(), None);
assert_eq!(crypto_perpetual_ethusdt.option_kind(), None);
assert_eq!(crypto_perpetual_ethusdt.strike_price(), None);
assert_eq!(crypto_perpetual_ethusdt.activation_ns(), None);
assert_eq!(crypto_perpetual_ethusdt.expiration_ns(), None);
}
#[rstest]
fn test_inverse_perp_accessors(xbtusd_bitmex: CryptoPerpetual) {
assert!(xbtusd_bitmex.is_inverse());
assert_eq!(xbtusd_bitmex.base_currency(), Some(Currency::BTC()));
assert_eq!(xbtusd_bitmex.quote_currency(), Currency::USD());
assert_eq!(xbtusd_bitmex.settlement_currency(), Currency::BTC());
assert_eq!(xbtusd_bitmex.cost_currency(), Currency::BTC());
}
#[rstest]
fn test_new_checked_price_precision_mismatch() {
let result = CryptoPerpetual::new_checked(
InstrumentId::from("TEST.EXCHANGE"),
Symbol::from("TEST"),
Currency::BTC(),
Currency::USDT(),
Currency::USDT(),
false,
3, 0,
Price::from("0.01"),
Quantity::from("1"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
0.into(),
0.into(),
);
assert!(result.is_err());
}
#[rstest]
fn test_new_checked_size_precision_mismatch() {
let result = CryptoPerpetual::new_checked(
InstrumentId::from("TEST.EXCHANGE"),
Symbol::from("TEST"),
Currency::BTC(),
Currency::USDT(),
Currency::USDT(),
false,
2,
5, Price::from("0.01"),
Quantity::from("1"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
0.into(),
0.into(),
);
assert!(result.is_err());
}
#[rstest]
fn test_serialization_roundtrip(crypto_perpetual_ethusdt: CryptoPerpetual) {
let json = serde_json::to_string(&crypto_perpetual_ethusdt).unwrap();
let deserialized: CryptoPerpetual = serde_json::from_str(&json).unwrap();
assert_eq!(crypto_perpetual_ethusdt, deserialized);
}
}