1use super::Screener;
9use crate::error::{Result, TvscreenerError};
10use crate::field::{
11 default_stock_fields, stock_market_capitalization, symbol_type, type_filter, Market,
12 SymbolType, TypeFilter,
13};
14use crate::filter::FilterOperator;
15use serde_json::json;
16use std::collections::HashSet;
17
18const SYMBOL_TYPE_TO_TYPE: &[(&str, &str)] = &[
19 ("COMMON_STOCK", "STOCK"),
20 ("DEPOSITORY_RECEIPT", "DEPOSITORY_RECEIPT"),
21 ("ETF", "FUND"),
22 ("MUTUAL_FUND", "FUND"),
23 ("REIT", "FUND"),
24 ("PREFERRED_STOCK", "STOCK"),
25 ("ETN", "STRUCTURED"),
26 ("STRUCTURED", "STRUCTURED"),
27 ("UIT", "FUND"),
28];
29
30#[derive(Debug, Clone)]
32pub struct StockScreener {
33 inner: Screener,
34}
35
36impl StockScreener {
37 pub fn new() -> Self {
39 let mut inner = Screener::new("global");
40 inner.set_url(crate::util::get_url("global"));
41 inner.set_all_fields_fn(default_stock_fields);
42 inner.select(default_stock_fields());
43 inner.set_markets([Market::america().value]);
44 inner.sort_by(stock_market_capitalization().field_name, false);
45 Self { inner }
46 }
47
48 pub fn set_markets(&mut self, markets: impl IntoIterator<Item = Market>) -> &mut Self {
50 let selected: Vec<Market> = markets.into_iter().collect();
51 let wire_values: Vec<String> = if selected.iter().any(|m| m.const_name == "ALL") {
52 crate::field::all_markets()
53 .iter()
54 .map(|m| m.value.clone())
55 .collect()
56 } else {
57 selected.into_iter().map(|m| m.value).collect()
58 };
59 self.inner.set_markets(wire_values);
60 self
61 }
62
63 pub fn set_symbol_types(
70 &mut self,
71 symbol_types: impl IntoIterator<Item = impl AsRef<str>>,
72 ) -> Result<&mut Self> {
73 let mut requested = Vec::new();
74 for name in symbol_types {
75 let const_name = name.as_ref();
76 let st = symbol_type(const_name).ok_or_else(|| {
77 TvscreenerError::InvalidRequest(format!("unknown symbol type: {const_name}"))
78 })?;
79 requested.push(st);
80 }
81
82 let type_values = collect_type_filters(&requested)?;
83 emit_type_filters(&mut self.inner, &type_values);
84 emit_subtype_filters(&mut self.inner, requested);
85
86 Ok(self)
87 }
88}
89
90fn collect_type_filters(requested: &[SymbolType]) -> Result<Vec<TypeFilter>> {
91 let mut types_to_add = HashSet::new();
92 for st in requested {
93 let mapped = SYMBOL_TYPE_TO_TYPE
94 .iter()
95 .find(|(sym, _)| *sym == st.const_name)
96 .map(|(_, ty)| *ty)
97 .ok_or_else(|| {
98 TvscreenerError::InvalidRequest(format!(
99 "symbol type `{}` has no type mapping",
100 st.const_name
101 ))
102 })?;
103 types_to_add.insert(mapped.to_string());
104 }
105 Ok(types_to_add
106 .into_iter()
107 .filter_map(|name| type_filter(&name))
108 .collect())
109}
110
111fn emit_type_filters(inner: &mut Screener, type_values: &[TypeFilter]) {
112 if type_values.len() > 1 {
113 inner
114 .add_filter(
115 "type",
116 FilterOperator::InRange,
117 type_values.iter().map(|t| json!(t.value)),
118 )
119 .expect("type is always an allowed filter left");
120 } else if let Some(one) = type_values.first() {
121 inner
122 .add_filter("type", FilterOperator::Equal, [json!(one.value)])
123 .expect("type is always an allowed filter left");
124 }
125}
126
127fn emit_subtype_filters(inner: &mut Screener, mut symbol_types_to_apply: Vec<SymbolType>) {
128 if symbol_types_to_apply
129 .iter()
130 .any(|st| st.const_name == "COMMON_STOCK")
131 && !symbol_types_to_apply
132 .iter()
133 .any(|st| st.const_name == "DEPOSITORY_RECEIPT")
134 {
135 if let Some(dr) = symbol_type("DEPOSITORY_RECEIPT") {
136 symbol_types_to_apply.push(dr);
137 }
138 }
139
140 for st in symbol_types_to_apply {
141 inner
142 .add_filter(
143 "subtype",
144 FilterOperator::InRange,
145 st.values.iter().map(|value| json!(value)),
146 )
147 .expect("subtype is always an allowed filter left");
148 }
149}
150
151impl_typed_screener_common!(StockScreener);
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use serde_json::{json, Value};
157
158 #[test]
159 fn set_symbol_types_common_stock_adds_depository_receipt() {
160 let mut ss = StockScreener::new();
161 ss.set_symbol_types(["COMMON_STOCK"]).expect("types");
162 let payload = ss.inner().build_payload().expect("payload");
163 let filters = payload["filter"].as_array().expect("filter");
164 let subtype_rights: Vec<&Value> = filters
165 .iter()
166 .filter(|f| f["left"] == "subtype")
167 .flat_map(|f| f["right"].as_array().into_iter().flatten())
168 .collect();
169 assert!(
170 subtype_rights.len() >= 2,
171 "COMMON_STOCK should also emit DEPOSITORY_RECEIPT subtypes; got {subtype_rights:?}"
172 );
173 }
174
175 #[test]
176 fn set_symbol_types_unknown_is_invalid() {
177 let mut ss = StockScreener::new();
178 let err = ss
179 .set_symbol_types(["NOT_A_TYPE"])
180 .expect_err("unknown type");
181 assert!(matches!(err, TvscreenerError::InvalidRequest(_)));
182 }
183
184 #[test]
185 fn set_symbol_types_multi_promotes_type_to_in_range() {
186 let mut ss = StockScreener::new();
187 ss.set_symbol_types(["ETF", "COMMON_STOCK"]).expect("types");
188 let payload = ss.inner().build_payload().expect("payload");
189 let filters = payload["filter"].as_array().expect("filter");
190 let type_filter = filters
191 .iter()
192 .find(|f| f["left"] == "type")
193 .expect("type filter");
194 assert_eq!(type_filter["operation"], json!("in_range"));
195 assert!(type_filter["right"].as_array().unwrap().len() >= 2);
196 }
197}