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