use super::store::records::{EquityChangeEvent, EquitySymbol, FiduciaryAccount}; use bigdecimal::{BigDecimal, Zero}; use miette::{ErrReport, miette}; type DateTime = chrono::DateTime; /// An estimated ledger is the effective sum of all the equity change events /// derived from record for a particular fiduciary account and equity symbol. pub struct EstimatedLedger<'a> { /// The reference to the fiduciary account that owns the events of the ledger pub fiduciary_account: &'a FiduciaryAccount, /// The reference to the equity symbol that all events describe pub equity_symbol: &'a EquitySymbol, /// The list of references to events that compose the ledger pub equity_change_events: Vec<&'a EquityChangeEvent>, /// The quantity of shares derived from all events for the account and symbol pub account_symbol_quantity: BigDecimal, /// The basis of all purchases and sales not derived from quantity pub account_symbol_cost_basis_total: BigDecimal, /// The estimated price of the equity from the latest event in the ledger pub account_symbol_last_effective_price: BigDecimal, /// The event timestmap of the latest event in the ledger pub account_symbol_last_update_timestamp: DateTime, } impl EstimatedLedger<'_> { fn new<'a>( fiduciary_account: &'a FiduciaryAccount, equity_symbol: &'a EquitySymbol, events: Vec<&'a EquityChangeEvent>, ) -> EstimatedLedger<'a> { EstimatedLedger { fiduciary_account, equity_symbol, equity_change_events: events, account_symbol_quantity: BigDecimal::zero(), account_symbol_cost_basis_total: BigDecimal::zero(), account_symbol_last_effective_price: BigDecimal::zero(), account_symbol_last_update_timestamp: DateTime::MIN_UTC, } } /// Takes a list of all change events, a fiduciary account, and an equity /// symbol for which to create a ledger. /// /// All values are taken by reference, so they need to be maintained elsewhere pub fn compile_change_events<'a>( fiduciary_account: &'a FiduciaryAccount, equity_symbol: &'a EquitySymbol, events: Vec<&'a EquityChangeEvent>, ) -> EstimatedLedger<'a> { let mut ledger = EstimatedLedger::new(fiduciary_account, equity_symbol, vec![]); for event in events { // Err result expected if event does not belong to ledger; for mass processing, we ignore let _ = ledger.add_event(event); } ledger } /// The estimated price of all events in the ledger derived from the cost basis and quantity pub fn get_cost_basis_price(&self) -> BigDecimal { if self.account_symbol_quantity.is_zero() { BigDecimal::zero() } else { self.account_symbol_cost_basis_total.clone() / self.account_symbol_quantity.clone() } } } impl<'a> EstimatedLedger<'a> { pub fn add_event(&mut self, event: &'a EquityChangeEvent) -> Result<(), ErrReport> { if event.fiduciary_account_id != self.fiduciary_account.fiduciary_account_id || event.equity_symbol_id != self.equity_symbol.equity_symbol_id { Err(miette!("Event does not belong to ledger"))?; } if self.account_symbol_last_update_timestamp < event.equity_change_timestamp { self.account_symbol_last_effective_price = event.estimated_price(); self.account_symbol_last_update_timestamp = event.equity_change_timestamp; } self.account_symbol_quantity += event.scored_quantity(); self.account_symbol_cost_basis_total += event.scored_cost_basis(); self.equity_change_events.push(event); Ok(()) } } mod test { use crate::app::store::records::*; use super::*; /// simple generic to provide auto-incrementing value for the ID type that's needed fn next + Copy>(id: &mut T) -> T { let tmp = *id; *id += T::from(true); tmp } fn dec(n: u32, scale: i64) -> BigDecimal { BigDecimal::from_biguint(bigdecimal::num_bigint::BigUint::from(n), scale) } #[test] fn estimated_ledger_new() { let account = FiduciaryAccount { fiduciary_account_id: 0, fiduciary_account_number: 12345678, fiduciary_account_name: "account".into(), fiduciary_account_description: "Brokerage Acct".into(), fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), }; let symbol = EquitySymbol { equity_symbol_id: 0, equity_symbol_name: "Business Company".into(), equity_symbol_managing_company: "Business Company".into(), equity_symbol_type: EquityType::STOCK, equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }; let events = vec![]; let ledger = EstimatedLedger::new(&account, &symbol, events); assert_eq!( ledger.fiduciary_account.fiduciary_account_id, account.fiduciary_account_id ); assert_eq!( ledger.equity_symbol.equity_symbol_id, symbol.equity_symbol_id ); assert!(ledger.equity_change_events.is_empty()); assert_eq!(ledger.account_symbol_quantity.to_plain_string(), "0"); assert_eq!( ledger.account_symbol_cost_basis_total.to_plain_string(), "0" ); assert_eq!( ledger.account_symbol_last_effective_price.to_plain_string(), "0" ); assert_eq!( ledger.account_symbol_last_update_timestamp, DateTime::MIN_UTC ); } #[test] fn compile_change_events_small() { let account = FiduciaryAccount { fiduciary_account_id: 0, fiduciary_account_number: 12345678, fiduciary_account_name: "account".into(), fiduciary_account_description: "Brokerage Acct".into(), fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), }; let symbol = EquitySymbol { equity_symbol_id: 0, equity_symbol_name: "Business Company".into(), equity_symbol_managing_company: "Business Company".into(), equity_symbol_type: EquityType::STOCK, equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }; let evid = &mut 0_usize; let events = vec![ EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id, equity_change_quantity: dec(125_u32, 3), equity_change_cost_basis: dec(8280_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }, EquityChangeEvent { // different symbol, same account equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id + 1, equity_change_quantity: dec(10001_u32, 2), equity_change_cost_basis: dec(545_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767350590).unwrap(), }, EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id, equity_change_quantity: dec(15_u32, 3), equity_change_cost_basis: dec(998_u32, 2), equity_change_type: ChangeEventType::SELL, equity_change_timestamp: DateTime::from_timestamp_secs(1767354190).unwrap(), }, ]; let event_refs = vec![&events[0], &events[1], &events[2]]; let ledger = EstimatedLedger::compile_change_events(&account, &symbol, event_refs); // Check account, symbol, events references of ledger are what expected assert_eq!( ledger.fiduciary_account.fiduciary_account_id, account.fiduciary_account_id ); assert_eq!( ledger.equity_symbol.equity_symbol_id, symbol.equity_symbol_id ); assert_eq!(ledger.equity_change_events.len(), 2); assert_eq!( ledger.equity_change_events[0].equity_change_event_id, events[0].equity_change_event_id ); assert_eq!( ledger.equity_change_events[1].equity_change_event_id, events[2].equity_change_event_id ); // Check computed values assert_eq!(ledger.account_symbol_quantity.to_plain_string(), "0.110"); assert_eq!( ledger.account_symbol_cost_basis_total.to_plain_string(), "72.82" ); // "665.33", repeating, of course assert_eq!( ledger.account_symbol_last_effective_price, events[2].estimated_price() ); assert_eq!( ledger.account_symbol_last_update_timestamp, events[2].equity_change_timestamp ); } #[test] fn compile_change_events_empty() { let account = FiduciaryAccount { fiduciary_account_id: 0, fiduciary_account_number: 12345678, fiduciary_account_name: "account".into(), fiduciary_account_description: "Brokerage Acct".into(), fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), }; let symbol = EquitySymbol { equity_symbol_id: 0, equity_symbol_name: "US Equity ETF".into(), equity_symbol_managing_company: "Business Company".into(), equity_symbol_type: EquityType::ETF, equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }; let events: Vec<&EquityChangeEvent> = vec![]; let ledger = EstimatedLedger::compile_change_events(&account, &symbol, events); // Check account, symbol, events references of ledger are what expected assert_eq!( ledger.fiduciary_account.fiduciary_account_id, account.fiduciary_account_id ); assert_eq!( ledger.equity_symbol.equity_symbol_id, symbol.equity_symbol_id ); assert!(ledger.equity_change_events.is_empty()); // Check computed values assert_eq!(ledger.account_symbol_quantity.to_plain_string(), "0"); assert_eq!( ledger.account_symbol_cost_basis_total.to_plain_string(), "0" ); assert_eq!( ledger.account_symbol_last_effective_price.to_plain_string(), "0" ); assert_eq!( ledger.account_symbol_last_update_timestamp, DateTime::MIN_UTC ); } #[test] fn get_cost_basis_price_small() { let account = FiduciaryAccount { fiduciary_account_id: 0, fiduciary_account_number: 12345678, fiduciary_account_name: "account".into(), fiduciary_account_description: "Brokerage Acct".into(), fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), }; let symbol = EquitySymbol { equity_symbol_id: 0, equity_symbol_name: "Business Company".into(), equity_symbol_managing_company: "Business Company".into(), equity_symbol_type: EquityType::STOCK, equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }; let evid = &mut 0_usize; let events = vec![ EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id, equity_change_quantity: dec(125_u32, 3), equity_change_cost_basis: dec(8280_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }, EquityChangeEvent { // different symbol, same account equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id + 1, equity_change_quantity: dec(10001_u32, 2), equity_change_cost_basis: dec(545_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767350590).unwrap(), }, EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id, equity_change_quantity: dec(15_u32, 3), equity_change_cost_basis: dec(998_u32, 2), equity_change_type: ChangeEventType::SELL, equity_change_timestamp: DateTime::from_timestamp_secs(1767354190).unwrap(), }, ]; let event_refs = vec![&events[0], &events[1], &events[2]]; let ledger = EstimatedLedger::compile_change_events(&account, &symbol, event_refs); assert_eq!(ledger.get_cost_basis_price().to_plain_string(), "662") } #[test] fn get_cost_basis_price_empty() { let account = FiduciaryAccount { fiduciary_account_id: 0, fiduciary_account_number: 12345678, fiduciary_account_name: "account".into(), fiduciary_account_description: "Brokerage Acct".into(), fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), }; let symbol = EquitySymbol { equity_symbol_id: 0, equity_symbol_name: "US Equity ETF".into(), equity_symbol_managing_company: "Business Company".into(), equity_symbol_type: EquityType::ETF, equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }; let events: Vec<&EquityChangeEvent> = vec![]; let ledger = EstimatedLedger::compile_change_events(&account, &symbol, events); // Check account, symbol, events references of ledger are what expected assert_eq!(ledger.get_cost_basis_price().to_plain_string(), "0"); } #[test] fn add_event_long() { let account = FiduciaryAccount { fiduciary_account_id: 0, fiduciary_account_number: 12345678, fiduciary_account_name: "account".into(), fiduciary_account_description: "Brokerage Acct".into(), fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), }; let symbol = EquitySymbol { equity_symbol_id: 0, equity_symbol_name: "Business Company".into(), equity_symbol_managing_company: "Business Company".into(), equity_symbol_type: EquityType::STOCK, equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }; let evid = &mut 0_usize; let events = vec![EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id, equity_change_quantity: dec(125_u32, 3), equity_change_cost_basis: dec(8280_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }]; let event_refs = vec![&events[0]]; let mut ledger = EstimatedLedger::compile_change_events(&account, &symbol, event_refs); // Check account, symbol, events references of ledger are what expected assert_eq!( ledger.fiduciary_account.fiduciary_account_id, account.fiduciary_account_id ); assert_eq!( ledger.equity_symbol.equity_symbol_id, symbol.equity_symbol_id ); assert_eq!(ledger.equity_change_events.len(), 1); assert_eq!( ledger.equity_change_events[0].equity_change_event_id, events[0].equity_change_event_id ); // Check computed values assert_eq!(ledger.account_symbol_quantity.to_plain_string(), "0.125"); assert_eq!( ledger.account_symbol_cost_basis_total.to_plain_string(), "82.80" ); assert_eq!( ledger.account_symbol_last_effective_price, events[0].estimated_price() ); assert_eq!( ledger.account_symbol_last_update_timestamp, events[0].equity_change_timestamp ); let new_event = EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id, equity_change_quantity: dec(125_u32, 3), equity_change_cost_basis: dec(7849_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767354190).unwrap(), }; let res = ledger.add_event(&new_event); assert!(res.is_ok()); assert_eq!(ledger.equity_change_events.len(), 2); assert_eq!( ledger.equity_change_events[1].equity_change_event_id, new_event.equity_change_event_id ); assert_eq!(ledger.account_symbol_quantity.to_plain_string(), "0.250"); assert_eq!( ledger.account_symbol_cost_basis_total.to_plain_string(), "161.29" ); assert_eq!( ledger.account_symbol_last_effective_price, new_event.estimated_price() ); assert_eq!( ledger.account_symbol_last_update_timestamp, new_event.equity_change_timestamp ); assert_eq!(ledger.get_cost_basis_price().to_plain_string(), "645.16"); } #[test] fn add_event_different_account() { let account = FiduciaryAccount { fiduciary_account_id: 0, fiduciary_account_number: 12345678, fiduciary_account_name: "account".into(), fiduciary_account_description: "Brokerage Acct".into(), fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), }; let symbol = EquitySymbol { equity_symbol_id: 0, equity_symbol_name: "Business Company".into(), equity_symbol_managing_company: "Business Company".into(), equity_symbol_type: EquityType::STOCK, equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }; let evid = &mut 0_usize; let events = vec![EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id, equity_change_quantity: dec(125_u32, 3), equity_change_cost_basis: dec(8280_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }]; let event_refs = vec![&events[0]]; let mut ledger = EstimatedLedger::compile_change_events(&account, &symbol, event_refs); let new_event = EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id + 2, equity_symbol_id: symbol.equity_symbol_id, equity_change_quantity: dec(125_u32, 3), equity_change_cost_basis: dec(7849_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767354190).unwrap(), }; let res = ledger.add_event(&new_event); assert!(res.is_err()); assert_eq!( res.err().unwrap().to_string(), "Event does not belong to ledger" ); } #[test] fn add_event_different_symbol() { let account = FiduciaryAccount { fiduciary_account_id: 0, fiduciary_account_number: 12345678, fiduciary_account_name: "account".into(), fiduciary_account_description: "Brokerage Acct".into(), fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), }; let symbol = EquitySymbol { equity_symbol_id: 0, equity_symbol_name: "Business Company".into(), equity_symbol_managing_company: "Business Company".into(), equity_symbol_type: EquityType::STOCK, equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }; let evid = &mut 0_usize; let events = vec![EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id, equity_change_quantity: dec(125_u32, 3), equity_change_cost_basis: dec(8280_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }]; let event_refs = vec![&events[0]]; let mut ledger = EstimatedLedger::compile_change_events(&account, &symbol, event_refs); let new_event = EquityChangeEvent { equity_change_event_id: next(evid), fiduciary_account_id: account.fiduciary_account_id, equity_symbol_id: symbol.equity_symbol_id + 3, equity_change_quantity: dec(125_u32, 3), equity_change_cost_basis: dec(7849_u32, 2), equity_change_type: ChangeEventType::BUY, equity_change_timestamp: DateTime::from_timestamp_secs(1767354190).unwrap(), }; let res = ledger.add_event(&new_event); assert!(res.is_err()); assert_eq!( res.err().unwrap().to_string(), "Event does not belong to ledger" ); } }