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 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 {
88 timestamp,
89 amount,
90 provider_id: None,
91 }
92 }
93
94 #[test]
95 fn test_empty_dividends() {
96 let a = DividendAnalytics::from_dividends(&[]);
97 assert_eq!(a.payment_count, 0);
98 assert_eq!(a.total_paid, 0.0);
99 assert!(a.cagr.is_none());
100 }
101
102 #[test]
103 fn test_single_dividend() {
104 let a = DividendAnalytics::from_dividends(&[div(1_000_000_000, 0.50)]);
105 assert_eq!(a.payment_count, 1);
106 assert!((a.total_paid - 0.50).abs() < 1e-9);
107 assert!(a.cagr.is_none()); }
109
110 #[test]
111 fn test_cagr_two_years() {
112 let secs_per_year = 31_557_600_i64;
114 let t0 = 1_600_000_000_i64;
115 let t1 = t0 + 2 * secs_per_year;
116 let a = DividendAnalytics::from_dividends(&[div(t0, 0.50), div(t1, 0.605)]);
117 assert!(a.cagr.is_some());
118 let cagr = a.cagr.unwrap();
119 assert!(
120 (cagr - 0.10).abs() < 0.01,
121 "expected ~10% CAGR, got {cagr:.4}"
122 );
123 }
124
125 #[test]
126 fn test_totals() {
127 let divs = [
128 div(1_000_000, 0.25),
129 div(2_000_000, 0.25),
130 div(3_000_000, 0.25),
131 ];
132 let a = DividendAnalytics::from_dividends(&divs);
133 assert_eq!(a.payment_count, 3);
134 assert!((a.total_paid - 0.75).abs() < 1e-9);
135 assert!((a.average_payment - 0.25).abs() < 1e-9);
136 }
137}