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.status.clone().unwrap_or_else(|| "unknown".to_string()),
75            open_time,
76            close_time,
77            timezone: timezone_info.and_then(|tz| tz.long.clone()),
78            timezone_short: timezone_info.and_then(|tz| tz.short.clone()),
79            gmt_offset: timezone_info.and_then(|tz| tz.gmtoffset),
80        })
81    }
82
83    /// Check if market is currently open
84    pub fn is_open(&self) -> bool {
85        self.status.to_lowercase() == "open"
86    }
87
88    /// Check if market is in pre-market hours
89    pub fn is_pre_market(&self) -> bool {
90        self.status.to_lowercase() == "pre"
91    }
92
93    /// Check if market is in after-hours
94    pub fn is_after_hours(&self) -> bool {
95        self.status.to_lowercase() == "post"
96    }
97}
98
99impl MarketSummaryResponse {
100    pub(crate) fn from_yahoo_response(
101        market: String,
102        summary_response: YahooMarketSummaryResponse,
103        status: Option<MarketStatus>,
104    ) -> Result<Self, crate::client::YahooError> {
105        let indices = summary_response
106            .market_summary_response
107            .result
108            .into_iter()
109            .map(|item| MarketSummaryItem {
110                exchange: item.exchange.unwrap_or_default(),
111                short_name: item.short_name.unwrap_or_default(),
112                symbol: item.symbol.unwrap_or_default(),
113                price: item.regular_market_price.unwrap_or(0.0),
114                change: item.regular_market_change.unwrap_or(0.0),
115                percent_change: item.regular_market_change_percent.unwrap_or(0.0),
116            })
117            .collect();
118
119        Ok(Self {
120            market,
121            status,
122            indices,
123        })
124    }
125}
126
127// Internal Yahoo response structures
128#[derive(Debug, Deserialize)]
129pub(crate) struct YahooMarketTimeResponse {
130    pub finance: FinanceData,
131}
132
133#[derive(Debug, Deserialize)]
134pub(crate) struct FinanceData {
135    #[serde(rename = "marketTimes")]
136    pub market_times: Vec<MarketTimeWrapper>,
137}
138
139#[derive(Debug, Deserialize)]
140pub(crate) struct MarketTimeWrapper {
141    #[serde(rename = "marketTime")]
142    pub market_time: Vec<MarketTimeData>,
143}
144
145#[derive(Debug, Deserialize)]
146pub(crate) struct MarketTimeData {
147    pub status: Option<String>,
148    pub open: Option<String>,
149    pub close: Option<String>,
150    pub timezone: Vec<TimezoneInfo>,
151}
152
153#[derive(Debug, Deserialize)]
154pub(crate) struct TimezoneInfo {
155    #[serde(deserialize_with = "deserialize_gmt_offset", default)]
156    pub gmtoffset: Option<i32>,
157    pub short: Option<String>,
158    pub long: Option<String>,
159}
160
161/// Deserialize gmtoffset which can be either a string or integer
162fn deserialize_gmt_offset<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
163where
164    D: serde::Deserializer<'de>,
165{
166    use serde::de::Error;
167
168    #[derive(Deserialize)]
169    #[serde(untagged)]
170    enum GmtOffset {
171        Int(i32),
172        String(String),
173    }
174
175    match Option::<GmtOffset>::deserialize(deserializer)? {
176        Some(GmtOffset::Int(i)) => Ok(Some(i)),
177        Some(GmtOffset::String(s)) => s.parse::<i32>().map(Some).map_err(D::Error::custom),
178        None => Ok(None),
179    }
180}
181
182#[derive(Debug, Deserialize)]
183pub(crate) struct YahooMarketSummaryResponse {
184    #[serde(rename = "marketSummaryResponse")]
185    pub market_summary_response: MarketSummaryData,
186}
187
188#[derive(Debug, Deserialize)]
189pub(crate) struct MarketSummaryData {
190    pub result: Vec<MarketSummaryResult>,
191}
192
193#[derive(Debug, Deserialize)]
194pub(crate) struct MarketSummaryResult {
195    pub exchange: Option<String>,
196    #[serde(rename = "shortName")]
197    pub short_name: Option<String>,
198    pub symbol: Option<String>,
199    #[serde(rename = "regularMarketPrice")]
200    pub regular_market_price: Option<f64>,
201    #[serde(rename = "regularMarketChange")]
202    pub regular_market_change: Option<f64>,
203    #[serde(rename = "regularMarketChangePercent")]
204    pub regular_market_change_percent: Option<f64>,
205}