finance_query_core/models/
actions.rs

1use chrono::{DateTime, TimeZone, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Represents a dividend payment
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct Dividend {
9    pub date: DateTime<Utc>,
10    pub amount: f64,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub currency: Option<String>,
13}
14
15/// Represents a stock split
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct StockSplit {
19    pub date: DateTime<Utc>,
20    pub numerator: f64,
21    pub denominator: f64,
22    /// Split ratio as string (e.g., "2:1")
23    pub split_ratio: String,
24}
25
26impl StockSplit {
27    pub fn new(date: DateTime<Utc>, numerator: f64, denominator: f64) -> Self {
28        let split_ratio = format!("{}:{}", numerator, denominator);
29        Self {
30            date,
31            numerator,
32            denominator,
33            split_ratio,
34        }
35    }
36}
37
38/// Represents a capital gain distribution (for ETFs/Mutual Funds)
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct CapitalGain {
42    pub date: DateTime<Utc>,
43    pub amount: f64,
44}
45
46/// Response containing all stock actions
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct ActionsResponse {
50    pub symbol: String,
51    pub dividends: Vec<Dividend>,
52    pub splits: Vec<StockSplit>,
53    pub capital_gains: Vec<CapitalGain>,
54}
55
56impl ActionsResponse {
57    pub fn new(symbol: String) -> Self {
58        Self {
59            symbol,
60            dividends: Vec::new(),
61            splits: Vec::new(),
62            capital_gains: Vec::new(),
63        }
64    }
65
66    /// Check if there are any actions
67    pub fn is_empty(&self) -> bool {
68        self.dividends.is_empty() && self.splits.is_empty() && self.capital_gains.is_empty()
69    }
70
71    /// Get total dividend amount
72    pub fn total_dividends(&self) -> f64 {
73        self.dividends.iter().map(|d| d.amount).sum()
74    }
75
76    /// Parse Yahoo Finance events response
77    pub(crate) fn from_yahoo_response(
78        symbol: String,
79        response: YahooEventsResponse,
80    ) -> Result<Self, crate::client::YahooError> {
81        let mut actions = Self::new(symbol);
82
83        if let Some(result) = response.chart.result.first() {
84            if let Some(events) = &result.events {
85                // Parse dividends
86                if let Some(divs) = &events.dividends {
87                    for div in divs.values() {
88                        actions.dividends.push(Dividend {
89                            date: Utc
90                                .timestamp_opt(div.date, 0)
91                                .single()
92                                .ok_or_else(|| {
93                                    crate::client::YahooError::ParseError(
94                                        "Invalid dividend timestamp".to_string(),
95                                    )
96                                })?,
97                            amount: div.amount,
98                            currency: None,
99                        });
100                    }
101                }
102
103                // Parse splits
104                if let Some(splits) = &events.splits {
105                    for split in splits.values() {
106                        actions.splits.push(StockSplit::new(
107                            Utc.timestamp_opt(split.date, 0)
108                                .single()
109                                .ok_or_else(|| {
110                                    crate::client::YahooError::ParseError(
111                                        "Invalid split timestamp".to_string(),
112                                    )
113                                })?,
114                            split.numerator,
115                            split.denominator,
116                        ));
117                    }
118                }
119
120                // Parse capital gains
121                if let Some(gains) = &events.capital_gains {
122                    for gain in gains.values() {
123                        actions.capital_gains.push(CapitalGain {
124                            date: Utc
125                                .timestamp_opt(gain.date, 0)
126                                .single()
127                                .ok_or_else(|| {
128                                    crate::client::YahooError::ParseError(
129                                        "Invalid capital gain timestamp".to_string(),
130                                    )
131                                })?,
132                            amount: gain.amount,
133                        });
134                    }
135                }
136            }
137        }
138
139        // Sort by date (oldest first)
140        actions.dividends.sort_by_key(|d| d.date);
141        actions.splits.sort_by_key(|s| s.date);
142        actions.capital_gains.sort_by_key(|g| g.date);
143
144        Ok(actions)
145    }
146}
147
148// Internal structures for parsing Yahoo response
149#[derive(Debug, Deserialize)]
150pub(crate) struct YahooEventsResponse {
151    pub chart: ChartData,
152}
153
154#[derive(Debug, Deserialize)]
155pub(crate) struct ChartData {
156    pub result: Vec<ChartResult>,
157}
158
159#[derive(Debug, Deserialize)]
160pub(crate) struct ChartResult {
161    pub events: Option<Events>,
162}
163
164#[derive(Debug, Deserialize)]
165pub(crate) struct Events {
166    pub dividends: Option<HashMap<String, YahooDividend>>,
167    pub splits: Option<HashMap<String, YahooSplit>>,
168    #[serde(rename = "capitalGains")]
169    pub capital_gains: Option<HashMap<String, YahooCapitalGain>>,
170}
171
172#[derive(Debug, Deserialize)]
173pub(crate) struct YahooDividend {
174    pub amount: f64,
175    pub date: i64,
176}
177
178#[derive(Debug, Deserialize)]
179pub(crate) struct YahooSplit {
180    pub date: i64,
181    pub numerator: f64,
182    pub denominator: f64,
183    #[serde(rename = "splitRatio")]
184    #[allow(dead_code)]
185    pub split_ratio: String,
186}
187
188#[derive(Debug, Deserialize)]
189pub(crate) struct YahooCapitalGain {
190    pub amount: f64,
191    pub date: i64,
192}