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.timestamp_opt(div.date, 0).single().ok_or_else(|| {
90                                crate::client::YahooError::ParseError(
91                                    "Invalid dividend timestamp".to_string(),
92                                )
93                            })?,
94                            amount: div.amount,
95                            currency: None,
96                        });
97                    }
98                }
99
100                // Parse splits
101                if let Some(splits) = &events.splits {
102                    for split in splits.values() {
103                        actions.splits.push(StockSplit::new(
104                            Utc.timestamp_opt(split.date, 0).single().ok_or_else(|| {
105                                crate::client::YahooError::ParseError(
106                                    "Invalid split timestamp".to_string(),
107                                )
108                            })?,
109                            split.numerator,
110                            split.denominator,
111                        ));
112                    }
113                }
114
115                // Parse capital gains
116                if let Some(gains) = &events.capital_gains {
117                    for gain in gains.values() {
118                        actions.capital_gains.push(CapitalGain {
119                            date: Utc.timestamp_opt(gain.date, 0).single().ok_or_else(|| {
120                                crate::client::YahooError::ParseError(
121                                    "Invalid capital gain timestamp".to_string(),
122                                )
123                            })?,
124                            amount: gain.amount,
125                        });
126                    }
127                }
128            }
129        }
130
131        // Sort by date (oldest first)
132        actions.dividends.sort_by_key(|d| d.date);
133        actions.splits.sort_by_key(|s| s.date);
134        actions.capital_gains.sort_by_key(|g| g.date);
135
136        Ok(actions)
137    }
138}
139
140// Internal structures for parsing Yahoo response
141#[derive(Debug, Deserialize)]
142pub(crate) struct YahooEventsResponse {
143    pub chart: ChartData,
144}
145
146#[derive(Debug, Deserialize)]
147pub(crate) struct ChartData {
148    pub result: Vec<ChartResult>,
149}
150
151#[derive(Debug, Deserialize)]
152pub(crate) struct ChartResult {
153    pub events: Option<Events>,
154}
155
156#[derive(Debug, Deserialize)]
157pub(crate) struct Events {
158    pub dividends: Option<HashMap<String, YahooDividend>>,
159    pub splits: Option<HashMap<String, YahooSplit>>,
160    #[serde(rename = "capitalGains")]
161    pub capital_gains: Option<HashMap<String, YahooCapitalGain>>,
162}
163
164#[derive(Debug, Deserialize)]
165pub(crate) struct YahooDividend {
166    pub amount: f64,
167    pub date: i64,
168}
169
170#[derive(Debug, Deserialize)]
171pub(crate) struct YahooSplit {
172    pub date: i64,
173    pub numerator: f64,
174    pub denominator: f64,
175    #[serde(rename = "splitRatio")]
176    #[allow(dead_code)]
177    pub split_ratio: String,
178}
179
180#[derive(Debug, Deserialize)]
181pub(crate) struct YahooCapitalGain {
182    pub amount: f64,
183    pub date: i64,
184}