finance_query/
constants.rs

1/// Predefined screener types for Yahoo Finance
2pub mod screener_types {
3    /// Enum of all predefined Yahoo Finance screeners
4    ///
5    /// These map to Yahoo Finance's predefined screener IDs and can be used
6    /// to fetch filtered stock/fund lists based on various criteria.
7    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8    pub enum ScreenerType {
9        // Equity screeners
10        /// Small caps with high EPS growth, sorted by volume
11        AggressiveSmallCaps,
12        /// Top gaining stocks (>3% change, >$2B market cap)
13        DayGainers,
14        /// Top losing stocks (<-2.5% change, >$2B market cap)
15        DayLosers,
16        /// Tech stocks with 25%+ revenue and EPS growth
17        GrowthTechnologyStocks,
18        /// Most actively traded stocks by volume
19        MostActives,
20        /// Stocks with highest short interest percentage
21        MostShortedStocks,
22        /// Small cap gainers (<$2B market cap)
23        SmallCapGainers,
24        /// Low P/E (<20), low PEG (<1), high EPS growth (25%+)
25        UndervaluedGrowthStocks,
26        /// Large caps ($10B-$100B) with low P/E and PEG
27        UndervaluedLargeCaps,
28        // Fund screeners
29        /// Low-risk foreign large cap funds (4-5 star rated)
30        ConservativeForeignFunds,
31        /// High yield bond funds (4-5 star rated)
32        HighYieldBond,
33        /// Large blend core funds (4-5 star rated)
34        PortfolioAnchors,
35        /// Large growth funds (4-5 star rated)
36        SolidLargeGrowthFunds,
37        /// Mid-cap growth funds (4-5 star rated)
38        SolidMidcapGrowthFunds,
39        /// Top performing mutual funds by percent change
40        TopMutualFunds,
41    }
42
43    impl ScreenerType {
44        /// Convert to Yahoo Finance scrId parameter value (SCREAMING_SNAKE_CASE)
45        pub fn as_scr_id(&self) -> &'static str {
46            match self {
47                ScreenerType::AggressiveSmallCaps => "aggressive_small_caps",
48                ScreenerType::DayGainers => "day_gainers",
49                ScreenerType::DayLosers => "day_losers",
50                ScreenerType::GrowthTechnologyStocks => "growth_technology_stocks",
51                ScreenerType::MostActives => "most_actives",
52                ScreenerType::MostShortedStocks => "most_shorted_stocks",
53                ScreenerType::SmallCapGainers => "small_cap_gainers",
54                ScreenerType::UndervaluedGrowthStocks => "undervalued_growth_stocks",
55                ScreenerType::UndervaluedLargeCaps => "undervalued_large_caps",
56                ScreenerType::ConservativeForeignFunds => "conservative_foreign_funds",
57                ScreenerType::HighYieldBond => "high_yield_bond",
58                ScreenerType::PortfolioAnchors => "portfolio_anchors",
59                ScreenerType::SolidLargeGrowthFunds => "solid_large_growth_funds",
60                ScreenerType::SolidMidcapGrowthFunds => "solid_midcap_growth_funds",
61                ScreenerType::TopMutualFunds => "top_mutual_funds",
62            }
63        }
64
65        /// Parse from string, returns None on invalid input
66        ///
67        /// # Example
68        /// ```
69        /// use finance_query::ScreenerType;
70        ///
71        /// assert_eq!(ScreenerType::parse("most-actives"), Some(ScreenerType::MostActives));
72        /// assert_eq!(ScreenerType::parse("day-gainers"), Some(ScreenerType::DayGainers));
73        /// ```
74        pub fn parse(s: &str) -> Option<Self> {
75            s.parse().ok()
76        }
77
78        /// List all valid screener types for error messages
79        pub fn valid_types() -> &'static str {
80            "aggressive-small-caps, day-gainers, day-losers, growth-technology-stocks, \
81             most-actives, most-shorted-stocks, small-cap-gainers, undervalued-growth-stocks, \
82             undervalued-large-caps, conservative-foreign-funds, high-yield-bond, \
83             portfolio-anchors, solid-large-growth-funds, solid-midcap-growth-funds, \
84             top-mutual-funds"
85        }
86
87        /// Get all screener types as an array
88        pub fn all() -> &'static [ScreenerType] {
89            &[
90                ScreenerType::AggressiveSmallCaps,
91                ScreenerType::DayGainers,
92                ScreenerType::DayLosers,
93                ScreenerType::GrowthTechnologyStocks,
94                ScreenerType::MostActives,
95                ScreenerType::MostShortedStocks,
96                ScreenerType::SmallCapGainers,
97                ScreenerType::UndervaluedGrowthStocks,
98                ScreenerType::UndervaluedLargeCaps,
99                ScreenerType::ConservativeForeignFunds,
100                ScreenerType::HighYieldBond,
101                ScreenerType::PortfolioAnchors,
102                ScreenerType::SolidLargeGrowthFunds,
103                ScreenerType::SolidMidcapGrowthFunds,
104                ScreenerType::TopMutualFunds,
105            ]
106        }
107    }
108
109    impl std::str::FromStr for ScreenerType {
110        type Err = ();
111
112        fn from_str(s: &str) -> Result<Self, Self::Err> {
113            match s.to_lowercase().replace('_', "-").as_str() {
114                "aggressive-small-caps" => Ok(ScreenerType::AggressiveSmallCaps),
115                "day-gainers" | "gainers" => Ok(ScreenerType::DayGainers),
116                "day-losers" | "losers" => Ok(ScreenerType::DayLosers),
117                "growth-technology-stocks" | "growth-tech" => {
118                    Ok(ScreenerType::GrowthTechnologyStocks)
119                }
120                "most-actives" | "actives" => Ok(ScreenerType::MostActives),
121                "most-shorted-stocks" | "most-shorted" => Ok(ScreenerType::MostShortedStocks),
122                "small-cap-gainers" => Ok(ScreenerType::SmallCapGainers),
123                "undervalued-growth-stocks" | "undervalued-growth" => {
124                    Ok(ScreenerType::UndervaluedGrowthStocks)
125                }
126                "undervalued-large-caps" | "undervalued-large" => {
127                    Ok(ScreenerType::UndervaluedLargeCaps)
128                }
129                "conservative-foreign-funds" => Ok(ScreenerType::ConservativeForeignFunds),
130                "high-yield-bond" => Ok(ScreenerType::HighYieldBond),
131                "portfolio-anchors" => Ok(ScreenerType::PortfolioAnchors),
132                "solid-large-growth-funds" => Ok(ScreenerType::SolidLargeGrowthFunds),
133                "solid-midcap-growth-funds" => Ok(ScreenerType::SolidMidcapGrowthFunds),
134                "top-mutual-funds" => Ok(ScreenerType::TopMutualFunds),
135                _ => Err(()),
136            }
137        }
138    }
139}
140
141/// Yahoo Finance sector types
142///
143/// These are the 11 GICS sectors available on Yahoo Finance.
144pub mod sector_types {
145    use serde::{Deserialize, Serialize};
146
147    /// Market sector types available on Yahoo Finance
148    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
149    #[serde(rename_all = "kebab-case")]
150    pub enum SectorType {
151        /// Technology sector (software, semiconductors, hardware)
152        Technology,
153        /// Financial Services sector (banks, insurance, asset management)
154        FinancialServices,
155        /// Consumer Cyclical sector (retail, automotive, leisure)
156        ConsumerCyclical,
157        /// Communication Services sector (telecom, media, entertainment)
158        CommunicationServices,
159        /// Healthcare sector (pharma, biotech, medical devices)
160        Healthcare,
161        /// Industrials sector (aerospace, machinery, construction)
162        Industrials,
163        /// Consumer Defensive sector (food, beverages, household products)
164        ConsumerDefensive,
165        /// Energy sector (oil, gas, renewable energy)
166        Energy,
167        /// Basic Materials sector (chemicals, metals, mining)
168        BasicMaterials,
169        /// Real Estate sector (REITs, property management)
170        RealEstate,
171        /// Utilities sector (electric, gas, water utilities)
172        Utilities,
173    }
174
175    impl SectorType {
176        /// Convert to Yahoo Finance API path segment (lowercase with hyphens)
177        pub fn as_api_path(&self) -> &'static str {
178            match self {
179                SectorType::Technology => "technology",
180                SectorType::FinancialServices => "financial-services",
181                SectorType::ConsumerCyclical => "consumer-cyclical",
182                SectorType::CommunicationServices => "communication-services",
183                SectorType::Healthcare => "healthcare",
184                SectorType::Industrials => "industrials",
185                SectorType::ConsumerDefensive => "consumer-defensive",
186                SectorType::Energy => "energy",
187                SectorType::BasicMaterials => "basic-materials",
188                SectorType::RealEstate => "real-estate",
189                SectorType::Utilities => "utilities",
190            }
191        }
192
193        /// Get human-readable display name
194        pub fn display_name(&self) -> &'static str {
195            match self {
196                SectorType::Technology => "Technology",
197                SectorType::FinancialServices => "Financial Services",
198                SectorType::ConsumerCyclical => "Consumer Cyclical",
199                SectorType::CommunicationServices => "Communication Services",
200                SectorType::Healthcare => "Healthcare",
201                SectorType::Industrials => "Industrials",
202                SectorType::ConsumerDefensive => "Consumer Defensive",
203                SectorType::Energy => "Energy",
204                SectorType::BasicMaterials => "Basic Materials",
205                SectorType::RealEstate => "Real Estate",
206                SectorType::Utilities => "Utilities",
207            }
208        }
209
210        /// List all valid sector types for error messages
211        pub fn valid_types() -> &'static str {
212            "technology, financial-services, consumer-cyclical, communication-services, \
213             healthcare, industrials, consumer-defensive, energy, basic-materials, \
214             real-estate, utilities"
215        }
216
217        /// Get all sector types as an array
218        pub fn all() -> &'static [SectorType] {
219            &[
220                SectorType::Technology,
221                SectorType::FinancialServices,
222                SectorType::ConsumerCyclical,
223                SectorType::CommunicationServices,
224                SectorType::Healthcare,
225                SectorType::Industrials,
226                SectorType::ConsumerDefensive,
227                SectorType::Energy,
228                SectorType::BasicMaterials,
229                SectorType::RealEstate,
230                SectorType::Utilities,
231            ]
232        }
233    }
234
235    impl std::str::FromStr for SectorType {
236        type Err = ();
237
238        fn from_str(s: &str) -> Result<Self, Self::Err> {
239            match s.to_lowercase().replace('_', "-").as_str() {
240                "technology" | "tech" => Ok(SectorType::Technology),
241                "financial-services" | "financials" | "financial" => {
242                    Ok(SectorType::FinancialServices)
243                }
244                "consumer-cyclical" => Ok(SectorType::ConsumerCyclical),
245                "communication-services" | "communication" => Ok(SectorType::CommunicationServices),
246                "healthcare" | "health" => Ok(SectorType::Healthcare),
247                "industrials" | "industrial" => Ok(SectorType::Industrials),
248                "consumer-defensive" => Ok(SectorType::ConsumerDefensive),
249                "energy" => Ok(SectorType::Energy),
250                "basic-materials" | "materials" => Ok(SectorType::BasicMaterials),
251                "real-estate" | "realestate" => Ok(SectorType::RealEstate),
252                "utilities" | "utility" => Ok(SectorType::Utilities),
253                _ => Err(()),
254            }
255        }
256    }
257
258    impl std::fmt::Display for SectorType {
259        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260            write!(f, "{}", self.display_name())
261        }
262    }
263}
264
265/// Custom screener query types and operators
266///
267/// Used to build custom screener queries with flexible filtering criteria.
268pub mod screener_query {
269    use serde::{Deserialize, Serialize};
270
271    /// Quote type for custom screeners
272    ///
273    /// Yahoo Finance only supports EQUITY and MUTUALFUND for custom screener queries.
274    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
275    #[serde(rename_all = "UPPERCASE")]
276    pub enum QuoteType {
277        /// Equity (stocks) - uses equity_fields for validation
278        #[default]
279        #[serde(rename = "EQUITY")]
280        Equity,
281        /// Mutual funds - uses fund_fields for validation
282        #[serde(rename = "MUTUALFUND")]
283        MutualFund,
284    }
285
286    impl QuoteType {
287        /// Get valid values for this quote type
288        pub fn valid_types() -> &'static str {
289            "equity, mutualfund"
290        }
291    }
292
293    impl std::str::FromStr for QuoteType {
294        type Err = ();
295
296        fn from_str(s: &str) -> Result<Self, Self::Err> {
297            match s.to_lowercase().replace(['-', '_'], "").as_str() {
298                "equity" | "stock" | "stocks" => Ok(QuoteType::Equity),
299                "mutualfund" | "fund" | "funds" => Ok(QuoteType::MutualFund),
300                _ => Err(()),
301            }
302        }
303    }
304
305    /// Sort direction for custom screener results
306    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
307    #[serde(rename_all = "UPPERCASE")]
308    pub enum SortType {
309        /// Sort ascending (smallest first)
310        #[serde(rename = "ASC")]
311        Asc,
312        /// Sort descending (largest first)
313        #[default]
314        #[serde(rename = "DESC")]
315        Desc,
316    }
317
318    impl std::str::FromStr for SortType {
319        type Err = ();
320
321        fn from_str(s: &str) -> Result<Self, Self::Err> {
322            match s.to_lowercase().as_str() {
323                "asc" | "ascending" => Ok(SortType::Asc),
324                "desc" | "descending" => Ok(SortType::Desc),
325                _ => Err(()),
326            }
327        }
328    }
329
330    /// Comparison operator for query conditions
331    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
332    #[serde(rename_all = "lowercase")]
333    pub enum Operator {
334        /// Equal to
335        #[serde(rename = "eq")]
336        Eq,
337        /// Greater than
338        #[serde(rename = "gt")]
339        Gt,
340        /// Greater than or equal to
341        #[serde(rename = "gte")]
342        Gte,
343        /// Less than
344        #[serde(rename = "lt")]
345        Lt,
346        /// Less than or equal to
347        #[serde(rename = "lte")]
348        Lte,
349        /// Between two values (inclusive)
350        #[serde(rename = "btwn")]
351        Between,
352    }
353
354    impl std::str::FromStr for Operator {
355        type Err = ();
356
357        fn from_str(s: &str) -> Result<Self, Self::Err> {
358            match s.to_lowercase().as_str() {
359                "eq" | "=" | "==" => Ok(Operator::Eq),
360                "gt" | ">" => Ok(Operator::Gt),
361                "gte" | ">=" => Ok(Operator::Gte),
362                "lt" | "<" => Ok(Operator::Lt),
363                "lte" | "<=" => Ok(Operator::Lte),
364                "btwn" | "between" => Ok(Operator::Between),
365                _ => Err(()),
366            }
367        }
368    }
369
370    /// Logical operator for combining conditions
371    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
372    #[serde(rename_all = "lowercase")]
373    pub enum LogicalOperator {
374        /// All conditions must match (AND)
375        #[default]
376        And,
377        /// Any condition can match (OR)
378        Or,
379    }
380
381    impl std::str::FromStr for LogicalOperator {
382        type Err = ();
383
384        fn from_str(s: &str) -> Result<Self, Self::Err> {
385            match s.to_lowercase().as_str() {
386                "and" | "&&" => Ok(LogicalOperator::And),
387                "or" | "||" => Ok(LogicalOperator::Or),
388                _ => Err(()),
389            }
390        }
391    }
392
393    /// Valid screener fields for equity queries
394    ///
395    /// Based on Yahoo Finance screener API. Fields grouped by category.
396    #[allow(missing_docs)]
397    pub mod equity_fields {
398        // Price fields
399        pub const EOD_PRICE: &str = "eodprice";
400        pub const INTRADAY_PRICE_CHANGE: &str = "intradaypricechange";
401        pub const INTRADAY_PRICE: &str = "intradayprice";
402        pub const PERCENT_CHANGE: &str = "percentchange";
403        pub const LASTCLOSE_52WK_HIGH: &str = "lastclose52weekhigh.lasttwelvemonths";
404        pub const FIFTY_TWO_WK_PCT_CHANGE: &str = "fiftytwowkpercentchange";
405        pub const LASTCLOSE_52WK_LOW: &str = "lastclose52weeklow.lasttwelvemonths";
406        pub const INTRADAY_MARKET_CAP: &str = "intradaymarketcap";
407        pub const LASTCLOSE_MARKET_CAP: &str = "lastclosemarketcap.lasttwelvemonths";
408
409        // Equality filter fields
410        pub const REGION: &str = "region";
411        pub const SECTOR: &str = "sector";
412        pub const PEER_GROUP: &str = "peer_group";
413        pub const INDUSTRY: &str = "industry";
414        pub const EXCHANGE: &str = "exchange";
415
416        // Trading fields
417        pub const BETA: &str = "beta";
418        pub const AVG_DAILY_VOL_3M: &str = "avgdailyvol3m";
419        pub const PCT_HELD_INSIDER: &str = "pctheldinsider";
420        pub const PCT_HELD_INST: &str = "pctheldinst";
421        pub const DAY_VOLUME: &str = "dayvolume";
422        pub const EOD_VOLUME: &str = "eodvolume";
423
424        // Short interest fields
425        pub const SHORT_PCT_SHARES_OUT: &str = "short_percentage_of_shares_outstanding.value";
426        pub const SHORT_INTEREST: &str = "short_interest.value";
427        pub const SHORT_PCT_FLOAT: &str = "short_percentage_of_float.value";
428        pub const DAYS_TO_COVER: &str = "days_to_cover_short.value";
429        pub const SHORT_INTEREST_PCT_CHANGE: &str = "short_interest_percentage_change.value";
430
431        // Valuation fields
432        pub const BOOK_VALUE_SHARE: &str = "bookvalueshare.lasttwelvemonths";
433        pub const MARKET_CAP_TO_REVENUE: &str = "lastclosemarketcaptotalrevenue.lasttwelvemonths";
434        pub const TEV_TO_REVENUE: &str = "lastclosetevtotalrevenue.lasttwelvemonths";
435        pub const PRICE_BOOK_RATIO: &str = "pricebookratio.quarterly";
436        pub const PE_RATIO: &str = "peratio.lasttwelvemonths";
437        pub const PRICE_TANGIBLE_BOOK: &str = "lastclosepricetangiblebookvalue.lasttwelvemonths";
438        pub const PRICE_EARNINGS: &str = "lastclosepriceearnings.lasttwelvemonths";
439        pub const PEG_RATIO_5Y: &str = "pegratio_5y";
440
441        // Profitability fields
442        pub const CONSECUTIVE_DIV_YEARS: &str = "consecutive_years_of_dividend_growth_count";
443        pub const ROA: &str = "returnonassets.lasttwelvemonths";
444        pub const ROE: &str = "returnonequity.lasttwelvemonths";
445        pub const FORWARD_DIV_PER_SHARE: &str = "forward_dividend_per_share";
446        pub const FORWARD_DIV_YIELD: &str = "forward_dividend_yield";
447        pub const RETURN_ON_CAPITAL: &str = "returnontotalcapital.lasttwelvemonths";
448
449        // Leverage fields
450        pub const TEV_EBIT: &str = "lastclosetevebit.lasttwelvemonths";
451        pub const NET_DEBT_EBITDA: &str = "netdebtebitda.lasttwelvemonths";
452        pub const TOTAL_DEBT_EQUITY: &str = "totaldebtequity.lasttwelvemonths";
453        pub const LT_DEBT_EQUITY: &str = "ltdebtequity.lasttwelvemonths";
454        pub const EBIT_INTEREST_EXP: &str = "ebitinterestexpense.lasttwelvemonths";
455        pub const EBITDA_INTEREST_EXP: &str = "ebitdainterestexpense.lasttwelvemonths";
456        pub const TEV_EBITDA: &str = "lastclosetevebitda.lasttwelvemonths";
457        pub const TOTAL_DEBT_EBITDA: &str = "totaldebtebitda.lasttwelvemonths";
458
459        // Liquidity fields
460        pub const QUICK_RATIO: &str = "quickratio.lasttwelvemonths";
461        pub const ALTMAN_Z_SCORE: &str =
462            "altmanzscoreusingtheaveragestockinformationforaperiod.lasttwelvemonths";
463        pub const CURRENT_RATIO: &str = "currentratio.lasttwelvemonths";
464        pub const OCF_TO_CURRENT_LIAB: &str =
465            "operatingcashflowtocurrentliabilities.lasttwelvemonths";
466
467        // Income statement fields
468        pub const TOTAL_REVENUES: &str = "totalrevenues.lasttwelvemonths";
469        pub const NET_INCOME_MARGIN: &str = "netincomemargin.lasttwelvemonths";
470        pub const GROSS_PROFIT: &str = "grossprofit.lasttwelvemonths";
471        pub const EBITDA_1YR_GROWTH: &str = "ebitda1yrgrowth.lasttwelvemonths";
472        pub const DILUTED_EPS_CONT_OPS: &str = "dilutedepscontinuingoperations.lasttwelvemonths";
473        pub const QUARTERLY_REV_GROWTH: &str = "quarterlyrevenuegrowth.quarterly";
474        pub const EPS_GROWTH: &str = "epsgrowth.lasttwelvemonths";
475        pub const NET_INCOME: &str = "netincomeis.lasttwelvemonths";
476        pub const EBITDA: &str = "ebitda.lasttwelvemonths";
477        pub const DILUTED_EPS_1YR_GROWTH: &str = "dilutedeps1yrgrowth.lasttwelvemonths";
478        pub const REVENUE_1YR_GROWTH: &str = "totalrevenues1yrgrowth.lasttwelvemonths";
479        pub const OPERATING_INCOME: &str = "operatingincome.lasttwelvemonths";
480        pub const NET_INCOME_1YR_GROWTH: &str = "netincome1yrgrowth.lasttwelvemonths";
481        pub const GROSS_PROFIT_MARGIN: &str = "grossprofitmargin.lasttwelvemonths";
482        pub const EBITDA_MARGIN: &str = "ebitdamargin.lasttwelvemonths";
483        pub const EBIT: &str = "ebit.lasttwelvemonths";
484        pub const BASIC_EPS_CONT_OPS: &str = "basicepscontinuingoperations.lasttwelvemonths";
485        pub const NET_EPS_BASIC: &str = "netepsbasic.lasttwelvemonths";
486        pub const NET_EPS_DILUTED: &str = "netepsdiluted.lasttwelvemonths";
487
488        // Balance sheet fields
489        pub const TOTAL_ASSETS: &str = "totalassets.lasttwelvemonths";
490        pub const COMMON_SHARES_OUT: &str = "totalcommonsharesoutstanding.lasttwelvemonths";
491        pub const TOTAL_DEBT: &str = "totaldebt.lasttwelvemonths";
492        pub const TOTAL_EQUITY: &str = "totalequity.lasttwelvemonths";
493        pub const TOTAL_CURRENT_ASSETS: &str = "totalcurrentassets.lasttwelvemonths";
494        pub const CASH_AND_ST_INVESTMENTS: &str =
495            "totalcashandshortterminvestments.lasttwelvemonths";
496        pub const TOTAL_COMMON_EQUITY: &str = "totalcommonequity.lasttwelvemonths";
497        pub const TOTAL_CURRENT_LIAB: &str = "totalcurrentliabilities.lasttwelvemonths";
498        pub const TOTAL_SHARES_OUT: &str = "totalsharesoutstanding";
499
500        // Cash flow fields
501        pub const LEVERED_FCF: &str = "leveredfreecashflow.lasttwelvemonths";
502        pub const CAPEX: &str = "capitalexpenditure.lasttwelvemonths";
503        pub const CASH_FROM_OPS: &str = "cashfromoperations.lasttwelvemonths";
504        pub const LEVERED_FCF_1YR_GROWTH: &str = "leveredfreecashflow1yrgrowth.lasttwelvemonths";
505        pub const UNLEVERED_FCF: &str = "unleveredfreecashflow.lasttwelvemonths";
506        pub const CASH_FROM_OPS_1YR_GROWTH: &str = "cashfromoperations1yrgrowth.lasttwelvemonths";
507
508        // ESG fields
509        pub const ESG_SCORE: &str = "esg_score";
510        pub const ENVIRONMENTAL_SCORE: &str = "environmental_score";
511        pub const GOVERNANCE_SCORE: &str = "governance_score";
512        pub const SOCIAL_SCORE: &str = "social_score";
513        pub const HIGHEST_CONTROVERSY: &str = "highest_controversy";
514    }
515
516    /// Valid screener fields for fund/mutual fund queries
517    #[allow(missing_docs)]
518    pub mod fund_fields {
519        // Common price fields (shared with equity)
520        pub const EOD_PRICE: &str = "eodprice";
521        pub const INTRADAY_PRICE_CHANGE: &str = "intradaypricechange";
522        pub const INTRADAY_PRICE: &str = "intradayprice";
523
524        // Fund-specific fields
525        pub const CATEGORY_NAME: &str = "categoryname";
526        pub const PERFORMANCE_RATING: &str = "performanceratingoverall";
527        pub const INITIAL_INVESTMENT: &str = "initialinvestment";
528        pub const ANNUAL_RETURN_RANK: &str = "annualreturnnavy1categoryrank";
529        pub const RISK_RATING: &str = "riskratingoverall";
530        pub const EXCHANGE: &str = "exchange";
531    }
532
533    /// All valid equity screener fields (for validation)
534    pub const VALID_EQUITY_FIELDS: &[&str] = &[
535        equity_fields::EOD_PRICE,
536        equity_fields::INTRADAY_PRICE_CHANGE,
537        equity_fields::INTRADAY_PRICE,
538        equity_fields::PERCENT_CHANGE,
539        equity_fields::LASTCLOSE_52WK_HIGH,
540        equity_fields::FIFTY_TWO_WK_PCT_CHANGE,
541        equity_fields::LASTCLOSE_52WK_LOW,
542        equity_fields::INTRADAY_MARKET_CAP,
543        equity_fields::LASTCLOSE_MARKET_CAP,
544        equity_fields::REGION,
545        equity_fields::SECTOR,
546        equity_fields::PEER_GROUP,
547        equity_fields::INDUSTRY,
548        equity_fields::EXCHANGE,
549        equity_fields::BETA,
550        equity_fields::AVG_DAILY_VOL_3M,
551        equity_fields::PCT_HELD_INSIDER,
552        equity_fields::PCT_HELD_INST,
553        equity_fields::DAY_VOLUME,
554        equity_fields::EOD_VOLUME,
555        equity_fields::SHORT_PCT_SHARES_OUT,
556        equity_fields::SHORT_INTEREST,
557        equity_fields::SHORT_PCT_FLOAT,
558        equity_fields::DAYS_TO_COVER,
559        equity_fields::SHORT_INTEREST_PCT_CHANGE,
560        equity_fields::BOOK_VALUE_SHARE,
561        equity_fields::MARKET_CAP_TO_REVENUE,
562        equity_fields::TEV_TO_REVENUE,
563        equity_fields::PRICE_BOOK_RATIO,
564        equity_fields::PE_RATIO,
565        equity_fields::PRICE_TANGIBLE_BOOK,
566        equity_fields::PRICE_EARNINGS,
567        equity_fields::PEG_RATIO_5Y,
568        equity_fields::CONSECUTIVE_DIV_YEARS,
569        equity_fields::ROA,
570        equity_fields::ROE,
571        equity_fields::FORWARD_DIV_PER_SHARE,
572        equity_fields::FORWARD_DIV_YIELD,
573        equity_fields::RETURN_ON_CAPITAL,
574        equity_fields::TEV_EBIT,
575        equity_fields::NET_DEBT_EBITDA,
576        equity_fields::TOTAL_DEBT_EQUITY,
577        equity_fields::LT_DEBT_EQUITY,
578        equity_fields::EBIT_INTEREST_EXP,
579        equity_fields::EBITDA_INTEREST_EXP,
580        equity_fields::TEV_EBITDA,
581        equity_fields::TOTAL_DEBT_EBITDA,
582        equity_fields::QUICK_RATIO,
583        equity_fields::ALTMAN_Z_SCORE,
584        equity_fields::CURRENT_RATIO,
585        equity_fields::OCF_TO_CURRENT_LIAB,
586        equity_fields::TOTAL_REVENUES,
587        equity_fields::NET_INCOME_MARGIN,
588        equity_fields::GROSS_PROFIT,
589        equity_fields::EBITDA_1YR_GROWTH,
590        equity_fields::DILUTED_EPS_CONT_OPS,
591        equity_fields::QUARTERLY_REV_GROWTH,
592        equity_fields::EPS_GROWTH,
593        equity_fields::NET_INCOME,
594        equity_fields::EBITDA,
595        equity_fields::DILUTED_EPS_1YR_GROWTH,
596        equity_fields::REVENUE_1YR_GROWTH,
597        equity_fields::OPERATING_INCOME,
598        equity_fields::NET_INCOME_1YR_GROWTH,
599        equity_fields::GROSS_PROFIT_MARGIN,
600        equity_fields::EBITDA_MARGIN,
601        equity_fields::EBIT,
602        equity_fields::BASIC_EPS_CONT_OPS,
603        equity_fields::NET_EPS_BASIC,
604        equity_fields::NET_EPS_DILUTED,
605        equity_fields::TOTAL_ASSETS,
606        equity_fields::COMMON_SHARES_OUT,
607        equity_fields::TOTAL_DEBT,
608        equity_fields::TOTAL_EQUITY,
609        equity_fields::TOTAL_CURRENT_ASSETS,
610        equity_fields::CASH_AND_ST_INVESTMENTS,
611        equity_fields::TOTAL_COMMON_EQUITY,
612        equity_fields::TOTAL_CURRENT_LIAB,
613        equity_fields::TOTAL_SHARES_OUT,
614        equity_fields::LEVERED_FCF,
615        equity_fields::CAPEX,
616        equity_fields::CASH_FROM_OPS,
617        equity_fields::LEVERED_FCF_1YR_GROWTH,
618        equity_fields::UNLEVERED_FCF,
619        equity_fields::CASH_FROM_OPS_1YR_GROWTH,
620        equity_fields::ESG_SCORE,
621        equity_fields::ENVIRONMENTAL_SCORE,
622        equity_fields::GOVERNANCE_SCORE,
623        equity_fields::SOCIAL_SCORE,
624        equity_fields::HIGHEST_CONTROVERSY,
625    ];
626
627    /// All valid fund screener fields (for validation)
628    pub const VALID_FUND_FIELDS: &[&str] = &[
629        fund_fields::EOD_PRICE,
630        fund_fields::INTRADAY_PRICE_CHANGE,
631        fund_fields::INTRADAY_PRICE,
632        fund_fields::CATEGORY_NAME,
633        fund_fields::PERFORMANCE_RATING,
634        fund_fields::INITIAL_INVESTMENT,
635        fund_fields::ANNUAL_RETURN_RANK,
636        fund_fields::RISK_RATING,
637        fund_fields::EXCHANGE,
638    ];
639
640    /// Check if a field is valid for equity screeners
641    pub fn is_valid_equity_field(field: &str) -> bool {
642        VALID_EQUITY_FIELDS.contains(&field)
643    }
644
645    /// Check if a field is valid for fund screeners
646    pub fn is_valid_fund_field(field: &str) -> bool {
647        VALID_FUND_FIELDS.contains(&field)
648    }
649
650    /// Check if a field is valid for the given quote type
651    pub fn is_valid_field(field: &str, quote_type: QuoteType) -> bool {
652        match quote_type {
653            QuoteType::Equity => is_valid_equity_field(field),
654            QuoteType::MutualFund => is_valid_fund_field(field),
655        }
656    }
657}
658
659/// World market indices
660pub mod indices {
661    /// Region categories for world indices
662    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
663    pub enum Region {
664        /// North and South America
665        Americas,
666        /// European markets
667        Europe,
668        /// Asia and Pacific markets
669        AsiaPacific,
670        /// Middle East and Africa
671        MiddleEastAfrica,
672        /// Currency indices
673        Currencies,
674    }
675
676    impl std::str::FromStr for Region {
677        type Err = ();
678
679        fn from_str(s: &str) -> Result<Self, Self::Err> {
680            match s.to_lowercase().replace(['-', '_'], "").as_str() {
681                "americas" | "america" => Ok(Region::Americas),
682                "europe" | "eu" => Ok(Region::Europe),
683                "asiapacific" | "asia" | "apac" => Ok(Region::AsiaPacific),
684                "middleeastafrica" | "mea" | "emea" => Ok(Region::MiddleEastAfrica),
685                "currencies" | "currency" | "fx" => Ok(Region::Currencies),
686                _ => Err(()),
687            }
688        }
689    }
690
691    impl Region {
692        /// Parse from string, returns None on invalid input
693        pub fn parse(s: &str) -> Option<Self> {
694            s.parse().ok()
695        }
696
697        /// Get the symbols for this region
698        pub fn symbols(&self) -> &'static [&'static str] {
699            match self {
700                Region::Americas => AMERICAS,
701                Region::Europe => EUROPE,
702                Region::AsiaPacific => ASIA_PACIFIC,
703                Region::MiddleEastAfrica => MIDDLE_EAST_AFRICA,
704                Region::Currencies => CURRENCIES,
705            }
706        }
707
708        /// Convert to string representation
709        pub fn as_str(&self) -> &'static str {
710            match self {
711                Region::Americas => "americas",
712                Region::Europe => "europe",
713                Region::AsiaPacific => "asia-pacific",
714                Region::MiddleEastAfrica => "middle-east-africa",
715                Region::Currencies => "currencies",
716            }
717        }
718
719        /// All region variants
720        pub fn all() -> &'static [Region] {
721            &[
722                Region::Americas,
723                Region::Europe,
724                Region::AsiaPacific,
725                Region::MiddleEastAfrica,
726                Region::Currencies,
727            ]
728        }
729    }
730
731    /// Americas indices
732    pub const AMERICAS: &[&str] = &[
733        "^GSPC",   // S&P 500
734        "^DJI",    // Dow Jones Industrial Average
735        "^IXIC",   // NASDAQ Composite
736        "^NYA",    // NYSE Composite Index
737        "^XAX",    // NYSE American Composite Index
738        "^RUT",    // Russell 2000 Index
739        "^VIX",    // CBOE Volatility Index
740        "^GSPTSE", // S&P/TSX Composite (Canada)
741        "^BVSP",   // IBOVESPA (Brazil)
742        "^MXX",    // IPC MEXICO
743        "^IPSA",   // S&P IPSA (Chile)
744        "^MERV",   // MERVAL (Argentina)
745    ];
746
747    /// Europe indices
748    pub const EUROPE: &[&str] = &[
749        "^FTSE",            // FTSE 100 (UK)
750        "^GDAXI",           // DAX (Germany)
751        "^FCHI",            // CAC 40 (France)
752        "^STOXX50E",        // EURO STOXX 50
753        "^N100",            // Euronext 100 Index
754        "^BFX",             // BEL 20 (Belgium)
755        "^BUK100P",         // Cboe UK 100
756        "MOEX.ME",          // Moscow Exchange
757        "^125904-USD-STRD", // MSCI EUROPE
758    ];
759
760    /// Asia Pacific indices
761    pub const ASIA_PACIFIC: &[&str] = &[
762        "^N225",     // Nikkei 225 (Japan)
763        "^HSI",      // Hang Seng Index (Hong Kong)
764        "000001.SS", // SSE Composite Index (China)
765        "^KS11",     // KOSPI (South Korea)
766        "^TWII",     // Taiwan Weighted Index
767        "^STI",      // STI Index (Singapore)
768        "^AXJO",     // S&P/ASX 200 (Australia)
769        "^AORD",     // All Ordinaries (Australia)
770        "^NZ50",     // S&P/NZX 50 (New Zealand)
771        "^BSESN",    // S&P BSE SENSEX (India)
772        "^JKSE",     // IDX Composite (Indonesia)
773        "^KLSE",     // FTSE Bursa Malaysia KLCI
774    ];
775
776    /// Middle East & Africa indices
777    pub const MIDDLE_EAST_AFRICA: &[&str] = &[
778        "^TA125.TA", // TA-125 (Israel)
779        "^CASE30",   // EGX 30 (Egypt)
780        "^JN0U.JO",  // Top 40 USD Net TRI (South Africa)
781    ];
782
783    /// Currency indices
784    pub const CURRENCIES: &[&str] = &[
785        "DX-Y.NYB", // US Dollar Index
786        "^XDB",     // British Pound Currency Index
787        "^XDE",     // Euro Currency Index
788        "^XDN",     // Japanese Yen Currency Index
789        "^XDA",     // Australian Dollar Currency Index
790    ];
791
792    /// All world indices (all regions combined)
793    pub fn all_symbols() -> Vec<&'static str> {
794        Region::all()
795            .iter()
796            .flat_map(|r| r.symbols().iter().copied())
797            .collect()
798    }
799}
800
801/// Fundamental timeseries field types for financial statements
802///
803/// These constants represent field names that must be prefixed with frequency ("annual" or "quarterly")
804/// Example: "annualTotalRevenue", "quarterlyTotalRevenue"
805#[allow(missing_docs)]
806pub mod fundamental_types {
807    // ==================
808    // INCOME STATEMENT (48 fields)
809    // ==================
810    pub const TOTAL_REVENUE: &str = "TotalRevenue";
811    pub const OPERATING_REVENUE: &str = "OperatingRevenue";
812    pub const COST_OF_REVENUE: &str = "CostOfRevenue";
813    pub const GROSS_PROFIT: &str = "GrossProfit";
814    pub const OPERATING_EXPENSE: &str = "OperatingExpense";
815    pub const SELLING_GENERAL_AND_ADMIN: &str = "SellingGeneralAndAdministration";
816    pub const RESEARCH_AND_DEVELOPMENT: &str = "ResearchAndDevelopment";
817    pub const OPERATING_INCOME: &str = "OperatingIncome";
818    pub const NET_INTEREST_INCOME: &str = "NetInterestIncome";
819    pub const INTEREST_EXPENSE: &str = "InterestExpense";
820    pub const INTEREST_INCOME: &str = "InterestIncome";
821    pub const NET_NON_OPERATING_INTEREST_INCOME_EXPENSE: &str =
822        "NetNonOperatingInterestIncomeExpense";
823    pub const OTHER_INCOME_EXPENSE: &str = "OtherIncomeExpense";
824    pub const PRETAX_INCOME: &str = "PretaxIncome";
825    pub const TAX_PROVISION: &str = "TaxProvision";
826    pub const NET_INCOME_COMMON_STOCKHOLDERS: &str = "NetIncomeCommonStockholders";
827    pub const NET_INCOME: &str = "NetIncome";
828    pub const DILUTED_EPS: &str = "DilutedEPS";
829    pub const BASIC_EPS: &str = "BasicEPS";
830    pub const DILUTED_AVERAGE_SHARES: &str = "DilutedAverageShares";
831    pub const BASIC_AVERAGE_SHARES: &str = "BasicAverageShares";
832    pub const EBIT: &str = "EBIT";
833    pub const EBITDA: &str = "EBITDA";
834    pub const RECONCILED_COST_OF_REVENUE: &str = "ReconciledCostOfRevenue";
835    pub const RECONCILED_DEPRECIATION: &str = "ReconciledDepreciation";
836    pub const NET_INCOME_FROM_CONTINUING_OPERATION_NET_MINORITY_INTEREST: &str =
837        "NetIncomeFromContinuingOperationNetMinorityInterest";
838    pub const NORMALIZED_EBITDA: &str = "NormalizedEBITDA";
839    pub const TOTAL_EXPENSES: &str = "TotalExpenses";
840    pub const TOTAL_OPERATING_INCOME_AS_REPORTED: &str = "TotalOperatingIncomeAsReported";
841
842    // ==================
843    // BALANCE SHEET (42 fields)
844    // ==================
845    pub const TOTAL_ASSETS: &str = "TotalAssets";
846    pub const CURRENT_ASSETS: &str = "CurrentAssets";
847    pub const CASH_CASH_EQUIVALENTS_AND_SHORT_TERM_INVESTMENTS: &str =
848        "CashCashEquivalentsAndShortTermInvestments";
849    pub const CASH_AND_CASH_EQUIVALENTS: &str = "CashAndCashEquivalents";
850    pub const CASH_FINANCIAL: &str = "CashFinancial";
851    pub const RECEIVABLES: &str = "Receivables";
852    pub const ACCOUNTS_RECEIVABLE: &str = "AccountsReceivable";
853    pub const INVENTORY: &str = "Inventory";
854    pub const PREPAID_ASSETS: &str = "PrepaidAssets";
855    pub const OTHER_CURRENT_ASSETS: &str = "OtherCurrentAssets";
856    pub const TOTAL_NON_CURRENT_ASSETS: &str = "TotalNonCurrentAssets";
857    pub const NET_PPE: &str = "NetPPE";
858    pub const GROSS_PPE: &str = "GrossPPE";
859    pub const ACCUMULATED_DEPRECIATION: &str = "AccumulatedDepreciation";
860    pub const GOODWILL: &str = "Goodwill";
861    pub const GOODWILL_AND_OTHER_INTANGIBLE_ASSETS: &str = "GoodwillAndOtherIntangibleAssets";
862    pub const OTHER_INTANGIBLE_ASSETS: &str = "OtherIntangibleAssets";
863    pub const INVESTMENTS_AND_ADVANCES: &str = "InvestmentsAndAdvances";
864    pub const LONG_TERM_EQUITY_INVESTMENT: &str = "LongTermEquityInvestment";
865    pub const OTHER_NON_CURRENT_ASSETS: &str = "OtherNonCurrentAssets";
866    pub const TOTAL_LIABILITIES_NET_MINORITY_INTEREST: &str = "TotalLiabilitiesNetMinorityInterest";
867    pub const CURRENT_LIABILITIES: &str = "CurrentLiabilities";
868    pub const PAYABLES_AND_ACCRUED_EXPENSES: &str = "PayablesAndAccruedExpenses";
869    pub const ACCOUNTS_PAYABLE: &str = "AccountsPayable";
870    pub const CURRENT_DEBT: &str = "CurrentDebt";
871    pub const CURRENT_DEFERRED_REVENUE: &str = "CurrentDeferredRevenue";
872    pub const OTHER_CURRENT_LIABILITIES: &str = "OtherCurrentLiabilities";
873    pub const TOTAL_NON_CURRENT_LIABILITIES_NET_MINORITY_INTEREST: &str =
874        "TotalNonCurrentLiabilitiesNetMinorityInterest";
875    pub const LONG_TERM_DEBT: &str = "LongTermDebt";
876    pub const LONG_TERM_DEBT_AND_CAPITAL_LEASE_OBLIGATION: &str =
877        "LongTermDebtAndCapitalLeaseObligation";
878    pub const NON_CURRENT_DEFERRED_REVENUE: &str = "NonCurrentDeferredRevenue";
879    pub const NON_CURRENT_DEFERRED_TAXES_LIABILITIES: &str = "NonCurrentDeferredTaxesLiabilities";
880    pub const OTHER_NON_CURRENT_LIABILITIES: &str = "OtherNonCurrentLiabilities";
881    pub const STOCKHOLDERS_EQUITY: &str = "StockholdersEquity";
882    pub const COMMON_STOCK_EQUITY: &str = "CommonStockEquity";
883    pub const COMMON_STOCK: &str = "CommonStock";
884    pub const RETAINED_EARNINGS: &str = "RetainedEarnings";
885    pub const ADDITIONAL_PAID_IN_CAPITAL: &str = "AdditionalPaidInCapital";
886    pub const TREASURY_STOCK: &str = "TreasuryStock";
887    pub const TOTAL_EQUITY_GROSS_MINORITY_INTEREST: &str = "TotalEquityGrossMinorityInterest";
888    pub const WORKING_CAPITAL: &str = "WorkingCapital";
889    pub const INVESTED_CAPITAL: &str = "InvestedCapital";
890    pub const TANGIBLE_BOOK_VALUE: &str = "TangibleBookValue";
891    pub const TOTAL_DEBT: &str = "TotalDebt";
892    pub const NET_DEBT: &str = "NetDebt";
893    pub const SHARE_ISSUED: &str = "ShareIssued";
894    pub const ORDINARY_SHARES_NUMBER: &str = "OrdinarySharesNumber";
895
896    // ==================
897    // CASH FLOW STATEMENT (48 fields)
898    // ==================
899    pub const OPERATING_CASH_FLOW: &str = "OperatingCashFlow";
900    pub const CASH_FLOW_FROM_CONTINUING_OPERATING_ACTIVITIES: &str =
901        "CashFlowFromContinuingOperatingActivities";
902    pub const NET_INCOME_FROM_CONTINUING_OPERATIONS: &str = "NetIncomeFromContinuingOperations";
903    pub const DEPRECIATION_AND_AMORTIZATION: &str = "DepreciationAndAmortization";
904    pub const DEFERRED_INCOME_TAX: &str = "DeferredIncomeTax";
905    pub const CHANGE_IN_WORKING_CAPITAL: &str = "ChangeInWorkingCapital";
906    pub const CHANGE_IN_RECEIVABLES: &str = "ChangeInReceivables";
907    pub const CHANGES_IN_ACCOUNT_RECEIVABLES: &str = "ChangesInAccountReceivables";
908    pub const CHANGE_IN_INVENTORY: &str = "ChangeInInventory";
909    pub const CHANGE_IN_ACCOUNT_PAYABLE: &str = "ChangeInAccountPayable";
910    pub const CHANGE_IN_OTHER_WORKING_CAPITAL: &str = "ChangeInOtherWorkingCapital";
911    pub const STOCK_BASED_COMPENSATION: &str = "StockBasedCompensation";
912    pub const OTHER_NON_CASH_ITEMS: &str = "OtherNonCashItems";
913    pub const INVESTING_CASH_FLOW: &str = "InvestingCashFlow";
914    pub const CASH_FLOW_FROM_CONTINUING_INVESTING_ACTIVITIES: &str =
915        "CashFlowFromContinuingInvestingActivities";
916    pub const NET_PPE_PURCHASE_AND_SALE: &str = "NetPPEPurchaseAndSale";
917    pub const PURCHASE_OF_PPE: &str = "PurchaseOfPPE";
918    pub const SALE_OF_PPE: &str = "SaleOfPPE";
919    pub const CAPITAL_EXPENDITURE: &str = "CapitalExpenditure";
920    pub const NET_BUSINESS_PURCHASE_AND_SALE: &str = "NetBusinessPurchaseAndSale";
921    pub const PURCHASE_OF_BUSINESS: &str = "PurchaseOfBusiness";
922    pub const SALE_OF_BUSINESS: &str = "SaleOfBusiness";
923    pub const NET_INVESTMENT_PURCHASE_AND_SALE: &str = "NetInvestmentPurchaseAndSale";
924    pub const PURCHASE_OF_INVESTMENT: &str = "PurchaseOfInvestment";
925    pub const SALE_OF_INVESTMENT: &str = "SaleOfInvestment";
926    pub const NET_OTHER_INVESTING_CHANGES: &str = "NetOtherInvestingChanges";
927    pub const FINANCING_CASH_FLOW: &str = "FinancingCashFlow";
928    pub const CASH_FLOW_FROM_CONTINUING_FINANCING_ACTIVITIES: &str =
929        "CashFlowFromContinuingFinancingActivities";
930    pub const NET_ISSUANCE_PAYMENTS_OF_DEBT: &str = "NetIssuancePaymentsOfDebt";
931    pub const NET_LONG_TERM_DEBT_ISSUANCE: &str = "NetLongTermDebtIssuance";
932    pub const LONG_TERM_DEBT_ISSUANCE: &str = "LongTermDebtIssuance";
933    pub const LONG_TERM_DEBT_PAYMENTS: &str = "LongTermDebtPayments";
934    pub const NET_SHORT_TERM_DEBT_ISSUANCE: &str = "NetShortTermDebtIssuance";
935    pub const NET_COMMON_STOCK_ISSUANCE: &str = "NetCommonStockIssuance";
936    pub const COMMON_STOCK_ISSUANCE: &str = "CommonStockIssuance";
937    pub const COMMON_STOCK_PAYMENTS: &str = "CommonStockPayments";
938    pub const REPURCHASE_OF_CAPITAL_STOCK: &str = "RepurchaseOfCapitalStock";
939    pub const CASH_DIVIDENDS_PAID: &str = "CashDividendsPaid";
940    pub const COMMON_STOCK_DIVIDEND_PAID: &str = "CommonStockDividendPaid";
941    pub const NET_OTHER_FINANCING_CHARGES: &str = "NetOtherFinancingCharges";
942    pub const END_CASH_POSITION: &str = "EndCashPosition";
943    pub const BEGINNING_CASH_POSITION: &str = "BeginningCashPosition";
944    pub const CHANGESIN_CASH: &str = "ChangesinCash";
945    pub const EFFECT_OF_EXCHANGE_RATE_CHANGES: &str = "EffectOfExchangeRateChanges";
946    pub const FREE_CASH_FLOW: &str = "FreeCashFlow";
947    pub const CAPITAL_EXPENDITURE_REPORTED: &str = "CapitalExpenditureReported";
948}
949
950/// Statement types for financial data
951#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
952pub enum StatementType {
953    /// Income statement
954    Income,
955    /// Balance sheet
956    Balance,
957    /// Cash flow statement
958    CashFlow,
959}
960
961impl StatementType {
962    /// Convert statement type to string representation
963    pub fn as_str(&self) -> &'static str {
964        match self {
965            StatementType::Income => "income",
966            StatementType::Balance => "balance",
967            StatementType::CashFlow => "cashflow",
968        }
969    }
970
971    /// Get the list of fields for this statement type
972    ///
973    /// Returns field names without frequency prefix (e.g., "TotalRevenue" not "annualTotalRevenue")
974    pub fn get_fields(&self) -> &'static [&'static str] {
975        match self {
976            StatementType::Income => &INCOME_STATEMENT_FIELDS,
977            StatementType::Balance => &BALANCE_SHEET_FIELDS,
978            StatementType::CashFlow => &CASH_FLOW_FIELDS,
979        }
980    }
981}
982
983/// Income statement fields (without frequency prefix)
984const INCOME_STATEMENT_FIELDS: [&str; 30] = [
985    fundamental_types::TOTAL_REVENUE,
986    fundamental_types::OPERATING_REVENUE,
987    fundamental_types::COST_OF_REVENUE,
988    fundamental_types::GROSS_PROFIT,
989    fundamental_types::OPERATING_EXPENSE,
990    fundamental_types::SELLING_GENERAL_AND_ADMIN,
991    fundamental_types::RESEARCH_AND_DEVELOPMENT,
992    fundamental_types::OPERATING_INCOME,
993    fundamental_types::NET_INTEREST_INCOME,
994    fundamental_types::INTEREST_EXPENSE,
995    fundamental_types::INTEREST_INCOME,
996    fundamental_types::NET_NON_OPERATING_INTEREST_INCOME_EXPENSE,
997    fundamental_types::OTHER_INCOME_EXPENSE,
998    fundamental_types::PRETAX_INCOME,
999    fundamental_types::TAX_PROVISION,
1000    fundamental_types::NET_INCOME_COMMON_STOCKHOLDERS,
1001    fundamental_types::NET_INCOME,
1002    fundamental_types::DILUTED_EPS,
1003    fundamental_types::BASIC_EPS,
1004    fundamental_types::DILUTED_AVERAGE_SHARES,
1005    fundamental_types::BASIC_AVERAGE_SHARES,
1006    fundamental_types::EBIT,
1007    fundamental_types::EBITDA,
1008    fundamental_types::RECONCILED_COST_OF_REVENUE,
1009    fundamental_types::RECONCILED_DEPRECIATION,
1010    fundamental_types::NET_INCOME_FROM_CONTINUING_OPERATION_NET_MINORITY_INTEREST,
1011    fundamental_types::NORMALIZED_EBITDA,
1012    fundamental_types::TOTAL_EXPENSES,
1013    fundamental_types::TOTAL_OPERATING_INCOME_AS_REPORTED,
1014    fundamental_types::DEPRECIATION_AND_AMORTIZATION,
1015];
1016
1017/// Balance sheet fields (without frequency prefix)
1018const BALANCE_SHEET_FIELDS: [&str; 48] = [
1019    fundamental_types::TOTAL_ASSETS,
1020    fundamental_types::CURRENT_ASSETS,
1021    fundamental_types::CASH_CASH_EQUIVALENTS_AND_SHORT_TERM_INVESTMENTS,
1022    fundamental_types::CASH_AND_CASH_EQUIVALENTS,
1023    fundamental_types::CASH_FINANCIAL,
1024    fundamental_types::RECEIVABLES,
1025    fundamental_types::ACCOUNTS_RECEIVABLE,
1026    fundamental_types::INVENTORY,
1027    fundamental_types::PREPAID_ASSETS,
1028    fundamental_types::OTHER_CURRENT_ASSETS,
1029    fundamental_types::TOTAL_NON_CURRENT_ASSETS,
1030    fundamental_types::NET_PPE,
1031    fundamental_types::GROSS_PPE,
1032    fundamental_types::ACCUMULATED_DEPRECIATION,
1033    fundamental_types::GOODWILL,
1034    fundamental_types::GOODWILL_AND_OTHER_INTANGIBLE_ASSETS,
1035    fundamental_types::OTHER_INTANGIBLE_ASSETS,
1036    fundamental_types::INVESTMENTS_AND_ADVANCES,
1037    fundamental_types::LONG_TERM_EQUITY_INVESTMENT,
1038    fundamental_types::OTHER_NON_CURRENT_ASSETS,
1039    fundamental_types::TOTAL_LIABILITIES_NET_MINORITY_INTEREST,
1040    fundamental_types::CURRENT_LIABILITIES,
1041    fundamental_types::PAYABLES_AND_ACCRUED_EXPENSES,
1042    fundamental_types::ACCOUNTS_PAYABLE,
1043    fundamental_types::CURRENT_DEBT,
1044    fundamental_types::CURRENT_DEFERRED_REVENUE,
1045    fundamental_types::OTHER_CURRENT_LIABILITIES,
1046    fundamental_types::TOTAL_NON_CURRENT_LIABILITIES_NET_MINORITY_INTEREST,
1047    fundamental_types::LONG_TERM_DEBT,
1048    fundamental_types::LONG_TERM_DEBT_AND_CAPITAL_LEASE_OBLIGATION,
1049    fundamental_types::NON_CURRENT_DEFERRED_REVENUE,
1050    fundamental_types::NON_CURRENT_DEFERRED_TAXES_LIABILITIES,
1051    fundamental_types::OTHER_NON_CURRENT_LIABILITIES,
1052    fundamental_types::STOCKHOLDERS_EQUITY,
1053    fundamental_types::COMMON_STOCK_EQUITY,
1054    fundamental_types::COMMON_STOCK,
1055    fundamental_types::RETAINED_EARNINGS,
1056    fundamental_types::ADDITIONAL_PAID_IN_CAPITAL,
1057    fundamental_types::TREASURY_STOCK,
1058    fundamental_types::TOTAL_EQUITY_GROSS_MINORITY_INTEREST,
1059    fundamental_types::WORKING_CAPITAL,
1060    fundamental_types::INVESTED_CAPITAL,
1061    fundamental_types::TANGIBLE_BOOK_VALUE,
1062    fundamental_types::TOTAL_DEBT,
1063    fundamental_types::NET_DEBT,
1064    fundamental_types::SHARE_ISSUED,
1065    fundamental_types::ORDINARY_SHARES_NUMBER,
1066    fundamental_types::DEPRECIATION_AND_AMORTIZATION,
1067];
1068
1069/// Cash flow statement fields (without frequency prefix)
1070const CASH_FLOW_FIELDS: [&str; 47] = [
1071    fundamental_types::OPERATING_CASH_FLOW,
1072    fundamental_types::CASH_FLOW_FROM_CONTINUING_OPERATING_ACTIVITIES,
1073    fundamental_types::NET_INCOME_FROM_CONTINUING_OPERATIONS,
1074    fundamental_types::DEPRECIATION_AND_AMORTIZATION,
1075    fundamental_types::DEFERRED_INCOME_TAX,
1076    fundamental_types::CHANGE_IN_WORKING_CAPITAL,
1077    fundamental_types::CHANGE_IN_RECEIVABLES,
1078    fundamental_types::CHANGES_IN_ACCOUNT_RECEIVABLES,
1079    fundamental_types::CHANGE_IN_INVENTORY,
1080    fundamental_types::CHANGE_IN_ACCOUNT_PAYABLE,
1081    fundamental_types::CHANGE_IN_OTHER_WORKING_CAPITAL,
1082    fundamental_types::STOCK_BASED_COMPENSATION,
1083    fundamental_types::OTHER_NON_CASH_ITEMS,
1084    fundamental_types::INVESTING_CASH_FLOW,
1085    fundamental_types::CASH_FLOW_FROM_CONTINUING_INVESTING_ACTIVITIES,
1086    fundamental_types::NET_PPE_PURCHASE_AND_SALE,
1087    fundamental_types::PURCHASE_OF_PPE,
1088    fundamental_types::SALE_OF_PPE,
1089    fundamental_types::CAPITAL_EXPENDITURE,
1090    fundamental_types::NET_BUSINESS_PURCHASE_AND_SALE,
1091    fundamental_types::PURCHASE_OF_BUSINESS,
1092    fundamental_types::SALE_OF_BUSINESS,
1093    fundamental_types::NET_INVESTMENT_PURCHASE_AND_SALE,
1094    fundamental_types::PURCHASE_OF_INVESTMENT,
1095    fundamental_types::SALE_OF_INVESTMENT,
1096    fundamental_types::NET_OTHER_INVESTING_CHANGES,
1097    fundamental_types::FINANCING_CASH_FLOW,
1098    fundamental_types::CASH_FLOW_FROM_CONTINUING_FINANCING_ACTIVITIES,
1099    fundamental_types::NET_ISSUANCE_PAYMENTS_OF_DEBT,
1100    fundamental_types::NET_LONG_TERM_DEBT_ISSUANCE,
1101    fundamental_types::LONG_TERM_DEBT_ISSUANCE,
1102    fundamental_types::LONG_TERM_DEBT_PAYMENTS,
1103    fundamental_types::NET_SHORT_TERM_DEBT_ISSUANCE,
1104    fundamental_types::NET_COMMON_STOCK_ISSUANCE,
1105    fundamental_types::COMMON_STOCK_ISSUANCE,
1106    fundamental_types::COMMON_STOCK_PAYMENTS,
1107    fundamental_types::REPURCHASE_OF_CAPITAL_STOCK,
1108    fundamental_types::CASH_DIVIDENDS_PAID,
1109    fundamental_types::COMMON_STOCK_DIVIDEND_PAID,
1110    fundamental_types::NET_OTHER_FINANCING_CHARGES,
1111    fundamental_types::END_CASH_POSITION,
1112    fundamental_types::BEGINNING_CASH_POSITION,
1113    fundamental_types::CHANGESIN_CASH,
1114    fundamental_types::EFFECT_OF_EXCHANGE_RATE_CHANGES,
1115    fundamental_types::FREE_CASH_FLOW,
1116    fundamental_types::CAPITAL_EXPENDITURE_REPORTED,
1117    fundamental_types::DEPRECIATION_AND_AMORTIZATION,
1118];
1119
1120/// Frequency for financial data (annual or quarterly)
1121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1122pub enum Frequency {
1123    /// Annual financial data
1124    Annual,
1125    /// Quarterly financial data
1126    Quarterly,
1127}
1128
1129impl Frequency {
1130    /// Convert frequency to string representation
1131    pub fn as_str(&self) -> &'static str {
1132        match self {
1133            Frequency::Annual => "annual",
1134            Frequency::Quarterly => "quarterly",
1135        }
1136    }
1137
1138    /// Build a fundamental type string with frequency prefix
1139    ///
1140    /// # Example
1141    ///
1142    /// ```
1143    /// use finance_query::Frequency;
1144    ///
1145    /// let field = Frequency::Annual.prefix("TotalRevenue");
1146    /// assert_eq!(field, "annualTotalRevenue");
1147    /// ```
1148    pub fn prefix(&self, field: &str) -> String {
1149        format!("{}{}", self.as_str(), field)
1150    }
1151}
1152
1153/// Chart intervals
1154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1155#[allow(dead_code)]
1156pub enum Interval {
1157    /// 1 minute
1158    OneMinute,
1159    /// 5 minutes
1160    FiveMinutes,
1161    /// 15 minutes
1162    FifteenMinutes,
1163    /// 30 minutes
1164    ThirtyMinutes,
1165    /// 1 hour
1166    OneHour,
1167    /// 1 day
1168    OneDay,
1169    /// 1 week
1170    OneWeek,
1171    /// 1 month
1172    OneMonth,
1173    /// 3 months
1174    ThreeMonths,
1175}
1176
1177#[allow(dead_code)]
1178impl Interval {
1179    /// Convert interval to Yahoo Finance API format
1180    pub fn as_str(&self) -> &'static str {
1181        match self {
1182            Interval::OneMinute => "1m",
1183            Interval::FiveMinutes => "5m",
1184            Interval::FifteenMinutes => "15m",
1185            Interval::ThirtyMinutes => "30m",
1186            Interval::OneHour => "1h",
1187            Interval::OneDay => "1d",
1188            Interval::OneWeek => "1wk",
1189            Interval::OneMonth => "1mo",
1190            Interval::ThreeMonths => "3mo",
1191        }
1192    }
1193}
1194
1195/// Time ranges for chart data
1196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1197#[allow(dead_code)]
1198pub enum TimeRange {
1199    /// 1 day
1200    OneDay,
1201    /// 5 days
1202    FiveDays,
1203    /// 1 month
1204    OneMonth,
1205    /// 3 months
1206    ThreeMonths,
1207    /// 6 months
1208    SixMonths,
1209    /// 1 year
1210    OneYear,
1211    /// 2 years
1212    TwoYears,
1213    /// 5 years
1214    FiveYears,
1215    /// 10 years
1216    TenYears,
1217    /// Year to date
1218    YearToDate,
1219    /// Maximum available
1220    Max,
1221}
1222
1223#[allow(dead_code)]
1224impl TimeRange {
1225    /// Convert time range to Yahoo Finance API format
1226    pub fn as_str(&self) -> &'static str {
1227        match self {
1228            TimeRange::OneDay => "1d",
1229            TimeRange::FiveDays => "5d",
1230            TimeRange::OneMonth => "1mo",
1231            TimeRange::ThreeMonths => "3mo",
1232            TimeRange::SixMonths => "6mo",
1233            TimeRange::OneYear => "1y",
1234            TimeRange::TwoYears => "2y",
1235            TimeRange::FiveYears => "5y",
1236            TimeRange::TenYears => "10y",
1237            TimeRange::YearToDate => "ytd",
1238            TimeRange::Max => "max",
1239        }
1240    }
1241}
1242
1243/// Supported regions for Yahoo Finance regional APIs
1244///
1245/// Each region has predefined language and region codes that work together.
1246/// Using the Region enum ensures correct lang/region pairing.
1247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1248pub enum Region {
1249    /// Argentina (es-AR, AR)
1250    Argentina,
1251    /// Australia (en-AU, AU)
1252    Australia,
1253    /// Brazil (pt-BR, BR)
1254    Brazil,
1255    /// Canada (en-CA, CA)
1256    Canada,
1257    /// China (zh-CN, CN)
1258    China,
1259    /// Denmark (da-DK, DK)
1260    Denmark,
1261    /// Finland (fi-FI, FI)
1262    Finland,
1263    /// France (fr-FR, FR)
1264    France,
1265    /// Germany (de-DE, DE)
1266    Germany,
1267    /// Greece (el-GR, GR)
1268    Greece,
1269    /// Hong Kong (zh-Hant-HK, HK)
1270    HongKong,
1271    /// India (en-IN, IN)
1272    India,
1273    /// Israel (he-IL, IL)
1274    Israel,
1275    /// Italy (it-IT, IT)
1276    Italy,
1277    /// Malaysia (ms-MY, MY)
1278    Malaysia,
1279    /// New Zealand (en-NZ, NZ)
1280    NewZealand,
1281    /// Norway (nb-NO, NO)
1282    Norway,
1283    /// Portugal (pt-PT, PT)
1284    Portugal,
1285    /// Russia (ru-RU, RU)
1286    Russia,
1287    /// Singapore (en-SG, SG)
1288    Singapore,
1289    /// Spain (es-ES, ES)
1290    Spain,
1291    /// Sweden (sv-SE, SE)
1292    Sweden,
1293    /// Taiwan (zh-TW, TW)
1294    Taiwan,
1295    /// Thailand (th-TH, TH)
1296    Thailand,
1297    /// Turkey (tr-TR, TR)
1298    Turkey,
1299    /// United Kingdom (en-GB, GB)
1300    UnitedKingdom,
1301    /// United States (en-US, US) - Default
1302    #[default]
1303    UnitedStates,
1304    /// Vietnam (vi-VN, VN)
1305    Vietnam,
1306}
1307
1308impl Region {
1309    /// Get the language code for this region
1310    ///
1311    /// # Example
1312    ///
1313    /// ```
1314    /// use finance_query::Region;
1315    ///
1316    /// assert_eq!(Region::France.lang(), "fr-FR");
1317    /// assert_eq!(Region::UnitedStates.lang(), "en-US");
1318    /// ```
1319    pub fn lang(&self) -> &'static str {
1320        match self {
1321            Region::Argentina => "es-AR",
1322            Region::Australia => "en-AU",
1323            Region::Brazil => "pt-BR",
1324            Region::Canada => "en-CA",
1325            Region::China => "zh-CN",
1326            Region::Denmark => "da-DK",
1327            Region::Finland => "fi-FI",
1328            Region::France => "fr-FR",
1329            Region::Germany => "de-DE",
1330            Region::Greece => "el-GR",
1331            Region::HongKong => "zh-Hant-HK",
1332            Region::India => "en-IN",
1333            Region::Israel => "he-IL",
1334            Region::Italy => "it-IT",
1335            Region::Malaysia => "ms-MY",
1336            Region::NewZealand => "en-NZ",
1337            Region::Norway => "nb-NO",
1338            Region::Portugal => "pt-PT",
1339            Region::Russia => "ru-RU",
1340            Region::Singapore => "en-SG",
1341            Region::Spain => "es-ES",
1342            Region::Sweden => "sv-SE",
1343            Region::Taiwan => "zh-TW",
1344            Region::Thailand => "th-TH",
1345            Region::Turkey => "tr-TR",
1346            Region::UnitedKingdom => "en-GB",
1347            Region::UnitedStates => "en-US",
1348            Region::Vietnam => "vi-VN",
1349        }
1350    }
1351
1352    /// Get the region code for this region
1353    ///
1354    /// # Example
1355    ///
1356    /// ```
1357    /// use finance_query::Region;
1358    ///
1359    /// assert_eq!(Region::France.region(), "FR");
1360    /// assert_eq!(Region::UnitedStates.region(), "US");
1361    /// ```
1362    pub fn region(&self) -> &'static str {
1363        match self {
1364            Region::Argentina => "AR",
1365            Region::Australia => "AU",
1366            Region::Brazil => "BR",
1367            Region::Canada => "CA",
1368            Region::China => "CN",
1369            Region::Denmark => "DK",
1370            Region::Finland => "FI",
1371            Region::France => "FR",
1372            Region::Germany => "DE",
1373            Region::Greece => "GR",
1374            Region::HongKong => "HK",
1375            Region::India => "IN",
1376            Region::Israel => "IL",
1377            Region::Italy => "IT",
1378            Region::Malaysia => "MY",
1379            Region::NewZealand => "NZ",
1380            Region::Norway => "NO",
1381            Region::Portugal => "PT",
1382            Region::Russia => "RU",
1383            Region::Singapore => "SG",
1384            Region::Spain => "ES",
1385            Region::Sweden => "SE",
1386            Region::Taiwan => "TW",
1387            Region::Thailand => "TH",
1388            Region::Turkey => "TR",
1389            Region::UnitedKingdom => "GB",
1390            Region::UnitedStates => "US",
1391            Region::Vietnam => "VN",
1392        }
1393    }
1394
1395    /// Get the CORS domain for this region
1396    ///
1397    /// # Example
1398    ///
1399    /// ```
1400    /// use finance_query::Region;
1401    ///
1402    /// assert_eq!(Region::UnitedStates.cors_domain(), "finance.yahoo.com");
1403    /// assert_eq!(Region::France.cors_domain(), "fr.finance.yahoo.com");
1404    /// ```
1405    pub fn cors_domain(&self) -> &'static str {
1406        match self {
1407            Region::Argentina => "ar.finance.yahoo.com",
1408            Region::Australia => "au.finance.yahoo.com",
1409            Region::Brazil => "br.financas.yahoo.com",
1410            Region::Canada => "ca.finance.yahoo.com",
1411            Region::China => "cn.finance.yahoo.com",
1412            Region::Denmark => "dk.finance.yahoo.com",
1413            Region::Finland => "fi.finance.yahoo.com",
1414            Region::France => "fr.finance.yahoo.com",
1415            Region::Germany => "de.finance.yahoo.com",
1416            Region::Greece => "gr.finance.yahoo.com",
1417            Region::HongKong => "hk.finance.yahoo.com",
1418            Region::India => "in.finance.yahoo.com",
1419            Region::Israel => "il.finance.yahoo.com",
1420            Region::Italy => "it.finance.yahoo.com",
1421            Region::Malaysia => "my.finance.yahoo.com",
1422            Region::NewZealand => "nz.finance.yahoo.com",
1423            Region::Norway => "no.finance.yahoo.com",
1424            Region::Portugal => "pt.finance.yahoo.com",
1425            Region::Russia => "ru.finance.yahoo.com",
1426            Region::Singapore => "sg.finance.yahoo.com",
1427            Region::Spain => "es.finance.yahoo.com",
1428            Region::Sweden => "se.finance.yahoo.com",
1429            Region::Taiwan => "tw.finance.yahoo.com",
1430            Region::Thailand => "th.finance.yahoo.com",
1431            Region::Turkey => "tr.finance.yahoo.com",
1432            Region::UnitedKingdom => "uk.finance.yahoo.com",
1433            Region::UnitedStates => "finance.yahoo.com",
1434            Region::Vietnam => "vn.finance.yahoo.com",
1435        }
1436    }
1437}
1438
1439impl std::str::FromStr for Region {
1440    type Err = ();
1441
1442    fn from_str(s: &str) -> Result<Self, Self::Err> {
1443        match s.to_uppercase().as_str() {
1444            "AR" => Ok(Region::Argentina),
1445            "AU" => Ok(Region::Australia),
1446            "BR" => Ok(Region::Brazil),
1447            "CA" => Ok(Region::Canada),
1448            "CN" => Ok(Region::China),
1449            "DK" => Ok(Region::Denmark),
1450            "FI" => Ok(Region::Finland),
1451            "FR" => Ok(Region::France),
1452            "DE" => Ok(Region::Germany),
1453            "GR" => Ok(Region::Greece),
1454            "HK" => Ok(Region::HongKong),
1455            "IN" => Ok(Region::India),
1456            "IL" => Ok(Region::Israel),
1457            "IT" => Ok(Region::Italy),
1458            "MY" => Ok(Region::Malaysia),
1459            "NZ" => Ok(Region::NewZealand),
1460            "NO" => Ok(Region::Norway),
1461            "PT" => Ok(Region::Portugal),
1462            "RU" => Ok(Region::Russia),
1463            "SG" => Ok(Region::Singapore),
1464            "ES" => Ok(Region::Spain),
1465            "SE" => Ok(Region::Sweden),
1466            "TW" => Ok(Region::Taiwan),
1467            "TH" => Ok(Region::Thailand),
1468            "TR" => Ok(Region::Turkey),
1469            "GB" | "UK" => Ok(Region::UnitedKingdom),
1470            "US" => Ok(Region::UnitedStates),
1471            "VN" => Ok(Region::Vietnam),
1472            _ => Err(()),
1473        }
1474    }
1475}
1476
1477/// Value format for API responses
1478///
1479/// Controls how `FormattedValue<T>` fields are serialized in responses.
1480/// This allows API consumers to choose between raw numeric values,
1481/// human-readable formatted strings, or both.
1482#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1483pub enum ValueFormat {
1484    /// Return only raw numeric values (e.g., `123.45`) - default
1485    /// Best for programmatic use, calculations, charts
1486    #[default]
1487    Raw,
1488    /// Return only formatted strings (e.g., `"$123.45"`, `"1.2B"`)
1489    /// Best for display purposes
1490    Pretty,
1491    /// Return both raw and formatted values
1492    /// Returns the full `{raw, fmt, longFmt}` object
1493    Both,
1494}
1495
1496impl std::str::FromStr for ValueFormat {
1497    type Err = ();
1498
1499    fn from_str(s: &str) -> Result<Self, Self::Err> {
1500        match s.to_lowercase().as_str() {
1501            "raw" => Ok(ValueFormat::Raw),
1502            "pretty" | "fmt" => Ok(ValueFormat::Pretty),
1503            "both" | "full" => Ok(ValueFormat::Both),
1504            _ => Err(()),
1505        }
1506    }
1507}
1508
1509impl ValueFormat {
1510    /// Parse from string (case-insensitive), returns None on invalid input
1511    pub fn parse(s: &str) -> Option<Self> {
1512        s.parse().ok()
1513    }
1514
1515    /// Convert to string representation
1516    pub fn as_str(&self) -> &'static str {
1517        match self {
1518            ValueFormat::Raw => "raw",
1519            ValueFormat::Pretty => "pretty",
1520            ValueFormat::Both => "both",
1521        }
1522    }
1523
1524    /// Transform a JSON value based on this format
1525    ///
1526    /// Recursively processes the JSON, detecting FormattedValue objects
1527    /// (objects with `raw` key and optionally `fmt`/`longFmt`) and
1528    /// transforming them according to the format setting.
1529    ///
1530    /// # Example
1531    ///
1532    /// ```
1533    /// use finance_query::ValueFormat;
1534    /// use serde_json::json;
1535    ///
1536    /// let data = json!({"price": {"raw": 123.45, "fmt": "$123.45"}});
1537    ///
1538    /// // Raw format extracts just the raw value (default)
1539    /// let raw = ValueFormat::default().transform(data.clone());
1540    /// assert_eq!(raw, json!({"price": 123.45}));
1541    ///
1542    /// // Pretty extracts just the formatted string
1543    /// let pretty = ValueFormat::Pretty.transform(data.clone());
1544    /// assert_eq!(pretty, json!({"price": "$123.45"}));
1545    ///
1546    /// // Both keeps the full object
1547    /// let both = ValueFormat::Both.transform(data);
1548    /// assert_eq!(both, json!({"price": {"raw": 123.45, "fmt": "$123.45"}}));
1549    /// ```
1550    pub fn transform(&self, value: serde_json::Value) -> serde_json::Value {
1551        match self {
1552            ValueFormat::Both => value, // No transformation needed
1553            _ => self.transform_recursive(value),
1554        }
1555    }
1556
1557    fn transform_recursive(&self, value: serde_json::Value) -> serde_json::Value {
1558        use serde_json::Value;
1559
1560        match value {
1561            Value::Object(map) => {
1562                // Check if this looks like a FormattedValue (has 'raw' key)
1563                if self.is_formatted_value(&map) {
1564                    return self.extract_value(&map);
1565                }
1566
1567                // Otherwise, recursively transform all values
1568                let transformed: serde_json::Map<String, Value> = map
1569                    .into_iter()
1570                    .map(|(k, v)| (k, self.transform_recursive(v)))
1571                    .collect();
1572                Value::Object(transformed)
1573            }
1574            Value::Array(arr) => Value::Array(
1575                arr.into_iter()
1576                    .map(|v| self.transform_recursive(v))
1577                    .collect(),
1578            ),
1579            // Primitives pass through unchanged
1580            other => other,
1581        }
1582    }
1583
1584    /// Check if an object looks like a FormattedValue
1585    fn is_formatted_value(&self, map: &serde_json::Map<String, serde_json::Value>) -> bool {
1586        // Must have 'raw' key (can be null)
1587        // May have 'fmt' and/or 'longFmt'
1588        // Should not have many other keys (FormattedValue only has these 3)
1589        if !map.contains_key("raw") {
1590            return false;
1591        }
1592
1593        let known_keys = ["raw", "fmt", "longFmt"];
1594        let unknown_keys = map
1595            .keys()
1596            .filter(|k| !known_keys.contains(&k.as_str()))
1597            .count();
1598
1599        // If there are unknown keys, it's probably not a FormattedValue
1600        unknown_keys == 0
1601    }
1602
1603    /// Extract the appropriate value based on format
1604    fn extract_value(&self, map: &serde_json::Map<String, serde_json::Value>) -> serde_json::Value {
1605        match self {
1606            ValueFormat::Raw => {
1607                // Return raw value directly (or null if not present)
1608                map.get("raw").cloned().unwrap_or(serde_json::Value::Null)
1609            }
1610            ValueFormat::Pretty => {
1611                // Prefer fmt, fall back to longFmt, then null
1612                map.get("fmt")
1613                    .or_else(|| map.get("longFmt"))
1614                    .cloned()
1615                    .unwrap_or(serde_json::Value::Null)
1616            }
1617            ValueFormat::Both => {
1618                // Keep as-is (shouldn't reach here, but handle anyway)
1619                serde_json::Value::Object(map.clone())
1620            }
1621        }
1622    }
1623}
1624
1625#[cfg(test)]
1626mod tests {
1627    use super::*;
1628
1629    #[test]
1630    fn test_interval_as_str() {
1631        assert_eq!(Interval::OneMinute.as_str(), "1m");
1632        assert_eq!(Interval::FiveMinutes.as_str(), "5m");
1633        assert_eq!(Interval::OneDay.as_str(), "1d");
1634        assert_eq!(Interval::OneWeek.as_str(), "1wk");
1635    }
1636
1637    #[test]
1638    fn test_time_range_as_str() {
1639        assert_eq!(TimeRange::OneDay.as_str(), "1d");
1640        assert_eq!(TimeRange::OneMonth.as_str(), "1mo");
1641        assert_eq!(TimeRange::OneYear.as_str(), "1y");
1642        assert_eq!(TimeRange::Max.as_str(), "max");
1643    }
1644}