Skip to main content

finance_query/models/screeners/
fields.rs

1//! Typed screener field enums for equity and fund screener queries.
2//!
3//! Use these enums with [`ScreenerFieldExt`](super::condition::ScreenerFieldExt) to build
4//! type-safe screener conditions with full IDE autocomplete:
5//!
6//! ```
7//! use finance_query::{EquityField, ScreenerFieldExt};
8//!
9//! let pe_filter     = EquityField::PeRatio.between(10.0, 25.0);
10//! let region_filter = EquityField::Region.eq_str("us");
11//! let volume_filter = EquityField::AvgDailyVol3M.gt(500_000.0);
12//! ```
13use super::condition::ScreenerField;
14use serde::Serialize;
15
16// ============================================================================
17// EquityField
18// ============================================================================
19
20/// Typed field names for equity custom screener queries.
21///
22/// Variants marked as *display-only* (`Ticker`, `CompanyShortName`) are used
23/// in `include_fields` to request those columns in the response. They do not
24/// support meaningful numeric or string filters via Yahoo's API.
25///
26/// All other variants support filtering via [`ScreenerFieldExt`](super::condition::ScreenerFieldExt)
27/// methods. Categorical fields (`Region`, `Sector`, `Industry`, `Exchange`, `PeerGroup`) use
28/// [`eq_str`](super::condition::ScreenerFieldExt::eq_str); all others use numeric operators.
29///
30/// # Example
31///
32/// ```
33/// use finance_query::{EquityField, EquityScreenerQuery, ScreenerFieldExt};
34///
35/// let query = EquityScreenerQuery::new()
36///     .sort_by(EquityField::IntradayMarketCap, false)
37///     .add_condition(EquityField::Region.eq_str("us"))
38///     .add_condition(EquityField::PeRatio.between(10.0, 25.0))
39///     .add_condition(EquityField::AvgDailyVol3M.gt(200_000.0))
40///     .include_fields(vec![
41///         EquityField::Ticker,
42///         EquityField::CompanyShortName,
43///         EquityField::IntradayPrice,
44///         EquityField::PeRatio,
45///     ]);
46/// ```
47#[non_exhaustive]
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum EquityField {
50    // Display-only fields (for include_fields, not filterable)
51    /// Ticker symbol — display only, use in `include_fields`.
52    Ticker,
53    /// Short company name — display only, use in `include_fields`.
54    CompanyShortName,
55
56    // Price / Market Cap
57    /// End-of-day price (`"eodprice"`).
58    EodPrice,
59    /// Intraday price (`"intradayprice"`).
60    IntradayPrice,
61    /// Intraday price change (`"intradaypricechange"`).
62    IntradayPriceChange,
63    /// Percent change (`"percentchange"`).
64    PercentChange,
65    /// Last close 52-week high (`"lastclose52weekhigh.lasttwelvemonths"`).
66    Lastclose52WkHigh,
67    /// 52-week percent change (`"fiftytwowkpercentchange"`).
68    FiftyTwoWkPctChange,
69    /// Last close 52-week low (`"lastclose52weeklow.lasttwelvemonths"`).
70    Lastclose52WkLow,
71    /// Intraday market cap (`"intradaymarketcap"`).
72    IntradayMarketCap,
73    /// Last close market cap (`"lastclosemarketcap.lasttwelvemonths"`).
74    LastcloseMarketCap,
75
76    // Categorical filters (use eq_str)
77    /// Geographic region — use `eq_str("us")`.
78    Region,
79    /// GICS sector — use `eq_str("Technology")` etc.
80    Sector,
81    /// Peer group (`"peer_group"`).
82    PeerGroup,
83    /// Industry (`"industry"`).
84    Industry,
85    /// Exchange (`"exchange"`).
86    Exchange,
87
88    // Trading
89    /// Beta (`"beta"`).
90    Beta,
91    /// 3-month average daily volume (`"avgdailyvol3m"`).
92    AvgDailyVol3M,
93    /// Percent held by insiders (`"pctheldinsider"`).
94    PctHeldInsider,
95    /// Percent held by institutions (`"pctheldinst"`).
96    PctHeldInst,
97    /// Intraday volume (`"dayvolume"`).
98    DayVolume,
99    /// End-of-day volume (`"eodvolume"`).
100    EodVolume,
101
102    // Short Interest
103    /// Short percentage of shares outstanding (`"short_percentage_of_shares_outstanding.value"`).
104    ShortPctSharesOut,
105    /// Short interest value (`"short_interest.value"`).
106    ShortInterest,
107    /// Short percentage of float (`"short_percentage_of_float.value"`).
108    ShortPctFloat,
109    /// Days to cover short (`"days_to_cover_short.value"`).
110    DaysToCover,
111    /// Short interest percent change (`"short_interest_percentage_change.value"`).
112    ShortInterestPctChange,
113
114    // Valuation
115    /// Book value per share (`"bookvalueshare.lasttwelvemonths"`).
116    BookValueShare,
117    /// Market cap to revenue (`"lastclosemarketcaptotalrevenue.lasttwelvemonths"`).
118    MarketCapToRevenue,
119    /// TEV to revenue (`"lastclosetevtotalrevenue.lasttwelvemonths"`).
120    TevToRevenue,
121    /// Price-to-book ratio (`"pricebookratio.quarterly"`).
122    PriceBookRatio,
123    /// Trailing twelve months P/E ratio (`"peratio.lasttwelvemonths"`).
124    PeRatio,
125    /// Price to tangible book value (`"lastclosepricetangiblebookvalue.lasttwelvemonths"`).
126    PriceTangibleBook,
127    /// Price to earnings (`"lastclosepriceearnings.lasttwelvemonths"`).
128    PriceEarnings,
129    /// 5-year PEG ratio (`"pegratio_5y"`).
130    PegRatio5Y,
131
132    // Profitability / Dividends
133    /// Consecutive years of dividend growth (`"consecutive_years_of_dividend_growth_count"`).
134    ConsecutiveDivYears,
135    /// Return on assets (`"returnonassets.lasttwelvemonths"`).
136    Roa,
137    /// Return on equity (`"returnonequity.lasttwelvemonths"`).
138    Roe,
139    /// Forward dividend per share (`"forward_dividend_per_share"`).
140    ForwardDivPerShare,
141    /// Forward dividend yield (`"forward_dividend_yield"`).
142    ForwardDivYield,
143    /// Return on total capital (`"returnontotalcapital.lasttwelvemonths"`).
144    ReturnOnCapital,
145
146    // Leverage
147    /// TEV / EBIT (`"lastclosetevebit.lasttwelvemonths"`).
148    TevEbit,
149    /// Net debt / EBITDA (`"netdebtebitda.lasttwelvemonths"`).
150    NetDebtEbitda,
151    /// Total debt / equity (`"totaldebtequity.lasttwelvemonths"`).
152    TotalDebtEquity,
153    /// Long-term debt / equity (`"ltdebtequity.lasttwelvemonths"`).
154    LtDebtEquity,
155    /// EBIT / interest expense (`"ebitinterestexpense.lasttwelvemonths"`).
156    EbitInterestExp,
157    /// EBITDA / interest expense (`"ebitdainterestexpense.lasttwelvemonths"`).
158    EbitdaInterestExp,
159    /// TEV / EBITDA (`"lastclosetevebitda.lasttwelvemonths"`).
160    TevEbitda,
161    /// Total debt / EBITDA (`"totaldebtebitda.lasttwelvemonths"`).
162    TotalDebtEbitda,
163
164    // Liquidity
165    /// Quick ratio (`"quickratio.lasttwelvemonths"`).
166    QuickRatio,
167    /// Altman Z-score (`"altmanzscoreusingtheaveragestockinformationforaperiod.lasttwelvemonths"`).
168    AltmanZScore,
169    /// Current ratio (`"currentratio.lasttwelvemonths"`).
170    CurrentRatio,
171    /// Operating cash flow to current liabilities (`"operatingcashflowtocurrentliabilities.lasttwelvemonths"`).
172    OcfToCurrentLiab,
173
174    // Income Statement
175    /// Total revenues (`"totalrevenues.lasttwelvemonths"`).
176    TotalRevenues,
177    /// Net income margin (`"netincomemargin.lasttwelvemonths"`).
178    NetIncomeMargin,
179    /// Gross profit (`"grossprofit.lasttwelvemonths"`).
180    GrossProfit,
181    /// EBITDA 1-year growth (`"ebitda1yrgrowth.lasttwelvemonths"`).
182    Ebitda1YrGrowth,
183    /// Diluted EPS from continuing operations (`"dilutedepscontinuingoperations.lasttwelvemonths"`).
184    DilutedEpsContOps,
185    /// Quarterly revenue growth (`"quarterlyrevenuegrowth.quarterly"`).
186    QuarterlyRevGrowth,
187    /// EPS growth (`"epsgrowth.lasttwelvemonths"`).
188    EpsGrowth,
189    /// Net income (`"netincomeis.lasttwelvemonths"`).
190    NetIncome,
191    /// EBITDA (`"ebitda.lasttwelvemonths"`).
192    Ebitda,
193    /// Diluted EPS 1-year growth (`"dilutedeps1yrgrowth.lasttwelvemonths"`).
194    DilutedEps1YrGrowth,
195    /// Revenue 1-year growth (`"totalrevenues1yrgrowth.lasttwelvemonths"`).
196    Revenue1YrGrowth,
197    /// Operating income (`"operatingincome.lasttwelvemonths"`).
198    OperatingIncome,
199    /// Net income 1-year growth (`"netincome1yrgrowth.lasttwelvemonths"`).
200    NetIncome1YrGrowth,
201    /// Gross profit margin (`"grossprofitmargin.lasttwelvemonths"`).
202    GrossProfitMargin,
203    /// EBITDA margin (`"ebitdamargin.lasttwelvemonths"`).
204    EbitdaMargin,
205    /// EBIT (`"ebit.lasttwelvemonths"`).
206    Ebit,
207    /// Basic EPS from continuing operations (`"basicepscontinuingoperations.lasttwelvemonths"`).
208    BasicEpsContOps,
209    /// Basic EPS (`"netepsbasic.lasttwelvemonths"`).
210    NetEpsBasic,
211    /// Diluted EPS (`"netepsdiluted.lasttwelvemonths"`).
212    NetEpsDiluted,
213
214    // Balance Sheet
215    /// Total assets (`"totalassets.lasttwelvemonths"`).
216    TotalAssets,
217    /// Common shares outstanding (`"totalcommonsharesoutstanding.lasttwelvemonths"`).
218    CommonSharesOut,
219    /// Total debt (`"totaldebt.lasttwelvemonths"`).
220    TotalDebt,
221    /// Total equity (`"totalequity.lasttwelvemonths"`).
222    TotalEquity,
223    /// Total current assets (`"totalcurrentassets.lasttwelvemonths"`).
224    TotalCurrentAssets,
225    /// Cash and short-term investments (`"totalcashandshortterminvestments.lasttwelvemonths"`).
226    CashAndStInvestments,
227    /// Total common equity (`"totalcommonequity.lasttwelvemonths"`).
228    TotalCommonEquity,
229    /// Total current liabilities (`"totalcurrentliabilities.lasttwelvemonths"`).
230    TotalCurrentLiab,
231    /// Total shares outstanding (`"totalsharesoutstanding"`).
232    TotalSharesOut,
233
234    // Cash Flow
235    /// Levered free cash flow (`"leveredfreecashflow.lasttwelvemonths"`).
236    LeveredFcf,
237    /// Capital expenditure (`"capitalexpenditure.lasttwelvemonths"`).
238    Capex,
239    /// Cash from operations (`"cashfromoperations.lasttwelvemonths"`).
240    CashFromOps,
241    /// Levered FCF 1-year growth (`"leveredfreecashflow1yrgrowth.lasttwelvemonths"`).
242    LeveredFcf1YrGrowth,
243    /// Unlevered free cash flow (`"unleveredfreecashflow.lasttwelvemonths"`).
244    UnleveredFcf,
245    /// Cash from operations 1-year growth (`"cashfromoperations1yrgrowth.lasttwelvemonths"`).
246    CashFromOps1YrGrowth,
247
248    // ESG
249    /// ESG score (`"esg_score"`).
250    EsgScore,
251    /// Environmental score (`"environmental_score"`).
252    EnvironmentalScore,
253    /// Governance score (`"governance_score"`).
254    GovernanceScore,
255    /// Social score (`"social_score"`).
256    SocialScore,
257    /// Highest controversy level (`"highest_controversy"`).
258    HighestControversy,
259}
260
261impl EquityField {
262    /// Returns the Yahoo Finance API field name string for this variant.
263    pub fn as_str(&self) -> &'static str {
264        match self {
265            EquityField::Ticker => "ticker",
266            EquityField::CompanyShortName => "companyshortname",
267            EquityField::EodPrice => "eodprice",
268            EquityField::IntradayPrice => "intradayprice",
269            EquityField::IntradayPriceChange => "intradaypricechange",
270            EquityField::PercentChange => "percentchange",
271            EquityField::Lastclose52WkHigh => "lastclose52weekhigh.lasttwelvemonths",
272            EquityField::FiftyTwoWkPctChange => "fiftytwowkpercentchange",
273            EquityField::Lastclose52WkLow => "lastclose52weeklow.lasttwelvemonths",
274            EquityField::IntradayMarketCap => "intradaymarketcap",
275            EquityField::LastcloseMarketCap => "lastclosemarketcap.lasttwelvemonths",
276            EquityField::Region => "region",
277            EquityField::Sector => "sector",
278            EquityField::PeerGroup => "peer_group",
279            EquityField::Industry => "industry",
280            EquityField::Exchange => "exchange",
281            EquityField::Beta => "beta",
282            EquityField::AvgDailyVol3M => "avgdailyvol3m",
283            EquityField::PctHeldInsider => "pctheldinsider",
284            EquityField::PctHeldInst => "pctheldinst",
285            EquityField::DayVolume => "dayvolume",
286            EquityField::EodVolume => "eodvolume",
287            EquityField::ShortPctSharesOut => "short_percentage_of_shares_outstanding.value",
288            EquityField::ShortInterest => "short_interest.value",
289            EquityField::ShortPctFloat => "short_percentage_of_float.value",
290            EquityField::DaysToCover => "days_to_cover_short.value",
291            EquityField::ShortInterestPctChange => "short_interest_percentage_change.value",
292            EquityField::BookValueShare => "bookvalueshare.lasttwelvemonths",
293            EquityField::MarketCapToRevenue => "lastclosemarketcaptotalrevenue.lasttwelvemonths",
294            EquityField::TevToRevenue => "lastclosetevtotalrevenue.lasttwelvemonths",
295            EquityField::PriceBookRatio => "pricebookratio.quarterly",
296            EquityField::PeRatio => "peratio.lasttwelvemonths",
297            EquityField::PriceTangibleBook => "lastclosepricetangiblebookvalue.lasttwelvemonths",
298            EquityField::PriceEarnings => "lastclosepriceearnings.lasttwelvemonths",
299            EquityField::PegRatio5Y => "pegratio_5y",
300            EquityField::ConsecutiveDivYears => "consecutive_years_of_dividend_growth_count",
301            EquityField::Roa => "returnonassets.lasttwelvemonths",
302            EquityField::Roe => "returnonequity.lasttwelvemonths",
303            EquityField::ForwardDivPerShare => "forward_dividend_per_share",
304            EquityField::ForwardDivYield => "forward_dividend_yield",
305            EquityField::ReturnOnCapital => "returnontotalcapital.lasttwelvemonths",
306            EquityField::TevEbit => "lastclosetevebit.lasttwelvemonths",
307            EquityField::NetDebtEbitda => "netdebtebitda.lasttwelvemonths",
308            EquityField::TotalDebtEquity => "totaldebtequity.lasttwelvemonths",
309            EquityField::LtDebtEquity => "ltdebtequity.lasttwelvemonths",
310            EquityField::EbitInterestExp => "ebitinterestexpense.lasttwelvemonths",
311            EquityField::EbitdaInterestExp => "ebitdainterestexpense.lasttwelvemonths",
312            EquityField::TevEbitda => "lastclosetevebitda.lasttwelvemonths",
313            EquityField::TotalDebtEbitda => "totaldebtebitda.lasttwelvemonths",
314            EquityField::QuickRatio => "quickratio.lasttwelvemonths",
315            EquityField::AltmanZScore => {
316                "altmanzscoreusingtheaveragestockinformationforaperiod.lasttwelvemonths"
317            }
318            EquityField::CurrentRatio => "currentratio.lasttwelvemonths",
319            EquityField::OcfToCurrentLiab => {
320                "operatingcashflowtocurrentliabilities.lasttwelvemonths"
321            }
322            EquityField::TotalRevenues => "totalrevenues.lasttwelvemonths",
323            EquityField::NetIncomeMargin => "netincomemargin.lasttwelvemonths",
324            EquityField::GrossProfit => "grossprofit.lasttwelvemonths",
325            EquityField::Ebitda1YrGrowth => "ebitda1yrgrowth.lasttwelvemonths",
326            EquityField::DilutedEpsContOps => "dilutedepscontinuingoperations.lasttwelvemonths",
327            EquityField::QuarterlyRevGrowth => "quarterlyrevenuegrowth.quarterly",
328            EquityField::EpsGrowth => "epsgrowth.lasttwelvemonths",
329            EquityField::NetIncome => "netincomeis.lasttwelvemonths",
330            EquityField::Ebitda => "ebitda.lasttwelvemonths",
331            EquityField::DilutedEps1YrGrowth => "dilutedeps1yrgrowth.lasttwelvemonths",
332            EquityField::Revenue1YrGrowth => "totalrevenues1yrgrowth.lasttwelvemonths",
333            EquityField::OperatingIncome => "operatingincome.lasttwelvemonths",
334            EquityField::NetIncome1YrGrowth => "netincome1yrgrowth.lasttwelvemonths",
335            EquityField::GrossProfitMargin => "grossprofitmargin.lasttwelvemonths",
336            EquityField::EbitdaMargin => "ebitdamargin.lasttwelvemonths",
337            EquityField::Ebit => "ebit.lasttwelvemonths",
338            EquityField::BasicEpsContOps => "basicepscontinuingoperations.lasttwelvemonths",
339            EquityField::NetEpsBasic => "netepsbasic.lasttwelvemonths",
340            EquityField::NetEpsDiluted => "netepsdiluted.lasttwelvemonths",
341            EquityField::TotalAssets => "totalassets.lasttwelvemonths",
342            EquityField::CommonSharesOut => "totalcommonsharesoutstanding.lasttwelvemonths",
343            EquityField::TotalDebt => "totaldebt.lasttwelvemonths",
344            EquityField::TotalEquity => "totalequity.lasttwelvemonths",
345            EquityField::TotalCurrentAssets => "totalcurrentassets.lasttwelvemonths",
346            EquityField::CashAndStInvestments => {
347                "totalcashandshortterminvestments.lasttwelvemonths"
348            }
349            EquityField::TotalCommonEquity => "totalcommonequity.lasttwelvemonths",
350            EquityField::TotalCurrentLiab => "totalcurrentliabilities.lasttwelvemonths",
351            EquityField::TotalSharesOut => "totalsharesoutstanding",
352            EquityField::LeveredFcf => "leveredfreecashflow.lasttwelvemonths",
353            EquityField::Capex => "capitalexpenditure.lasttwelvemonths",
354            EquityField::CashFromOps => "cashfromoperations.lasttwelvemonths",
355            EquityField::LeveredFcf1YrGrowth => "leveredfreecashflow1yrgrowth.lasttwelvemonths",
356            EquityField::UnleveredFcf => "unleveredfreecashflow.lasttwelvemonths",
357            EquityField::CashFromOps1YrGrowth => "cashfromoperations1yrgrowth.lasttwelvemonths",
358            EquityField::EsgScore => "esg_score",
359            EquityField::EnvironmentalScore => "environmental_score",
360            EquityField::GovernanceScore => "governance_score",
361            EquityField::SocialScore => "social_score",
362            EquityField::HighestControversy => "highest_controversy",
363        }
364    }
365
366    /// Returns a slice of all `EquityField` variants.
367    ///
368    /// Useful for validation: `EquityField::all().iter().find(|f| f.as_str() == s)`.
369    pub fn all() -> &'static [EquityField] {
370        &[
371            EquityField::Ticker,
372            EquityField::CompanyShortName,
373            EquityField::EodPrice,
374            EquityField::IntradayPrice,
375            EquityField::IntradayPriceChange,
376            EquityField::PercentChange,
377            EquityField::Lastclose52WkHigh,
378            EquityField::FiftyTwoWkPctChange,
379            EquityField::Lastclose52WkLow,
380            EquityField::IntradayMarketCap,
381            EquityField::LastcloseMarketCap,
382            EquityField::Region,
383            EquityField::Sector,
384            EquityField::PeerGroup,
385            EquityField::Industry,
386            EquityField::Exchange,
387            EquityField::Beta,
388            EquityField::AvgDailyVol3M,
389            EquityField::PctHeldInsider,
390            EquityField::PctHeldInst,
391            EquityField::DayVolume,
392            EquityField::EodVolume,
393            EquityField::ShortPctSharesOut,
394            EquityField::ShortInterest,
395            EquityField::ShortPctFloat,
396            EquityField::DaysToCover,
397            EquityField::ShortInterestPctChange,
398            EquityField::BookValueShare,
399            EquityField::MarketCapToRevenue,
400            EquityField::TevToRevenue,
401            EquityField::PriceBookRatio,
402            EquityField::PeRatio,
403            EquityField::PriceTangibleBook,
404            EquityField::PriceEarnings,
405            EquityField::PegRatio5Y,
406            EquityField::ConsecutiveDivYears,
407            EquityField::Roa,
408            EquityField::Roe,
409            EquityField::ForwardDivPerShare,
410            EquityField::ForwardDivYield,
411            EquityField::ReturnOnCapital,
412            EquityField::TevEbit,
413            EquityField::NetDebtEbitda,
414            EquityField::TotalDebtEquity,
415            EquityField::LtDebtEquity,
416            EquityField::EbitInterestExp,
417            EquityField::EbitdaInterestExp,
418            EquityField::TevEbitda,
419            EquityField::TotalDebtEbitda,
420            EquityField::QuickRatio,
421            EquityField::AltmanZScore,
422            EquityField::CurrentRatio,
423            EquityField::OcfToCurrentLiab,
424            EquityField::TotalRevenues,
425            EquityField::NetIncomeMargin,
426            EquityField::GrossProfit,
427            EquityField::Ebitda1YrGrowth,
428            EquityField::DilutedEpsContOps,
429            EquityField::QuarterlyRevGrowth,
430            EquityField::EpsGrowth,
431            EquityField::NetIncome,
432            EquityField::Ebitda,
433            EquityField::DilutedEps1YrGrowth,
434            EquityField::Revenue1YrGrowth,
435            EquityField::OperatingIncome,
436            EquityField::NetIncome1YrGrowth,
437            EquityField::GrossProfitMargin,
438            EquityField::EbitdaMargin,
439            EquityField::Ebit,
440            EquityField::BasicEpsContOps,
441            EquityField::NetEpsBasic,
442            EquityField::NetEpsDiluted,
443            EquityField::TotalAssets,
444            EquityField::CommonSharesOut,
445            EquityField::TotalDebt,
446            EquityField::TotalEquity,
447            EquityField::TotalCurrentAssets,
448            EquityField::CashAndStInvestments,
449            EquityField::TotalCommonEquity,
450            EquityField::TotalCurrentLiab,
451            EquityField::TotalSharesOut,
452            EquityField::LeveredFcf,
453            EquityField::Capex,
454            EquityField::CashFromOps,
455            EquityField::LeveredFcf1YrGrowth,
456            EquityField::UnleveredFcf,
457            EquityField::CashFromOps1YrGrowth,
458            EquityField::EsgScore,
459            EquityField::EnvironmentalScore,
460            EquityField::GovernanceScore,
461            EquityField::SocialScore,
462            EquityField::HighestControversy,
463        ]
464    }
465}
466
467impl std::str::FromStr for EquityField {
468    type Err = ();
469
470    fn from_str(s: &str) -> Result<Self, Self::Err> {
471        EquityField::all()
472            .iter()
473            .find(|f| f.as_str() == s)
474            .copied()
475            .ok_or(())
476    }
477}
478
479impl Serialize for EquityField {
480    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
481        serializer.serialize_str(self.as_str())
482    }
483}
484
485impl ScreenerField for EquityField {
486    fn as_str(&self) -> &'static str {
487        self.as_str()
488    }
489}
490
491// ============================================================================
492// FundField
493// ============================================================================
494
495/// Typed field names for mutual fund custom screener queries.
496///
497/// Use with [`FundScreenerQuery`](super::query::FundScreenerQuery) and
498/// [`ScreenerFieldExt`](super::condition::ScreenerFieldExt).
499///
500/// # Example
501///
502/// ```
503/// use finance_query::{FundField, FundScreenerQuery, ScreenerFieldExt};
504///
505/// let query = FundScreenerQuery::new()
506///     .sort_by(FundField::PerformanceRating, false)
507///     .add_condition(FundField::RiskRating.lte(3.0))
508///     .include_fields(vec![
509///         FundField::Ticker,
510///         FundField::CompanyShortName,
511///         FundField::IntradayPrice,
512///         FundField::PerformanceRating,
513///     ]);
514/// ```
515#[non_exhaustive]
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
517pub enum FundField {
518    // Display-only fields (for include_fields)
519    /// Ticker symbol — display only, use in `include_fields`.
520    Ticker,
521    /// Short company name — display only, use in `include_fields`.
522    CompanyShortName,
523
524    // Price fields
525    /// End-of-day price (`"eodprice"`).
526    EodPrice,
527    /// Intraday price change (`"intradaypricechange"`).
528    IntradayPriceChange,
529    /// Intraday price (`"intradayprice"`).
530    IntradayPrice,
531
532    // Fund-specific fields
533    /// Fund category name (`"categoryname"`).
534    CategoryName,
535    /// Overall performance rating (`"performanceratingoverall"`).
536    PerformanceRating,
537    /// Minimum initial investment (`"initialinvestment"`).
538    InitialInvestment,
539    /// Annual return rank within category (`"annualreturnnavy1categoryrank"`).
540    AnnualReturnRank,
541    /// Overall risk rating (`"riskratingoverall"`).
542    RiskRating,
543    /// Exchange (`"exchange"`).
544    Exchange,
545}
546
547impl FundField {
548    /// Returns the Yahoo Finance API field name string for this variant.
549    pub fn as_str(&self) -> &'static str {
550        match self {
551            FundField::Ticker => "ticker",
552            FundField::CompanyShortName => "companyshortname",
553            FundField::EodPrice => "eodprice",
554            FundField::IntradayPriceChange => "intradaypricechange",
555            FundField::IntradayPrice => "intradayprice",
556            FundField::CategoryName => "categoryname",
557            FundField::PerformanceRating => "performanceratingoverall",
558            FundField::InitialInvestment => "initialinvestment",
559            FundField::AnnualReturnRank => "annualreturnnavy1categoryrank",
560            FundField::RiskRating => "riskratingoverall",
561            FundField::Exchange => "exchange",
562        }
563    }
564
565    /// Returns a slice of all `FundField` variants.
566    pub fn all() -> &'static [FundField] {
567        &[
568            FundField::Ticker,
569            FundField::CompanyShortName,
570            FundField::EodPrice,
571            FundField::IntradayPriceChange,
572            FundField::IntradayPrice,
573            FundField::CategoryName,
574            FundField::PerformanceRating,
575            FundField::InitialInvestment,
576            FundField::AnnualReturnRank,
577            FundField::RiskRating,
578            FundField::Exchange,
579        ]
580    }
581}
582
583impl std::str::FromStr for FundField {
584    type Err = ();
585
586    fn from_str(s: &str) -> Result<Self, Self::Err> {
587        FundField::all()
588            .iter()
589            .find(|f| f.as_str() == s)
590            .copied()
591            .ok_or(())
592    }
593}
594
595impl Serialize for FundField {
596    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
597        serializer.serialize_str(self.as_str())
598    }
599}
600
601impl ScreenerField for FundField {
602    fn as_str(&self) -> &'static str {
603        self.as_str()
604    }
605}
606
607// ============================================================================
608// Tests
609// ============================================================================
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614
615    #[test]
616    fn test_equity_field_as_str() {
617        assert_eq!(EquityField::PeRatio.as_str(), "peratio.lasttwelvemonths");
618        assert_eq!(EquityField::AvgDailyVol3M.as_str(), "avgdailyvol3m");
619        assert_eq!(EquityField::Region.as_str(), "region");
620        assert_eq!(EquityField::IntradayMarketCap.as_str(), "intradaymarketcap");
621        assert_eq!(EquityField::Ticker.as_str(), "ticker");
622    }
623
624    #[test]
625    fn test_equity_field_from_str_round_trip() {
626        for field in EquityField::all() {
627            let s = field.as_str();
628            let parsed: EquityField = s.parse().unwrap_or_else(|_| {
629                panic!("Failed to parse EquityField from '{s}'");
630            });
631            assert_eq!(&parsed, field, "Round-trip failed for field '{s}'");
632        }
633    }
634
635    #[test]
636    fn test_equity_field_from_str_unknown_returns_err() {
637        assert!("invalid_field_name".parse::<EquityField>().is_err());
638        assert!("".parse::<EquityField>().is_err());
639    }
640
641    #[test]
642    fn test_equity_field_serializes_as_string() {
643        let json = serde_json::to_value(EquityField::PeRatio).unwrap();
644        assert_eq!(json, "peratio.lasttwelvemonths");
645    }
646
647    #[test]
648    fn test_fund_field_as_str() {
649        assert_eq!(
650            FundField::PerformanceRating.as_str(),
651            "performanceratingoverall"
652        );
653        assert_eq!(FundField::RiskRating.as_str(), "riskratingoverall");
654        assert_eq!(FundField::CategoryName.as_str(), "categoryname");
655    }
656
657    #[test]
658    fn test_fund_field_from_str_round_trip() {
659        for field in FundField::all() {
660            let s = field.as_str();
661            let parsed: FundField = s.parse().unwrap_or_else(|_| {
662                panic!("Failed to parse FundField from '{s}'");
663            });
664            assert_eq!(&parsed, field, "Round-trip failed for fund field '{s}'");
665        }
666    }
667
668    #[test]
669    fn test_fund_field_from_str_unknown_returns_err() {
670        assert!("intradaymarketcap".parse::<FundField>().is_err());
671    }
672}