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