use crate::models::screeners::condition::{
LogicalOperator, QueryCondition, QueryGroup, QueryOperand, ScreenerField, ScreenerFieldExt,
};
use crate::models::screeners::fields::{EquityField, FundField};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum QuoteType {
#[default]
#[serde(rename = "EQUITY")]
Equity,
#[serde(rename = "MUTUALFUND")]
MutualFund,
}
impl std::str::FromStr for QuoteType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().replace(['-', '_'], "").as_str() {
"equity" | "stock" | "stocks" => Ok(QuoteType::Equity),
"mutualfund" | "fund" | "funds" => Ok(QuoteType::MutualFund),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum SortType {
#[serde(rename = "ASC")]
Asc,
#[default]
#[serde(rename = "DESC")]
Desc,
}
impl std::str::FromStr for SortType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"asc" | "ascending" => Ok(SortType::Asc),
"desc" | "descending" => Ok(SortType::Desc),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ScreenerQuery<F: ScreenerField = EquityField> {
pub size: u32,
pub offset: u32,
pub sort_type: SortType,
pub sort_field: F,
pub include_fields: Vec<F>,
pub top_operator: LogicalOperator,
pub query: QueryGroup<F>,
pub quote_type: QuoteType,
}
pub type EquityScreenerQuery = ScreenerQuery<EquityField>;
pub type FundScreenerQuery = ScreenerQuery<FundField>;
impl Default for ScreenerQuery<EquityField> {
fn default() -> Self {
Self {
size: 25,
offset: 0,
sort_type: SortType::Desc,
sort_field: EquityField::IntradayMarketCap,
include_fields: vec![
EquityField::Ticker,
EquityField::CompanyShortName,
EquityField::IntradayPrice,
EquityField::IntradayPriceChange,
EquityField::PercentChange,
EquityField::IntradayMarketCap,
EquityField::DayVolume,
EquityField::AvgDailyVol3M,
EquityField::PeRatio,
EquityField::FiftyTwoWkPctChange,
],
top_operator: LogicalOperator::And,
query: QueryGroup::new(LogicalOperator::And),
quote_type: QuoteType::Equity,
}
}
}
impl Default for ScreenerQuery<FundField> {
fn default() -> Self {
Self {
size: 25,
offset: 0,
sort_type: SortType::Desc,
sort_field: FundField::IntradayPrice,
include_fields: vec![
FundField::Ticker,
FundField::CompanyShortName,
FundField::IntradayPrice,
FundField::IntradayPriceChange,
FundField::CategoryName,
FundField::PerformanceRating,
FundField::RiskRating,
],
top_operator: LogicalOperator::And,
query: QueryGroup::new(LogicalOperator::And),
quote_type: QuoteType::MutualFund,
}
}
}
impl<F: ScreenerField> ScreenerQuery<F> {
pub fn new() -> Self
where
Self: Default,
{
Self::default()
}
pub fn size(mut self, size: u32) -> Self {
self.size = size.min(250);
self
}
pub fn offset(mut self, offset: u32) -> Self {
self.offset = offset;
self
}
pub fn sort_by(mut self, field: F, ascending: bool) -> Self {
self.sort_field = field;
self.sort_type = if ascending {
SortType::Asc
} else {
SortType::Desc
};
self
}
pub fn top_operator(mut self, op: LogicalOperator) -> Self {
self.top_operator = op;
self
}
pub fn include_fields(mut self, fields: Vec<F>) -> Self {
self.include_fields = fields;
self
}
pub fn add_include_field(mut self, field: F) -> Self {
self.include_fields.push(field);
self
}
pub fn add_condition(mut self, condition: QueryCondition<F>) -> Self {
self.query.add_operand(QueryOperand::Condition(condition));
self
}
pub fn add_or_conditions(mut self, conditions: Vec<QueryCondition<F>>) -> Self {
let mut or_group = QueryGroup::new(LogicalOperator::Or);
for condition in conditions {
or_group.add_operand(QueryOperand::Condition(condition));
}
self.query.add_operand(QueryOperand::Group(or_group));
self
}
}
impl ScreenerQuery<EquityField> {
pub fn most_shorted() -> Self {
Self::new()
.sort_by(EquityField::ShortPctFloat, false)
.add_condition(EquityField::Region.eq_str("us"))
.add_condition(EquityField::AvgDailyVol3M.gt(200_000.0))
}
pub fn high_dividend() -> Self {
Self::new()
.sort_by(EquityField::ForwardDivYield, false)
.add_condition(EquityField::Region.eq_str("us"))
.add_condition(EquityField::ForwardDivYield.gt(3.0))
.add_condition(EquityField::AvgDailyVol3M.gt(100_000.0))
}
pub fn large_cap_growth() -> Self {
Self::new()
.sort_by(EquityField::IntradayMarketCap, false)
.add_condition(EquityField::Region.eq_str("us"))
.add_condition(EquityField::IntradayMarketCap.gt(10_000_000_000.0))
.add_condition(EquityField::EpsGrowth.gt(0.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::screeners::condition::ScreenerFieldExt;
#[test]
fn test_default_equity_query() {
let query = EquityScreenerQuery::new();
assert_eq!(query.size, 25);
assert_eq!(query.offset, 0);
assert_eq!(query.quote_type, QuoteType::Equity);
assert_eq!(query.sort_field, EquityField::IntradayMarketCap);
}
#[test]
fn test_default_fund_query() {
let query = FundScreenerQuery::new();
assert_eq!(query.size, 25);
assert_eq!(query.quote_type, QuoteType::MutualFund);
assert_eq!(query.sort_field, FundField::IntradayPrice);
}
#[test]
fn test_most_shorted_preset() {
let query = EquityScreenerQuery::most_shorted();
assert_eq!(query.sort_field, EquityField::ShortPctFloat);
assert_eq!(query.sort_type, SortType::Desc);
}
#[test]
fn test_high_dividend_preset() {
let query = EquityScreenerQuery::high_dividend();
assert_eq!(query.sort_field, EquityField::ForwardDivYield);
}
#[test]
fn test_large_cap_growth_preset() {
let query = EquityScreenerQuery::large_cap_growth();
assert_eq!(query.sort_field, EquityField::IntradayMarketCap);
}
#[test]
fn test_sort_by_typed_field() {
let query = EquityScreenerQuery::new().sort_by(EquityField::PeRatio, true);
assert_eq!(query.sort_field, EquityField::PeRatio);
assert_eq!(query.sort_type, SortType::Asc);
}
#[test]
fn test_size_capped_at_250() {
let query = EquityScreenerQuery::new().size(9999);
assert_eq!(query.size, 250);
}
#[test]
fn test_query_serializes_sort_field_as_string() {
let query = EquityScreenerQuery::new().sort_by(EquityField::PeRatio, false);
let json = serde_json::to_value(&query).unwrap();
assert_eq!(json["sortField"], "peratio.lasttwelvemonths");
assert_eq!(json["sortType"], "DESC");
}
#[test]
fn test_query_serializes_include_fields_as_strings() {
let query = EquityScreenerQuery::new()
.include_fields(vec![EquityField::Ticker, EquityField::PeRatio]);
let json = serde_json::to_value(&query).unwrap();
let fields = json["includeFields"].as_array().unwrap();
assert_eq!(fields[0], "ticker");
assert_eq!(fields[1], "peratio.lasttwelvemonths");
}
#[test]
fn test_add_condition_adds_directly_to_and_group() {
let query = EquityScreenerQuery::new().add_condition(EquityField::Region.eq_str("us"));
let json = serde_json::to_value(&query).unwrap();
let outer_operands = json["query"]["operands"].as_array().unwrap();
assert_eq!(outer_operands.len(), 1);
assert_eq!(outer_operands[0]["operator"], "eq");
assert_eq!(outer_operands[0]["operands"][0], "region");
}
#[test]
fn test_full_query_serialization() {
let query = EquityScreenerQuery::new()
.size(10)
.add_condition(EquityField::Region.eq_str("us"))
.add_condition(EquityField::AvgDailyVol3M.gt(200_000.0));
let json = serde_json::to_string(&query).unwrap();
assert!(json.contains("\"size\":10"));
assert!(json.contains("\"region\""));
assert!(json.contains("\"avgdailyvol3m\""));
assert!(json.contains("\"EQUITY\""));
}
}