finance_query_core/models/
market.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase")]
7pub struct MarketStatus {
8 pub market: String,
9 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#[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#[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 pub fn is_open(&self) -> bool {
85 self.status.to_lowercase() == "open"
86 }
87
88 pub fn is_pre_market(&self) -> bool {
90 self.status.to_lowercase() == "pre"
91 }
92
93 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#[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
161fn 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}