use bigdecimal::{BigDecimal, Context, RoundingMode}; use std::sync::LazyLock; pub static ROUNDING_MODE: RoundingMode = RoundingMode::HalfEven; /// Will use default precision (either 100 or ${RUST_BIGDECIMAL_DEFAULT_PRECISION}) pub static DECIMAL_CONTEXT: LazyLock = std::sync::LazyLock::new(|| Context::default().with_rounding_mode(ROUNDING_MODE)); type DateTime = chrono::DateTime; /// Represents an account that holds equity. An account may hold equity with /// multiple equity symbols. pub struct FiduciaryAccount { /// The internal ID of the fiduciary account pub fiduciary_account_id: u8, /// The account number as defined by the fiduciary holder pub fiduciary_account_number: usize, /// Representative name of the fiduciary account pub fiduciary_account_name: String, /// A description of the fiduciary account pub fiduciary_account_description: String, /// Estimated created date and time of the fiduciary account (does not need to be historically accurate, can be seeded from initial transaction recorded; is used relationally) pub fiduciary_account_created_timestamp: DateTime, } /// An atomic enum to describe for what element of equity a symbol tracks /// (stock, fund, etc.) pub enum EquityType { STOCK, ETF, CRYPTO } impl std::fmt::Display for EquityType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", match self { EquityType::STOCK => "STOCK", EquityType::ETF => "ETF", EquityType::CRYPTO => "CRYPTO", }) } } /// Represents a symbol used to ascribe speculative value pub struct EquitySymbol { /// The internal ID of the equity symbol pub equity_symbol_id: usize, /// The name of the equity symbol pub equity_symbol_name: String, /// The basic type of the symbol, for broader categorization pub equity_symbol_type: EquityType, /// The owning/managing company of the equity symbol pub equity_symbol_managing_company: String, /// Estimated created date and time of the equity symbol (does not need to be historically accurate, can be seeded from initial transaction recorded; is used relationally) pub equity_symbol_created_timestamp: DateTime, } /// An atomic enum to describe the basic action of an equity change event pub enum ChangeEventType { BUY, SELL } impl ChangeEventType { /// Relativizes the effect of a change event for numeration pub fn score(&self) -> i8 { match self { ChangeEventType::BUY => 1, ChangeEventType::SELL => -1, } } } impl std::fmt::Display for ChangeEventType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", match self { ChangeEventType::BUY => "BUY", ChangeEventType::SELL => "SELL", }) } } /// An immutable record describing a quantative change for a symbolized /// equity attributed to an account. pub struct EquityChangeEvent { /// The internal ID representing the change event pub equity_change_event_id: usize, /// The internal ID of the fiduciary account pub fiduciary_account_id: u8, /// The internal ID of the equity symbol pub equity_symbol_id: usize, /// The quantity of shares that changed hands pub equity_change_quantity: BigDecimal, /// Cost basis of the event; in more simple terms, this was how much monetarily was actually bought or sold pub equity_change_cost_basis: BigDecimal, /// The type of the event: BUY (purchase of shares), or SELL (sale of shares) pub equity_change_type: ChangeEventType, /// The precise time the event occurred pub equity_change_timestamp: DateTime, } impl EquityChangeEvent { /// Provides an estimate of the numerated change based on the cost basis /// of the equity and the quantity pub fn estimated_price(&self) -> BigDecimal { DECIMAL_CONTEXT.multiply(&self.equity_change_cost_basis, &DECIMAL_CONTEXT.invert(&self.equity_change_quantity)) } /// Provides the quantity of the event relativized by the event type's score pub fn scored_quantity(&self) -> BigDecimal { self.equity_change_quantity.clone() * self.equity_change_type.score() } /// Provides the estimated cost basis of the event relativized by the event type's score pub fn scored_cost_basis(&self) -> BigDecimal { self.equity_change_cost_basis.clone() * self.equity_change_type.score() } } mod tests { use super::*; use std::str::FromStr; fn nat(num: BigDecimal) -> BigDecimal { num.with_scale_round(2_i64, ROUNDING_MODE) } #[test] fn equity_type_display() { assert_eq!(EquityType::STOCK.to_string(), "STOCK"); assert_eq!(EquityType::ETF.to_string(), "ETF"); assert_eq!(EquityType::CRYPTO.to_string(), "CRYPTO"); } #[test] fn event_type_display() { assert_eq!(ChangeEventType::BUY.to_string(), "BUY"); assert_eq!(ChangeEventType::SELL.to_string(), "SELL"); } #[test] fn event_type_score_buy() { let event_type = ChangeEventType::BUY; assert_eq!(event_type.score(), 1_i8); } #[test] fn event_type_score_sell() { let event_type = ChangeEventType::SELL; assert_eq!(event_type.score(), -1_i8); } #[test] fn change_event_estimated_price() { let event = EquityChangeEvent { equity_change_event_id: 0, fiduciary_account_id: 0, equity_symbol_id: 0, equity_change_quantity: BigDecimal::from_str("0.125").unwrap(), equity_change_cost_basis: BigDecimal::from_str("82.80").unwrap(), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: chrono::Local::now().to_utc(), }; assert_eq!(nat(event.estimated_price()).to_plain_string(), "662.40"); } #[test] fn change_event_estimated_price_rounded() { let event = EquityChangeEvent { equity_change_event_id: 0, fiduciary_account_id: 0, equity_symbol_id: 0, equity_change_quantity: BigDecimal::from_str("87.12808").unwrap(), equity_change_cost_basis: BigDecimal::from_str("14.91").unwrap(), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: chrono::Local::now().to_utc(), }; // actual: 0.171127379371 assert_eq!(nat(event.estimated_price()).to_plain_string(), "0.17"); } #[test] fn change_event_scored_quantity_buy() { let event = EquityChangeEvent { equity_change_event_id: 0, fiduciary_account_id: 0, equity_symbol_id: 0, equity_change_quantity: BigDecimal::from_str("0.125").unwrap(), equity_change_cost_basis: BigDecimal::from_str("82.80").unwrap(), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: chrono::Local::now().to_utc(), }; assert_eq!(event.scored_quantity().to_plain_string(), "0.125"); } #[test] fn change_event_scored_quantity_sell() { let event = EquityChangeEvent { equity_change_event_id: 0, fiduciary_account_id: 0, equity_symbol_id: 0, equity_change_quantity: BigDecimal::from_str("0.125").unwrap(), equity_change_cost_basis: BigDecimal::from_str("82.80").unwrap(), equity_change_type: ChangeEventType::SELL, equity_change_timestamp: chrono::Local::now().to_utc(), }; assert_eq!(event.scored_quantity().to_plain_string(), "-0.125"); } #[test] fn change_event_scored_estimated_price_buy() { let event = EquityChangeEvent { equity_change_event_id: 0, fiduciary_account_id: 0, equity_symbol_id: 0, equity_change_quantity: BigDecimal::from_str("0.125").unwrap(), equity_change_cost_basis: BigDecimal::from_str("82.80").unwrap(), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: chrono::Local::now().to_utc(), }; assert_eq!(nat(event.scored_cost_basis()).to_plain_string(), "82.80"); } #[test] fn change_event_scored_estimated_price_sell() { let event = EquityChangeEvent { equity_change_event_id: 0, fiduciary_account_id: 0, equity_symbol_id: 0, equity_change_quantity: BigDecimal::from_str("0.125").unwrap(), equity_change_cost_basis: BigDecimal::from_str("82.80").unwrap(), equity_change_type: ChangeEventType::SELL, equity_change_timestamp: chrono::Local::now().to_utc(), }; assert_eq!(nat(event.scored_cost_basis()).to_plain_string(), "-82.80"); } }