Skip to main content

finance_query/models/discovery/
listings.rs

1//! Historical stock-directory requests and complete listing identity.
2
3use crate::{FinanceError, Result};
4use serde::{Deserialize, Serialize};
5
6pub(crate) fn date(value: &str) -> Result<()> {
7    if chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")
8        .is_ok_and(|d| d.format("%Y-%m-%d").to_string() == value)
9    {
10        Ok(())
11    } else {
12        Err(FinanceError::InvalidParameter {
13            param: "date".into(),
14            reason: "expected a valid YYYY-MM-DD date".into(),
15        })
16    }
17}
18
19/// A stock directory snapshot; independent of free-text search.
20#[derive(Debug, Clone, Serialize)]
21pub struct StockListingRequest {
22    pub(crate) date: String,
23    pub(crate) active: bool,
24    pub(crate) stock_type: Option<String>,
25    pub(crate) locale: String,
26    pub(crate) limit: u32,
27}
28
29impl StockListingRequest {
30    /// Request active or inactive stocks on an inclusive calendar date.
31    pub fn new(as_of: &str, active: bool) -> Result<Self> {
32        date(as_of)?;
33        Ok(Self {
34            date: as_of.into(),
35            active,
36            stock_type: None,
37            locale: "us".into(),
38            limit: 1000,
39        })
40    }
41
42    /// Filter by a provider stock-type code, such as CS or ADRC.
43    pub fn with_stock_type(mut self, code: impl Into<String>) -> Self {
44        self.stock_type = Some(code.into());
45        self
46    }
47
48    /// Select a market locale. Defaults to us.
49    pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
50        self.locale = locale.into();
51        self
52    }
53
54    /// Snapshot date (`YYYY-MM-DD`).
55    pub fn as_of(&self) -> &str {
56        &self.date
57    }
58
59    /// Whether active or inactive stocks are requested.
60    pub fn active(&self) -> bool {
61        self.active
62    }
63
64    /// Stock-type filter, if any.
65    pub fn stock_type(&self) -> Option<&str> {
66        self.stock_type.as_deref()
67    }
68
69    /// Requested market locale.
70    pub fn locale(&self) -> &str {
71        &self.locale
72    }
73
74    /// Maximum entries per provider page.
75    pub fn page_size(&self) -> u32 {
76        self.limit
77    }
78
79    /// The fields a continuation must match. Changing them requires a new cursor version.
80    pub(crate) fn cursor_identity(&self) -> String {
81        serde_json::json!([
82            self.date,
83            self.active,
84            self.stock_type,
85            self.locale,
86            self.limit
87        ])
88        .to_string()
89    }
90
91    /// Limit a page to 1–1000 directory entries.
92    pub fn with_page_size(mut self, limit: u32) -> Result<Self> {
93        if !(1..=1000).contains(&limit) {
94            return Err(FinanceError::InvalidParameter {
95                param: "page_size".into(),
96                reason: "expected 1..=1000".into(),
97            });
98        }
99        self.limit = limit;
100        Ok(self)
101    }
102}
103
104/// Provider-reported stock identity and listing lifecycle.
105#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106#[non_exhaustive]
107pub struct StockListing {
108    /// Stock symbol.
109    pub symbol: String,
110    /// Company or security name.
111    pub name: Option<String>,
112    /// Primary exchange code.
113    pub exchange: Option<String>,
114    /// Provider stock-type code.
115    pub stock_type: Option<String>,
116    /// Market locale.
117    pub locale: Option<String>,
118    /// Provider-reported listing status.
119    pub active: Option<bool>,
120    /// Company identifier, preserving leading zeros.
121    pub cik: Option<String>,
122    /// Composite security identifier.
123    pub composite_figi: Option<String>,
124    /// Share-class identifier.
125    pub share_class_figi: Option<String>,
126    /// Provider-reported listing date.
127    pub list_date: Option<String>,
128    /// Provider-reported delisting timestamp.
129    pub delisted_utc: Option<String>,
130}
131
132/// A ticker a security started trading under on a date.
133#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
134#[non_exhaustive]
135pub struct TickerChange {
136    /// First day under this ticker (`YYYY-MM-DD`).
137    pub date: String,
138    /// The ticker from that day.
139    pub ticker: String,
140}
141
142/// An open-ended provider stock-type code.
143#[derive(Debug, Clone, Default, Serialize, Deserialize)]
144#[non_exhaustive]
145pub struct StockType {
146    /// Provider code; unknown future codes are retained.
147    pub code: String,
148    /// Human-readable description.
149    pub description: Option<String>,
150    /// Asset class.
151    pub asset_class: Option<String>,
152    /// Market locale.
153    pub locale: Option<String>,
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn cursor_identity_is_pinned_to_the_query_fields() {
162        let request = StockListingRequest::new("2020-01-02", false)
163            .unwrap()
164            .with_stock_type("CS")
165            .with_page_size(2)
166            .unwrap();
167        assert_eq!(
168            request.cursor_identity(),
169            r#"["2020-01-02",false,"CS","us",2]"#
170        );
171    }
172}