nautilus_model/instruments/
crypto_perpetual.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19    correctness::{check_equal_u8, FAILED},
20    UnixNanos,
21};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use ustr::Ustr;
25
26use super::any::InstrumentAny;
27use crate::{
28    enums::{AssetClass, InstrumentClass, OptionKind},
29    identifiers::{InstrumentId, Symbol},
30    instruments::Instrument,
31    types::{
32        currency::Currency,
33        money::Money,
34        price::{check_positive_price, Price},
35        quantity::{check_positive_quantity, Quantity},
36    },
37};
38
39/// Represents a crypto perpetual futures contract instrument (a.k.a. perpetual swap).
40#[repr(C)]
41#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
42#[cfg_attr(
43    feature = "python",
44    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
45)]
46pub struct CryptoPerpetual {
47    /// The instrument ID for the instrument.
48    pub id: InstrumentId,
49    /// The raw/local/native symbol for the instrument, assigned by the venue.
50    pub raw_symbol: Symbol,
51    /// The base currency.
52    pub base_currency: Currency,
53    /// The quote currency.
54    pub quote_currency: Currency,
55    /// The settlement currency.
56    pub settlement_currency: Currency,
57    /// If the instrument costing is inverse (quantity expressed in quote currency units).
58    pub is_inverse: bool,
59    /// The price decimal precision.
60    pub price_precision: u8,
61    /// The trading size decimal precision.
62    pub size_precision: u8,
63    /// The minimum price increment (tick size).
64    pub price_increment: Price,
65    /// The minimum size increment.
66    pub size_increment: Quantity,
67    /// The contract multiplier.
68    pub multiplier: Quantity,
69    /// The rounded lot unit size (standard/board).
70    pub lot_size: Quantity,
71    /// The initial (order) margin requirement in percentage of order value.
72    pub margin_init: Decimal,
73    /// The maintenance (position) margin in percentage of position value.
74    pub margin_maint: Decimal,
75    /// The fee rate for liquidity makers as a percentage of order value.
76    pub maker_fee: Decimal,
77    /// The fee rate for liquidity takers as a percentage of order value.
78    pub taker_fee: Decimal,
79    /// The maximum allowable order quantity.
80    pub max_quantity: Option<Quantity>,
81    /// The minimum allowable order quantity.
82    pub min_quantity: Option<Quantity>,
83    /// The maximum allowable order notional value.
84    pub max_notional: Option<Money>,
85    /// The minimum allowable order notional value.
86    pub min_notional: Option<Money>,
87    /// The maximum allowable quoted price.
88    pub max_price: Option<Price>,
89    /// The minimum allowable quoted price.
90    pub min_price: Option<Price>,
91    /// UNIX timestamp (nanoseconds) when the data event occurred.
92    pub ts_event: UnixNanos,
93    /// UNIX timestamp (nanoseconds) when the data object was initialized.
94    pub ts_init: UnixNanos,
95}
96
97impl CryptoPerpetual {
98    /// Creates a new [`CryptoPerpetual`] instance with correctness checking.
99    ///
100    /// # Notes
101    ///
102    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
103    #[allow(clippy::too_many_arguments)]
104    pub fn new_checked(
105        id: InstrumentId,
106        raw_symbol: Symbol,
107        base_currency: Currency,
108        quote_currency: Currency,
109        settlement_currency: Currency,
110        is_inverse: bool,
111        price_precision: u8,
112        size_precision: u8,
113        price_increment: Price,
114        size_increment: Quantity,
115        multiplier: Option<Quantity>,
116        lot_size: Option<Quantity>,
117        max_quantity: Option<Quantity>,
118        min_quantity: Option<Quantity>,
119        max_notional: Option<Money>,
120        min_notional: Option<Money>,
121        max_price: Option<Price>,
122        min_price: Option<Price>,
123        margin_init: Option<Decimal>,
124        margin_maint: Option<Decimal>,
125        maker_fee: Option<Decimal>,
126        taker_fee: Option<Decimal>,
127        ts_event: UnixNanos,
128        ts_init: UnixNanos,
129    ) -> anyhow::Result<Self> {
130        check_equal_u8(
131            price_precision,
132            price_increment.precision,
133            stringify!(price_precision),
134            stringify!(price_increment.precision),
135        )?;
136        check_equal_u8(
137            size_precision,
138            size_increment.precision,
139            stringify!(size_precision),
140            stringify!(size_increment.precision),
141        )?;
142        check_positive_price(price_increment.raw, stringify!(price_increment.raw))?;
143        check_positive_quantity(size_increment.raw, stringify!(size_increment.raw))?;
144
145        Ok(Self {
146            id,
147            raw_symbol,
148            base_currency,
149            quote_currency,
150            settlement_currency,
151            is_inverse,
152            price_precision,
153            size_precision,
154            price_increment,
155            size_increment,
156            multiplier: multiplier.unwrap_or(Quantity::from(1)),
157            lot_size: lot_size.unwrap_or(Quantity::from(1)),
158            margin_init: margin_init.unwrap_or_default(),
159            margin_maint: margin_maint.unwrap_or_default(),
160            maker_fee: maker_fee.unwrap_or_default(),
161            taker_fee: taker_fee.unwrap_or_default(),
162            max_quantity,
163            min_quantity,
164            max_notional,
165            min_notional,
166            max_price,
167            min_price,
168            ts_event,
169            ts_init,
170        })
171    }
172
173    /// Creates a new [`CryptoPerpetual`] instance
174    #[allow(clippy::too_many_arguments)]
175    pub fn new(
176        id: InstrumentId,
177        raw_symbol: Symbol,
178        base_currency: Currency,
179        quote_currency: Currency,
180        settlement_currency: Currency,
181        is_inverse: bool,
182        price_precision: u8,
183        size_precision: u8,
184        price_increment: Price,
185        size_increment: Quantity,
186        multiplier: Option<Quantity>,
187        lot_size: Option<Quantity>,
188        max_quantity: Option<Quantity>,
189        min_quantity: Option<Quantity>,
190        max_notional: Option<Money>,
191        min_notional: Option<Money>,
192        max_price: Option<Price>,
193        min_price: Option<Price>,
194        margin_init: Option<Decimal>,
195        margin_maint: Option<Decimal>,
196        maker_fee: Option<Decimal>,
197        taker_fee: Option<Decimal>,
198        ts_event: UnixNanos,
199        ts_init: UnixNanos,
200    ) -> Self {
201        Self::new_checked(
202            id,
203            raw_symbol,
204            base_currency,
205            quote_currency,
206            settlement_currency,
207            is_inverse,
208            price_precision,
209            size_precision,
210            price_increment,
211            size_increment,
212            multiplier,
213            lot_size,
214            max_quantity,
215            min_quantity,
216            max_notional,
217            min_notional,
218            max_price,
219            min_price,
220            margin_init,
221            margin_maint,
222            maker_fee,
223            taker_fee,
224            ts_event,
225            ts_init,
226        )
227        .expect(FAILED)
228    }
229}
230
231impl PartialEq<Self> for CryptoPerpetual {
232    fn eq(&self, other: &Self) -> bool {
233        self.id == other.id
234    }
235}
236
237impl Eq for CryptoPerpetual {}
238
239impl Hash for CryptoPerpetual {
240    fn hash<H: Hasher>(&self, state: &mut H) {
241        self.id.hash(state);
242    }
243}
244
245impl Instrument for CryptoPerpetual {
246    fn into_any(self) -> InstrumentAny {
247        InstrumentAny::CryptoPerpetual(self)
248    }
249
250    fn id(&self) -> InstrumentId {
251        self.id
252    }
253
254    fn raw_symbol(&self) -> Symbol {
255        self.raw_symbol
256    }
257
258    fn asset_class(&self) -> AssetClass {
259        AssetClass::Cryptocurrency
260    }
261
262    fn instrument_class(&self) -> InstrumentClass {
263        InstrumentClass::Swap
264    }
265    fn underlying(&self) -> Option<Ustr> {
266        None
267    }
268
269    fn base_currency(&self) -> Option<Currency> {
270        Some(self.base_currency)
271    }
272
273    fn quote_currency(&self) -> Currency {
274        self.quote_currency
275    }
276
277    fn settlement_currency(&self) -> Currency {
278        self.settlement_currency
279    }
280
281    fn isin(&self) -> Option<Ustr> {
282        None
283    }
284    fn option_kind(&self) -> Option<OptionKind> {
285        None
286    }
287    fn exchange(&self) -> Option<Ustr> {
288        None
289    }
290    fn strike_price(&self) -> Option<Price> {
291        None
292    }
293
294    fn activation_ns(&self) -> Option<UnixNanos> {
295        None
296    }
297
298    fn expiration_ns(&self) -> Option<UnixNanos> {
299        None
300    }
301
302    fn is_inverse(&self) -> bool {
303        self.is_inverse
304    }
305
306    fn price_precision(&self) -> u8 {
307        self.price_precision
308    }
309
310    fn size_precision(&self) -> u8 {
311        self.size_precision
312    }
313
314    fn price_increment(&self) -> Price {
315        self.price_increment
316    }
317
318    fn size_increment(&self) -> Quantity {
319        self.size_increment
320    }
321
322    fn multiplier(&self) -> Quantity {
323        Quantity::from(1)
324    }
325
326    fn lot_size(&self) -> Option<Quantity> {
327        Some(self.lot_size)
328    }
329
330    fn max_quantity(&self) -> Option<Quantity> {
331        self.max_quantity
332    }
333
334    fn min_quantity(&self) -> Option<Quantity> {
335        self.min_quantity
336    }
337
338    fn max_notional(&self) -> Option<Money> {
339        self.max_notional
340    }
341
342    fn min_notional(&self) -> Option<Money> {
343        self.min_notional
344    }
345
346    fn max_price(&self) -> Option<Price> {
347        self.max_price
348    }
349
350    fn min_price(&self) -> Option<Price> {
351        self.min_price
352    }
353
354    fn margin_init(&self) -> Decimal {
355        self.margin_init
356    }
357
358    fn margin_maint(&self) -> Decimal {
359        self.margin_maint
360    }
361
362    fn maker_fee(&self) -> Decimal {
363        self.maker_fee
364    }
365
366    fn taker_fee(&self) -> Decimal {
367        self.taker_fee
368    }
369
370    fn ts_event(&self) -> UnixNanos {
371        self.ts_event
372    }
373
374    fn ts_init(&self) -> UnixNanos {
375        self.ts_init
376    }
377}
378
379////////////////////////////////////////////////////////////////////////////////
380// Tests
381////////////////////////////////////////////////////////////////////////////////
382#[cfg(test)]
383mod tests {
384    use rstest::rstest;
385
386    use crate::instruments::{stubs::*, CryptoPerpetual};
387
388    #[rstest]
389    fn test_equality(crypto_perpetual_ethusdt: CryptoPerpetual) {
390        let cloned = crypto_perpetual_ethusdt;
391        assert_eq!(crypto_perpetual_ethusdt, cloned);
392    }
393}