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
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 pub fn is_open(&self) -> bool {
88 self.status.to_lowercase() == "open"
89 }
90
91 pub fn is_pre_market(&self) -> bool {
93 self.status.to_lowercase() == "pre"
94 }
95
96 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#[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
164fn 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}