use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use crate::types::wire::wire_enum;
wire_enum! {
pub enum Market {
Bmo => "BMO",
Bnym => "BNYM",
Boats => "BOATS",
Ceux => "CEUX",
Chix => "CHIX",
Hkex => "HKEX",
Iex => "IEX",
Iexg => "IEXG",
Ise => "ISE",
Lse => "LSE",
Mta => "MTA",
Mtaa => "MTAA",
Nasdaq => "NASDAQ",
Nyse => "NYSE",
Ocea => "OCEA",
Opra => "OPRA",
Otc => "OTC",
Otcm => "OTCM",
Sifma => "SIFMA",
Tadawul => "TADAWUL",
Xams => "XAMS",
Xbru => "XBRU",
Xdub => "XDUB",
Xetr => "XETR",
Xetra => "XETRA",
Xhkg => "XHKG",
Xlis => "XLIS",
Xlon => "XLON",
Xnas => "XNAS",
Xnys => "XNYS",
Xpar => "XPAR",
Xsau => "XSAU",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MarketSession {
pub date: NaiveDate,
pub core_start: DateTime<Utc>,
pub core_end: DateTime<Utc>,
#[serde(default)]
pub pre_start: Option<DateTime<Utc>>,
#[serde(default)]
pub pre_end: Option<DateTime<Utc>>,
#[serde(default)]
pub post_start: Option<DateTime<Utc>>,
#[serde(default)]
pub post_end: Option<DateTime<Utc>>,
#[serde(default)]
pub lunch_start: Option<DateTime<Utc>>,
#[serde(default)]
pub lunch_end: Option<DateTime<Utc>>,
#[serde(default)]
pub settlement_date: Option<NaiveDate>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MarketInfo {
pub acronym: String,
pub name: String,
pub timezone: String,
#[serde(default)]
pub mic: Option<String>,
#[serde(default)]
pub bic: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MarketCalendar {
pub market: MarketInfo,
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub calendar: Vec<MarketSession>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetMarketCalendarRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
}
impl GetMarketCalendarRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn between(mut self, start: NaiveDate, end: NaiveDate) -> crate::Result<Self> {
if end < start {
return Err(crate::Error::InvalidRequest(format!(
"end ({end}) is before start ({start})"
)));
}
self.start = Some(start);
self.end = Some(end);
Ok(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_market_session_is_instants_not_naive_datetimes() {
let session: MarketSession = serde_json::from_value(serde_json::json!({
"date": "2026-01-02",
"core_start": "2026-01-02T14:30:00Z",
"core_end": "2026-01-02T21:00:00Z",
}))
.unwrap();
assert_eq!(session.core_start.to_rfc3339(), "2026-01-02T14:30:00+00:00");
assert_eq!(session.lunch_start, None);
}
#[test]
fn a_backwards_window_is_refused() {
let start: NaiveDate = "2026-01-10".parse().unwrap();
let end: NaiveDate = "2026-01-01".parse().unwrap();
assert!(GetMarketCalendarRequest::new().between(start, end).is_err());
}
#[test]
fn both_spellings_of_a_venue_are_accepted() {
assert_eq!(Market::Nasdaq.as_str(), "NASDAQ");
assert_eq!(Market::Xnas.as_str(), "XNAS");
}
}