Skip to main content

financial_types/
lib.rs

1//! # Financial Types
2//!
3//! Core financial type definitions for trading systems.
4//!
5//! This crate provides fundamental enums used across financial applications:
6//! - [`UnderlyingAssetType`] — Classification of asset classes (stocks, crypto, forex, etc.)
7//! - [`Action`] — Trading actions (buy, sell)
8//! - [`Side`] — Position directional exposure (long, short)
9//! - [`OptionStyle`] — Option contract style (call, put)
10//!
11//! All enums use `#[repr(u8)]` for compact memory layout and are designed for
12//! high-performance financial systems.
13//!
14//! ## Features
15//!
16//! - Full `serde` serialization/deserialization support
17//! - Optional `utoipa` support for OpenAPI schema generation (enable the `utoipa` feature)
18//! - `#[repr(u8)]` on all enums for deterministic layout
19//! - `#[must_use]` on pure helper methods
20//!
21//! ## Usage
22//!
23//! ```rust
24//! use financial_types::{Action, Side, OptionStyle, UnderlyingAssetType};
25//!
26//! let action = Action::Buy;
27//! let side = Side::Long;
28//! let style = OptionStyle::Call;
29//! let asset = UnderlyingAssetType::Stock;
30//!
31//! assert_eq!(format!("{action}"), "Buy");
32//! assert_eq!(format!("{side}"), "Long");
33//! assert_eq!(format!("{style}"), "Call");
34//! assert_eq!(format!("{asset}"), "Stock");
35//!
36//! assert!(style.is_call());
37//! assert!(side.is_long());
38//! assert!(action.is_buy());
39//! ```
40
41pub mod prelude;
42
43use serde::{Deserialize, Serialize};
44use std::fmt;
45
46/// Classification of the underlying asset for a financial instrument.
47///
48/// Represents the broad asset class to which an instrument belongs.
49/// Used for routing, risk bucketing, and display purposes.
50///
51/// # Examples
52///
53/// ```rust
54/// use financial_types::UnderlyingAssetType;
55///
56/// let asset = UnderlyingAssetType::default();
57/// assert_eq!(asset, UnderlyingAssetType::Stock);
58/// assert_eq!(format!("{asset}"), "Stock");
59/// ```
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
61#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
62#[repr(u8)]
63pub enum UnderlyingAssetType {
64    /// Cryptocurrency assets (e.g., BTC, ETH).
65    Crypto = 0,
66    /// Stock/equity assets (e.g., AAPL, GOOGL).
67    #[default]
68    Stock = 1,
69    /// Foreign exchange currency pairs (e.g., EUR/USD).
70    Forex = 2,
71    /// Commodity assets (e.g., Gold, Oil).
72    Commodity = 3,
73    /// Bond/fixed income securities.
74    Bond = 4,
75    /// Other asset types not covered by the above categories.
76    Other = 5,
77}
78
79impl UnderlyingAssetType {
80    /// Returns `true` if this is a [`Stock`](Self::Stock) variant.
81    #[must_use]
82    #[inline]
83    pub const fn is_stock(&self) -> bool {
84        matches!(self, Self::Stock)
85    }
86
87    /// Returns `true` if this is a [`Crypto`](Self::Crypto) variant.
88    #[must_use]
89    #[inline]
90    pub const fn is_crypto(&self) -> bool {
91        matches!(self, Self::Crypto)
92    }
93
94    /// Returns `true` if this is a [`Forex`](Self::Forex) variant.
95    #[must_use]
96    #[inline]
97    pub const fn is_forex(&self) -> bool {
98        matches!(self, Self::Forex)
99    }
100
101    /// Returns `true` if this is a [`Commodity`](Self::Commodity) variant.
102    #[must_use]
103    #[inline]
104    pub const fn is_commodity(&self) -> bool {
105        matches!(self, Self::Commodity)
106    }
107
108    /// Returns `true` if this is a [`Bond`](Self::Bond) variant.
109    #[must_use]
110    #[inline]
111    pub const fn is_bond(&self) -> bool {
112        matches!(self, Self::Bond)
113    }
114}
115
116impl fmt::Display for UnderlyingAssetType {
117    #[inline]
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::Crypto => write!(f, "Crypto"),
121            Self::Stock => write!(f, "Stock"),
122            Self::Forex => write!(f, "Forex"),
123            Self::Commodity => write!(f, "Commodity"),
124            Self::Bond => write!(f, "Bond"),
125            Self::Other => write!(f, "Other"),
126        }
127    }
128}
129
130/// Represents trading actions in a financial context.
131///
132/// Defines the fundamental trade operations that can be performed in a
133/// trading system. These actions represent the direction of a trade
134/// transaction.
135///
136/// # Examples
137///
138/// ```rust
139/// use financial_types::Action;
140///
141/// let action = Action::Buy;
142/// assert!(action.is_buy());
143/// assert_eq!(format!("{action}"), "Buy");
144/// ```
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
146#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
147#[repr(u8)]
148pub enum Action {
149    /// Represents a purchase transaction, where assets are acquired.
150    #[default]
151    Buy = 0,
152    /// Represents a selling transaction, where assets are disposed of.
153    Sell = 1,
154    /// Action is not applicable to this type of transaction.
155    Other = 2,
156}
157
158impl Action {
159    /// Returns `true` if this is a [`Buy`](Self::Buy) action.
160    #[must_use]
161    #[inline]
162    pub const fn is_buy(&self) -> bool {
163        matches!(self, Self::Buy)
164    }
165
166    /// Returns `true` if this is a [`Sell`](Self::Sell) action.
167    #[must_use]
168    #[inline]
169    pub const fn is_sell(&self) -> bool {
170        matches!(self, Self::Sell)
171    }
172}
173
174impl fmt::Display for Action {
175    #[inline]
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            Self::Buy => write!(f, "Buy"),
179            Self::Sell => write!(f, "Sell"),
180            Self::Other => write!(f, "Other"),
181        }
182    }
183}
184
185/// Defines the directional exposure of a financial position.
186///
187/// Indicates whether a trader expects to profit from rising prices (Long)
188/// or falling prices (Short). This is a fundamental concept in trading that
189/// determines how profits and losses are calculated and affects risk
190/// management considerations.
191///
192/// # Examples
193///
194/// ```rust
195/// use financial_types::Side;
196///
197/// let side = Side::Long;
198/// assert!(side.is_long());
199/// assert!(!side.is_short());
200/// assert_eq!(format!("{side}"), "Long");
201/// ```
202#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
203#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
204#[repr(u8)]
205pub enum Side {
206    /// Profits when the underlying asset's price increases.
207    /// Long positions involve buying an asset with the expectation of selling
208    /// at a higher price.
209    #[default]
210    Long = 0,
211    /// Profits when the underlying asset's price decreases.
212    /// Short positions involve selling an asset (often borrowed) with the
213    /// expectation of buying it back at a lower price.
214    Short = 1,
215}
216
217impl Side {
218    /// Returns `true` if this is a [`Long`](Self::Long) position.
219    #[must_use]
220    #[inline]
221    pub const fn is_long(&self) -> bool {
222        matches!(self, Self::Long)
223    }
224
225    /// Returns `true` if this is a [`Short`](Self::Short) position.
226    #[must_use]
227    #[inline]
228    pub const fn is_short(&self) -> bool {
229        matches!(self, Self::Short)
230    }
231
232    /// Returns the opposite side.
233    ///
234    /// - `Long` → `Short`
235    /// - `Short` → `Long`
236    #[must_use]
237    #[inline]
238    pub const fn opposite(&self) -> Self {
239        match self {
240            Self::Long => Self::Short,
241            Self::Short => Self::Long,
242        }
243    }
244}
245
246impl fmt::Display for Side {
247    #[inline]
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        match self {
250            Self::Long => write!(f, "Long"),
251            Self::Short => write!(f, "Short"),
252        }
253    }
254}
255
256impl fmt::Debug for Side {
257    #[inline]
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        match self {
260            Self::Long => write!(f, "Side::Long"),
261            Self::Short => write!(f, "Side::Short"),
262        }
263    }
264}
265
266/// Specifies the style of an option contract.
267///
268/// Defines the fundamental classification of options contracts based on their
269/// payoff direction. The style determines whether the holder has the right to
270/// buy (Call) or sell (Put) the underlying asset.
271///
272/// This is a critical attribute for options contracts as it directly affects
273/// valuation, pricing models, and exercise strategies.
274///
275/// # Examples
276///
277/// ```rust
278/// use financial_types::OptionStyle;
279///
280/// let style = OptionStyle::Call;
281/// assert!(style.is_call());
282/// assert!(!style.is_put());
283/// assert_eq!(format!("{style}"), "Call");
284/// assert!(OptionStyle::Call < OptionStyle::Put);
285/// ```
286#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
287#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
288#[repr(u8)]
289pub enum OptionStyle {
290    /// A call option gives the holder the right (but not obligation) to **buy**
291    /// the underlying asset at the strike price before or at expiration.
292    /// Call options typically increase in value when the underlying asset price rises.
293    #[default]
294    Call = 0,
295    /// A put option gives the holder the right (but not obligation) to **sell**
296    /// the underlying asset at the strike price before or at expiration.
297    /// Put options typically increase in value when the underlying asset price falls.
298    Put = 1,
299}
300
301impl OptionStyle {
302    /// Returns `true` if this is a [`Call`](Self::Call) option.
303    #[must_use]
304    #[inline]
305    pub const fn is_call(&self) -> bool {
306        matches!(self, Self::Call)
307    }
308
309    /// Returns `true` if this is a [`Put`](Self::Put) option.
310    #[must_use]
311    #[inline]
312    pub const fn is_put(&self) -> bool {
313        matches!(self, Self::Put)
314    }
315
316    /// Returns the opposite option style.
317    ///
318    /// - `Call` → `Put`
319    /// - `Put` → `Call`
320    #[must_use]
321    #[inline]
322    pub const fn opposite(&self) -> Self {
323        match self {
324            Self::Call => Self::Put,
325            Self::Put => Self::Call,
326        }
327    }
328}
329
330impl fmt::Display for OptionStyle {
331    #[inline]
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        match self {
334            Self::Call => write!(f, "Call"),
335            Self::Put => write!(f, "Put"),
336        }
337    }
338}
339
340impl fmt::Debug for OptionStyle {
341    #[inline]
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        match self {
344            Self::Call => write!(f, "OptionStyle::Call"),
345            Self::Put => write!(f, "OptionStyle::Put"),
346        }
347    }
348}
349
350#[cfg(test)]
351#[allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
352mod tests_underlying_asset_type {
353    use super::*;
354
355    #[test]
356    fn test_default() {
357        assert_eq!(UnderlyingAssetType::default(), UnderlyingAssetType::Stock);
358    }
359
360    #[test]
361    fn test_display() {
362        assert_eq!(format!("{}", UnderlyingAssetType::Crypto), "Crypto");
363        assert_eq!(format!("{}", UnderlyingAssetType::Stock), "Stock");
364        assert_eq!(format!("{}", UnderlyingAssetType::Forex), "Forex");
365        assert_eq!(format!("{}", UnderlyingAssetType::Commodity), "Commodity");
366        assert_eq!(format!("{}", UnderlyingAssetType::Bond), "Bond");
367        assert_eq!(format!("{}", UnderlyingAssetType::Other), "Other");
368    }
369
370    #[test]
371    fn test_is_helpers() {
372        assert!(UnderlyingAssetType::Stock.is_stock());
373        assert!(UnderlyingAssetType::Crypto.is_crypto());
374        assert!(UnderlyingAssetType::Forex.is_forex());
375        assert!(UnderlyingAssetType::Commodity.is_commodity());
376        assert!(UnderlyingAssetType::Bond.is_bond());
377        assert!(!UnderlyingAssetType::Other.is_stock());
378        assert!(!UnderlyingAssetType::Stock.is_crypto());
379    }
380
381    #[test]
382    fn test_copy() {
383        let asset = UnderlyingAssetType::Crypto;
384        let copied = asset;
385        assert_eq!(asset, copied);
386    }
387
388    #[test]
389    fn test_hash() {
390        use std::collections::HashSet;
391        let mut set = HashSet::new();
392        set.insert(UnderlyingAssetType::Stock);
393        set.insert(UnderlyingAssetType::Crypto);
394        set.insert(UnderlyingAssetType::Stock); // duplicate
395        assert_eq!(set.len(), 2);
396    }
397
398    #[test]
399    fn test_serialization_roundtrip() {
400        let asset = UnderlyingAssetType::Forex;
401        let json = serde_json::to_string(&asset).unwrap();
402        let deserialized: UnderlyingAssetType = serde_json::from_str(&json).unwrap();
403        assert_eq!(asset, deserialized);
404    }
405
406    #[test]
407    fn test_all_variants_serialize() {
408        let variants = [
409            UnderlyingAssetType::Crypto,
410            UnderlyingAssetType::Stock,
411            UnderlyingAssetType::Forex,
412            UnderlyingAssetType::Commodity,
413            UnderlyingAssetType::Bond,
414            UnderlyingAssetType::Other,
415        ];
416        for variant in variants {
417            let json = serde_json::to_string(&variant).unwrap();
418            let deserialized: UnderlyingAssetType = serde_json::from_str(&json).unwrap();
419            assert_eq!(variant, deserialized);
420        }
421    }
422
423    #[test]
424    fn test_repr_u8_size() {
425        assert_eq!(
426            std::mem::size_of::<UnderlyingAssetType>(),
427            1,
428            "UnderlyingAssetType should be 1 byte with #[repr(u8)]"
429        );
430    }
431}
432
433#[cfg(test)]
434#[allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
435mod tests_action {
436    use super::*;
437
438    #[test]
439    fn test_default() {
440        assert_eq!(Action::default(), Action::Buy);
441    }
442
443    #[test]
444    fn test_display() {
445        assert_eq!(format!("{}", Action::Buy), "Buy");
446        assert_eq!(format!("{}", Action::Sell), "Sell");
447        assert_eq!(format!("{}", Action::Other), "Other");
448    }
449
450    #[test]
451    fn test_is_helpers() {
452        assert!(Action::Buy.is_buy());
453        assert!(!Action::Buy.is_sell());
454        assert!(Action::Sell.is_sell());
455        assert!(!Action::Sell.is_buy());
456        assert!(!Action::Other.is_buy());
457        assert!(!Action::Other.is_sell());
458    }
459
460    #[test]
461    fn test_copy() {
462        let action = Action::Buy;
463        let copied = action;
464        assert_eq!(action, copied);
465    }
466
467    #[test]
468    fn test_serialization_roundtrip() {
469        let action = Action::Sell;
470        let json = serde_json::to_string(&action).unwrap();
471        let deserialized: Action = serde_json::from_str(&json).unwrap();
472        assert_eq!(action, deserialized);
473    }
474
475    #[test]
476    fn test_repr_u8_size() {
477        assert_eq!(
478            std::mem::size_of::<Action>(),
479            1,
480            "Action should be 1 byte with #[repr(u8)]"
481        );
482    }
483}
484
485#[cfg(test)]
486#[allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
487mod tests_side {
488    use super::*;
489
490    #[test]
491    fn test_default() {
492        assert_eq!(Side::default(), Side::Long);
493    }
494
495    #[test]
496    fn test_display() {
497        assert_eq!(format!("{}", Side::Long), "Long");
498        assert_eq!(format!("{}", Side::Short), "Short");
499    }
500
501    #[test]
502    fn test_debug() {
503        assert_eq!(format!("{:?}", Side::Long), "Side::Long");
504        assert_eq!(format!("{:?}", Side::Short), "Side::Short");
505    }
506
507    #[test]
508    fn test_is_helpers() {
509        assert!(Side::Long.is_long());
510        assert!(!Side::Long.is_short());
511        assert!(Side::Short.is_short());
512        assert!(!Side::Short.is_long());
513    }
514
515    #[test]
516    fn test_opposite() {
517        assert_eq!(Side::Long.opposite(), Side::Short);
518        assert_eq!(Side::Short.opposite(), Side::Long);
519    }
520
521    #[test]
522    fn test_copy() {
523        let side = Side::Long;
524        let copied = side;
525        assert_eq!(side, copied);
526    }
527
528    #[test]
529    fn test_hash() {
530        use std::collections::HashSet;
531        let mut set = HashSet::new();
532        set.insert(Side::Long);
533        set.insert(Side::Short);
534        set.insert(Side::Long); // duplicate
535        assert_eq!(set.len(), 2);
536    }
537
538    #[test]
539    fn test_serialization_roundtrip() {
540        let side = Side::Short;
541        let json = serde_json::to_string(&side).unwrap();
542        let deserialized: Side = serde_json::from_str(&json).unwrap();
543        assert_eq!(side, deserialized);
544    }
545
546    #[test]
547    fn test_repr_u8_size() {
548        assert_eq!(
549            std::mem::size_of::<Side>(),
550            1,
551            "Side should be 1 byte with #[repr(u8)]"
552        );
553    }
554}
555
556#[cfg(test)]
557#[allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
558mod tests_option_style {
559    use super::*;
560
561    #[test]
562    fn test_default() {
563        assert_eq!(OptionStyle::default(), OptionStyle::Call);
564    }
565
566    #[test]
567    fn test_display() {
568        assert_eq!(format!("{}", OptionStyle::Call), "Call");
569        assert_eq!(format!("{}", OptionStyle::Put), "Put");
570    }
571
572    #[test]
573    fn test_debug() {
574        assert_eq!(format!("{:?}", OptionStyle::Call), "OptionStyle::Call");
575        assert_eq!(format!("{:?}", OptionStyle::Put), "OptionStyle::Put");
576    }
577
578    #[test]
579    fn test_is_helpers() {
580        assert!(OptionStyle::Call.is_call());
581        assert!(!OptionStyle::Call.is_put());
582        assert!(OptionStyle::Put.is_put());
583        assert!(!OptionStyle::Put.is_call());
584    }
585
586    #[test]
587    fn test_opposite() {
588        assert_eq!(OptionStyle::Call.opposite(), OptionStyle::Put);
589        assert_eq!(OptionStyle::Put.opposite(), OptionStyle::Call);
590    }
591
592    #[test]
593    fn test_ordering() {
594        assert!(OptionStyle::Call < OptionStyle::Put);
595    }
596
597    #[test]
598    fn test_copy() {
599        let style = OptionStyle::Call;
600        let copied = style;
601        assert_eq!(style, copied);
602    }
603
604    #[test]
605    fn test_hash() {
606        use std::collections::HashSet;
607        let mut set = HashSet::new();
608        set.insert(OptionStyle::Call);
609        set.insert(OptionStyle::Put);
610        set.insert(OptionStyle::Call); // duplicate
611        assert_eq!(set.len(), 2);
612    }
613
614    #[test]
615    fn test_serialization_roundtrip() {
616        let style = OptionStyle::Put;
617        let json = serde_json::to_string(&style).unwrap();
618        let deserialized: OptionStyle = serde_json::from_str(&json).unwrap();
619        assert_eq!(style, deserialized);
620    }
621
622    #[test]
623    fn test_repr_u8_size() {
624        assert_eq!(
625            std::mem::size_of::<OptionStyle>(),
626            1,
627            "OptionStyle should be 1 byte with #[repr(u8)]"
628        );
629    }
630}