aboutsummaryrefslogtreecommitdiff
path: root/src/app/ledger.rs
blob: d20a4fd789eebcc4e0a8baa3f255ab355c38a62a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use bigdecimal::{BigDecimal, Zero};
use crate::app::store::records::{EquityChangeEvent, EquitySymbol, FiduciaryAccount, ROUNDING_MODE};

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> {
    pub fiduciary_account: &'a FiduciaryAccount,
    pub equity_symbol: &'a EquitySymbol,
    pub equity_change_events: Vec<&'a EquityChangeEvent>,
    pub account_symbol_quantity: BigDecimal,
    pub account_symbol_cost_basis_price: BigDecimal,
    pub account_symbol_cost_basis_total: BigDecimal,
    pub account_symbol_last_effective_price: BigDecimal,
    pub account_symbol_last_update_timestamp: DateTime,
}

impl EstimatedLedger<'_> {

    /// 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 acsym_events = vec![];
        let mut quantity = BigDecimal::zero();
        let mut cost_basis_total = BigDecimal::zero();
        let mut last_event: Option<&EquityChangeEvent> = None;

        for event in events {
            if event.fiduciary_account_id != fiduciary_account.fiduciary_account_id || event.equity_symbol_id != equity_symbol.equity_symbol_id {
                continue;
            }

            if last_event.is_none() || last_event.unwrap().equity_change_timestamp < event.equity_change_timestamp {
                last_event = Some(event);
            }

            quantity += event.scored_quantity();
            cost_basis_total += event.scored_cost_basis();

            acsym_events.push(event);
        }

        let cost_basis_price = (cost_basis_total.clone() / quantity.clone()).with_scale_round(2, ROUNDING_MODE);

        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_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),
        }
    }

}

mod test {

    use std::str::FromStr;

    use crate::app::store::records::{ChangeEventType, EquityType};

    use super::*;

    #[test]
    fn compile_change_events() {
        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()
        };
        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(),
        };
        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_type: ChangeEventType::BUY,
                equity_change_timestamp: DateTime::from_str("2026-01-02 09:43:10.000Z").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(),
                equity_change_type: ChangeEventType::BUY,
                equity_change_timestamp: DateTime::from_str("2026-01-02 10:43:10.000Z").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_type: ChangeEventType::SELL,
                equity_change_timestamp: DateTime::from_str("2026-01-02 11:43:10.000Z").unwrap(),
            }
        ];
        let event_refs = vec![&events[0], &events[1], &events[2]];

        let ledger = EstimatedLedger::compile_change_events(&account, &symbol, event_refs);

        assert_eq!(ledger.equity_change_events.len(), 2);
    }
}