1use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::str::FromStr;
5
6use chrono::NaiveDate;
7#[cfg(feature = "dataframe")]
8use df_derive_macros::ToDataFrame;
9use paft_domain::Isin;
10#[cfg(feature = "dataframe")]
11use paft_utils::dataframe::{Columnar, ToDataFrame, ToDataFrameVec};
12
13use crate::FundamentalsError;
14
15paft_core::other_string_code_type!(
16 pub struct OtherFundKind for FundKind;
18 type Error = FundamentalsError;
19 parse(input) => FundKind::from_str(input);
20 invalid(input) => FundamentalsError::InvalidEnumValue {
21 enum_name: "FundKind",
22 value: input.to_string(),
23 };
24);
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum FundKind {
40 Etf,
42 MutualFund,
44 IndexFund,
46 ClosedEndFund,
48 MoneyMarketFund,
50 HedgeFund,
52 Reit,
54 UnitInvestmentTrust,
56 Other(OtherFundKind),
58}
59
60impl FundKind {
61 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
66 pub fn try_from_str(input: &str) -> Result<Self, FundamentalsError> {
67 Self::from_str(input)
68 }
69
70 pub fn other(input: &str) -> Result<Self, FundamentalsError> {
76 OtherFundKind::new(input).map(Self::Other)
77 }
78}
79
80paft_core::string_enum_with_code!(
82 FundKind, Other(OtherFundKind), "FundKind",
83 type Error = FundamentalsError;
84 invalid(input) => FundamentalsError::InvalidEnumValue {
85 enum_name: "FundKind",
86 value: input.to_string(),
87 };
88 {
89 "ETF" => FundKind::Etf,
90 "MUTUAL_FUND" => FundKind::MutualFund,
91 "INDEX_FUND" => FundKind::IndexFund,
92 "CLOSED_END_FUND" => FundKind::ClosedEndFund,
93 "MONEY_MARKET_FUND" => FundKind::MoneyMarketFund,
94 "HEDGE_FUND" => FundKind::HedgeFund,
95 "REIT" => FundKind::Reit,
96 "UIT" => FundKind::UnitInvestmentTrust
97 },
98 {
99 "EXCHANGE_TRADED_FUND" => FundKind::Etf,
100 "MUTUAL" => FundKind::MutualFund,
101 "INDEX" => FundKind::IndexFund,
102 "CEF" => FundKind::ClosedEndFund,
103 "MMF" => FundKind::MoneyMarketFund,
104 "REAL_ESTATE_INVESTMENT_TRUST" => FundKind::Reit,
105 "UNIT_INVESTMENT_TRUST" => FundKind::UnitInvestmentTrust
106 }
107);
108
109paft_core::impl_display_via_code!(FundKind);
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
112#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
113pub struct Address {
115 pub street1: Option<String>,
117 pub street2: Option<String>,
119 pub city: Option<String>,
121 pub state: Option<String>,
123 pub country: Option<String>,
125 pub zip: Option<String>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
132pub struct CompanyProfile {
133 pub name: String,
135 pub sector: Option<String>,
137 pub industry: Option<String>,
139 pub website: Option<String>,
141 pub address: Option<Address>,
143 pub summary: Option<String>,
145 #[cfg_attr(feature = "dataframe", df_derive(as_str))]
147 pub isin: Option<Isin>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
153pub struct FundProfile {
154 pub name: String,
156 pub family: Option<String>,
158 #[cfg_attr(feature = "dataframe", df_derive(as_str))]
160 pub kind: FundKind,
161 #[cfg_attr(feature = "dataframe", df_derive(as_str))]
163 pub isin: Option<Isin>,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
173#[non_exhaustive]
174pub enum Profile {
175 Company(CompanyProfile),
177 Fund(FundProfile),
179}
180
181#[derive(Serialize, Deserialize)]
182#[serde(tag = "kind", rename_all = "snake_case")]
183enum ProfileWire {
184 Company {
185 name: String,
186 sector: Option<String>,
187 industry: Option<String>,
188 website: Option<String>,
189 address: Option<Address>,
190 summary: Option<String>,
191 isin: Option<Isin>,
192 },
193 Fund {
194 name: String,
195 family: Option<String>,
196 fund_kind: FundKind,
197 isin: Option<Isin>,
198 },
199}
200
201impl From<&Profile> for ProfileWire {
202 fn from(profile: &Profile) -> Self {
203 match profile {
204 Profile::Company(company) => Self::Company {
205 name: company.name.clone(),
206 sector: company.sector.clone(),
207 industry: company.industry.clone(),
208 website: company.website.clone(),
209 address: company.address.clone(),
210 summary: company.summary.clone(),
211 isin: company.isin.clone(),
212 },
213 Profile::Fund(fund) => Self::Fund {
214 name: fund.name.clone(),
215 family: fund.family.clone(),
216 fund_kind: fund.kind.clone(),
217 isin: fund.isin.clone(),
218 },
219 }
220 }
221}
222
223impl From<ProfileWire> for Profile {
224 fn from(wire: ProfileWire) -> Self {
225 match wire {
226 ProfileWire::Company {
227 name,
228 sector,
229 industry,
230 website,
231 address,
232 summary,
233 isin,
234 } => Self::Company(CompanyProfile {
235 name,
236 sector,
237 industry,
238 website,
239 address,
240 summary,
241 isin,
242 }),
243 ProfileWire::Fund {
244 name,
245 family,
246 fund_kind,
247 isin,
248 } => Self::Fund(FundProfile {
249 name,
250 family,
251 kind: fund_kind,
252 isin,
253 }),
254 }
255 }
256}
257
258impl Serialize for Profile {
259 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
260 where
261 S: Serializer,
262 {
263 ProfileWire::from(self).serialize(serializer)
264 }
265}
266
267impl<'de> Deserialize<'de> for Profile {
268 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
269 where
270 D: Deserializer<'de>,
271 {
272 Ok(Self::from(ProfileWire::deserialize(deserializer)?))
273 }
274}
275
276impl Profile {
277 #[must_use]
279 pub const fn isin(&self) -> Option<&Isin> {
280 match self {
281 Self::Company(c) => c.isin.as_ref(),
282 Self::Fund(f) => f.isin.as_ref(),
283 }
284 }
285}
286
287#[cfg(feature = "dataframe")]
288#[derive(Debug, Clone)]
289#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
290struct ProfileRow {
291 pub profile_type: String,
292 pub name: String,
293 pub sector: Option<String>,
294 pub industry: Option<String>,
295 pub website: Option<String>,
296 pub address: Option<Address>,
297 pub summary: Option<String>,
298 pub family: Option<String>,
299 pub fund_kind: Option<String>,
300 pub isin: Option<String>,
301}
302
303#[cfg(feature = "dataframe")]
304impl From<&Profile> for ProfileRow {
305 fn from(profile: &Profile) -> Self {
306 match profile {
307 Profile::Company(company) => Self {
308 profile_type: "Company".to_string(),
309 name: company.name.clone(),
310 sector: company.sector.clone(),
311 industry: company.industry.clone(),
312 website: company.website.clone(),
313 address: company.address.clone(),
314 summary: company.summary.clone(),
315 family: None,
316 fund_kind: None,
317 isin: company.isin.as_ref().map(ToString::to_string),
318 },
319 Profile::Fund(fund) => Self {
320 profile_type: "Fund".to_string(),
321 name: fund.name.clone(),
322 sector: None,
323 industry: None,
324 website: None,
325 address: None,
326 summary: None,
327 family: fund.family.clone(),
328 fund_kind: Some(fund.kind.to_string()),
329 isin: fund.isin.as_ref().map(ToString::to_string),
330 },
331 }
332 }
333}
334
335#[cfg(feature = "dataframe")]
336impl ToDataFrame for Profile {
337 fn to_dataframe(&self) -> polars::prelude::PolarsResult<polars::prelude::DataFrame> {
338 ProfileRow::from(self).to_dataframe()
339 }
340
341 fn empty_dataframe() -> polars::prelude::PolarsResult<polars::prelude::DataFrame> {
342 ProfileRow::empty_dataframe()
343 }
344
345 fn schema() -> polars::prelude::PolarsResult<Vec<(String, polars::datatypes::DataType)>> {
346 ProfileRow::schema()
347 }
348}
349
350#[cfg(feature = "dataframe")]
351impl Columnar for Profile {
352 fn columnar_from_refs(
353 items: &[&Self],
354 ) -> polars::prelude::PolarsResult<polars::prelude::DataFrame> {
355 let rows: Vec<ProfileRow> = items.iter().copied().map(ProfileRow::from).collect();
356 rows.as_slice().to_dataframe()
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362#[cfg_attr(feature = "dataframe", derive(ToDataFrame))]
363pub struct ShareCount {
364 pub date: NaiveDate,
366 pub shares: u64,
368}