use serde::{Deserialize, Serialize};
use crate::adhoc_screen::{AdhocScreenIncludeSource, AdhocScreenInstruments, AdhocScreenResponse};
use crate::client::Client;
use crate::graphql::GraphQLRequest;
use crate::types::{ResponseColumn, symbols_to_owned};
const QUERY_GET_ALL_WATCHLIST_NAMES: &str = r#"query GetAllWatchlistNames($pub: String!) {
watchlists(pub: $pub) {
id
name
lastModifiedDateUtc
description
}
}"#;
const QUERY_FLAGGED_SYMBOLS: &str = r#"query FlaggedSymbols($pub: String!, $watchlistId: ID!) {
watchlist(pub: $pub, watchlistId: $watchlistId) {
id
name
lastModifiedDateUtc
description
items {
key
dowJonesKey
}
}
}"#;
const DEFAULT_WATCHLIST_PUB: &str = "msr";
const DEFAULT_SCREENER_WATCHLIST_CORRELATION_TAG: &str = "Screen With Watchlist";
const DEFAULT_SCREENER_WATCHLIST_DIALECT: &str = "CHARTING";
#[derive(Serialize)]
struct GetAllWatchlistNamesVariables {
#[serde(rename = "pub")]
publication: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FlaggedSymbolsVariables {
#[serde(rename = "pub")]
publication: String,
watchlist_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WatchlistNamesResponse {
#[serde(default)]
pub watchlists: Vec<WatchlistSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WatchlistSummary {
pub id: Option<String>,
pub name: Option<String>,
pub last_modified_date_utc: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FlaggedSymbolsResponse {
pub watchlist: Option<WatchlistDetail>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WatchlistDetail {
pub id: Option<String>,
pub name: Option<String>,
pub last_modified_date_utc: Option<String>,
pub description: Option<String>,
#[serde(default)]
pub items: Vec<WatchlistItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WatchlistItem {
pub key: Option<String>,
pub dow_jones_key: Option<String>,
}
pub type ScreenerWatchlistItemsResponse = AdhocScreenResponse;
impl Client {
pub async fn get_all_watchlist_names(&self) -> crate::error::Result<WatchlistNamesResponse> {
let variables = GetAllWatchlistNamesVariables {
publication: DEFAULT_WATCHLIST_PUB.to_string(),
};
let request = GraphQLRequest {
operation_name: "GetAllWatchlistNames".to_string(),
variables,
query: QUERY_GET_ALL_WATCHLIST_NAMES.to_string(),
};
self.graphql_post(&request).await
}
pub async fn flagged_symbols(
&self,
watchlist_id: &str,
) -> crate::error::Result<FlaggedSymbolsResponse> {
let variables = FlaggedSymbolsVariables {
publication: DEFAULT_WATCHLIST_PUB.to_string(),
watchlist_id: watchlist_id.to_string(),
};
let request = GraphQLRequest {
operation_name: "FlaggedSymbols".to_string(),
variables,
query: QUERY_FLAGGED_SYMBOLS.to_string(),
};
self.graphql_post(&request).await
}
pub async fn screener_watchlist_items(
&self,
symbols: &[&str],
response_columns: Vec<ResponseColumn>,
) -> crate::error::Result<ScreenerWatchlistItemsResponse> {
let include_source = AdhocScreenIncludeSource {
screen_id: None,
instruments: Some(AdhocScreenInstruments {
symbols: symbols_to_owned(symbols),
dialect: DEFAULT_SCREENER_WATCHLIST_DIALECT.to_string(),
}),
};
self.screener_watchlist(
DEFAULT_SCREENER_WATCHLIST_CORRELATION_TAG,
response_columns,
include_source,
1,
1,
0,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::mock_test;
#[tokio::test]
async fn get_all_watchlist_names_parses_response() {
let (_server, client, mock) = mock_test("GetAllWatchlistNames").await;
let resp = client
.get_all_watchlist_names()
.await
.expect("get_all_watchlist_names should succeed");
assert_eq!(resp.watchlists.len(), 1);
let wl = &resp.watchlists[0];
assert_eq!(wl.id.as_deref(), Some("12345"));
assert_eq!(wl.name.as_deref(), Some("My Watchlist"));
assert_eq!(
wl.last_modified_date_utc.as_deref(),
Some("2025-01-01T00:00:00Z")
);
assert_eq!(wl.description.as_deref(), Some("Test watchlist"));
mock.assert();
}
#[tokio::test]
async fn flagged_symbols_parses_response() {
let (_server, client, mock) = mock_test("FlaggedSymbols").await;
let resp = client
.flagged_symbols("12345")
.await
.expect("flagged_symbols should succeed");
let watchlist = resp.watchlist.as_ref().expect("watchlist");
assert_eq!(watchlist.id.as_deref(), Some("12345"));
assert_eq!(watchlist.name.as_deref(), Some("My Watchlist"));
assert_eq!(watchlist.items.len(), 2);
assert_eq!(watchlist.items[0].key.as_deref(), Some("AAPL"));
assert_eq!(watchlist.items[0].dow_jones_key.as_deref(), Some("US:AAPL"));
assert_eq!(watchlist.items[1].key.as_deref(), Some("MSFT"));
assert_eq!(watchlist.items[1].dow_jones_key.as_deref(), Some("US:MSFT"));
mock.assert();
}
#[tokio::test]
async fn screener_watchlist_items_parses_response() {
let (_server, client, mock) = mock_test("ScreenerWatchlist").await;
let columns = vec![
ResponseColumn {
name: "EPSRating".to_string(),
sort_information: None,
},
ResponseColumn {
name: "RSRating".to_string(),
sort_information: None,
},
ResponseColumn {
name: "AccDisRating".to_string(),
sort_information: None,
},
];
let resp = client
.screener_watchlist_items(&["AMD"], columns)
.await
.expect("screener_watchlist_items should succeed");
let result = resp
.market_data_adhoc_screen
.as_ref()
.expect("market_data_adhoc_screen");
assert_eq!(
result.correlation_tag.as_deref(),
Some("Screen With Watchlist")
);
assert_eq!(result.response_values.len(), 1);
assert_eq!(result.response_values[0].len(), 3);
let eps = &result.response_values[0][0];
assert_eq!(eps.value.as_deref(), Some("95"));
let eps_item = eps.md_item.as_ref().expect("md_item");
assert_eq!(eps_item.name.as_deref(), Some("EPSRating"));
mock.assert();
}
#[cfg(not(coverage))]
#[tokio::test]
#[ignore]
async fn integration_watchlist() {
let client = crate::test_support::live_client().await;
let resp = client
.get_all_watchlist_names()
.await
.expect("live get_all_watchlist_names should succeed");
assert!(!resp.watchlists.is_empty());
}
#[cfg(not(coverage))]
#[tokio::test]
#[ignore]
async fn integration_flagged_symbols() {
let client = crate::test_support::live_client().await;
let watchlists = client
.get_all_watchlist_names()
.await
.expect("live get_all_watchlist_names should succeed");
let watchlist_id = watchlists
.watchlists
.first()
.and_then(|watchlist| watchlist.id.as_deref())
.expect("expected at least one live watchlist with an id");
let resp = client
.flagged_symbols(watchlist_id)
.await
.expect("live flagged_symbols should succeed");
assert!(resp.watchlist.is_some());
}
}