nautilus_model/instruments/
crypto_perpetual.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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    UnixNanos,
20    correctness::{FAILED, check_equal_u8},
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::{Price, check_positive_price},
35        quantity::{Quantity, check_positive_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 = "posei_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    /// # Errors
104    ///
105    /// Returns an error if any input validation fails.
106    #[allow(clippy::too_many_arguments)]
107    pub fn new_checked(
108        instrument_id: InstrumentId,
109        raw_symbol: Symbol,
110        base_currency: Currency,
111        quote_currency: Currency,
112        settlement_currency: Currency,
113        is_inverse: bool,
114        price_precision: u8,
115        size_precision: u8,
116        price_increment: Price,
117        size_increment: Quantity,
118        multiplier: Option<Quantity>,
119        lot_size: Option<Quantity>,
120        max_quantity: Option<Quantity>,
121        min_quantity: Option<Quantity>,
122        max_notional: Option<Money>,
123        min_notional: Option<Money>,
124        max_price: Option<Price>,
125        min_price: Option<Price>,
126        margin_init: Option<Decimal>,
127        margin_maint: Option<Decimal>,
128        maker_fee: Option<Decimal>,
129        taker_fee: Option<Decimal>,
130        ts_event: UnixNanos,
131        ts_init: UnixNanos,
132    ) -> anyhow::Result<Self> {
133        check_equal_u8(
134            price_precision,
135            price_increment.precision,
136            stringify!(price_precision),
137            stringify!(price_increment.precision),
138        )?;
139        check_equal_u8(
140            size_precision,
141            size_increment.precision,
142            stringify!(size_precision),
143            stringify!(size_increment.precision),
144        )?;
145        check_positive_price(price_increment, stringify!(price_increment))?;
146        check_positive_quantity(size_increment, stringify!(size_increment))?;
147
148        Ok(Self {
149            id: instrument_id,
150            raw_symbol,
151            base_currency,
152            quote_currency,
153            settlement_currency,
154            is_inverse,
155            price_precision,
156            size_precision,
157            price_increment,
158            size_increment,
159            multiplier: multiplier.unwrap_or(Quantity::from(1)),
160            lot_size: lot_size.unwrap_or(Quantity::from(1)),
161            margin_init: margin_init.unwrap_or_default(),
162            margin_maint: margin_maint.unwrap_or_default(),
163            maker_fee: maker_fee.unwrap_or_default(),
164            taker_fee: taker_fee.unwrap_or_default(),
165            max_quantity,
166            min_quantity,
167            max_notional,
168            min_notional,
169            max_price,
170            min_price,
171            ts_event,
172            ts_init,
173        })
174    }
175
176    /// Creates a new [`CryptoPerpetual`] instance.
177    ///
178    /// # Panics
179    ///
180    /// Panics if any input parameter is invalid (see `new_checked`).
181    #[allow(clippy::too_many_arguments)]
182    pub fn new(
183        instrument_id: InstrumentId,
184        raw_symbol: Symbol,
185        base_currency: Currency,
186        quote_currency: Currency,
187        settlement_currency: Currency,
188        is_inverse: bool,
189        price_precision: u8,
190        size_precision: u8,
191        price_increment: Price,
192        size_increment: Quantity,
193        multiplier: Option<Quantity>,
194        lot_size: Option<Quantity>,
195        max_quantity: Option<Quantity>,
196        min_quantity: Option<Quantity>,
197        max_notional: Option<Money>,
198        min_notional: Option<Money>,
199        max_price: Option<Price>,
200        min_price: Option<Price>,
201        margin_init: Option<Decimal>,
202        margin_maint: Option<Decimal>,
203        maker_fee: Option<Decimal>,
204        taker_fee: Option<Decimal>,
205        ts_event: UnixNanos,
206        ts_init: UnixNanos,
207    ) -> Self {
208        Self::new_checked(
209            instrument_id,
210            raw_symbol,
211            base_currency,
212            quote_currency,
213            settlement_currency,
214            is_inverse,
215            price_precision,
216            size_precision,
217            price_increment,
218            size_increment,
219            multiplier,
220            lot_size,
221            max_quantity,
222            min_quantity,
223            max_notional,
224            min_notional,
225            max_price,
226            min_price,
227            margin_init,
228            margin_maint,
229            maker_fee,
230            taker_fee,
231            ts_event,
232            ts_init,
233        )
234        .expect(FAILED)
235    }
236}
237
238impl PartialEq<Self> for CryptoPerpetual {
239    fn eq(&self, other: &Self) -> bool {
240        self.id == other.id
241    }
242}
243
244impl Eq for CryptoPerpetual {}
245
246impl Hash for CryptoPerpetual {
247    fn hash<H: Hasher>(&self, state: &mut H) {
248        self.id.hash(state);
249    }
250}
251
252impl Instrument for CryptoPerpetual {
253    fn into_any(self) -> InstrumentAny {
254        InstrumentAny::CryptoPerpetual(self)
255    }
256
257    fn id(&self) -> InstrumentId {
258        self.id
259    }
260
261    fn raw_symbol(&self) -> Symbol {
262        self.raw_symbol
263    }
264
265    fn asset_class(&self) -> AssetClass {
266        AssetClass::Cryptocurrency
267    }
268
269    fn instrument_class(&self) -> InstrumentClass {
270        InstrumentClass::Swap
271    }
272    fn underlying(&self) -> Option<Ustr> {
273        None
274    }
275
276    fn base_currency(&self) -> Option<Currency> {
277        Some(self.base_currency)
278    }
279
280    fn quote_currency(&self) -> Currency {
281        self.quote_currency
282    }
283
284    fn settlement_currency(&self) -> Currency {
285        self.settlement_currency
286    }
287
288    fn isin(&self) -> Option<Ustr> {
289        None
290    }
291    fn option_kind(&self) -> Option<OptionKind> {
292        None
293    }
294    fn exchange(&self) -> Option<Ustr> {
295        None
296    }
297    fn strike_price(&self) -> Option<Price> {
298        None
299    }
300
301    fn activation_ns(&self) -> Option<UnixNanos> {
302        None
303    }
304
305    fn expiration_ns(&self) -> Option<UnixNanos> {
306        None
307    }
308
309    fn is_inverse(&self) -> bool {
310        self.is_inverse
311    }
312
313    fn price_precision(&self) -> u8 {
314        self.price_precision
315    }
316
317    fn size_precision(&self) -> u8 {
318        self.size_precision
319    }
320
321    fn price_increment(&self) -> Price {
322        self.price_increment
323    }
324
325    fn size_increment(&self) -> Quantity {
326        self.size_increment
327    }
328
329    fn multiplier(&self) -> Quantity {
330        Quantity::from(1)
331    }
332
333    fn lot_size(&self) -> Option<Quantity> {
334        Some(self.lot_size)
335    }
336
337    fn max_quantity(&self) -> Option<Quantity> {
338        self.max_quantity
339    }
340
341    fn min_quantity(&self) -> Option<Quantity> {
342        self.min_quantity
343    }
344
345    fn max_notional(&self) -> Option<Money> {
346        self.max_notional
347    }
348
349    fn min_notional(&self) -> Option<Money> {
350        self.min_notional
351    }
352
353    fn max_price(&self) -> Option<Price> {
354        self.max_price
355    }
356
357    fn min_price(&self) -> Option<Price> {
358        self.min_price
359    }
360
361    fn margin_init(&self) -> Decimal {
362        self.margin_init
363    }
364
365    fn margin_maint(&self) -> Decimal {
366        self.margin_maint
367    }
368
369    fn maker_fee(&self) -> Decimal {
370        self.maker_fee
371    }
372
373    fn taker_fee(&self) -> Decimal {
374        self.taker_fee
375    }
376
377    fn ts_event(&self) -> UnixNanos {
378        self.ts_event
379    }
380
381    fn ts_init(&self) -> UnixNanos {
382        self.ts_init
383    }
384}
385
386////////////////////////////////////////////////////////////////////////////////
387// Tests
388////////////////////////////////////////////////////////////////////////////////
389#[cfg(test)]
390mod tests {
391    use rstest::rstest;
392
393    use crate::instruments::{CryptoPerpetual, stubs::*};
394
395    #[rstest]
396    fn test_equality(crypto_perpetual_ethusdt: CryptoPerpetual) {
397        let cloned = crypto_perpetual_ethusdt;
398        assert_eq!(crypto_perpetual_ethusdt, cloned);
399    }
400}