finance_query/models/screeners/
query.rs1use crate::constants::screener_query::{
2 LogicalOperator, Operator, QuoteType, SortType, equity_fields, fund_fields,
3};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct ScreenerQuery {
28 pub size: u32,
30
31 pub offset: u32,
33
34 pub sort_type: SortType,
36
37 pub sort_field: String,
39
40 pub include_fields: Vec<String>,
42
43 pub top_operator: LogicalOperator,
45
46 pub query: QueryGroup,
48
49 pub quote_type: QuoteType,
51}
52
53const DEFAULT_EQUITY_FIELDS: &[&str] = &[
55 "ticker",
56 "companyshortname",
57 equity_fields::INTRADAY_PRICE,
58 equity_fields::INTRADAY_PRICE_CHANGE,
59 equity_fields::PERCENT_CHANGE,
60 equity_fields::INTRADAY_MARKET_CAP,
61 equity_fields::DAY_VOLUME,
62 equity_fields::AVG_DAILY_VOL_3M,
63 equity_fields::PE_RATIO,
64 equity_fields::FIFTY_TWO_WK_PCT_CHANGE,
65];
66
67const DEFAULT_FUND_FIELDS: &[&str] = &[
69 "ticker",
70 "companyshortname",
71 fund_fields::INTRADAY_PRICE,
72 fund_fields::INTRADAY_PRICE_CHANGE,
73 fund_fields::CATEGORY_NAME,
74 fund_fields::PERFORMANCE_RATING,
75 fund_fields::RISK_RATING,
76];
77
78impl Default for ScreenerQuery {
79 fn default() -> Self {
80 Self {
81 size: 25,
82 offset: 0,
83 sort_type: SortType::Desc,
84 sort_field: equity_fields::INTRADAY_MARKET_CAP.to_string(),
85 include_fields: DEFAULT_EQUITY_FIELDS
86 .iter()
87 .map(|s| s.to_string())
88 .collect(),
89 top_operator: LogicalOperator::And,
90 query: QueryGroup::new(LogicalOperator::And),
91 quote_type: QuoteType::Equity,
92 }
93 }
94}
95
96impl ScreenerQuery {
97 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn size(mut self, size: u32) -> Self {
104 self.size = size.min(250);
105 self
106 }
107
108 pub fn offset(mut self, offset: u32) -> Self {
110 self.offset = offset;
111 self
112 }
113
114 pub fn sort_by(mut self, field: impl Into<String>, ascending: bool) -> Self {
121 self.sort_field = field.into();
122 self.sort_type = if ascending {
123 SortType::Asc
124 } else {
125 SortType::Desc
126 };
127 self
128 }
129
130 pub fn quote_type(mut self, quote_type: QuoteType) -> Self {
135 self.quote_type = quote_type;
136 let (default_fields, default_sort) = match quote_type {
138 QuoteType::Equity => (DEFAULT_EQUITY_FIELDS, equity_fields::INTRADAY_MARKET_CAP),
139 QuoteType::MutualFund => (DEFAULT_FUND_FIELDS, fund_fields::INTRADAY_PRICE),
140 };
141 self.include_fields = default_fields.iter().map(|s| s.to_string()).collect();
142 self.sort_field = default_sort.to_string();
143 self
144 }
145
146 pub fn include_fields(mut self, fields: Vec<String>) -> Self {
148 self.include_fields = fields;
149 self
150 }
151
152 pub fn add_include_field(mut self, field: impl Into<String>) -> Self {
154 self.include_fields.push(field.into());
155 self
156 }
157
158 pub fn top_operator(mut self, op: LogicalOperator) -> Self {
160 self.top_operator = op;
161 self
162 }
163
164 pub fn add_condition(mut self, condition: QueryCondition) -> Self {
176 let mut or_group = QueryGroup::new(LogicalOperator::Or);
178 or_group.add_operand(QueryOperand::Condition(condition));
179 self.query.add_operand(QueryOperand::Group(or_group));
180 self
181 }
182
183 pub fn add_or_conditions(mut self, conditions: Vec<QueryCondition>) -> Self {
198 let mut or_group = QueryGroup::new(LogicalOperator::Or);
199 for condition in conditions {
200 or_group.add_operand(QueryOperand::Condition(condition));
201 }
202 self.query.add_operand(QueryOperand::Group(or_group));
203 self
204 }
205
206 pub fn most_shorted() -> Self {
210 Self::new()
211 .sort_by(equity_fields::SHORT_PCT_FLOAT, false)
212 .add_condition(QueryCondition::new(equity_fields::REGION, Operator::Eq).value_str("us"))
213 .add_condition(
214 QueryCondition::new(equity_fields::AVG_DAILY_VOL_3M, Operator::Gt).value(200000),
215 )
216 }
217
218 pub fn high_dividend() -> Self {
222 Self::new()
223 .sort_by(equity_fields::FORWARD_DIV_YIELD, false)
224 .add_condition(QueryCondition::new(equity_fields::REGION, Operator::Eq).value_str("us"))
225 .add_condition(
226 QueryCondition::new(equity_fields::FORWARD_DIV_YIELD, Operator::Gt).value(3.0),
227 )
228 .add_condition(
229 QueryCondition::new(equity_fields::AVG_DAILY_VOL_3M, Operator::Gt).value(100000),
230 )
231 }
232
233 pub fn large_cap_growth() -> Self {
237 Self::new()
238 .sort_by(equity_fields::INTRADAY_MARKET_CAP, false)
239 .add_condition(QueryCondition::new(equity_fields::REGION, Operator::Eq).value_str("us"))
240 .add_condition(
241 QueryCondition::new(equity_fields::INTRADAY_MARKET_CAP, Operator::Gt)
242 .value(10_000_000_000.0f64),
243 )
244 .add_condition(QueryCondition::new(equity_fields::EPS_GROWTH, Operator::Gt).value(0.0))
245 }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct QueryGroup {
251 pub operator: LogicalOperator,
253 pub operands: Vec<QueryOperand>,
255}
256
257impl QueryGroup {
258 pub fn new(operator: LogicalOperator) -> Self {
260 Self {
261 operator,
262 operands: Vec::new(),
263 }
264 }
265
266 pub fn add_operand(&mut self, operand: QueryOperand) {
268 self.operands.push(operand);
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274#[serde(untagged)]
275pub enum QueryOperand {
276 Condition(QueryCondition),
278 Group(QueryGroup),
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct QueryCondition {
285 pub operator: Operator,
287 pub operands: Vec<QueryValue>,
289}
290
291impl QueryCondition {
292 pub fn new(field: impl Into<String>, operator: Operator) -> Self {
299 Self {
300 operator,
301 operands: vec![QueryValue::String(field.into())],
302 }
303 }
304
305 pub fn value<T: Into<f64>>(mut self, value: T) -> Self {
307 self.operands.push(QueryValue::Number(value.into()));
308 self
309 }
310
311 pub fn value_str(mut self, value: impl Into<String>) -> Self {
313 self.operands.push(QueryValue::String(value.into()));
314 self
315 }
316
317 pub fn between<T: Into<f64>>(mut self, min: T, max: T) -> Self {
319 self.operands.push(QueryValue::Number(min.into()));
320 self.operands.push(QueryValue::Number(max.into()));
321 self
322 }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(untagged)]
328pub enum QueryValue {
329 String(String),
331 Number(f64),
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_default_query() {
341 let query = ScreenerQuery::new();
342 assert_eq!(query.size, 25);
343 assert_eq!(query.offset, 0);
344 assert_eq!(query.quote_type, QuoteType::Equity);
345 }
346
347 #[test]
348 fn test_most_shorted_preset() {
349 let query = ScreenerQuery::most_shorted();
350 assert_eq!(query.sort_field, equity_fields::SHORT_PCT_FLOAT);
351 assert_eq!(query.sort_type, SortType::Desc);
352 }
353
354 #[test]
355 fn test_condition_builder() {
356 let condition = QueryCondition::new("region", Operator::Eq).value_str("us");
357 assert_eq!(condition.operator, Operator::Eq);
358 assert_eq!(condition.operands.len(), 2);
359 }
360
361 #[test]
362 fn test_query_serialization() {
363 let query = ScreenerQuery::new()
364 .size(10)
365 .add_condition(QueryCondition::new("region", Operator::Eq).value_str("us"));
366
367 let json = serde_json::to_string(&query).unwrap();
368 assert!(json.contains("\"size\":10"));
369 assert!(json.contains("\"region\""));
370 }
371}