finance_query/models/hours/
response.rs

1use serde::{Deserialize, Serialize};
2
3// ============================================================================
4// Raw Yahoo Finance response structures (internal)
5// ============================================================================
6
7/// Raw response from Yahoo Finance markettime endpoint
8#[derive(Debug, Clone, Deserialize)]
9struct RawHoursResponse {
10    finance: RawFinance,
11}
12
13#[derive(Debug, Clone, Deserialize)]
14#[serde(rename_all = "camelCase")]
15struct RawFinance {
16    #[serde(default)]
17    market_times: Vec<RawMarketTimes>,
18}
19
20#[derive(Debug, Clone, Deserialize)]
21#[serde(rename_all = "camelCase")]
22struct RawMarketTimes {
23    #[serde(default)]
24    market_time: Vec<RawMarketTime>,
25}
26
27#[derive(Debug, Clone, Deserialize)]
28#[serde(rename_all = "camelCase")]
29struct RawMarketTime {
30    id: String,
31    name: String,
32    status: String,
33    #[serde(default)]
34    message: Option<String>,
35    #[serde(default)]
36    open: Option<String>,
37    #[serde(default)]
38    close: Option<String>,
39    #[serde(default)]
40    time: Option<String>,
41    #[serde(default)]
42    timezone: Vec<RawTimezone>,
43}
44
45#[derive(Debug, Clone, Deserialize)]
46struct RawTimezone {
47    #[serde(default)]
48    dst: Option<String>,
49    #[serde(default)]
50    gmtoffset: Option<String>,
51    #[serde(default)]
52    short: Option<String>,
53    #[serde(rename = "$text", default)]
54    text: Option<String>,
55}
56
57// ============================================================================
58// Public API structures
59// ============================================================================
60
61/// Market time information for a specific market
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
64#[serde(rename_all = "camelCase")]
65#[non_exhaustive]
66pub struct MarketTime {
67    /// Market identifier (e.g., "us", "uk", "jp")
68    pub id: String,
69
70    /// Human-readable market name (e.g., "U.S. markets")
71    pub name: String,
72
73    /// Market status (e.g., "open", "closed")
74    pub status: String,
75
76    /// Status message (e.g., "U.S. markets closed")
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub message: Option<String>,
79
80    /// Market open time (ISO 8601 format)
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub open: Option<String>,
83
84    /// Market close time (ISO 8601 format)
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub close: Option<String>,
87
88    /// Current time (ISO 8601 format)
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub time: Option<String>,
91
92    /// Timezone name (e.g., "America/New_York")
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub timezone: Option<String>,
95
96    /// Short timezone name (e.g., "EST")
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub timezone_short: Option<String>,
99
100    /// GMT offset in seconds (e.g., -18000 for EST)
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub gmt_offset: Option<i32>,
103
104    /// Whether daylight saving time is in effect
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub dst: Option<bool>,
107}
108
109/// Flattened response for market hours
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112#[non_exhaustive]
113pub struct MarketHours {
114    /// List of market times
115    pub markets: Vec<MarketTime>,
116}
117
118impl MarketHours {
119    /// Create a flattened response from raw Yahoo Finance JSON
120    ///
121    /// Converts the nested Yahoo Finance response structure into a clean,
122    /// user-friendly format.
123    pub(crate) fn from_response(raw: &serde_json::Value) -> Result<Self, String> {
124        let raw_response: RawHoursResponse = serde_json::from_value(raw.clone())
125            .map_err(|e| format!("Failed to parse hours response: {}", e))?;
126
127        let mut markets = Vec::new();
128
129        for market_times in &raw_response.finance.market_times {
130            for market_time in &market_times.market_time {
131                // Extract timezone info from the first timezone entry
132                let tz = market_time.timezone.first();
133
134                let gmt_offset = tz
135                    .and_then(|t| t.gmtoffset.as_ref())
136                    .and_then(|s| s.parse::<i32>().ok());
137
138                let dst = tz
139                    .and_then(|t| t.dst.as_ref())
140                    .map(|s| s.eq_ignore_ascii_case("true"));
141
142                markets.push(MarketTime {
143                    id: market_time.id.clone(),
144                    name: market_time.name.clone(),
145                    status: market_time.status.clone(),
146                    message: market_time.message.clone(),
147                    open: market_time.open.clone(),
148                    close: market_time.close.clone(),
149                    time: market_time.time.clone(),
150                    timezone: tz.and_then(|t| t.text.clone()),
151                    timezone_short: tz.and_then(|t| t.short.clone()),
152                    gmt_offset,
153                    dst,
154                });
155            }
156        }
157
158        Ok(Self { markets })
159    }
160}
161
162#[cfg(feature = "dataframe")]
163impl MarketHours {
164    /// Converts the market times to a polars DataFrame.
165    pub fn to_dataframe(&self) -> ::polars::prelude::PolarsResult<::polars::prelude::DataFrame> {
166        MarketTime::vec_to_dataframe(&self.markets)
167    }
168}