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