paft_fundamentals/
holders.rs

1//! Holder, insider activity, and ownership summary types.
2
3use 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;
13
14/// Transaction types for insider activities with canonical variants and extensible fallback.
15///
16/// This enum provides type-safe handling of transaction types while gracefully
17/// handling unknown or provider-specific transaction types through the `Other` variant.
18#[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 TransactionType {
24    /// Purchase or acquisition of shares
25    #[strum(to_string = "BUY", serialize = "PURCHASE", serialize = "ACQUISITION")]
26    Buy,
27    /// Sale or disposal of shares
28    #[strum(to_string = "SELL", serialize = "SALE", serialize = "DISPOSAL")]
29    Sell,
30    /// Stock award or grant
31    #[strum(to_string = "AWARD", serialize = "GRANT", serialize = "STOCK_AWARD")]
32    Award,
33    /// Exercise of options
34    #[strum(to_string = "EXERCISE", serialize = "OPTION_EXERCISE")]
35    Exercise,
36    /// Gift of shares
37    #[strum(to_string = "GIFT")]
38    Gift,
39    /// Conversion of securities
40    #[strum(to_string = "CONVERSION")]
41    Conversion,
42    /// Unknown or provider-specific transaction type
43    Other(String),
44}
45
46impl From<String> for TransactionType {
47    fn from(s: String) -> Self {
48        // Try to parse as a known variant first
49        Self::from_str(&s).unwrap_or_else(|_| Self::Other(s.to_uppercase()))
50    }
51}
52
53impl From<TransactionType> for String {
54    fn from(transaction_type: TransactionType) -> Self {
55        match transaction_type {
56            TransactionType::Other(s) => s,
57            _ => transaction_type.to_string(),
58        }
59    }
60}
61
62/// Insider positions in a company with canonical variants and extensible fallback.
63///
64/// This enum provides type-safe handling of insider positions while gracefully
65/// handling unknown or provider-specific positions through the `Other` variant.
66#[derive(
67    Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, AsRefStr, EnumString,
68)]
69#[strum(ascii_case_insensitive)]
70#[serde(from = "String", into = "String")]
71pub enum InsiderPosition {
72    /// Officer of the company
73    #[strum(to_string = "OFFICER")]
74    Officer,
75    /// Director or board member
76    #[strum(to_string = "DIRECTOR", serialize = "BOARD_MEMBER")]
77    Director,
78    /// Beneficial owner (typically >10% ownership)
79    #[strum(
80        to_string = "OWNER",
81        serialize = "BENEFICIAL_OWNER",
82        serialize = "10%_OWNER"
83    )]
84    Owner,
85    /// Chief Executive Officer
86    #[strum(to_string = "CEO", serialize = "CHIEF_EXECUTIVE_OFFICER")]
87    Ceo,
88    /// Chief Financial Officer
89    #[strum(to_string = "CFO", serialize = "CHIEF_FINANCIAL_OFFICER")]
90    Cfo,
91    /// Chief Operating Officer
92    #[strum(to_string = "COO", serialize = "CHIEF_OPERATING_OFFICER")]
93    Coo,
94    /// Chief Technology Officer
95    #[strum(to_string = "CTO", serialize = "CHIEF_TECHNOLOGY_OFFICER")]
96    Cto,
97    /// President
98    #[strum(to_string = "PRESIDENT")]
99    President,
100    /// Vice President
101    #[strum(to_string = "VP", serialize = "VICE_PRESIDENT")]
102    VicePresident,
103    /// Secretary
104    #[strum(to_string = "SECRETARY")]
105    Secretary,
106    /// Treasurer
107    #[strum(to_string = "TREASURER")]
108    Treasurer,
109    /// Unknown or provider-specific position
110    Other(String),
111}
112
113impl From<String> for InsiderPosition {
114    fn from(s: String) -> Self {
115        // Try to parse as a known variant first
116        Self::from_str(&s).unwrap_or_else(|_| Self::Other(s.to_uppercase()))
117    }
118}
119
120impl From<InsiderPosition> for String {
121    fn from(position: InsiderPosition) -> Self {
122        match position {
123            InsiderPosition::Other(s) => s,
124            _ => position.to_string(),
125        }
126    }
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
131/// Summary percentages for major holder categories.
132pub struct MajorHolder {
133    /// The category of the holder (e.g., "% of Shares Held by All Insider").
134    pub category: String,
135    /// The value associated with the category as a numeric fraction (e.g., 0.255 for 25.5%).
136    pub value: f64,
137}
138
139/// Represents a single institutional or mutual fund holder.
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
142pub struct InstitutionalHolder {
143    /// The name of the holding institution or fund.
144    pub holder: String,
145    /// The number of shares held.
146    pub shares: Option<u64>,
147    /// The date of the last reported position as a Unix timestamp.
148    #[serde(with = "chrono::serde::ts_seconds")]
149    pub date_reported: DateTime<Utc>,
150    /// The percentage of the company's outstanding shares held by this entity.
151    pub pct_held: Option<f64>,
152    /// The market value of the shares held.
153    pub value: Option<Money>,
154}
155
156/// Represents a single insider transaction.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
159pub struct InsiderTransaction {
160    /// The name of the insider who executed the transaction.
161    pub insider: String,
162    /// The insider's relationship to the company with canonical variants and extensible fallback.
163    #[cfg_attr(feature = "dataframe", df_derive(as_string))]
164    pub position: InsiderPosition,
165    /// The type of transaction with canonical variants and extensible fallback.
166    #[cfg_attr(feature = "dataframe", df_derive(as_string))]
167    pub transaction_type: TransactionType,
168    /// The number of shares involved in the transaction.
169    pub shares: Option<u64>,
170    /// The total value of the transaction.
171    pub value: Option<Money>,
172    /// The transaction date as a Unix timestamp.
173    #[serde(with = "chrono::serde::ts_seconds")]
174    pub transaction_date: DateTime<Utc>,
175    /// A URL to the source filing for the transaction, if available.
176    pub url: String,
177}
178
179/// Represents a single insider on the company's roster.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
182pub struct InsiderRosterHolder {
183    /// The name of the insider.
184    pub name: String,
185    /// The insider's position in the company with canonical variants and extensible fallback.
186    #[cfg_attr(feature = "dataframe", df_derive(as_string))]
187    pub position: InsiderPosition,
188    /// A description of the most recent transaction made by this insider.
189    #[cfg_attr(feature = "dataframe", df_derive(as_string))]
190    pub most_recent_transaction: TransactionType,
191    /// The date of the latest transaction as a Unix timestamp.
192    #[serde(with = "chrono::serde::ts_seconds")]
193    pub latest_transaction_date: DateTime<Utc>,
194    /// The number of shares owned directly by the insider.
195    pub shares_owned_directly: Option<u64>,
196    /// The date of the direct ownership filing as a Unix timestamp.
197    #[serde(with = "chrono::serde::ts_seconds")]
198    pub position_direct_date: DateTime<Utc>,
199}
200
201/// A summary of net share purchase activity by insiders over a specific period.
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
204pub struct NetSharePurchaseActivity {
205    /// The period the summary covers (e.g., "3m").
206    pub period: String,
207    /// The total number of shares purchased by insiders.
208    pub buy_shares: Option<u64>,
209    /// The number of separate buy transactions.
210    pub buy_count: Option<u64>,
211    /// The total number of shares sold by insiders.
212    pub sell_shares: Option<u64>,
213    /// The number of separate sell transactions.
214    pub sell_count: Option<u64>,
215    /// The net number of shares purchased or sold.
216    pub net_shares: Option<i64>,
217    /// The net number of transactions.
218    pub net_count: Option<i64>,
219    /// The total number of shares held by all insiders.
220    pub total_insider_shares: Option<u64>,
221    /// The net shares purchased/sold as a percentage of total insider shares.
222    pub net_percent_insider_shares: Option<f64>,
223}