Skip to main content

finance_query/models/chart/
events.rs

1//! Chart events module
2//!
3//! Contains dividend, split, and capital gain data structures.
4
5use crate::Provider;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::OnceLock;
9
10/// Chart events containing dividends, splits, and capital gains
11///
12/// Events are deserialized from HashMaps, then lazily converted to sorted vectors
13/// on first access and cached for subsequent calls.
14#[derive(Debug, Default, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16#[non_exhaustive]
17pub struct ChartEvents {
18    /// Dividend events keyed by timestamp
19    #[serde(default)]
20    pub(crate) dividends: HashMap<String, DividendEvent>,
21    /// Stock split events keyed by timestamp
22    #[serde(default)]
23    pub(crate) splits: HashMap<String, SplitEvent>,
24    /// Capital gain events keyed by timestamp
25    #[serde(default)]
26    pub(crate) capital_gains: HashMap<String, CapitalGainEvent>,
27
28    /// Cached sorted dividend vector (computed once on first access)
29    #[serde(skip)]
30    dividends_cache: OnceLock<Vec<Dividend>>,
31    /// Cached sorted splits vector (computed once on first access)
32    #[serde(skip)]
33    splits_cache: OnceLock<Vec<Split>>,
34    /// Cached sorted capital gains vector (computed once on first access)
35    #[serde(skip)]
36    capital_gains_cache: OnceLock<Vec<CapitalGain>>,
37}
38
39/// Raw dividend event from Yahoo Finance
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub(crate) struct DividendEvent {
42    /// Dividend amount per share
43    pub amount: f64,
44    /// Timestamp of the dividend
45    pub date: i64,
46}
47
48/// Raw split event from Yahoo Finance
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub(crate) struct SplitEvent {
52    /// Timestamp of the split
53    pub date: i64,
54    /// Numerator of the split ratio
55    pub numerator: f64,
56    /// Denominator of the split ratio
57    pub denominator: f64,
58    /// Split ratio as string (e.g., "2:1", "10:1")
59    pub split_ratio: String,
60}
61
62/// Raw capital gain event from Yahoo Finance
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub(crate) struct CapitalGainEvent {
65    /// Capital gain amount per share
66    pub amount: f64,
67    /// Timestamp of the capital gain distribution
68    pub date: i64,
69}
70
71/// Public dividend data
72///
73/// Note: This struct cannot be manually constructed - obtain via `Ticker::dividends()`.
74#[non_exhaustive]
75#[derive(Debug, Clone, Serialize, Deserialize)]
76#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
77pub struct Dividend {
78    /// Timestamp (Unix)
79    pub timestamp: i64,
80    /// Dividend amount per share
81    pub amount: f64,
82
83    /// Which data provider served this data (e.g., "yahoo", "polygon").
84    #[serde(skip_serializing_if = "Option::is_none", default)]
85    pub provider_id: Option<Provider>,
86}
87
88/// Public stock split data
89///
90/// Note: This struct cannot be manually constructed - obtain via `Ticker::splits()`.
91#[non_exhaustive]
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
94pub struct Split {
95    /// Timestamp (Unix)
96    pub timestamp: i64,
97    /// Numerator of the split ratio
98    pub numerator: f64,
99    /// Denominator of the split ratio
100    pub denominator: f64,
101    /// Split ratio as string (e.g., "2:1", "10:1")
102    pub ratio: String,
103
104    /// Which data provider served this data (e.g., "yahoo", "polygon").
105    #[serde(skip_serializing_if = "Option::is_none", default)]
106    pub provider_id: Option<Provider>,
107}
108
109/// Public capital gain data
110///
111/// Note: This struct cannot be manually constructed - obtain via `Ticker::capital_gains()`.
112#[non_exhaustive]
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
115pub struct CapitalGain {
116    /// Timestamp (Unix)
117    pub timestamp: i64,
118    /// Capital gain amount per share
119    pub amount: f64,
120
121    /// Which data provider served this data (e.g., "yahoo", "polygon").
122    #[serde(skip_serializing_if = "Option::is_none", default)]
123    pub provider_id: Option<Provider>,
124}
125
126impl Clone for ChartEvents {
127    fn clone(&self) -> Self {
128        // Helper to clone OnceLock if initialized
129        fn clone_cache<T: Clone>(cache: &OnceLock<T>) -> OnceLock<T> {
130            let new_cache = OnceLock::new();
131            if let Some(value) = cache.get() {
132                let _ = new_cache.set(value.clone());
133            }
134            new_cache
135        }
136
137        Self {
138            dividends: self.dividends.clone(),
139            splits: self.splits.clone(),
140            capital_gains: self.capital_gains.clone(),
141            dividends_cache: clone_cache(&self.dividends_cache),
142            splits_cache: clone_cache(&self.splits_cache),
143            capital_gains_cache: clone_cache(&self.capital_gains_cache),
144        }
145    }
146}
147
148impl ChartEvents {
149    /// Build events from public model values.
150    ///
151    /// The only way to construct this outside the crate, for a
152    /// [`CorporateProvider`](crate::ProviderAdapter) implementation returning
153    /// events it fetched itself.
154    pub fn from_parts(
155        dividends: Vec<Dividend>,
156        splits: Vec<Split>,
157        capital_gains: Vec<CapitalGain>,
158    ) -> Self {
159        let events = Self {
160            dividends: dividends
161                .iter()
162                .map(|d| {
163                    (
164                        d.timestamp.to_string(),
165                        DividendEvent {
166                            amount: d.amount,
167                            date: d.timestamp,
168                        },
169                    )
170                })
171                .collect(),
172            splits: splits
173                .iter()
174                .map(|s| {
175                    (
176                        s.timestamp.to_string(),
177                        SplitEvent {
178                            date: s.timestamp,
179                            numerator: s.numerator,
180                            denominator: s.denominator,
181                            split_ratio: s.ratio.clone(),
182                        },
183                    )
184                })
185                .collect(),
186            capital_gains: capital_gains
187                .iter()
188                .map(|g| {
189                    (
190                        g.timestamp.to_string(),
191                        CapitalGainEvent {
192                            amount: g.amount,
193                            date: g.timestamp,
194                        },
195                    )
196                })
197                .collect(),
198            ..Default::default()
199        };
200        // Seeding the caches keeps `provider_id`, which the wire DTOs cannot carry.
201        let sorted = |mut v: Vec<Dividend>| {
202            v.sort_by_key(|d| d.timestamp);
203            v
204        };
205        let _ = events.dividends_cache.set(sorted(dividends));
206        let mut splits = splits;
207        splits.sort_by_key(|s| s.timestamp);
208        let _ = events.splits_cache.set(splits);
209        let mut capital_gains = capital_gains;
210        capital_gains.sort_by_key(|g| g.timestamp);
211        let _ = events.capital_gains_cache.set(capital_gains);
212        events
213    }
214
215    /// Get sorted list of dividends (cached after first call)
216    pub fn to_dividends(&self) -> Vec<Dividend> {
217        self.dividends_cache
218            .get_or_init(|| {
219                let mut dividends: Vec<Dividend> = self
220                    .dividends
221                    .values()
222                    .map(|d| Dividend {
223                        timestamp: d.date,
224                        amount: d.amount,
225                        provider_id: None,
226                    })
227                    .collect();
228                dividends.sort_by_key(|d| d.timestamp);
229                dividends
230            })
231            .clone()
232    }
233
234    /// Get sorted list of splits (cached after first call)
235    pub fn to_splits(&self) -> Vec<Split> {
236        self.splits_cache
237            .get_or_init(|| {
238                let mut splits: Vec<Split> = self
239                    .splits
240                    .values()
241                    .map(|s| Split {
242                        timestamp: s.date,
243                        numerator: s.numerator,
244                        denominator: s.denominator,
245                        ratio: s.split_ratio.clone(),
246                        provider_id: None,
247                    })
248                    .collect();
249                splits.sort_by_key(|s| s.timestamp);
250                splits
251            })
252            .clone()
253    }
254
255    /// Get sorted list of capital gains (cached after first call)
256    pub fn to_capital_gains(&self) -> Vec<CapitalGain> {
257        self.capital_gains_cache
258            .get_or_init(|| {
259                let mut gains: Vec<CapitalGain> = self
260                    .capital_gains
261                    .values()
262                    .map(|g| CapitalGain {
263                        timestamp: g.date,
264                        amount: g.amount,
265                        provider_id: None,
266                    })
267                    .collect();
268                gains.sort_by_key(|g| g.timestamp);
269                gains
270            })
271            .clone()
272    }
273}