use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::types::serde_util::comma_separated;
use crate::types::wire::wire_enum;
wire_enum! {
pub enum BondStatus {
Outstanding => "outstanding",
Matured => "matured",
PreIssuance => "pre_issuance",
}
}
wire_enum! {
pub enum CouponFrequency {
Annual => "annual",
SemiAnnual => "semi_annual",
Quarterly => "quarterly",
Monthly => "monthly",
Zero => "zero",
}
}
wire_enum! {
pub enum CouponType {
Fixed => "fixed",
Floating => "floating",
Zero => "zero",
}
}
wire_enum! {
pub enum DayCount {
Actual360 => "A/360",
Actual365 => "A/365",
Thirty360 => "30/360",
Thirty365 => "30/365",
ActualActual => "A/A",
ThirtyE360 => "30E/360",
Business252 => "B/252",
Actual364 => "A/364",
}
}
wire_enum! {
pub enum CallType {
Ordinary => "ordinary",
MakeWhole => "make_whole",
Regulatory => "regulatory",
Special => "special",
}
}
wire_enum! {
pub enum CreditOutlook {
Positive => "positive",
Negative => "negative",
Developing => "developing",
Stable => "stable",
NotRated => "not_rated",
NotMeaningful => "not_meaningful",
}
}
wire_enum! {
pub enum TreasurySubtype {
Bond => "bond",
Bill => "bill",
Note => "note",
Strips => "strips",
Tips => "tips",
Floating => "floating",
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UsCorporate {
pub cusip: String,
pub isin: String,
pub ticker: String,
pub issuer: String,
pub description: String,
pub description_short: String,
pub bond_status: BondStatus,
pub sector: String,
pub country_domicile: String,
pub seniority: String,
pub coupon: f64,
pub coupon_frequency: CouponFrequency,
pub coupon_type: CouponType,
pub day_count: DayCount,
pub dated_date: NaiveDate,
pub issue_date: NaiveDate,
pub issue_price: f64,
pub issue_size: f64,
pub issue_minimum_denomination: f64,
pub par_value: f64,
pub callable: bool,
pub convertible: bool,
pub puttable: bool,
pub perpetual: bool,
pub reg_s: bool,
pub tradable: bool,
#[serde(default)]
pub fractionable: bool,
pub marginable: bool,
#[serde(default)]
pub maturity_date: Option<NaiveDate>,
#[serde(default)]
pub accrued_interest: Option<f64>,
#[serde(default)]
pub call_type: Option<CallType>,
#[serde(default)]
pub next_call_date: Option<NaiveDate>,
#[serde(default)]
pub next_call_price: Option<f64>,
#[serde(default)]
pub first_coupon_date: Option<NaiveDate>,
#[serde(default)]
pub last_coupon_date: Option<NaiveDate>,
#[serde(default)]
pub next_coupon_date: Option<NaiveDate>,
#[serde(default)]
pub close_price: Option<f64>,
#[serde(default)]
pub close_price_date: Option<NaiveDate>,
#[serde(default)]
pub close_yield_to_maturity: Option<f64>,
#[serde(default)]
pub close_yield_to_worst: Option<f64>,
#[serde(default)]
pub reissue_date: Option<NaiveDate>,
#[serde(default)]
pub reissue_price: Option<f64>,
#[serde(default)]
pub reissue_size: Option<f64>,
#[serde(default)]
pub sp_rating: Option<String>,
#[serde(default)]
pub sp_rating_date: Option<NaiveDate>,
#[serde(default)]
pub sp_outlook: Option<CreditOutlook>,
#[serde(default)]
pub sp_outlook_date: Option<NaiveDate>,
#[serde(default)]
pub sp_creditwatch: Option<String>,
#[serde(default)]
pub sp_creditwatch_date: Option<NaiveDate>,
#[serde(default)]
pub liquidity_institutional_aggregate: Option<f64>,
#[serde(default)]
pub liquidity_institutional_buy: Option<f64>,
#[serde(default)]
pub liquidity_institutional_sell: Option<f64>,
#[serde(default)]
pub liquidity_retail_aggregate: Option<f64>,
#[serde(default)]
pub liquidity_retail_buy: Option<f64>,
#[serde(default)]
pub liquidity_retail_sell: Option<f64>,
#[serde(default)]
pub liquidity_micro_aggregate: Option<f64>,
#[serde(default)]
pub liquidity_micro_buy: Option<f64>,
#[serde(default)]
pub liquidity_micro_sell: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UsTreasury {
pub cusip: String,
pub isin: String,
pub description: String,
pub description_short: String,
pub subtype: TreasurySubtype,
pub bond_status: BondStatus,
pub coupon: f64,
pub coupon_frequency: CouponFrequency,
pub coupon_type: CouponType,
pub issue_date: NaiveDate,
pub maturity_date: NaiveDate,
pub tradable: bool,
#[serde(default)]
pub fractionable: bool,
#[serde(default)]
pub first_coupon_date: Option<NaiveDate>,
#[serde(default)]
pub last_coupon_date: Option<NaiveDate>,
#[serde(default)]
pub next_coupon_date: Option<NaiveDate>,
#[serde(default)]
pub close_price: Option<f64>,
#[serde(default)]
pub close_price_date: Option<NaiveDate>,
#[serde(default)]
pub close_yield_to_maturity: Option<f64>,
#[serde(default)]
pub close_yield_to_worst: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UsCorporates {
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub us_corporates: Vec<UsCorporate>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UsTreasuries {
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub us_treasuries: Vec<UsTreasury>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EntryRequirement {
pub symbol: String,
#[serde(default, with = "crate::types::option_decimal")]
pub regt_long: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub regt_short: Option<Decimal>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetUsCorporatesRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bond_status: Option<BondStatus>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub isins: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub cusips: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub tickers: Option<Vec<String>>,
}
impl GetUsCorporatesRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn cusips(mut self, cusips: Vec<String>) -> Self {
self.cusips = Some(cusips);
self
}
#[must_use]
pub fn tickers(mut self, tickers: Vec<String>) -> Self {
self.tickers = Some(tickers);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetUsTreasuriesRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subtype: Option<TreasurySubtype>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bond_status: Option<BondStatus>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub isins: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub cusips: Option<Vec<String>>,
}
impl GetUsTreasuriesRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn subtype(mut self, subtype: TreasurySubtype) -> Self {
self.subtype = Some(subtype);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetEntryRequirementsRequest {
#[serde(serialize_with = "crate::types::serde_util::comma_separated_required")]
pub symbols: Vec<String>,
}
impl GetEntryRequirementsRequest {
#[must_use]
pub fn new(symbols: Vec<String>) -> Self {
Self { symbols }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bill_has_no_coupon_dates_and_still_decodes() {
let treasury: UsTreasury = serde_json::from_value(serde_json::json!({
"bond_status": "outstanding",
"close_price": 99.6459,
"coupon": 0,
"coupon_frequency": "zero",
"coupon_type": "zero",
"cusip": "912797KJ5",
"description": "test bill",
"description_short": "tb",
"isin": "US912797KJ59",
"issue_date": "2026-04-03",
"maturity_date": "2026-07-03",
"subtype": "bill",
"tradable": true,
}))
.unwrap();
assert_eq!(treasury.subtype, TreasurySubtype::Bill);
assert_eq!(treasury.first_coupon_date, None);
assert_eq!(treasury.coupon, 0.0);
}
#[test]
fn day_counts_keep_their_slashes() {
assert_eq!(DayCount::Thirty360.as_str(), "30/360");
assert_eq!(DayCount::ActualActual.as_str(), "A/A");
}
#[test]
fn entry_requirements_are_decimals_because_they_arrive_as_strings() {
let requirement: EntryRequirement = serde_json::from_value(serde_json::json!({
"symbol": "AAPL",
"regt_long": "0.5",
"regt_short": "1.5",
}))
.unwrap();
assert_eq!(requirement.regt_long, Some(Decimal::new(5, 1)));
}
}