aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Hoerr <kjhoerr@submelon.dev>2026-04-21 15:11:24 -0400
committerKevin Hoerr <kjhoerr@submelon.dev>2026-04-21 15:11:24 -0400
commite108814108c4e584ee377fa90ab2f26be82d6bba (patch)
treef1358f5c797f987f82aaf6e8d7eeca30bc73e273
parent1bde32bfbd798dbeb7acff0ad1f139515f17271e (diff)
downloadequity-tracker-e108814108c4e584ee377fa90ab2f26be82d6bba.tar.gz
equity-tracker-e108814108c4e584ee377fa90ab2f26be82d6bba.tar.bz2
equity-tracker-e108814108c4e584ee377fa90ab2f26be82d6bba.zip
ledger.rs: Add unit tests, fix no-events case
-rw-r--r--src/app/ledger.rs121
1 files changed, 93 insertions, 28 deletions
diff --git a/src/app/ledger.rs b/src/app/ledger.rs
index d20a4fd..7829d0b 100644
--- a/src/app/ledger.rs
+++ b/src/app/ledger.rs
@@ -6,13 +6,21 @@ type DateTime = chrono::DateTime<chrono::Utc>;
/// 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 estimated price of all events in the ledger derived from the cost basis and quantity
pub account_symbol_cost_basis_price: 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,
}
@@ -43,17 +51,17 @@ impl EstimatedLedger<'_> {
acsym_events.push(event);
}
- let cost_basis_price = (cost_basis_total.clone() / quantity.clone()).with_scale_round(2, ROUNDING_MODE);
+ let cost_basis_price = if quantity.is_zero() { BigDecimal::zero() } else { cost_basis_total.clone() / quantity.clone() };
EstimatedLedger {
fiduciary_account,
equity_symbol,
equity_change_events: acsym_events,
account_symbol_quantity: quantity,
- account_symbol_cost_basis_price: cost_basis_price,
+ account_symbol_cost_basis_price: cost_basis_price.with_scale_round(2, ROUNDING_MODE),
account_symbol_cost_basis_total: cost_basis_total.with_scale_round(2, ROUNDING_MODE),
account_symbol_last_effective_price: last_event.map_or_else(|| BigDecimal::zero().with_scale(2), |ev| ev.estimated_price()),
- account_symbol_last_update_timestamp: last_event.map_or_else(|| chrono::DateTime::<chrono::Utc>::MIN_UTC, |ev| ev.equity_change_timestamp),
+ account_symbol_last_update_timestamp: last_event.map_or_else(|| DateTime::MIN_UTC, |ev| ev.equity_change_timestamp),
}
}
@@ -61,61 +69,118 @@ impl EstimatedLedger<'_> {
mod test {
- use std::str::FromStr;
-
- use crate::app::store::records::{ChangeEventType, EquityType};
+ use crate::app::store::records::*;
use super::*;
+ /// simple generic to provide auto-incrementing value for the ID type that's needed
+ fn next<T : std::ops::AddAssign + From::<bool> + 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 compile_change_events() {
+ 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_str("2025-10-25 13:41:00.000Z").unwrap()
+ 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_str("2026-01-02 09:43:10.000Z").unwrap(),
+ equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(),
};
+ let evid = &mut 0_usize;
let events = vec![
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_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_str("2026-01-02 09:43:10.000Z").unwrap(),
+ equity_change_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(),
},
- EquityChangeEvent {
- equity_change_event_id: 1,
- fiduciary_account_id: 0,
- equity_symbol_id: 1,
- equity_change_quantity: BigDecimal::from_str("100.01").unwrap(),
- equity_change_cost_basis: BigDecimal::from_str("5.45").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_str("2026-01-02 10:43:10.000Z").unwrap(),
+ equity_change_timestamp: DateTime::from_timestamp_secs(1767350590).unwrap(),
},
EquityChangeEvent {
- equity_change_event_id: 2,
- fiduciary_account_id: 0,
- equity_symbol_id: 0,
- equity_change_quantity: BigDecimal::from_str("0.015").unwrap(),
- equity_change_cost_basis: BigDecimal::from_str("82.50").unwrap(),
+ 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_str("2026-01-02 11:43:10.000Z").unwrap(),
+ 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");
+ assert_eq!(ledger.account_symbol_cost_basis_price.to_plain_string(), "662.00");
+ // "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.00");
+ assert_eq!(ledger.account_symbol_cost_basis_price.to_plain_string(), "0.00");
+ assert_eq!(ledger.account_symbol_last_effective_price.to_plain_string(), "0.00");
+ assert_eq!(ledger.account_symbol_last_update_timestamp, DateTime::MIN_UTC);
}
} \ No newline at end of file