use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::types::symbols_to_owned;
const QUERY_CHART_MARKET_DATA: &str = include_str!("graphql/chart_market_data.graphql");
const QUERY_CHART_MARKET_DATA_WEEKLY: &str =
include_str!("graphql/chart_market_data_weekly.graphql");
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ChartMarketDataVariables {
symbols: Vec<String>,
symbol_dialect_type: String,
#[serde(rename = "where")]
filter: TimeSeriesFilterInput,
exchange_name: String,
holiday_start_date_time: String,
holiday_end_date_time: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ChartMarketDataWeeklyVariables {
symbols: Vec<String>,
symbol_dialect_type: String,
#[serde(rename = "where")]
filter: TimeSeriesFilterInput,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct TimeSeriesFilterInput {
start_date_time: EqFilter,
end_date_time: EqFilter,
time_series_type: EqFilter,
include_intraday_data: bool,
}
#[derive(Serialize)]
struct EqFilter {
eq: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartMarketDataResponse {
#[serde(default)]
pub market_data: Vec<ChartMarketDataItem>,
pub exchange_data: Option<ChartExchangeData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartMarketDataItem {
pub id: String,
pub origin_request: Option<ChartOriginRequest>,
pub pricing: Option<ChartPricing>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartOriginRequest {
pub from_dialect: String,
pub symbol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartPricing {
pub time_series: Option<ChartTimeSeries>,
pub quote: Option<ChartQuote>,
pub premarket_quote: Option<ChartQuote>,
pub postmarket_quote: Option<ChartQuote>,
pub current_market_state: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartTimeSeries {
pub period: String,
#[serde(default)]
pub data_points: Vec<ChartDataPoint>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartDataPoint {
pub start_date_time: String,
pub end_date_time: String,
pub volume: Option<ChartValue>,
pub last: Option<ChartValue>,
pub low: Option<ChartValue>,
pub high: Option<ChartValue>,
pub open: Option<ChartValue>,
}
pub type ChartValue = crate::types::FloatValue;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartQuote {
pub trade_date_time: Option<String>,
pub timeliness: Option<String>,
pub quote_type: Option<String>,
pub volume: Option<ChartFormattedValue>,
pub percent_change: Option<ChartFormattedValue>,
pub net_change: Option<ChartFormattedValue>,
pub last: Option<ChartFormattedValue>,
}
pub type ChartFormattedValue = crate::types::FormattedFloat;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartExchangeData {
pub city: Option<String>,
pub country_code: Option<String>,
#[serde(rename = "exchangeISO")]
pub exchange_iso: Option<String>,
pub id: Option<String>,
#[serde(default)]
pub holidays: Vec<ChartExchangeHoliday>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartExchangeHoliday {
pub name: String,
pub holiday_type: Option<String>,
pub description: Option<String>,
pub start_date_time: String,
pub end_date_time: String,
}
impl Client {
#[allow(clippy::too_many_arguments)]
pub async fn chart_market_data(
&self,
symbols: &[&str],
symbol_dialect_type: &str,
start_date_time: &str,
end_date_time: &str,
time_series_type: &str,
include_intraday: bool,
exchange_name: &str,
) -> crate::error::Result<ChartMarketDataResponse> {
let variables = ChartMarketDataVariables {
symbols: symbols_to_owned(symbols),
symbol_dialect_type: symbol_dialect_type.to_string(),
filter: TimeSeriesFilterInput {
start_date_time: EqFilter {
eq: start_date_time.to_string(),
},
end_date_time: EqFilter {
eq: end_date_time.to_string(),
},
time_series_type: EqFilter {
eq: time_series_type.to_string(),
},
include_intraday_data: include_intraday,
},
exchange_name: exchange_name.to_string(),
holiday_start_date_time: start_date_time.to_string(),
holiday_end_date_time: end_date_time.to_string(),
};
self.graphql_operation("ChartMarketData", variables, QUERY_CHART_MARKET_DATA)
.await
}
pub async fn chart_market_data_weekly(
&self,
symbols: &[&str],
symbol_dialect_type: &str,
start_date_time: &str,
end_date_time: &str,
) -> crate::error::Result<ChartMarketDataResponse> {
let variables = ChartMarketDataWeeklyVariables {
symbols: symbols_to_owned(symbols),
symbol_dialect_type: symbol_dialect_type.to_string(),
filter: TimeSeriesFilterInput {
start_date_time: EqFilter {
eq: start_date_time.to_string(),
},
end_date_time: EqFilter {
eq: end_date_time.to_string(),
},
time_series_type: EqFilter {
eq: "ONE_WEEK".to_string(),
},
include_intraday_data: true,
},
};
self.graphql_operation("ChartMarketData", variables, QUERY_CHART_MARKET_DATA_WEEKLY)
.await
}
}
#[cfg(test)]
mod tests {
use crate::test_support::{mock_test, mock_test_with_fixture};
#[tokio::test]
async fn chart_market_data_parses_response() {
let (_server, client, mock) = mock_test("ChartMarketData").await;
let resp = client
.chart_market_data(
&["AAPL"],
"CHARTING",
"2025-01-01T00:00:00.000Z",
"2025-05-02T23:59:59.000Z",
"ONE_DAY",
true,
"NYSE",
)
.await
.expect("chart_market_data should succeed");
assert_eq!(resp.market_data.len(), 1);
let item = &resp.market_data[0];
assert_eq!(item.id, "AAPL-CHARTING");
let origin = item.origin_request.as_ref().expect("origin_request");
assert_eq!(origin.symbol, "AAPL");
assert_eq!(origin.from_dialect, "CHARTING");
let pricing = item.pricing.as_ref().expect("pricing");
let ts = pricing.time_series.as_ref().expect("time_series");
assert_eq!(ts.period, "ONE_DAY");
assert_eq!(ts.data_points.len(), 2);
assert_eq!(ts.data_points[0].last.as_ref().unwrap().value, Some(210.45));
let quote = pricing.quote.as_ref().expect("quote");
assert_eq!(quote.last.as_ref().unwrap().value, Some(212.30));
assert_eq!(
quote.last.as_ref().unwrap().formatted_value.as_deref(),
Some("212.30")
);
assert_eq!(pricing.current_market_state.as_deref(), Some("POST_MARKET"));
let exchange = resp.exchange_data.as_ref().expect("exchange_data");
assert_eq!(exchange.city.as_deref(), Some("New York"));
assert_eq!(exchange.exchange_iso.as_deref(), Some("XNYS"));
assert_eq!(exchange.holidays.len(), 1);
assert_eq!(exchange.holidays[0].name, "Independence Day");
mock.assert();
}
#[tokio::test]
async fn chart_market_data_weekly_parses_response() {
let (_server, client, mock) =
mock_test_with_fixture("ChartMarketDataWeekly", "ChartMarketData").await;
let resp = client
.chart_market_data_weekly(
&["AAPL"],
"CHARTING",
"2024-05-01T00:00:00.000Z",
"2025-05-02T23:59:59.000Z",
)
.await
.expect("chart_market_data_weekly should succeed");
assert_eq!(resp.market_data.len(), 1);
let item = &resp.market_data[0];
assert_eq!(item.id, "AAPL-CHARTING");
let pricing = item.pricing.as_ref().expect("pricing");
let ts = pricing.time_series.as_ref().expect("time_series");
assert_eq!(ts.period, "ONE_WEEK");
assert_eq!(ts.data_points.len(), 1);
assert!(resp.exchange_data.is_none());
mock.assert();
}
#[cfg(not(coverage))]
#[tokio::test]
#[ignore]
async fn integration_chart_market_data() {
let client = crate::test_support::live_client().await;
let resp = client
.chart_market_data(
&["AAPL"],
"CHARTING",
"2025-01-01T00:00:00.000Z",
"2025-05-02T23:59:59.000Z",
"ONE_DAY",
true,
"NYSE",
)
.await
.expect("live chart_market_data should succeed");
assert!(!resp.market_data.is_empty());
}
#[cfg(not(coverage))]
#[tokio::test]
#[ignore]
async fn integration_chart_market_data_weekly() {
let client = crate::test_support::live_client().await;
let resp = client
.chart_market_data_weekly(
&["AAPL"],
"CHARTING",
"2024-05-01T00:00:00.000Z",
"2025-05-02T23:59:59.000Z",
)
.await
.expect("live chart_market_data_weekly should succeed");
assert!(!resp.market_data.is_empty());
}
}