use arrow_array::RecordBatch;
use xbbg_async::engine::{Engine, RequestParams};
use xbbg_async::services::{Operation, Service};
use xbbg_ext::transforms::bql::{build_corporate_bonds_query, build_preferreds_query};
use xbbg_ext::transforms::fixed_income::{build_yas_overrides, YieldType};
use crate::error::Result;
#[allow(clippy::too_many_arguments)]
pub async fn recipe_yas(
engine: &Engine,
tickers: Vec<String>,
fields: Vec<String>,
settle_dt: Option<String>,
yield_type: Option<YieldType>,
spread: Option<f64>,
yield_val: Option<f64>,
price: Option<f64>,
benchmark: Option<String>,
) -> Result<RecordBatch> {
let overrides = build_yas_overrides(
settle_dt.as_deref(),
yield_type,
spread,
yield_val,
price,
benchmark.as_deref(),
);
let params = RequestParams {
service: Service::RefData.to_string(),
operation: Operation::ReferenceData.to_string(),
securities: Some(tickers),
fields: Some(fields),
overrides: Some(overrides),
..Default::default()
};
let batch = engine.request(params).await?;
Ok(batch)
}
pub async fn recipe_preferreds(
engine: &Engine,
equity_ticker: String,
fields: Option<Vec<String>>,
) -> Result<RecordBatch> {
let extra = fields.unwrap_or_default();
let extra_refs: Vec<&str> = extra.iter().map(String::as_str).collect();
let bql_query = build_preferreds_query(&equity_ticker, &extra_refs);
let params = RequestParams {
service: Service::BqlSvc.to_string(),
operation: Operation::BqlSendQuery.to_string(),
elements: Some(vec![("expression".to_string(), bql_query)]),
..Default::default()
};
engine.request(params).await.map_err(Into::into)
}
pub async fn recipe_corporate_bonds(
engine: &Engine,
ticker: String,
ccy: Option<String>,
fields: Option<Vec<String>>,
active_only: bool,
) -> Result<RecordBatch> {
let extra = fields.unwrap_or_default();
let extra_refs: Vec<&str> = extra.iter().map(String::as_str).collect();
let bql_query = build_corporate_bonds_query(&ticker, ccy.as_deref(), &extra_refs, active_only);
let params = RequestParams {
service: Service::BqlSvc.to_string(),
operation: Operation::BqlSendQuery.to_string(),
elements: Some(vec![("expression".to_string(), bql_query)]),
..Default::default()
};
engine.request(params).await.map_err(Into::into)
}
pub async fn recipe_bqr(
engine: &Engine,
ticker: String,
start_datetime: String,
end_datetime: String,
event_types: Option<Vec<String>>,
include_broker_codes: bool,
) -> Result<RecordBatch> {
let evts = event_types.unwrap_or_else(|| vec!["BID".to_string(), "ASK".to_string()]);
let mut options = vec![];
if include_broker_codes {
options.push(("includeBrokerCodes".to_string(), "true".to_string()));
}
let params = RequestParams {
service: Service::RefData.to_string(),
operation: Operation::IntradayTick.to_string(),
security: Some(ticker),
start_datetime: Some(start_datetime),
end_datetime: Some(end_datetime),
event_types: Some(evts),
options: if options.is_empty() {
None
} else {
Some(options)
},
..Default::default()
};
engine.request(params).await.map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recipe_yas_builds_correct_params() {
let overrides = build_yas_overrides(
Some("20240115"),
Some(YieldType::YTM),
None,
None,
Some(99.5),
None,
);
assert!(overrides
.iter()
.any(|(k, v)| k == "YAS_SETTLE_DT" && v == "20240115"));
assert!(overrides
.iter()
.any(|(k, v)| k == "YAS_YLD_FLAG" && v == "1"));
assert!(overrides
.iter()
.any(|(k, v)| k == "YAS_BOND_PX" && v == "99.5"));
}
#[test]
fn test_recipe_preferreds_default_fields() {
let fields: Option<Vec<String>> = None;
let all_fields = match fields {
Some(mut flds) => {
let mut defaults = vec!["id".to_string(), "name".to_string()];
defaults.append(&mut flds);
defaults
}
None => vec!["id".to_string(), "name".to_string()],
};
assert_eq!(all_fields, vec!["id", "name"]);
}
#[test]
fn test_recipe_preferreds_custom_fields() {
let fields = Some(vec!["px_last".to_string(), "dvd_yld".to_string()]);
let all_fields = match fields {
Some(mut flds) => {
let mut defaults = vec!["id".to_string(), "name".to_string()];
defaults.append(&mut flds);
defaults
}
None => vec!["id".to_string(), "name".to_string()],
};
assert_eq!(all_fields, vec!["id", "name", "px_last", "dvd_yld"]);
}
#[test]
fn test_recipe_corporate_bonds_filter_building() {
let ticker = "AAPL".to_string();
let ccy = Some("USD".to_string());
let active_only = true;
let mut conditions = vec![
"SRCH_ASSET_CLASS=='Corporates'".to_string(),
format!("TICKER=='{ticker}'"),
];
if let Some(c) = ccy {
conditions.push(format!("CRNCY=='{c}'"));
}
let filter_str = conditions.join(" AND ");
let universe = if active_only { "active" } else { "all" };
assert_eq!(
filter_str,
"SRCH_ASSET_CLASS=='Corporates' AND TICKER=='AAPL' AND CRNCY=='USD'"
);
assert_eq!(universe, "active");
}
#[test]
fn test_recipe_corporate_bonds_no_ccy() {
let ticker = "MSFT".to_string();
let ccy: Option<String> = None;
let mut conditions = vec![
"SRCH_ASSET_CLASS=='Corporates'".to_string(),
format!("TICKER=='{ticker}'"),
];
if let Some(c) = ccy {
conditions.push(format!("CRNCY=='{c}'"));
}
let filter_str = conditions.join(" AND ");
assert_eq!(
filter_str,
"SRCH_ASSET_CLASS=='Corporates' AND TICKER=='MSFT'"
);
}
#[test]
fn test_recipe_bqr_default_event_types() {
let evts = resolve_event_types(None);
assert_eq!(evts, vec!["BID", "ASK"]);
}
#[test]
fn test_recipe_bqr_custom_event_types() {
let evts = resolve_event_types(Some(vec!["TRADE".to_string()]));
assert_eq!(evts, vec!["TRADE"]);
}
fn resolve_event_types(event_types: Option<Vec<String>>) -> Vec<String> {
event_types.unwrap_or_else(|| vec!["BID".to_string(), "ASK".to_string()])
}
}