1use serde::{Deserialize, Serialize};
4use std::str::FromStr;
5use strum::{AsRefStr, Display, EnumString};
6
7use chrono::{DateTime, Utc};
8#[cfg(feature = "dataframe")]
9use df_derive::ToDataFrame;
10#[cfg(feature = "dataframe")]
11use paft_core::dataframe::ToDataFrame;
12use paft_core::domain::{Money, Period};
13
14#[derive(
19 Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, AsRefStr, EnumString,
20)]
21#[strum(ascii_case_insensitive)]
22#[serde(from = "String", into = "String")]
23pub enum RecommendationGrade {
24 #[strum(to_string = "STRONG_BUY", serialize = "STRONG BUY", serialize = "BUY+")]
26 StrongBuy,
27 #[strum(to_string = "BUY")]
29 Buy,
30 #[strum(to_string = "HOLD", serialize = "NEUTRAL")]
32 Hold,
33 #[strum(to_string = "SELL")]
35 Sell,
36 #[strum(
38 to_string = "STRONG_SELL",
39 serialize = "STRONG SELL",
40 serialize = "SELL-"
41 )]
42 StrongSell,
43 #[strum(to_string = "OUTPERFORM", serialize = "OVERWEIGHT")]
45 Outperform,
46 #[strum(to_string = "UNDERPERFORM", serialize = "UNDERWEIGHT")]
48 Underperform,
49 Other(String),
51}
52
53impl From<String> for RecommendationGrade {
54 fn from(s: String) -> Self {
55 Self::from_str(&s).unwrap_or_else(|_| Self::Other(s.to_uppercase()))
57 }
58}
59
60impl From<RecommendationGrade> for String {
61 fn from(grade: RecommendationGrade) -> Self {
62 match grade {
63 RecommendationGrade::Other(s) => s,
64 _ => grade.to_string(),
65 }
66 }
67}
68
69#[derive(
74 Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, AsRefStr, EnumString,
75)]
76#[strum(ascii_case_insensitive)]
77#[serde(from = "String", into = "String")]
78pub enum RecommendationAction {
79 #[strum(to_string = "UPGRADE", serialize = "UP")]
81 Upgrade,
82 #[strum(to_string = "DOWNGRADE", serialize = "DOWN")]
84 Downgrade,
85 #[strum(to_string = "INIT", serialize = "INITIATED", serialize = "INITIATE")]
87 Initiate,
88 #[strum(to_string = "MAINTAIN", serialize = "REITERATE")]
90 Maintain,
91 #[strum(to_string = "RESUME")]
93 Resume,
94 #[strum(to_string = "SUSPEND")]
96 Suspend,
97 Other(String),
99}
100
101impl From<String> for RecommendationAction {
102 fn from(s: String) -> Self {
103 Self::from_str(&s).unwrap_or_else(|_| Self::Other(s.to_uppercase()))
105 }
106}
107
108impl From<RecommendationAction> for String {
109 fn from(action: RecommendationAction) -> Self {
110 match action {
111 RecommendationAction::Other(s) => s,
112 _ => action.to_string(),
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
118#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
119pub struct Earnings {
121 pub yearly: Vec<EarningsYear>,
123 pub quarterly: Vec<EarningsQuarter>,
125 pub quarterly_eps: Vec<EarningsQuarterEps>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
130#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
131pub struct EarningsYear {
133 pub year: i32,
135 pub revenue: Option<Money>,
137 pub earnings: Option<Money>,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
143pub struct EarningsQuarter {
145 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
147 pub period: Period,
148 pub revenue: Option<Money>,
150 pub earnings: Option<Money>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
156pub struct EarningsQuarterEps {
158 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
160 pub period: Period,
161 pub actual: Option<Money>,
163 pub estimate: Option<Money>,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
168#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
169pub struct PriceTarget {
171 pub mean: Option<Money>,
173 pub high: Option<Money>,
175 pub low: Option<Money>,
177 pub number_of_analysts: Option<u32>,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
183pub struct RecommendationRow {
185 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
187 pub period: Period,
188 pub strong_buy: Option<u32>,
190 pub buy: Option<u32>,
192 pub hold: Option<u32>,
194 pub sell: Option<u32>,
196 pub strong_sell: Option<u32>,
198}
199
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
201#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
202pub struct RecommendationSummary {
204 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
206 pub latest_period: Option<Period>,
207 pub strong_buy: Option<u32>,
209 pub buy: Option<u32>,
211 pub hold: Option<u32>,
213 pub sell: Option<u32>,
215 pub strong_sell: Option<u32>,
217 pub mean: Option<f64>,
219 pub mean_rating_text: Option<String>,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
225pub struct UpgradeDowngradeRow {
227 #[serde(with = "chrono::serde::ts_seconds")]
229 pub ts: DateTime<Utc>,
230 pub firm: Option<String>,
232 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
234 pub from_grade: Option<RecommendationGrade>,
235 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
237 pub to_grade: Option<RecommendationGrade>,
238 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
240 pub action: Option<RecommendationAction>,
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
244#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
245pub struct AnalysisSummary {
247 pub target_mean_price: Option<Money>,
249 pub target_high_price: Option<Money>,
251 pub target_low_price: Option<Money>,
253 pub number_of_analyst_opinions: Option<u32>,
255 pub recommendation_mean: Option<f64>,
257 pub recommendation_text: Option<String>,
259}
260
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
262#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
263pub struct EarningsEstimate {
265 pub avg: Option<Money>,
267 pub low: Option<Money>,
269 pub high: Option<Money>,
271 pub year_ago_eps: Option<Money>,
273 pub num_analysts: Option<u32>,
275 pub growth: Option<f64>,
277}
278
279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
280#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
281pub struct RevenueEstimate {
283 pub avg: Option<Money>,
285 pub low: Option<Money>,
287 pub high: Option<Money>,
289 pub year_ago_revenue: Option<Money>,
291 pub num_analysts: Option<u32>,
293 pub growth: Option<f64>,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
303pub struct TrendPoint {
304 pub period: String,
307 pub value: Money,
309}
310
311impl TrendPoint {
312 pub fn new(period: impl Into<String>, value: Money) -> Self {
314 Self {
315 period: period.into(),
316 value,
317 }
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
322#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
323pub struct EpsTrend {
328 pub current: Option<Money>,
330 pub historical: Vec<TrendPoint>,
335}
336
337impl EpsTrend {
338 #[must_use]
340 pub const fn new(current: Option<Money>, historical: Vec<TrendPoint>) -> Self {
341 Self {
342 current,
343 historical,
344 }
345 }
346
347 #[must_use]
349 pub fn find_by_period(&self, period: &str) -> Option<&TrendPoint> {
350 self.historical.iter().find(|point| point.period == period)
351 }
352
353 #[must_use]
355 pub fn available_periods(&self) -> Vec<&str> {
356 self.historical
357 .iter()
358 .map(|point| point.period.as_str())
359 .collect()
360 }
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
369pub struct RevisionPoint {
370 pub period: String,
373 pub up_count: u32,
375 pub down_count: u32,
377}
378
379impl RevisionPoint {
380 pub fn new(period: impl Into<String>, up_count: u32, down_count: u32) -> Self {
382 Self {
383 period: period.into(),
384 up_count,
385 down_count,
386 }
387 }
388
389 #[must_use]
391 pub const fn total_revisions(&self) -> u32 {
392 self.up_count + self.down_count
393 }
394
395 #[must_use]
398 #[allow(clippy::cast_possible_wrap)]
399 pub const fn net_revisions(&self) -> i32 {
400 self.up_count as i32 - self.down_count as i32
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
405#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
406pub struct EpsRevisions {
411 pub historical: Vec<RevisionPoint>,
416}
417
418impl EpsRevisions {
419 #[must_use]
421 pub const fn new(historical: Vec<RevisionPoint>) -> Self {
422 Self { historical }
423 }
424
425 #[must_use]
427 pub fn find_by_period(&self, period: &str) -> Option<&RevisionPoint> {
428 self.historical.iter().find(|point| point.period == period)
429 }
430
431 #[must_use]
433 pub fn available_periods(&self) -> Vec<&str> {
434 self.historical
435 .iter()
436 .map(|point| point.period.as_str())
437 .collect()
438 }
439
440 #[must_use]
442 pub fn total_up_revisions(&self) -> u32 {
443 self.historical.iter().map(|point| point.up_count).sum()
444 }
445
446 #[must_use]
448 pub fn total_down_revisions(&self) -> u32 {
449 self.historical.iter().map(|point| point.down_count).sum()
450 }
451
452 #[must_use]
454 #[allow(clippy::cast_possible_wrap)]
455 pub fn net_revisions(&self) -> i32 {
456 self.total_up_revisions() as i32 - self.total_down_revisions() as i32
457 }
458}
459
460#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
461#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
462pub struct EarningsTrendRow {
464 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
466 pub period: Period,
467 pub growth: Option<f64>,
469 #[serde(flatten)]
471 pub earnings_estimate: EarningsEstimate,
472 #[serde(flatten)]
474 pub revenue_estimate: RevenueEstimate,
475 #[serde(flatten)]
477 pub eps_trend: EpsTrend,
478 #[serde(flatten)]
480 pub eps_revisions: EpsRevisions,
481}