use super::*;
use crate::common::test_utils::wire_enum::{check_wire_enum_rejects_unknown, check_wire_enum_round_trip};
use time::macros::date;
use time::{Date, Month};
fn check_str_partial_eq_round_trip<T>(matches: &str, differs: &str)
where
T: PartialEq<str> + for<'a> PartialEq<&'a str> + for<'a> From<&'a str> + std::fmt::Debug,
str: PartialEq<T>,
for<'a> &'a str: PartialEq<T>,
{
let v: T = matches.into();
assert_eq!(v, *matches);
assert_eq!(v, matches);
assert_eq!(*matches, v);
assert_eq!(matches, v);
assert_ne!(v, *differs);
assert_ne!(*differs, v);
}
#[test]
fn symbol_partial_eq_str_round_trip() {
check_str_partial_eq_round_trip::<Symbol>("AAPL", "MSFT");
}
#[test]
fn exchange_partial_eq_str_round_trip() {
check_str_partial_eq_round_trip::<Exchange>("NASDAQ", "NYSE");
}
#[test]
fn currency_partial_eq_str_round_trip() {
check_str_partial_eq_round_trip::<Currency>("USD", "EUR");
}
macro_rules! string_newtype_surface {
($name:ident, $t:ty, $sample:expr) => {
#[test]
fn $name() {
let owned: String = String::from($sample);
let from_new_str = <$t>::new($sample);
let from_new_string = <$t>::new(owned.clone());
let from_str: $t = <&str>::into($sample);
let from_string: $t = owned.clone().into();
let from_ref_string: $t = (&owned).into();
assert_eq!(from_new_str.as_str(), $sample);
assert_eq!(from_new_string, from_new_str);
assert_eq!(from_str, from_new_str);
assert_eq!(from_string, from_new_str);
assert_eq!(from_ref_string, from_new_str);
assert_eq!(format!("{}", from_new_str), $sample);
}
};
}
string_newtype_surface!(symbol_surface, Symbol, "AAPL");
string_newtype_surface!(exchange_surface, Exchange, "NASDAQ");
string_newtype_surface!(currency_surface, Currency, "USD");
string_newtype_surface!(cusip_surface, Cusip, "037833100");
string_newtype_surface!(isin_surface, Isin, "US0378331005");
#[test]
fn symbol_to_field_emits_raw() {
let s = Symbol::new("AAPL");
assert_eq!(s.to_field(), "AAPL");
}
#[test]
fn exchange_to_field_emits_raw() {
let e = Exchange::new("NASDAQ");
assert_eq!(e.to_field(), "NASDAQ");
}
#[test]
fn currency_to_field_emits_raw() {
let c = Currency::new("USD");
assert_eq!(c.to_field(), "USD");
}
#[test]
fn exchange_default_is_smart() {
assert_eq!(Exchange::default().as_str(), "SMART");
}
#[test]
fn exchange_is_empty_distinguishes() {
assert!(Exchange::new("").is_empty());
assert!(!Exchange::new("NYSE").is_empty());
}
#[test]
fn currency_default_is_usd() {
assert_eq!(Currency::default().as_str(), "USD");
}
#[test]
fn symbol_default_is_empty() {
assert_eq!(Symbol::default().as_str(), "");
}
#[test]
fn option_right_round_trip() {
check_wire_enum_round_trip(&[(OptionRight::Call, "C"), (OptionRight::Put, "P")]);
}
#[test]
fn option_right_from_str_rejects_unknown() {
check_wire_enum_rejects_unknown::<OptionRight>(&["", "INVALID", "c", "p", "CALL", "PUT"]);
}
#[test]
fn leg_action_round_trip() {
check_wire_enum_round_trip(&[(LegAction::Buy, "BUY"), (LegAction::Sell, "SELL"), (LegAction::SellShort, "SSHORT")]);
}
#[test]
fn leg_action_from_str_rejects_unknown() {
check_wire_enum_rejects_unknown::<LegAction>(&["", "INVALID", "buy", "SLONG"]);
}
#[test]
fn leg_action_default_is_buy() {
assert_eq!(LegAction::default(), LegAction::Buy);
}
#[test]
fn security_id_type_round_trip() {
check_wire_enum_round_trip(&[
(SecurityIdType::Cusip, "CUSIP"),
(SecurityIdType::Isin, "ISIN"),
(SecurityIdType::Sedol, "SEDOL"),
(SecurityIdType::Ric, "RIC"),
(SecurityIdType::Figi, "FIGI"),
]);
}
#[test]
fn security_id_type_from_str_rejects_unknown() {
check_wire_enum_rejects_unknown::<SecurityIdType>(&["", "INVALID", "cusip", "isin ", "Figi"]);
}
#[test]
fn strike_new_accepts_positive() {
let s = Strike::new(150.25).expect("positive strike must succeed");
assert_eq!(s.value(), 150.25);
}
#[test]
fn strike_new_rejects_zero_and_negative() {
assert!(Strike::new(0.0).is_err());
assert!(Strike::new(-1.0).is_err());
}
#[test]
fn strike_new_unchecked_succeeds_for_positive() {
let s = Strike::new_unchecked(42.0);
assert_eq!(s.value(), 42.0);
}
#[test]
#[should_panic(expected = "Strike price must be positive")]
fn strike_new_unchecked_panics_for_non_positive() {
let _ = Strike::new_unchecked(0.0);
}
#[test]
fn expiration_date_display_format() {
let d = ExpirationDate::new(2025, 3, 21);
assert_eq!(format!("{}", d), "20250321");
}
#[test]
fn expiration_date_days_until_friday_table() {
use time::Weekday::*;
assert_eq!(ExpirationDate::days_until_friday(Friday), 0);
assert_eq!(ExpirationDate::days_until_friday(Thursday), 1);
assert_eq!(ExpirationDate::days_until_friday(Wednesday), 2);
assert_eq!(ExpirationDate::days_until_friday(Tuesday), 3);
assert_eq!(ExpirationDate::days_until_friday(Monday), 4);
assert_eq!(ExpirationDate::days_until_friday(Sunday), 5);
assert_eq!(ExpirationDate::days_until_friday(Saturday), 6);
}
fn as_date(d: &ExpirationDate) -> Date {
let month = Month::try_from(d.month).expect("valid month");
Date::from_calendar_date(d.year as i32, month, d.day).expect("valid date")
}
#[test]
fn expiration_date_next_friday_is_a_future_friday() {
let nf = ExpirationDate::next_friday();
let date = as_date(&nf);
assert_eq!(date.weekday(), time::Weekday::Friday);
let today = OffsetDateTime::now_utc().date();
let delta = (date - today).whole_days();
assert!((1..=7).contains(&delta), "expected 1..=7 days, got {}", delta);
}
#[test]
fn expiration_date_third_friday_is_a_friday_in_15_to_21() {
let tf = ExpirationDate::third_friday_of_month();
let date = as_date(&tf);
assert_eq!(date.weekday(), time::Weekday::Friday);
assert!((15..=21).contains(&date.day()), "third Friday day must be 15..=21, got {}", date.day());
let today = OffsetDateTime::now_utc().date();
assert!(date >= today, "third Friday must be today or later, got {} vs {}", date, today);
}
#[test]
fn next_friday_from_friday_jumps_one_week() {
let nf = ExpirationDate::next_friday_from(date!(2025 - 03 - 21));
assert_eq!(format!("{}", nf), "20250328");
}
#[test]
fn next_friday_from_monday_advances_four_days() {
let nf = ExpirationDate::next_friday_from(date!(2025 - 03 - 17));
assert_eq!(format!("{}", nf), "20250321");
}
#[test]
fn third_friday_when_before_third_friday() {
let tf = ExpirationDate::third_friday_from(date!(2025 - 03 - 01));
assert_eq!(format!("{}", tf), "20250321");
}
#[test]
fn third_friday_when_past_rolls_to_next_month() {
let tf = ExpirationDate::third_friday_from(date!(2025 - 03 - 22));
assert_eq!(format!("{}", tf), "20250418");
}
#[test]
fn third_friday_december_rolls_to_january_next_year() {
let tf = ExpirationDate::third_friday_from(date!(2025 - 12 - 31));
assert_eq!(format!("{}", tf), "20260116");
}
#[test]
fn contract_month_display_format() {
let m = ContractMonth::new(2025, 6);
assert_eq!(format!("{}", m), "202506");
}
#[test]
fn contract_month_front_is_current_or_next() {
let m = ContractMonth::front();
assert!((1..=12).contains(&m.month), "month must be 1..=12, got {}", m.month);
let now = OffsetDateTime::now_utc();
let cur_year = now.year() as u16;
let cur_month = now.month() as u8;
let cur_day = now.day();
let (expected_year, expected_month) = if cur_day > 15 {
if cur_month == 12 {
(cur_year + 1, 1)
} else {
(cur_year, cur_month + 1)
}
} else {
(cur_year, cur_month)
};
assert_eq!(m.year, expected_year);
assert_eq!(m.month, expected_month);
}
#[test]
fn contract_month_next_quarter_is_quarterly() {
let m = ContractMonth::next_quarter();
assert!(matches!(m.month, 3 | 6 | 9 | 12), "next_quarter month must be quarterly, got {}", m.month);
let now = OffsetDateTime::now_utc();
let cur_year = now.year() as u16;
assert!(m.year == cur_year || m.year == cur_year + 1);
}
#[test]
fn contract_month_front_from_branches() {
assert_eq!(format!("{}", ContractMonth::front_from(2025, 6, 10)), "202506");
assert_eq!(format!("{}", ContractMonth::front_from(2025, 6, 20)), "202507");
assert_eq!(format!("{}", ContractMonth::front_from(2025, 12, 31)), "202601");
}
#[test]
fn contract_month_next_quarter_from_table() {
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 1, 10)), "202503");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 2, 28)), "202503");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 3, 1)), "202503");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 3, 20)), "202506");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 4, 5)), "202506");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 5, 31)), "202506");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 6, 1)), "202506");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 6, 30)), "202509");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 7, 4)), "202509");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 8, 31)), "202509");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 9, 1)), "202509");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 9, 30)), "202512");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 10, 5)), "202512");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 11, 30)), "202512");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 12, 1)), "202512");
assert_eq!(format!("{}", ContractMonth::next_quarter_from(2025, 12, 31)), "202603");
}
fn check_serde_round_trip<T>(sample: &str)
where
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug + for<'a> From<&'a str>,
{
let v: T = sample.into();
let json = serde_json::to_string(&v).expect("serialize");
assert_eq!(json, format!("\"{}\"", sample));
assert_eq!(serde_json::from_str::<T>(&json).expect("deserialize"), v);
}
#[test]
fn symbol_serde_round_trip() {
check_serde_round_trip::<Symbol>("AAPL");
}
#[test]
fn exchange_serde_round_trip() {
check_serde_round_trip::<Exchange>("NASDAQ");
}
#[test]
fn currency_serde_round_trip() {
check_serde_round_trip::<Currency>("USD");
}
#[test]
fn bond_identifier_variants_equality_and_match() {
let c = BondIdentifier::Cusip(Cusip::new("037833100"));
let i = BondIdentifier::Isin(Isin::new("US0378331005"));
assert_eq!(c, BondIdentifier::Cusip(Cusip::new("037833100")));
assert_eq!(i, BondIdentifier::Isin(Isin::new("US0378331005")));
assert_ne!(c, i);
match c {
BondIdentifier::Cusip(ref id) => assert_eq!(id.as_str(), "037833100"),
BondIdentifier::Isin(_) => panic!("expected Cusip"),
}
}
#[test]
fn missing_marker_is_copyable_and_debug() {
let m = Missing;
let copy = m;
let _ = copy;
assert_eq!(format!("{:?}", Missing), "Missing");
}