Skip to main content

paft_fundamentals/
holders.rs

1//! Holder, insider activity, and ownership summary types.
2
3use serde::{Deserialize, Serialize};
4use std::str::FromStr;
5
6use chrono::NaiveDate;
7#[cfg(feature = "dataframe")]
8use df_derive_macros::ToDataFrame;
9use paft_decimal::{Decimal, Ratio};
10use paft_domain::ReportingPeriod;
11use paft_money::Money;
12
13use crate::FundamentalsError;
14
15paft_core::other_string_code_type!(
16    /// Provider-specific transaction type not modeled by [`TransactionType`].
17    pub struct OtherTransactionType for TransactionType;
18    type Error = FundamentalsError;
19    parse(input) => TransactionType::from_str(input);
20    invalid(input) => FundamentalsError::InvalidEnumValue {
21        enum_name: "TransactionType",
22        value: input.to_string(),
23    };
24);
25
26/// Transaction types for insider activities with canonical variants and extensible fallback.
27///
28/// This enum provides type-safe handling of transaction types while gracefully
29/// handling unknown or provider-specific transaction types through the `Other` variant.
30///
31/// Canonical/serde rules:
32/// - Emission uses a single canonical form per variant (UPPERCASE ASCII, no spaces)
33/// - Parser accepts a superset of tokens (aliases, case-insensitive)
34/// - `Other(s)` serializes to its canonical `code()` string (no escape prefix)
35/// - `Display` output matches the canonical code for known variants and the raw `s` for `Other(s)`
36/// - Serde round-trips preserve identity for canonical variants; unknown tokens normalize to `Other(UPPERCASE)`
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum TransactionType {
40    /// Purchase or acquisition of shares
41    Buy,
42    /// Sale or disposal of shares
43    Sell,
44    /// Stock award or grant
45    Award,
46    /// Exercise of options
47    Exercise,
48    /// Gift of shares
49    Gift,
50    /// Conversion of securities
51    Conversion,
52    /// Unknown or provider-specific transaction type
53    Other(OtherTransactionType),
54}
55
56impl TransactionType {
57    /// Attempts to parse a transaction type, uppercasing unknown inputs into `Other`.
58    ///
59    /// # Errors
60    /// Returns `FundamentalsError::InvalidEnumValue` when `input` is empty/whitespace.
61    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
62    pub fn try_from_str(input: &str) -> Result<Self, FundamentalsError> {
63        Self::from_str(input)
64    }
65
66    /// Builds an unknown transaction type, rejecting modeled types and aliases.
67    ///
68    /// # Errors
69    /// Returns an error if `input` is empty, cannot be canonicalized, or parses
70    /// to a modeled [`TransactionType`] variant.
71    pub fn other(input: &str) -> Result<Self, FundamentalsError> {
72        OtherTransactionType::new(input).map(Self::Other)
73    }
74}
75
76// Centralized code() and string impls via macro
77paft_core::string_enum_with_code!(
78    TransactionType, Other(OtherTransactionType), "TransactionType",
79    type Error = FundamentalsError;
80    invalid(input) => FundamentalsError::InvalidEnumValue {
81        enum_name: "TransactionType",
82        value: input.to_string(),
83    };
84    {
85        "BUY" => TransactionType::Buy,
86        "SELL" => TransactionType::Sell,
87        "AWARD" => TransactionType::Award,
88        "EXERCISE" => TransactionType::Exercise,
89        "GIFT" => TransactionType::Gift,
90        "CONVERSION" => TransactionType::Conversion
91    },
92    {
93        // Aliases
94        "PURCHASE" => TransactionType::Buy,
95        "ACQUISITION" => TransactionType::Buy,
96        "SALE" => TransactionType::Sell,
97        "DISPOSAL" => TransactionType::Sell,
98        "GRANT" => TransactionType::Award,
99        "STOCK_AWARD" => TransactionType::Award,
100        "OPTION_EXERCISE" => TransactionType::Exercise
101    }
102);
103
104// Display equals code for these enums
105paft_core::impl_display_via_code!(TransactionType);
106
107paft_core::other_string_code_type!(
108    /// Provider-specific insider position not modeled by [`InsiderPosition`].
109    pub struct OtherInsiderPosition for InsiderPosition;
110    type Error = FundamentalsError;
111    parse(input) => InsiderPosition::from_str(input);
112    invalid(input) => FundamentalsError::InvalidEnumValue {
113        enum_name: "InsiderPosition",
114        value: input.to_string(),
115    };
116);
117
118/// Insider positions in a company with canonical variants and extensible fallback.
119///
120/// This enum provides type-safe handling of insider positions while gracefully
121/// handling unknown or provider-specific positions through the `Other` variant.
122///
123/// Canonical/serde rules:
124/// - Emission uses a single canonical form per variant (UPPERCASE ASCII, no spaces)
125/// - Parser accepts a superset of tokens (aliases, case-insensitive)
126/// - `Other(s)` serializes to its canonical `code()` string (no escape prefix)
127/// - `Display` output matches the canonical code for known variants and the raw `s` for `Other(s)`
128/// - Serde round-trips preserve identity for canonical variants; unknown tokens normalize to `Other(UPPERCASE)`
129#[derive(Debug, Clone, PartialEq, Eq, Hash)]
130#[non_exhaustive]
131pub enum InsiderPosition {
132    /// Officer of the company
133    Officer,
134    /// Director or board member
135    Director,
136    /// Beneficial owner (typically >10% ownership)
137    Owner,
138    /// Chief Executive Officer
139    Ceo,
140    /// Chief Financial Officer
141    Cfo,
142    /// Chief Operating Officer
143    Coo,
144    /// Chief Technology Officer
145    Cto,
146    /// President
147    President,
148    /// Vice President
149    VicePresident,
150    /// Secretary
151    Secretary,
152    /// Treasurer
153    Treasurer,
154    /// Unknown or provider-specific position
155    Other(OtherInsiderPosition),
156}
157
158impl InsiderPosition {
159    /// Attempts to parse an insider position, uppercasing unknown inputs into `Other`.
160    ///
161    /// # Errors
162    /// Returns `FundamentalsError::InvalidEnumValue` when `input` is empty/whitespace.
163    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
164    pub fn try_from_str(input: &str) -> Result<Self, FundamentalsError> {
165        Self::from_str(input)
166    }
167
168    /// Builds an unknown insider position, rejecting modeled positions and aliases.
169    ///
170    /// # Errors
171    /// Returns an error if `input` is empty, cannot be canonicalized, or parses
172    /// to a modeled [`InsiderPosition`] variant.
173    pub fn other(input: &str) -> Result<Self, FundamentalsError> {
174        OtherInsiderPosition::new(input).map(Self::Other)
175    }
176}
177
178// Centralized code() and string impls via macro
179paft_core::string_enum_with_code!(
180    InsiderPosition, Other(OtherInsiderPosition), "InsiderPosition",
181    type Error = FundamentalsError;
182    invalid(input) => FundamentalsError::InvalidEnumValue {
183        enum_name: "InsiderPosition",
184        value: input.to_string(),
185    };
186    {
187        "OFFICER" => InsiderPosition::Officer,
188        "DIRECTOR" => InsiderPosition::Director,
189        "OWNER" => InsiderPosition::Owner,
190        "CEO" => InsiderPosition::Ceo,
191        "CFO" => InsiderPosition::Cfo,
192        "COO" => InsiderPosition::Coo,
193        "CTO" => InsiderPosition::Cto,
194        "PRESIDENT" => InsiderPosition::President,
195        "VICE_PRESIDENT" => InsiderPosition::VicePresident,
196        "SECRETARY" => InsiderPosition::Secretary,
197        "TREASURER" => InsiderPosition::Treasurer
198    },
199    {
200        // Aliases
201        "BOARD_MEMBER" => InsiderPosition::Director,
202        "BENEFICIAL_OWNER" => InsiderPosition::Owner,
203        "10_OWNER" => InsiderPosition::Owner,
204        "CHIEF_EXECUTIVE_OFFICER" => InsiderPosition::Ceo,
205        "CHIEF_FINANCIAL_OFFICER" => InsiderPosition::Cfo,
206        "CHIEF_OPERATING_OFFICER" => InsiderPosition::Coo,
207        "CHIEF_TECHNOLOGY_OFFICER" => InsiderPosition::Cto,
208        "VP" => InsiderPosition::VicePresident
209    }
210);
211
212paft_core::impl_display_via_code!(InsiderPosition);
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
216/// Summary percentages for major holder categories.
217pub struct MajorHolder {
218    /// The category of the holder (e.g., "% of Shares Held by All Insider").
219    pub category: String,
220    /// The value associated with the category as a numeric fraction (e.g., 0.255 for 25.5%).
221    #[cfg_attr(feature = "dataframe", df_derive(decimal(precision = 38, scale = 10)))]
222    pub value: Ratio,
223}
224
225/// Represents a single institutional or mutual fund holder.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
228pub struct InstitutionalHolder {
229    /// The name of the holding institution or fund.
230    pub holder: String,
231    /// The number of shares held.
232    pub shares: Option<u64>,
233    /// The calendar date of the last reported position.
234    pub date_reported: NaiveDate,
235    /// The percentage of the company's outstanding shares held by this entity.
236    #[cfg_attr(feature = "dataframe", df_derive(decimal(precision = 38, scale = 10)))]
237    pub pct_held: Option<Ratio>,
238    /// The market value of the shares held.
239    pub value: Option<Money>,
240}
241
242/// Represents a single insider transaction.
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
245pub struct InsiderTransaction {
246    /// The name of the insider who executed the transaction.
247    pub insider: String,
248    /// The insider's relationship to the company with canonical variants and extensible fallback.
249    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
250    pub position: InsiderPosition,
251    /// The type of transaction with canonical variants and extensible fallback.
252    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
253    pub transaction_type: TransactionType,
254    /// The number of shares involved in the transaction.
255    pub shares: Option<u64>,
256    /// The total value of the transaction.
257    pub value: Option<Money>,
258    /// The transaction calendar date.
259    pub transaction_date: NaiveDate,
260    /// A URL to the source filing for the transaction, if available.
261    pub url: Option<String>,
262}
263
264/// Represents a single insider on the company's roster.
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
267pub struct InsiderRosterHolder {
268    /// The name of the insider.
269    pub name: String,
270    /// The insider's position in the company with canonical variants and extensible fallback.
271    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
272    pub position: InsiderPosition,
273    /// A description of the most recent transaction made by this insider.
274    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
275    pub most_recent_transaction: TransactionType,
276    /// The calendar date of the latest transaction.
277    pub latest_transaction_date: NaiveDate,
278    /// The number of shares owned directly by the insider.
279    pub shares_owned_directly: Option<u64>,
280    /// The calendar date of the direct ownership filing.
281    pub position_direct_date: NaiveDate,
282}
283
284/// A summary of net share purchase activity by insiders over a specific period.
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
287pub struct NetSharePurchaseActivity {
288    /// The period the summary covers (e.g., `ReportingPeriod::quarterly(2023, 4)?`).
289    #[cfg_attr(feature = "dataframe", df_derive(as_string))]
290    pub period: ReportingPeriod,
291    /// The total number of shares purchased by insiders.
292    pub buy_shares: Option<u64>,
293    /// The number of separate buy transactions.
294    pub buy_count: Option<u64>,
295    /// The total number of shares sold by insiders.
296    pub sell_shares: Option<u64>,
297    /// The number of separate sell transactions.
298    pub sell_count: Option<u64>,
299    /// The net number of shares purchased or sold.
300    pub net_shares: Option<i64>,
301    /// The net number of transactions.
302    pub net_count: Option<i64>,
303    /// The total number of shares held by all insiders.
304    pub total_insider_shares: Option<u64>,
305    /// The net shares purchased/sold as a percentage of total insider shares.
306    #[serde(default, with = "paft_decimal::serde::option_canonical_str")]
307    pub net_percent_insider_shares: Option<Decimal>,
308}