Skip to main content

finance_query/adapters/polygon/
reference.rs

1//! Reference data endpoints: tickers, exchanges, conditions, market holidays, market status.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::{FinanceError, Result};
7
8use super::build_client;
9use super::models::PaginatedResponse;
10
11/// Ticker reference entry.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct TickerRef {
15    /// Ticker symbol.
16    pub ticker: Option<String>,
17    /// Security name.
18    pub name: Option<String>,
19    /// Market (e.g., `"stocks"`, `"crypto"`, `"fx"`).
20    pub market: Option<String>,
21    /// Locale (e.g., `"us"`).
22    pub locale: Option<String>,
23    /// Primary exchange.
24    pub primary_exchange: Option<String>,
25    /// Asset type.
26    #[serde(rename = "type")]
27    pub asset_type: Option<String>,
28    /// Whether the ticker is active.
29    pub active: Option<bool>,
30    /// Currency name.
31    pub currency_name: Option<String>,
32    /// CIK number.
33    pub cik: Option<String>,
34    /// Composite FIGI.
35    pub composite_figi: Option<String>,
36    /// Share class FIGI.
37    pub share_class_figi: Option<String>,
38    /// Last updated date.
39    pub last_updated_utc: Option<String>,
40}
41
42/// Detailed ticker overview.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[non_exhaustive]
45pub struct TickerDetails {
46    /// Ticker symbol.
47    pub ticker: Option<String>,
48    /// Company name.
49    pub name: Option<String>,
50    /// Market.
51    pub market: Option<String>,
52    /// Locale.
53    pub locale: Option<String>,
54    /// Primary exchange.
55    pub primary_exchange: Option<String>,
56    /// Asset type.
57    #[serde(rename = "type")]
58    pub asset_type: Option<String>,
59    /// Active.
60    pub active: Option<bool>,
61    /// Currency.
62    pub currency_name: Option<String>,
63    /// CIK.
64    pub cik: Option<String>,
65    /// SIC code.
66    pub sic_code: Option<String>,
67    /// SIC description.
68    pub sic_description: Option<String>,
69    /// Description.
70    pub description: Option<String>,
71    /// Homepage URL.
72    pub homepage_url: Option<String>,
73    /// Total employees.
74    pub total_employees: Option<u64>,
75    /// Market cap.
76    pub market_cap: Option<f64>,
77    /// Phone number.
78    pub phone_number: Option<String>,
79    /// Address.
80    pub address: Option<serde_json::Value>,
81    /// Branding (logo, icon).
82    pub branding: Option<serde_json::Value>,
83    /// List date.
84    pub list_date: Option<String>,
85    /// Share class shares outstanding.
86    pub share_class_shares_outstanding: Option<f64>,
87    /// Weighted shares outstanding.
88    pub weighted_shares_outstanding: Option<f64>,
89}
90
91/// Ticker details response wrapper.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[non_exhaustive]
94pub struct TickerDetailsResponse {
95    /// Request ID.
96    pub request_id: Option<String>,
97    /// Status.
98    pub status: Option<String>,
99    /// Ticker details.
100    pub results: Option<TickerDetails>,
101}
102
103/// Ticker type.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[non_exhaustive]
106pub struct TickerType {
107    /// Type code.
108    pub code: Option<String>,
109    /// Description.
110    pub description: Option<String>,
111    /// Asset class.
112    pub asset_class: Option<String>,
113    /// Locale.
114    pub locale: Option<String>,
115}
116
117/// Related ticker.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[non_exhaustive]
120pub struct RelatedTicker {
121    /// Ticker symbol.
122    pub ticker: Option<String>,
123}
124
125/// Exchange.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[non_exhaustive]
128pub struct Exchange {
129    /// Exchange ID.
130    pub id: Option<i64>,
131    /// Exchange type.
132    #[serde(rename = "type")]
133    pub exchange_type: Option<String>,
134    /// Asset class.
135    pub asset_class: Option<String>,
136    /// Locale.
137    pub locale: Option<String>,
138    /// Exchange name.
139    pub name: Option<String>,
140    /// MIC code.
141    pub mic: Option<String>,
142    /// Operating MIC.
143    pub operating_mic: Option<String>,
144    /// Participant ID.
145    pub participant_id: Option<String>,
146    /// URL.
147    pub url: Option<String>,
148}
149
150/// Condition code.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[non_exhaustive]
153pub struct Condition {
154    /// Condition ID.
155    pub id: Option<i32>,
156    /// Condition type.
157    #[serde(rename = "type")]
158    pub condition_type: Option<String>,
159    /// Name.
160    pub name: Option<String>,
161    /// Description.
162    pub description: Option<String>,
163    /// Asset class.
164    pub asset_class: Option<String>,
165    /// SIP mapping.
166    pub sip_mapping: Option<serde_json::Value>,
167    /// Data types.
168    pub data_types: Option<Vec<String>>,
169    /// Legacy flag.
170    pub legacy: Option<bool>,
171}
172
173/// Market holiday.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[non_exhaustive]
176pub struct MarketHoliday {
177    /// Holiday name.
178    pub name: Option<String>,
179    /// Date.
180    pub date: Option<String>,
181    /// Exchange (e.g., `"NYSE"`, `"NASDAQ"`).
182    pub exchange: Option<String>,
183    /// Status (e.g., `"closed"`, `"early-close"`).
184    pub status: Option<String>,
185    /// Open time (if early close).
186    pub open: Option<String>,
187    /// Close time (if early close).
188    pub close: Option<String>,
189}
190
191/// Market status for exchanges.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[non_exhaustive]
194pub struct MarketStatusResponse {
195    /// After hours trading.
196    #[serde(rename = "afterHours")]
197    pub after_hours: Option<bool>,
198    /// Early hours trading.
199    #[serde(rename = "earlyHours")]
200    pub early_hours: Option<bool>,
201    /// Market status (e.g., `"open"`, `"closed"`).
202    pub market: Option<String>,
203    /// Server time.
204    #[serde(rename = "serverTime")]
205    pub server_time: Option<String>,
206    /// Individual exchange statuses.
207    pub exchanges: Option<serde_json::Value>,
208    /// Individual currency statuses.
209    pub currencies: Option<serde_json::Value>,
210}
211
212/// Fetch all tickers.
213///
214/// * `params` - Query params: `type`, `market`, `exchange`, `cusip`, `cik`, `active`, `sort`, `order`, `limit`, `search`
215pub async fn all_tickers(params: &[(&str, &str)]) -> Result<PaginatedResponse<TickerRef>> {
216    let client = build_client()?;
217    client.get("/v3/reference/tickers", params).await
218}
219
220/// Fetch detailed ticker information.
221pub async fn ticker_details(ticker: &str) -> Result<TickerDetailsResponse> {
222    let client = build_client()?;
223    let path = format!("/v3/reference/tickers/{}", encode_path_segment(ticker));
224    let json = client.get_raw(&path, &[]).await?;
225    serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
226        field: "ticker_details".to_string(),
227        context: format!("Failed to parse ticker details: {e}"),
228    })
229}
230
231/// Fetch ticker types.
232pub async fn ticker_types(params: &[(&str, &str)]) -> Result<PaginatedResponse<TickerType>> {
233    let client = build_client()?;
234    client.get("/v3/reference/tickers/types", params).await
235}
236
237/// Fetch related tickers.
238pub async fn related_tickers(ticker: &str) -> Result<PaginatedResponse<RelatedTicker>> {
239    let client = build_client()?;
240    let path = format!("/v1/related-companies/{}", encode_path_segment(ticker));
241    client.get(&path, &[]).await
242}
243
244/// Fetch exchanges list.
245pub async fn exchanges(params: &[(&str, &str)]) -> Result<PaginatedResponse<Exchange>> {
246    let client = build_client()?;
247    client.get("/v3/reference/exchanges", params).await
248}
249
250/// Fetch condition codes.
251pub async fn condition_codes(params: &[(&str, &str)]) -> Result<PaginatedResponse<Condition>> {
252    let client = build_client()?;
253    client.get("/v3/reference/conditions", params).await
254}
255
256/// Fetch upcoming market holidays.
257pub async fn market_holidays() -> Result<Vec<MarketHoliday>> {
258    let client = build_client()?;
259    let json = client.get_raw("/v1/marketstatus/upcoming", &[]).await?;
260    serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
261        field: "market_holidays".to_string(),
262        context: format!("Failed to parse market holidays: {e}"),
263    })
264}
265
266/// Fetch current market status.
267pub async fn market_status() -> Result<MarketStatusResponse> {
268    let client = build_client()?;
269    let json = client.get_raw("/v1/marketstatus/now", &[]).await?;
270    serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
271        field: "market_status".to_string(),
272        context: format!("Failed to parse market status: {e}"),
273    })
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[tokio::test]
281    async fn test_ticker_details_mock() {
282        let mut server = mockito::Server::new_async().await;
283        let _mock = server
284            .mock("GET", "/v3/reference/tickers/AAPL")
285            .match_query(mockito::Matcher::AllOf(vec![
286                mockito::Matcher::UrlEncoded("apiKey".into(), "test-key".into()),
287            ]))
288            .with_status(200)
289            .with_body(serde_json::json!({
290                "request_id": "abc",
291                "status": "OK",
292                "results": {
293                    "ticker": "AAPL",
294                    "name": "Apple Inc.",
295                    "market": "stocks",
296                    "locale": "us",
297                    "primary_exchange": "XNAS",
298                    "type": "CS",
299                    "active": true,
300                    "currency_name": "usd",
301                    "market_cap": 2850000000000.0,
302                    "description": "Apple Inc. designs, manufactures, and markets smartphones..."
303                }
304            }).to_string())
305            .create_async().await;
306
307        let client = super::super::build_test_client(&server.url()).unwrap();
308        let json = client
309            .get_raw("/v3/reference/tickers/AAPL", &[])
310            .await
311            .unwrap();
312        let resp: TickerDetailsResponse = serde_json::from_value(json).unwrap();
313        let details = resp.results.unwrap();
314        assert_eq!(details.name.as_deref(), Some("Apple Inc."));
315        assert_eq!(details.ticker.as_deref(), Some("AAPL"));
316        assert!((details.market_cap.unwrap() - 2850000000000.0).abs() < 1.0);
317    }
318
319    #[tokio::test]
320    async fn test_market_status_mock() {
321        let mut server = mockito::Server::new_async().await;
322        let _mock = server
323            .mock("GET", "/v1/marketstatus/now")
324            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
325                "apiKey".into(),
326                "test-key".into(),
327            )]))
328            .with_status(200)
329            .with_body(
330                serde_json::json!({
331                    "market": "open",
332                    "earlyHours": false,
333                    "afterHours": false,
334                    "serverTime": "2024-01-15T12:00:00-05:00"
335                })
336                .to_string(),
337            )
338            .create_async()
339            .await;
340
341        let client = super::super::build_test_client(&server.url()).unwrap();
342        let json = client.get_raw("/v1/marketstatus/now", &[]).await.unwrap();
343        let resp: MarketStatusResponse = serde_json::from_value(json).unwrap();
344        assert_eq!(resp.market.as_deref(), Some("open"));
345    }
346}