nautilus_model/instruments/
mod.rs1pub mod any;
19pub mod betting;
20pub mod binary_option;
21pub mod crypto_future;
22pub mod crypto_option;
23pub mod crypto_perpetual;
24pub mod currency_pair;
25pub mod equity;
26pub mod futures_contract;
27pub mod futures_spread;
28pub mod option_contract;
29pub mod option_spread;
30pub mod synthetic;
31
32#[cfg(feature = "stubs")]
33pub mod stubs;
34
35use enum_dispatch::enum_dispatch;
36use nautilus_core::UnixNanos;
37use rust_decimal::Decimal;
38use rust_decimal_macros::dec;
39use ustr::Ustr;
40
41pub use crate::instruments::{
43 any::InstrumentAny, betting::BettingInstrument, binary_option::BinaryOption,
44 crypto_future::CryptoFuture, crypto_option::CryptoOption, crypto_perpetual::CryptoPerpetual,
45 currency_pair::CurrencyPair, equity::Equity, futures_contract::FuturesContract,
46 futures_spread::FuturesSpread, option_contract::OptionContract, option_spread::OptionSpread,
47 synthetic::SyntheticInstrument,
48};
49use crate::{
50 enums::{AssetClass, InstrumentClass, OptionKind},
51 identifiers::{InstrumentId, Symbol, Venue},
52 types::{Currency, Money, Price, Quantity},
53};
54
55#[enum_dispatch]
56pub trait Instrument: 'static + Send {
57 fn into_any(self) -> InstrumentAny;
58 fn id(&self) -> InstrumentId;
59 fn symbol(&self) -> Symbol {
60 self.id().symbol
61 }
62 fn venue(&self) -> Venue {
63 self.id().venue
64 }
65 fn raw_symbol(&self) -> Symbol;
66 fn asset_class(&self) -> AssetClass;
67 fn instrument_class(&self) -> InstrumentClass;
68 fn underlying(&self) -> Option<Ustr>;
69 fn base_currency(&self) -> Option<Currency>;
70 fn quote_currency(&self) -> Currency;
71 fn settlement_currency(&self) -> Currency;
72 fn cost_currency(&self) -> Currency {
73 if self.is_inverse() {
74 self.base_currency()
75 .expect("Inverse instruments must have a base currency")
76 } else {
77 self.quote_currency()
78 }
79 }
80 fn isin(&self) -> Option<Ustr>;
81 fn option_kind(&self) -> Option<OptionKind>;
82 fn exchange(&self) -> Option<Ustr>;
83 fn strike_price(&self) -> Option<Price>;
84 fn activation_ns(&self) -> Option<UnixNanos>;
85 fn expiration_ns(&self) -> Option<UnixNanos>;
86 fn is_inverse(&self) -> bool;
87 fn is_quanto(&self) -> bool {
88 if let Some(base_currency) = self.base_currency() {
89 self.settlement_currency() != base_currency
90 } else {
91 false
92 }
93 }
94 fn price_precision(&self) -> u8;
95 fn size_precision(&self) -> u8;
96 fn price_increment(&self) -> Price;
97 fn size_increment(&self) -> Quantity;
98 fn multiplier(&self) -> Quantity;
99 fn lot_size(&self) -> Option<Quantity>;
100 fn max_quantity(&self) -> Option<Quantity>;
101 fn min_quantity(&self) -> Option<Quantity>;
102 fn max_notional(&self) -> Option<Money>;
103 fn min_notional(&self) -> Option<Money>;
104 fn max_price(&self) -> Option<Price>;
105 fn min_price(&self) -> Option<Price>;
106 fn margin_init(&self) -> Decimal {
107 dec!(0) }
109
110 fn margin_maint(&self) -> Decimal {
111 dec!(0) }
113
114 fn maker_fee(&self) -> Decimal {
115 dec!(0) }
117
118 fn taker_fee(&self) -> Decimal {
119 dec!(0) }
121 fn ts_event(&self) -> UnixNanos;
122 fn ts_init(&self) -> UnixNanos;
123
124 fn make_price(&self, value: f64) -> Price {
126 Price::new(value, self.price_precision())
127 }
128
129 fn make_qty(&self, value: f64, round_down: Option<bool>) -> Quantity {
131 if round_down.unwrap_or(false) {
132 let increment = 10f64.powi(-i32::from(self.size_precision()));
134 let rounded_value = (value / increment).floor() * increment;
135 Quantity::new(rounded_value, self.size_precision())
136 } else {
137 Quantity::new(value, self.size_precision())
139 }
140 }
141
142 fn calculate_notional_value(
149 &self,
150 quantity: Quantity,
151 price: Price,
152 use_quote_for_inverse: Option<bool>,
153 ) -> Money {
154 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
155 let (amount, currency) = if self.is_inverse() {
156 if use_quote_for_inverse {
157 (quantity.as_f64(), self.quote_currency())
158 } else {
159 let amount =
160 quantity.as_f64() * self.multiplier().as_f64() * (1.0 / price.as_f64());
161 let currency = self
162 .base_currency()
163 .expect("Error: no base currency for notional calculation");
164 (amount, currency)
165 }
166 } else {
167 let amount = quantity.as_f64() * self.multiplier().as_f64() * price.as_f64();
168 let currency = self.quote_currency();
169 (amount, currency)
170 };
171
172 Money::new(amount, currency)
173 }
174
175 fn calculate_base_quantity(&self, quantity: Quantity, last_px: Price) -> Quantity {
177 let value = quantity.as_f64() * (1.0 / last_px.as_f64());
178 Quantity::new(value, self.size_precision())
179 }
180}
181
182pub const EXPIRING_INSTRUMENT_TYPES: [InstrumentClass; 4] = [
183 InstrumentClass::Future,
184 InstrumentClass::FuturesSpread,
185 InstrumentClass::Option,
186 InstrumentClass::OptionSpread,
187];
188
189#[cfg(test)]
190mod tests {
191 use rstest::rstest;
192
193 use crate::instruments::{CurrencyPair, Instrument, stubs::*};
194
195 #[rstest]
196 fn test_make_qty_standard_rounding(currency_pair_btcusdt: CurrencyPair) {
197 assert_eq!(
198 currency_pair_btcusdt.make_qty(1.5, None).to_string(),
199 "1.500000"
200 ); assert_eq!(
202 currency_pair_btcusdt.make_qty(2.5, None).to_string(),
203 "2.500000"
204 ); assert_eq!(
206 currency_pair_btcusdt.make_qty(1.2345678, None).to_string(),
207 "1.234568"
208 ); }
210
211 #[rstest]
212 fn test_make_qty_round_down(currency_pair_btcusdt: CurrencyPair) {
213 assert_eq!(
214 currency_pair_btcusdt.make_qty(1.5, Some(true)).to_string(),
215 "1.500000"
216 ); assert_eq!(
218 currency_pair_btcusdt.make_qty(2.5, Some(true)).to_string(),
219 "2.500000"
220 ); assert_eq!(
222 currency_pair_btcusdt
223 .make_qty(1.2345678, Some(true))
224 .to_string(),
225 "1.234567"
226 ); assert_eq!(
228 currency_pair_btcusdt
229 .make_qty(1.9999999, Some(true))
230 .to_string(),
231 "1.999999"
232 ); }
234
235 #[rstest]
236 fn test_make_qty_boundary_cases(currency_pair_btcusdt: CurrencyPair) {
237 let increment = 0.000001;
239
240 let value_just_above = 1.0 + (increment * 1.1);
242 assert_eq!(
243 currency_pair_btcusdt
244 .make_qty(value_just_above, Some(true))
245 .to_string(),
246 "1.000001"
247 ); assert_eq!(
249 currency_pair_btcusdt
250 .make_qty(value_just_above, None)
251 .to_string(),
252 "1.000001"
253 ); let value_half_increment = 1.0000015;
257 assert_eq!(
258 currency_pair_btcusdt
259 .make_qty(value_half_increment, Some(true))
260 .to_string(),
261 "1.000001"
262 ); assert_eq!(
264 currency_pair_btcusdt
265 .make_qty(value_half_increment, None)
266 .to_string(),
267 "1.000002"
268 ); }
270
271 #[rstest]
272 fn test_make_qty_zero_value(currency_pair_btcusdt: CurrencyPair) {
273 assert_eq!(
275 currency_pair_btcusdt.make_qty(0.0, None).to_string(),
276 "0.000000"
277 );
278 assert_eq!(
279 currency_pair_btcusdt.make_qty(0.0, Some(true)).to_string(),
280 "0.000000"
281 );
282 }
283
284 #[rstest]
285 fn test_make_qty_different_precision(currency_pair_ethusdt: CurrencyPair) {
286 assert_eq!(
288 currency_pair_ethusdt.make_qty(1.2345678, None).to_string(),
289 "1.23457"
290 ); assert_eq!(
292 currency_pair_ethusdt
293 .make_qty(1.2345678, Some(true))
294 .to_string(),
295 "1.23456"
296 ); }
298}