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