1use serde::{Deserialize, Serialize};
4use std::str::FromStr;
5
6use chrono::{DateTime, Utc};
7#[cfg(feature = "dataframe")]
8use df_derive_macros::ToDataFrame;
9use paft_decimal::Decimal;
10use paft_domain::{DomainError, Horizon, PeriodYear, ReportingPeriod};
11use paft_money::{Money, Price};
12
13use crate::FundamentalsError;
14
15paft_core::other_string_code_type!(
16 pub struct OtherRecommendationGrade for RecommendationGrade;
18 type Error = FundamentalsError;
19 parse(input) => RecommendationGrade::from_str(input);
20 invalid(input) => FundamentalsError::InvalidEnumValue {
21 enum_name: "RecommendationGrade",
22 value: input.to_string(),
23 };
24);
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum RecommendationGrade {
40 StrongBuy,
42 Buy,
44 Hold,
46 Sell,
48 StrongSell,
50 Outperform,
52 Underperform,
54 Other(OtherRecommendationGrade),
56}
57
58impl RecommendationGrade {
59 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
64 pub fn try_from_str(input: &str) -> Result<Self, FundamentalsError> {
65 Self::from_str(input)
66 }
67
68 pub fn other(input: &str) -> Result<Self, FundamentalsError> {
74 OtherRecommendationGrade::new(input).map(Self::Other)
75 }
76}
77
78paft_core::string_enum_with_code!(
82 RecommendationGrade, Other(OtherRecommendationGrade), "RecommendationGrade",
83 type Error = FundamentalsError;
84 invalid(input) => FundamentalsError::InvalidEnumValue {
85 enum_name: "RecommendationGrade",
86 value: input.to_string(),
87 };
88 {
89 "STRONG_BUY" => RecommendationGrade::StrongBuy,
90 "BUY" => RecommendationGrade::Buy,
91 "HOLD" => RecommendationGrade::Hold,
92 "SELL" => RecommendationGrade::Sell,
93 "STRONG_SELL" => RecommendationGrade::StrongSell,
94 "OUTPERFORM" => RecommendationGrade::Outperform,
95 "UNDERPERFORM" => RecommendationGrade::Underperform
96 },
97 {
98 "NEUTRAL" => RecommendationGrade::Hold,
100 "MARKET_PERFORM" => RecommendationGrade::Hold,
101 "OVERWEIGHT" => RecommendationGrade::Outperform,
102 "UNDERWEIGHT" => RecommendationGrade::Underperform
103 }
104);
105
106paft_core::impl_display_via_code!(RecommendationGrade);
108
109paft_core::other_string_code_type!(
110 pub struct OtherRecommendationAction for RecommendationAction;
112 type Error = FundamentalsError;
113 parse(input) => RecommendationAction::from_str(input);
114 invalid(input) => FundamentalsError::InvalidEnumValue {
115 enum_name: "RecommendationAction",
116 value: input.to_string(),
117 };
118);
119
120#[derive(Debug, Clone, PartialEq, Eq, Hash)]
132#[non_exhaustive]
133pub enum RecommendationAction {
134 Upgrade,
136 Downgrade,
138 Initiate,
140 Maintain,
142 Resume,
144 Suspend,
146 Other(OtherRecommendationAction),
148}
149
150impl RecommendationAction {
151 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
156 pub fn try_from_str(input: &str) -> Result<Self, FundamentalsError> {
157 Self::from_str(input)
158 }
159
160 pub fn other(input: &str) -> Result<Self, FundamentalsError> {
166 OtherRecommendationAction::new(input).map(Self::Other)
167 }
168}
169
170paft_core::string_enum_with_code!(
172 RecommendationAction, Other(OtherRecommendationAction), "RecommendationAction",
173 type Error = FundamentalsError;
174 invalid(input) => FundamentalsError::InvalidEnumValue {
175 enum_name: "RecommendationAction",
176 value: input.to_string(),
177 };
178 {
179 "UPGRADE" => RecommendationAction::Upgrade,
180 "DOWNGRADE" => RecommendationAction::Downgrade,
181 "INITIATE" => RecommendationAction::Initiate,
182 "MAINTAIN" => RecommendationAction::Maintain,
183 "RESUME" => RecommendationAction::Resume,
184 "SUSPEND" => RecommendationAction::Suspend
185 },
186 {
187 "UP" => RecommendationAction::Upgrade,
189 "DOWN" => RecommendationAction::Downgrade,
190 "INIT" => RecommendationAction::Initiate,
191 "INITIATED" => RecommendationAction::Initiate,
192 "REITERATE" => RecommendationAction::Maintain
193 }
194);
195
196paft_core::impl_display_via_code!(RecommendationAction);
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
199#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
200pub struct Earnings {
202 pub yearly: Vec<EarningsYear>,
204 pub quarterly: Vec<EarningsQuarter>,
206 pub quarterly_eps: Vec<EarningsQuarterEps>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
212pub struct EarningsYear {
214 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
216 pub year: PeriodYear,
217 pub revenue: Option<Money>,
219 pub earnings: Option<Money>,
221}
222
223impl EarningsYear {
224 pub fn new(year: i32) -> Result<Self, DomainError> {
230 Ok(Self {
231 year: PeriodYear::new(year)?,
232 revenue: None,
233 earnings: None,
234 })
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
240pub struct EarningsQuarter {
242 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
244 pub period: ReportingPeriod,
245 pub revenue: Option<Money>,
247 pub earnings: Option<Money>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
253pub struct EarningsQuarterEps {
255 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
257 pub period: ReportingPeriod,
258 pub actual: Option<Price>,
260 pub estimate: Option<Price>,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
265#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
266pub struct PriceTarget {
268 pub mean: Option<Price>,
270 pub high: Option<Price>,
272 pub low: Option<Price>,
274 pub number_of_analysts: Option<u32>,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
280pub struct RecommendationRow {
282 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
284 pub period: ReportingPeriod,
285 pub strong_buy: Option<u32>,
287 pub buy: Option<u32>,
289 pub hold: Option<u32>,
291 pub sell: Option<u32>,
293 pub strong_sell: Option<u32>,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
298#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
299pub struct RecommendationSummary {
301 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
303 pub latest_period: Option<ReportingPeriod>,
304 pub strong_buy: Option<u32>,
306 pub buy: Option<u32>,
308 pub hold: Option<u32>,
310 pub sell: Option<u32>,
312 pub strong_sell: Option<u32>,
314 #[serde(default, with = "paft_decimal::serde::option_canonical_str")]
316 pub mean: Option<Decimal>,
317 pub mean_rating_text: Option<String>,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
323pub struct UpgradeDowngradeRow {
325 #[serde(with = "chrono::serde::ts_milliseconds")]
327 pub ts: DateTime<Utc>,
328 pub firm: Option<String>,
330 #[cfg_attr(feature = "dataframe", df_derive(as_str))]
332 pub from_grade: Option<RecommendationGrade>,
333 #[cfg_attr(feature = "dataframe", df_derive(as_str))]
335 pub to_grade: Option<RecommendationGrade>,
336 #[cfg_attr(feature = "dataframe", df_derive(as_str))]
338 pub action: Option<RecommendationAction>,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
342#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
343pub struct AnalysisSummary {
345 pub target_mean_price: Option<Price>,
347 pub target_high_price: Option<Price>,
349 pub target_low_price: Option<Price>,
351 pub number_of_analyst_opinions: Option<u32>,
353 #[serde(default, with = "paft_decimal::serde::option_canonical_str")]
355 pub recommendation_mean: Option<Decimal>,
356 pub recommendation_text: Option<String>,
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
361#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
362pub struct EarningsEstimate {
364 pub avg: Option<Price>,
366 pub low: Option<Price>,
368 pub high: Option<Price>,
370 pub year_ago_eps: Option<Price>,
372 pub num_analysts: Option<u32>,
374 #[serde(default, with = "paft_decimal::serde::option_canonical_str")]
376 pub growth: Option<Decimal>,
377}
378
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
380#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
381pub struct RevenueEstimate {
383 pub avg: Option<Money>,
385 pub low: Option<Money>,
387 pub high: Option<Money>,
389 pub year_ago_revenue: Option<Money>,
391 pub num_analysts: Option<u32>,
393 #[serde(default, with = "paft_decimal::serde::option_canonical_str")]
395 pub growth: Option<Decimal>,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
405pub struct TrendPoint {
406 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
409 pub horizon: Horizon,
410 pub value: Price,
412}
413
414impl TrendPoint {
415 #[must_use]
417 pub const fn new(horizon: Horizon, value: Price) -> Self {
418 Self { horizon, value }
419 }
420
421 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
426 pub fn try_new_str(horizon: &str, value: Price) -> Result<Self, DomainError> {
427 Ok(Self {
428 horizon: horizon.parse()?,
429 value,
430 })
431 }
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
439#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
440pub struct EpsTrend {
441 pub current: Option<Price>,
443 pub historical: Vec<TrendPoint>,
448}
449
450impl EpsTrend {
451 #[must_use]
453 pub const fn new(current: Option<Price>, historical: Vec<TrendPoint>) -> Self {
454 Self {
455 current,
456 historical,
457 }
458 }
459
460 #[must_use]
462 pub fn find_by_horizon(&self, horizon: &Horizon) -> Option<&TrendPoint> {
463 self.historical
464 .iter()
465 .find(|point| &point.horizon == horizon)
466 }
467
468 pub fn find_by_horizon_str(&self, horizon: &str) -> Result<Option<&TrendPoint>, DomainError> {
475 let parsed: Horizon = horizon.parse()?;
476 Ok(self.find_by_horizon(&parsed))
477 }
478
479 #[must_use]
481 pub fn available_horizons(&self) -> Vec<Horizon> {
482 self.historical
483 .iter()
484 .map(|point| point.horizon.clone())
485 .collect()
486 }
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
495#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
496pub struct RevisionPoint {
497 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
500 pub horizon: Horizon,
501 pub up_count: u32,
503 pub down_count: u32,
505}
506
507impl RevisionPoint {
508 #[must_use]
510 pub const fn new(horizon: Horizon, up_count: u32, down_count: u32) -> Self {
511 Self {
512 horizon,
513 up_count,
514 down_count,
515 }
516 }
517
518 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
523 pub fn try_new_str(horizon: &str, up: u32, down: u32) -> Result<Self, DomainError> {
524 Ok(Self {
525 horizon: horizon.parse()?,
526 up_count: up,
527 down_count: down,
528 })
529 }
530
531 #[must_use]
533 pub fn total_revisions(&self) -> u64 {
534 u64::from(self.up_count) + u64::from(self.down_count)
535 }
536
537 #[must_use]
540 pub fn net_revisions(&self) -> i64 {
541 i64::from(self.up_count) - i64::from(self.down_count)
542 }
543}
544
545#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
550#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
551pub struct EpsRevisions {
552 pub historical: Vec<RevisionPoint>,
557}
558
559impl EpsRevisions {
560 #[must_use]
562 pub const fn new(historical: Vec<RevisionPoint>) -> Self {
563 Self { historical }
564 }
565
566 #[must_use]
568 pub fn find_by_horizon(&self, horizon: &Horizon) -> Option<&RevisionPoint> {
569 self.historical
570 .iter()
571 .find(|point| &point.horizon == horizon)
572 }
573
574 pub fn find_by_horizon_str(
581 &self,
582 horizon: &str,
583 ) -> Result<Option<&RevisionPoint>, DomainError> {
584 let parsed: Horizon = horizon.parse()?;
585 Ok(self.find_by_horizon(&parsed))
586 }
587
588 #[must_use]
590 pub fn available_horizons(&self) -> Vec<Horizon> {
591 self.historical
592 .iter()
593 .map(|point| point.horizon.clone())
594 .collect()
595 }
596}
597
598#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
599#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
600pub struct EarningsTrendRow {
602 #[cfg_attr(feature = "dataframe", df_derive(as_string))]
604 pub period: ReportingPeriod,
605 #[serde(default, with = "paft_decimal::serde::option_canonical_str")]
607 pub growth: Option<Decimal>,
608 pub earnings_estimate: EarningsEstimate,
610 pub revenue_estimate: RevenueEstimate,
612 pub eps_trend: EpsTrend,
614 pub eps_revisions: EpsRevisions,
616}