Skip to main content

finance_query/
constants.rs

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