finance_query/models/chart/
dividend_analytics.rs1use serde::{Deserialize, Serialize};
4
5use super::events::Dividend;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
11#[non_exhaustive]
12pub struct DividendAnalytics {
13 pub total_paid: f64,
15 pub payment_count: usize,
17 pub average_payment: f64,
19 pub cagr: Option<f64>,
23 pub last_payment: Option<Dividend>,
25 pub first_payment: Option<Dividend>,
27}
28
29impl DividendAnalytics {
30 pub(crate) fn from_dividends(dividends: &[Dividend]) -> Self {
32 if dividends.is_empty() {
33 return Self {
34 total_paid: 0.0,
35 payment_count: 0,
36 average_payment: 0.0,
37 cagr: None,
38 last_payment: None,
39 first_payment: None,
40 };
41 }
42
43 let total_paid: f64 = dividends.iter().map(|d| d.amount).sum();
44 let payment_count = dividends.len();
45 let average_payment = total_paid / payment_count as f64;
46
47 let first = dividends.first().cloned();
48 let last = dividends.last().cloned();
49
50 let cagr = compute_cagr(&first, &last);
51
52 Self {
53 total_paid,
54 payment_count,
55 average_payment,
56 cagr,
57 last_payment: last,
58 first_payment: first,
59 }
60 }
61}
62
63fn compute_cagr(first: &Option<Dividend>, last: &Option<Dividend>) -> Option<f64> {
67 let first = first.as_ref()?;
68 let last = last.as_ref()?;
69
70 if first.amount <= 0.0 || last.amount <= 0.0 {
71 return None;
72 }
73
74 let years = (last.timestamp - first.timestamp) as f64 / 31_557_600.0; if years < 1.0 {
76 return None;
77 }
78
79 Some((last.amount / first.amount).powf(1.0 / years) - 1.0)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 fn div(timestamp: i64, amount: f64) -> Dividend {
87 Dividend { timestamp, amount }
88 }
89
90 #[test]
91 fn test_empty_dividends() {
92 let a = DividendAnalytics::from_dividends(&[]);
93 assert_eq!(a.payment_count, 0);
94 assert_eq!(a.total_paid, 0.0);
95 assert!(a.cagr.is_none());
96 }
97
98 #[test]
99 fn test_single_dividend() {
100 let a = DividendAnalytics::from_dividends(&[div(1_000_000_000, 0.50)]);
101 assert_eq!(a.payment_count, 1);
102 assert!((a.total_paid - 0.50).abs() < 1e-9);
103 assert!(a.cagr.is_none()); }
105
106 #[test]
107 fn test_cagr_two_years() {
108 let secs_per_year = 31_557_600_i64;
110 let t0 = 1_600_000_000_i64;
111 let t1 = t0 + 2 * secs_per_year;
112 let a = DividendAnalytics::from_dividends(&[div(t0, 0.50), div(t1, 0.605)]);
113 assert!(a.cagr.is_some());
114 let cagr = a.cagr.unwrap();
115 assert!(
116 (cagr - 0.10).abs() < 0.01,
117 "expected ~10% CAGR, got {cagr:.4}"
118 );
119 }
120
121 #[test]
122 fn test_totals() {
123 let divs = [
124 div(1_000_000, 0.25),
125 div(2_000_000, 0.25),
126 div(3_000_000, 0.25),
127 ];
128 let a = DividendAnalytics::from_dividends(&divs);
129 assert_eq!(a.payment_count, 3);
130 assert!((a.total_paid - 0.75).abs() < 1e-9);
131 assert!((a.average_payment - 0.25).abs() < 1e-9);
132 }
133}