1use crate::error::{Result, TvscreenerError};
10use std::collections::HashMap;
11use std::sync::OnceLock;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum Asset {
16 Crypto,
18 Stock,
20 Forex,
22 Bond,
24 Futures,
26 Coin,
28}
29
30impl Asset {
31 #[must_use]
33 pub const fn as_str(self) -> &'static str {
34 match self {
35 Self::Crypto => "crypto",
36 Self::Stock => "stock",
37 Self::Forex => "forex",
38 Self::Bond => "bond",
39 Self::Futures => "futures",
40 Self::Coin => "coin",
41 }
42 }
43
44 #[must_use]
46 pub fn parse(s: &str) -> Option<Self> {
47 match s {
48 "crypto" => Some(Self::Crypto),
49 "stock" => Some(Self::Stock),
50 "forex" => Some(Self::Forex),
51 "bond" => Some(Self::Bond),
52 "futures" => Some(Self::Futures),
53 "coin" => Some(Self::Coin),
54 _ => None,
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct Market {
62 pub const_name: String,
64 pub value: String,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct NamedValue {
71 pub const_name: String,
73 pub value: String,
75}
76
77#[derive(Debug, Clone, Copy)]
79pub struct RatingBand {
80 pub const_name: &'static str,
82 pub min: f64,
84 pub max: f64,
86 pub label: &'static str,
88}
89
90impl RatingBand {
91 #[must_use]
93 pub fn contains(self, value: f64) -> bool {
94 if self.min.is_nan() || self.max.is_nan() || value.is_nan() {
95 return false;
96 }
97 self.min <= value && value <= self.max
98 }
99
100 #[must_use]
102 pub const fn range(self) -> Option<[f64; 2]> {
103 if self.min.is_nan() || self.max.is_nan() {
104 None
105 } else {
106 Some([self.min, self.max])
107 }
108 }
109}
110
111impl Market {
112 #[must_use]
118 pub fn america() -> Self {
119 market("AMERICA").expect("AMERICA market must exist in fields.json")
120 }
121
122 #[must_use]
128 pub fn all() -> Self {
129 market("ALL").expect("ALL market must exist in fields.json")
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct TypeFilter {
136 pub const_name: String,
138 pub value: String,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct SymbolType {
145 pub const_name: String,
147 pub values: Vec<String>,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
156pub struct FieldDef {
157 pub label: String,
159 pub field_name: String,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub format: Option<String>,
164 pub interval: bool,
166 pub historical: bool,
168}
169
170impl FieldDef {
171 pub fn new(label: impl Into<String>, field_name: impl Into<String>) -> Self {
173 Self {
174 label: label.into(),
175 field_name: field_name.into(),
176 format: None,
177 interval: false,
178 historical: false,
179 }
180 }
181
182 #[must_use]
184 pub fn has_recommendation(&self) -> bool {
185 self.format.as_deref() == Some("recommendation")
186 }
187}
188
189#[derive(serde::Deserialize)]
190struct RawCatalogEntry {
191 label: String,
192 field_name: String,
193 #[serde(default)]
194 format: Option<String>,
195 #[serde(default)]
196 interval: bool,
197 #[serde(default)]
198 historical: bool,
199}
200
201#[derive(serde::Deserialize)]
202struct RawFieldsJson {
203 defaults: HashMap<String, Vec<String>>,
204 catalog: HashMap<String, HashMap<String, RawCatalogEntry>>,
205 presets: HashMap<String, Vec<String>>,
206 markets: Vec<RawMarket>,
207 types: Vec<RawType>,
208 symbol_types: Vec<RawSymbolType>,
209 #[serde(default)]
210 index_symbols: Vec<RawIndexSymbol>,
211 #[serde(default)]
212 sectors: Vec<RawNamedValue>,
213 #[serde(default)]
214 countries: Vec<RawNamedValue>,
215 #[serde(default)]
216 industries: Vec<RawNamedValue>,
217 #[serde(default)]
218 exchanges: Vec<RawNamedValue>,
219}
220
221#[derive(serde::Deserialize)]
222struct RawMarket {
223 #[serde(rename = "const")]
224 const_name: String,
225 value: String,
226}
227
228#[derive(serde::Deserialize)]
229struct RawType {
230 #[serde(rename = "const")]
231 const_name: String,
232 value: String,
233}
234
235#[derive(serde::Deserialize)]
236struct RawSymbolType {
237 #[serde(rename = "const")]
238 const_name: String,
239 value: Vec<String>,
240}
241
242#[derive(serde::Deserialize)]
243struct RawIndexSymbol {
244 #[serde(rename = "const")]
245 const_name: String,
246 value: String,
247 #[serde(default)]
248 label: String,
249}
250
251#[derive(serde::Deserialize)]
252struct RawNamedValue {
253 #[serde(rename = "const")]
254 const_name: String,
255 value: String,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
260pub struct IndexSymbolDef {
261 pub const_name: String,
263 pub value: String,
265 pub label: String,
267}
268
269struct FieldsCatalog {
270 defaults: HashMap<String, Vec<String>>,
271 catalog: HashMap<String, HashMap<String, FieldDef>>,
272 presets: HashMap<String, Vec<String>>,
273 markets: Vec<Market>,
274 types: Vec<TypeFilter>,
275 symbol_types: Vec<SymbolType>,
276 index_symbols: Vec<IndexSymbolDef>,
277 sectors: Vec<NamedValue>,
278 countries: Vec<NamedValue>,
279 industries: Vec<NamedValue>,
280 exchanges: Vec<NamedValue>,
281}
282
283fn map_named(raw: Vec<RawNamedValue>) -> Vec<NamedValue> {
284 raw.into_iter()
285 .map(|n| NamedValue {
286 const_name: n.const_name,
287 value: n.value,
288 })
289 .collect()
290}
291
292fn find_named<'a>(items: &'a [NamedValue], const_name: &str) -> Option<&'a NamedValue> {
293 items.iter().find(|n| n.const_name == const_name)
294}
295
296fn named_value_str<'a>(items: &'a [NamedValue], const_name: &str) -> Option<&'a str> {
297 find_named(items, const_name).map(|n| n.value.as_str())
298}
299
300fn resolve_named_wire<'a>(items: &'a [NamedValue], token: &str) -> Option<&'a str> {
301 let upper = token.to_ascii_uppercase().replace(' ', "_");
302 if let Some(v) = named_value_str(items, &upper) {
303 return Some(v);
304 }
305 items
306 .iter()
307 .find(|n| n.value.eq_ignore_ascii_case(token))
308 .map(|n| n.value.as_str())
309}
310
311impl FieldsCatalog {
312 fn load() -> Self {
313 let raw: RawFieldsJson = serde_json::from_str(include_str!("../../data/fields.json"))
314 .expect("fields.json must deserialize");
315
316 let catalog = raw
317 .catalog
318 .into_iter()
319 .map(|(asset, entries)| {
320 let mapped = entries
321 .into_iter()
322 .map(|(name, entry)| {
323 (
324 name,
325 FieldDef {
326 label: entry.label,
327 field_name: entry.field_name,
328 format: entry.format,
329 interval: entry.interval,
330 historical: entry.historical,
331 },
332 )
333 })
334 .collect();
335 (asset, mapped)
336 })
337 .collect();
338
339 Self {
340 defaults: raw.defaults,
341 presets: raw.presets,
342 catalog,
343 markets: raw
344 .markets
345 .into_iter()
346 .map(|m| Market {
347 const_name: m.const_name,
348 value: m.value,
349 })
350 .collect(),
351 types: raw
352 .types
353 .into_iter()
354 .map(|t| TypeFilter {
355 const_name: t.const_name,
356 value: t.value,
357 })
358 .collect(),
359 symbol_types: raw
360 .symbol_types
361 .into_iter()
362 .map(|st| SymbolType {
363 const_name: st.const_name,
364 values: st.value,
365 })
366 .collect(),
367 index_symbols: raw
368 .index_symbols
369 .into_iter()
370 .map(|idx| IndexSymbolDef {
371 const_name: idx.const_name,
372 value: idx.value,
373 label: idx.label,
374 })
375 .collect(),
376 sectors: map_named(raw.sectors),
377 countries: map_named(raw.countries),
378 industries: map_named(raw.industries),
379 exchanges: map_named(raw.exchanges),
380 }
381 }
382
383 fn field(&self, asset: Asset, const_name: &str) -> Option<FieldDef> {
384 self.catalog.get(asset.as_str())?.get(const_name).cloned()
385 }
386
387 fn defaults_for(&self, asset: Asset) -> Vec<FieldDef> {
388 self.defaults
389 .get(asset.as_str())
390 .map(|names| {
391 names
392 .iter()
393 .filter_map(|name| self.field(asset, name))
394 .collect()
395 })
396 .unwrap_or_default()
397 }
398
399 fn resolve_preset_ref(&self, reference: &str) -> Option<FieldDef> {
400 let (asset_str, const_name) = reference.split_once('.')?;
401 let asset = Asset::parse(asset_str)?;
402 self.field(asset, const_name)
403 }
404
405 fn preset(&self, name: &str) -> Result<Vec<FieldDef>> {
406 let refs = self
407 .presets
408 .get(name)
409 .ok_or_else(|| TvscreenerError::InvalidRequest(format!("unknown preset: {name}")))?;
410 let fields: Vec<FieldDef> = refs
411 .iter()
412 .filter_map(|reference| self.resolve_preset_ref(reference))
413 .collect();
414 if fields.is_empty() {
415 return Err(TvscreenerError::InvalidRequest(format!(
416 "preset `{name}` resolved to no fields"
417 )));
418 }
419 Ok(fields)
420 }
421
422 fn search(&self, asset: Asset, query: &str) -> Vec<FieldDef> {
423 let query = query.to_ascii_lowercase();
424 self.catalog
425 .get(asset.as_str())
426 .map(|entries| {
427 entries
428 .iter()
429 .filter(|(name, field)| {
430 name.to_ascii_lowercase().contains(&query)
431 || field.label.to_ascii_lowercase().contains(&query)
432 || field.field_name.to_ascii_lowercase().contains(&query)
433 })
434 .map(|(_, field)| field.clone())
435 .collect()
436 })
437 .unwrap_or_default()
438 }
439}
440
441static CATALOG: OnceLock<FieldsCatalog> = OnceLock::new();
442
443fn catalog() -> &'static FieldsCatalog {
444 CATALOG.get_or_init(FieldsCatalog::load)
445}
446
447#[must_use]
449pub(crate) fn field(asset: Asset, const_name: &str) -> Option<FieldDef> {
450 catalog().field(asset, const_name)
451}
452
453#[must_use]
458pub fn resolve_field(asset: Asset, name: &str) -> Option<(String, FieldDef)> {
459 let trimmed = name.trim();
460 if trimmed.is_empty() {
461 return None;
462 }
463 let upper = trimmed.to_ascii_uppercase().replace(' ', "_");
464 if let Some(def) = catalog().field(asset, &upper) {
465 return Some((upper, def));
466 }
467 let entries = catalog().catalog.get(asset.as_str())?;
468 let lower = trimmed.to_ascii_lowercase();
469 entries.iter().find_map(|(const_name, def)| {
470 if def.field_name.eq_ignore_ascii_case(trimmed)
471 || def.label.to_ascii_lowercase() == lower
472 || const_name.eq_ignore_ascii_case(&upper)
473 {
474 Some((const_name.clone(), def.clone()))
475 } else {
476 None
477 }
478 })
479}
480
481pub fn require_field(asset: Asset, const_name: &str) -> Result<FieldDef> {
487 field(asset, const_name).ok_or_else(|| {
488 TvscreenerError::InvalidRequest(format!(
489 "unknown field `{const_name}` for asset `{}`",
490 asset.as_str()
491 ))
492 })
493}
494
495pub fn get_preset(name: &str) -> Result<Vec<FieldDef>> {
501 catalog().preset(name)
502}
503
504#[must_use]
506pub fn list_presets() -> Vec<String> {
507 let mut names: Vec<String> = catalog().presets.keys().cloned().collect();
508 names.sort();
509 names
510}
511
512#[must_use]
514pub fn search_fields(asset: Asset, query: &str) -> Vec<FieldDef> {
515 catalog().search(asset, query)
516}
517
518#[must_use]
520pub fn list_fields(asset: Asset) -> Vec<FieldDef> {
521 catalog()
522 .catalog
523 .get(asset.as_str())
524 .map(|entries| entries.values().cloned().collect())
525 .unwrap_or_default()
526}
527
528#[must_use]
530pub fn catalog_len(asset: Asset) -> usize {
531 catalog()
532 .catalog
533 .get(asset.as_str())
534 .map_or(0, HashMap::len)
535}
536
537#[must_use]
539pub fn all_index_symbols() -> &'static [IndexSymbolDef] {
540 &catalog().index_symbols
541}
542
543#[must_use]
545pub fn index_symbol_value(const_name: &str) -> Option<&'static str> {
546 catalog()
547 .index_symbols
548 .iter()
549 .find(|idx| idx.const_name == const_name)
550 .map(|idx| idx.value.as_str())
551}
552
553#[must_use]
557pub fn to_symbolset_wire(symbol: &str) -> String {
558 if symbol.starts_with("SYML:") {
559 symbol.to_string()
560 } else {
561 format!("SYML:{symbol}")
562 }
563}
564
565#[must_use]
567pub fn all_markets() -> &'static [Market] {
568 &catalog().markets
569}
570
571#[must_use]
573pub fn market(const_name: &str) -> Option<Market> {
574 catalog()
575 .markets
576 .iter()
577 .find(|m| m.const_name == const_name)
578 .cloned()
579}
580
581#[must_use]
583pub fn all_sectors() -> &'static [NamedValue] {
584 &catalog().sectors
585}
586
587#[must_use]
589pub fn sector_value(const_name: &str) -> Option<&'static str> {
590 named_value_str(&catalog().sectors, const_name)
591}
592
593#[must_use]
595pub fn resolve_sector(token: &str) -> Option<&'static str> {
596 resolve_named_wire(&catalog().sectors, token)
597}
598
599#[must_use]
601pub fn all_countries() -> &'static [NamedValue] {
602 &catalog().countries
603}
604
605#[must_use]
607pub fn country_value(const_name: &str) -> Option<&'static str> {
608 named_value_str(&catalog().countries, const_name)
609}
610
611#[must_use]
613pub fn resolve_country(token: &str) -> Option<&'static str> {
614 resolve_named_wire(&catalog().countries, token)
615}
616
617#[must_use]
619pub fn all_industries() -> &'static [NamedValue] {
620 &catalog().industries
621}
622
623#[must_use]
625pub fn industry_value(const_name: &str) -> Option<&'static str> {
626 named_value_str(&catalog().industries, const_name)
627}
628
629#[must_use]
631pub fn resolve_industry(token: &str) -> Option<&'static str> {
632 resolve_named_wire(&catalog().industries, token)
633}
634
635#[must_use]
637pub fn all_exchanges() -> &'static [NamedValue] {
638 &catalog().exchanges
639}
640
641#[must_use]
643pub fn exchange_value(const_name: &str) -> Option<&'static str> {
644 named_value_str(&catalog().exchanges, const_name)
645}
646
647#[must_use]
649pub fn resolve_exchange(token: &str) -> Option<&'static str> {
650 resolve_named_wire(&catalog().exchanges, token)
651}
652
653#[must_use]
655pub const fn all_ratings() -> &'static [RatingBand] {
656 rating::ALL
657}
658
659#[must_use]
661pub fn rating(const_name: &str) -> Option<RatingBand> {
662 rating::ALL
663 .iter()
664 .copied()
665 .find(|r| r.const_name == const_name)
666}
667
668#[must_use]
670pub fn rating_find(value: f64) -> RatingBand {
671 for band in rating::ALL {
672 if band.const_name == "UNKNOWN" {
673 continue;
674 }
675 if band.contains(value) {
676 return *band;
677 }
678 }
679 rating::UNKNOWN
680}
681
682#[must_use]
684pub fn type_filter(const_name: &str) -> Option<TypeFilter> {
685 catalog()
686 .types
687 .iter()
688 .find(|t| t.const_name == const_name)
689 .cloned()
690}
691
692#[must_use]
694pub fn symbol_type(const_name: &str) -> Option<SymbolType> {
695 catalog()
696 .symbol_types
697 .iter()
698 .find(|st| st.const_name == const_name)
699 .cloned()
700}
701
702#[must_use]
704pub fn default_crypto_fields() -> Vec<FieldDef> {
705 catalog().defaults_for(Asset::Crypto)
706}
707
708#[must_use]
710pub fn default_stock_fields() -> Vec<FieldDef> {
711 catalog().defaults_for(Asset::Stock)
712}
713
714#[must_use]
716pub fn default_forex_fields() -> Vec<FieldDef> {
717 catalog().defaults_for(Asset::Forex)
718}
719
720#[must_use]
722pub fn default_bond_fields() -> Vec<FieldDef> {
723 let fields = catalog().defaults_for(Asset::Bond);
724 if fields.is_empty() {
725 get_preset("bond_basic").unwrap_or_default()
726 } else {
727 fields
728 }
729}
730
731#[must_use]
733pub fn default_futures_fields() -> Vec<FieldDef> {
734 catalog().defaults_for(Asset::Futures)
735}
736
737#[must_use]
739pub fn default_coin_fields() -> Vec<FieldDef> {
740 catalog().defaults_for(Asset::Coin)
741}
742
743#[must_use]
745pub fn default_fields(asset: Asset) -> Vec<FieldDef> {
746 match asset {
747 Asset::Stock => default_stock_fields(),
748 Asset::Crypto => default_crypto_fields(),
749 Asset::Forex => default_forex_fields(),
750 Asset::Bond => default_bond_fields(),
751 Asset::Futures => default_futures_fields(),
752 Asset::Coin => default_coin_fields(),
753 }
754}
755
756#[must_use]
762pub fn crypto_volume_24h_in_usd() -> FieldDef {
763 require_field(Asset::Crypto, "VOLUME_24H_IN_USD").expect("VOLUME_24H_IN_USD")
764}
765
766#[must_use]
772pub fn stock_market_capitalization() -> FieldDef {
773 require_field(Asset::Stock, "MARKET_CAPITALIZATION").expect("MARKET_CAPITALIZATION")
774}
775
776#[must_use]
782pub fn forex_name() -> FieldDef {
783 require_field(Asset::Forex, "NAME").expect("NAME")
784}
785
786#[must_use]
792pub fn bond_volume() -> FieldDef {
793 require_field(Asset::Bond, "VOLUME").expect("VOLUME")
794}
795
796#[must_use]
802pub fn futures_volume() -> FieldDef {
803 require_field(Asset::Futures, "VOLUME").expect("VOLUME")
804}
805
806#[must_use]
812pub fn coin_market_cap() -> FieldDef {
813 require_field(Asset::Coin, "MARKET_CAP").expect("MARKET_CAP")
814}
815
816#[must_use]
822pub fn crypto_rsi_14() -> FieldDef {
823 require_field(Asset::Crypto, "RELATIVE_STRENGTH_INDEX_14").expect("RELATIVE_STRENGTH_INDEX_14")
824}
825
826#[must_use]
832pub fn crypto_price() -> FieldDef {
833 require_field(Asset::Crypto, "PRICE").expect("PRICE")
834}
835
836pub mod index_symbol {
840 include!("generated/index_symbol.rs");
841}
842
843pub mod sector {
847 include!("generated/sector.rs");
848}
849
850pub mod country {
854 include!("generated/country.rs");
855}
856
857pub mod industry {
861 include!("generated/industry.rs");
862}
863
864pub mod exchange {
868 include!("generated/exchange.rs");
869}
870
871pub mod rating {
875 use super::RatingBand;
876 include!("generated/rating.rs");
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882
883 #[test]
884 fn resolve_field_const_technical_and_label() {
885 let (name, def) = resolve_field(Asset::Crypto, "PRICE").expect("const");
886 assert_eq!(name, "PRICE");
887 assert_eq!(def.field_name, "close");
888
889 let (name, def) = resolve_field(Asset::Crypto, "close").expect("technical");
890 assert_eq!(name, "PRICE");
891 assert_eq!(def.field_name, "close");
892
893 let by_label = resolve_field(Asset::Crypto, &def.label);
894 assert!(by_label.is_some());
895 assert!(resolve_field(Asset::Crypto, "not_a_real_field_xyz").is_none());
896 }
897
898 #[test]
899 fn require_field_unknown_is_invalid() {
900 let err = require_field(Asset::Crypto, "NOPE").expect_err("missing");
901 assert!(matches!(err, crate::TvscreenerError::InvalidRequest(_)));
902 assert!(err.to_string().contains("crypto"));
903 }
904
905 #[test]
906 fn asset_parse_round_trip() {
907 assert_eq!(Asset::parse("crypto"), Some(Asset::Crypto));
908 assert_eq!(Asset::parse("stock"), Some(Asset::Stock));
909 assert_eq!(Asset::parse("forex"), Some(Asset::Forex));
910 assert_eq!(Asset::parse("bond"), Some(Asset::Bond));
911 assert_eq!(Asset::parse("futures"), Some(Asset::Futures));
912 assert_eq!(Asset::parse("coin"), Some(Asset::Coin));
913 assert_eq!(Asset::parse("nope"), None);
914 assert_eq!(Asset::Crypto.as_str(), "crypto");
915 }
916
917 #[test]
918 fn default_crypto_fields_non_empty() {
919 let fields = default_crypto_fields();
920 assert!(fields.len() > 50);
921 assert!(fields.iter().any(|f| f.field_name == "name"));
922 }
923
924 #[test]
925 fn get_preset_crypto_price() {
926 let fields = get_preset("crypto_price").expect("preset");
927 assert!(fields.iter().any(|f| f.label == "Name"));
928 assert_eq!(fields.len(), 7);
929 }
930
931 #[test]
932 fn list_presets_includes_crypto_price() {
933 let names = list_presets();
934 assert!(names.contains(&"crypto_price".to_string()));
935 }
936
937 #[test]
938 fn search_fields_finds_volume() {
939 let hits = search_fields(Asset::Crypto, "volume");
940 assert!(hits
941 .iter()
942 .any(|f| f.field_name.contains("vol") || f.label.contains("Volume")));
943 }
944
945 #[test]
946 fn full_catalog_is_much_larger_than_defaults() {
947 assert!(catalog_len(Asset::Crypto) > 3000);
948 assert!(catalog_len(Asset::Stock) > 3000);
949 assert_eq!(list_fields(Asset::Crypto).len(), catalog_len(Asset::Crypto));
950 assert!(default_crypto_fields().len() < catalog_len(Asset::Crypto));
951 }
952
953 #[test]
954 fn index_symbol_sp500_wire_and_catalog() {
955 assert_eq!(index_symbol::SP500, "SP;SPX");
956 assert_eq!(index_symbol_value("SP500"), Some("SP;SPX"));
957 assert_eq!(to_symbolset_wire("SP;SPX"), "SYML:SP;SPX");
958 assert_eq!(to_symbolset_wire("SYML:SP;SPX"), "SYML:SP;SPX");
959 assert_eq!(
960 all_index_symbols().len(),
961 index_symbol::ALL_CONST_NAMES.len()
962 );
963 assert!(all_index_symbols().len() >= 50);
964 }
965
966 #[test]
967 fn market_america_value() {
968 assert_eq!(Market::america().value, "america");
969 }
970
971 #[test]
972 fn sector_country_industry_exchange_catalog() {
973 assert_eq!(sector::TECHNOLOGY_SERVICES, "Technology Services");
974 assert_eq!(
975 sector_value("TECHNOLOGY_SERVICES"),
976 Some("Technology Services")
977 );
978 assert_eq!(
979 resolve_sector("Technology Services"),
980 Some("Technology Services")
981 );
982 assert_eq!(all_sectors().len(), sector::ALL_CONST_NAMES.len());
983 assert_eq!(country::UNITED_STATES, "United States");
984 assert_eq!(resolve_country("UNITED_STATES"), Some("United States"));
985 assert_eq!(resolve_country("United States"), Some("United States"));
986 assert_eq!(all_countries().len(), country::ALL_CONST_NAMES.len());
987 assert_eq!(industry::SEMICONDUCTORS, "Semiconductors");
988 assert_eq!(resolve_industry("SEMICONDUCTORS"), Some("Semiconductors"));
989 assert_eq!(all_industries().len(), industry::ALL_CONST_NAMES.len());
990 assert_eq!(exchange::NASDAQ, "NASDAQ");
991 assert_eq!(resolve_exchange("NASDAQ"), Some("NASDAQ"));
992 assert_eq!(all_exchanges().len(), exchange::ALL_CONST_NAMES.len());
993 assert_eq!(all_ratings().len(), rating::ALL_CONST_NAMES.len());
994 }
995
996 #[test]
997 fn rating_find_bands() {
998 assert_eq!(rating_find(0.7).const_name, "STRONG_BUY");
999 assert_eq!(rating_find(0.0).const_name, "NEUTRAL");
1000 assert_eq!(rating_find(-0.9).const_name, "STRONG_SELL");
1001 assert_eq!(rating_find(f64::NAN).const_name, "UNKNOWN");
1002 assert_eq!(rating("BUY").map(|r| r.label), Some("Buy"));
1003 assert_eq!(rating::BUY.range(), Some([0.1, 0.5]));
1004 }
1005
1006 #[test]
1007 fn bond_defaults_or_preset_fallback() {
1008 let fields = default_bond_fields();
1009 assert_ne!(fields.len(), 0);
1010 }
1011}