use bigdecimal::BigDecimal; use super::ContextError; 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.) #[derive(Debug, PartialEq)] pub enum EquityType { STOCK, ETF, CRYPTO, } impl TryFrom<&str> for EquityType { type Error = ContextError; fn try_from(value: &str) -> Result { match value { "STOCK" => Ok(EquityType::STOCK), "ETF" => Ok(EquityType::ETF), "CRYPTO" => Ok(EquityType::CRYPTO), _ => Err(ContextError::EnumTranslationError(value.into())), } } } 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 #[derive(Debug, PartialEq)] pub enum ChangeEventType { BUY, SELL, } impl TryFrom<&str> for ChangeEventType { type Error = ContextError; fn try_from(value: &str) -> Result { match value { "BUY" => Ok(ChangeEventType::BUY), "SELL" => Ok(ChangeEventType::SELL), _ => Err(ContextError::EnumTranslationError(value.into())), } } } 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 { self.equity_change_cost_basis.clone() / self.equity_change_quantity.clone() } /// 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, bigdecimal::RoundingMode::HalfEven) } #[test] fn equity_type_try_from() { assert_eq!(EquityType::try_from("STOCK").unwrap(), EquityType::STOCK); assert_eq!(EquityType::try_from("ETF").unwrap(), EquityType::ETF); assert_eq!(EquityType::try_from("CRYPTO").unwrap(), EquityType::CRYPTO); assert!(EquityType::try_from("unknown").is_err()); match EquityType::try_from("unknown") { Err(ContextError::EnumTranslationError(e)) => assert_eq!(e, "unknown"), _ => assert!(false), }; } #[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_try_from() { assert_eq!(ChangeEventType::try_from("BUY").unwrap(), ChangeEventType::BUY); assert_eq!(ChangeEventType::try_from("SELL").unwrap(), ChangeEventType::SELL); assert!(ChangeEventType::try_from("unknown").is_err()); match ChangeEventType::try_from("unknown") { Err(ContextError::EnumTranslationError(e)) => assert_eq!(e, "unknown"), _ => assert!(false), }; } #[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"); } }