Skip to main content

finance_query_core/models/
market.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4/// Market status information
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase")]
7pub struct MarketStatus {
8    pub market: String,
9    /// Current status: "open", "closed", "pre", "post"
10    pub status: String,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub open_time: Option<DateTime<Utc>>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub close_time: Option<DateTime<Utc>>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub timezone: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub timezone_short: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub gmt_offset: Option<i32>,
21}
22
23/// Market summary with index data
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct MarketSummaryItem {
27    pub exchange: String,
28    pub short_name: String,
29    pub symbol: String,
30    pub price: f64,
31    pub change: f64,
32    pub percent_change: f64,
33}
34
35/// Complete market summary response
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct MarketSummaryResponse {
39    pub market: String,
40    pub status: Option<MarketStatus>,
41    pub indices: Vec<MarketSummaryItem>,
42}
43
44impl MarketStatus {
45    pub(crate) fn from_yahoo_response(
46        market: String,
47        response: YahooMarketTimeResponse,
48    ) -> Result<Self, crate::client::YahooError> {
49        let market_time = response
50            .finance
51            .market_times
52            .first()
53            .and_then(|mt| mt.market_time.first())
54            .ok_or_else(|| {
55                crate::client::YahooError::ParseError("No market time data".to_string())
56            })?;
57
58        let timezone_info = market_time.timezone.first();
59
60        let open_time = market_time
61            .open
62            .as_ref()
63            .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
64            .map(|dt| dt.with_timezone(&Utc));
65
66        let close_time = market_time
67            .close
68            .as_ref()
69            .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
70            .map(|dt| dt.with_timezone(&Utc));
71
72        Ok(Self {
73            market,
74            status: market_time
75                .status
76                .clone()
77                .unwrap_or_else(|| "unknown".to_string()),
78            open_time,
79            close_time,
80            timezone: timezone_info.and_then(|tz| tz.long.clone()),
81            timezone_short: timezone_info.and_then(|tz| tz.short.clone()),
82            gmt_offset: timezone_info.and_then(|tz| tz.gmtoffset),
83        })
84    }
85
86    /// Check if market is currently open
87    pub fn is_open(&self) -> bool {
88        self.status.to_lowercase() == "open"
89    }
90
91    /// Check if market is in pre-market hours
92    pub fn is_pre_market(&self) -> bool {
93        self.status.to_lowercase() == "pre"
94    }
95
96    /// Check if market is in after-hours
97    pub fn is_after_hours(&self) -> bool {
98        self.status.to_lowercase() == "post"
99    }
100}
101
102impl MarketSummaryResponse {
103    pub(crate) fn from_yahoo_response(
104        market: String,
105        summary_response: YahooMarketSummaryResponse,
106        status: Option<MarketStatus>,
107    ) -> Result<Self, crate::client::YahooError> {
108        let indices = summary_response
109            .market_summary_response
110            .result
111            .into_iter()
112            .map(|item| MarketSummaryItem {
113                exchange: item.exchange.unwrap_or_default(),
114                short_name: item.short_name.unwrap_or_default(),
115                symbol: item.symbol.unwrap_or_default(),
116                price: item.regular_market_price.unwrap_or(0.0),
117                change: item.regular_market_change.unwrap_or(0.0),
118                percent_change: item.regular_market_change_percent.unwrap_or(0.0),
119            })
120            .collect();
121
122        Ok(Self {
123            market,
124            status,
125            indices,
126        })
127    }
128}
129
130// Internal Yahoo response structures
131#[derive(Debug, Deserialize)]
132pub(crate) struct YahooMarketTimeResponse {
133    pub finance: FinanceData,
134}
135
136#[derive(Debug, Deserialize)]
137pub(crate) struct FinanceData {
138    #[serde(rename = "marketTimes")]
139    pub market_times: Vec<MarketTimeWrapper>,
140}
141
142#[derive(Debug, Deserialize)]
143pub(crate) struct MarketTimeWrapper {
144    #[serde(rename = "marketTime")]
145    pub market_time: Vec<MarketTimeData>,
146}
147
148#[derive(Debug, Deserialize)]
149pub(crate) struct MarketTimeData {
150    pub status: Option<String>,
151    pub open: Option<String>,
152    pub close: Option<String>,
153    pub timezone: Vec<TimezoneInfo>,
154}
155
156#[derive(Debug, Deserialize)]
157pub(crate) struct TimezoneInfo {
158    #[serde(deserialize_with = "deserialize_gmt_offset", default)]
159    pub gmtoffset: Option<i32>,
160    pub short: Option<String>,
161    pub long: Option<String>,
162}
163
164/// Deserialize gmtoffset which can be either a string or integer
165fn deserialize_gmt_offset<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
166where
167    D: serde::Deserializer<'de>,
168{
169    use serde::de::Error;
170
171    #[derive(Deserialize)]
172    #[serde(untagged)]
173    enum GmtOffset {
174        Int(i32),
175        String(String),
176    }
177
178    match Option::<GmtOffset>::deserialize(deserializer)? {
179        Some(GmtOffset::Int(i)) => Ok(Some(i)),
180        Some(GmtOffset::String(s)) => s.parse::<i32>().map(Some).map_err(D::Error::custom),
181        None => Ok(None),
182    }
183}
184
185#[derive(Debug, Deserialize)]
186pub(crate) struct YahooMarketSummaryResponse {
187    #[serde(rename = "marketSummaryResponse")]
188    pub market_summary_response: MarketSummaryData,
189}
190
191#[derive(Debug, Deserialize)]
192pub(crate) struct MarketSummaryData {
193    pub result: Vec<MarketSummaryResult>,
194}
195
196#[derive(Debug, Deserialize)]
197pub(crate) struct MarketSummaryResult {
198    pub exchange: Option<String>,
199    #[serde(rename = "shortName")]
200    pub short_name: Option<String>,
201    pub symbol: Option<String>,
202    #[serde(rename = "regularMarketPrice")]
203    pub regular_market_price: Option<f64>,
204    #[serde(rename = "regularMarketChange")]
205    pub regular_market_change: Option<f64>,
206    #[serde(rename = "regularMarketChangePercent")]
207    pub regular_market_change_percent: Option<f64>,
208}