use super::*;
use crate::common::test_utils::wire_enum::{check_wire_enum_rejects_unknown, check_wire_enum_round_trip};
#[test]
fn v2_builders() {
let stock = Contract::stock("AAPL").build();
assert_eq!(stock.symbol, Symbol::from("AAPL"), "stock.symbol");
assert_eq!(stock.security_type, SecurityType::Stock, "stock.security_type");
assert_eq!(stock.currency, Currency::from("USD"), "stock.currency");
assert_eq!(stock.exchange, Exchange::from("SMART"), "stock.exchange");
let toyota = Contract::stock("7203").on_exchange("TSEJ").in_currency("JPY").build();
assert_eq!(toyota.symbol, Symbol::from("7203"));
assert_eq!(toyota.exchange, Exchange::from("TSEJ"));
assert_eq!(toyota.currency, Currency::from("JPY"));
let call = Contract::call("AAPL").strike(150.0).expires_on(2023, 12, 15).build();
assert_eq!(call.symbol, Symbol::from("AAPL"));
assert_eq!(call.security_type, SecurityType::Option);
assert_eq!(call.strike, 150.0);
assert_eq!(call.right, Some(OptionRight::Call));
assert_eq!(call.last_trade_date_or_contract_month, "20231215");
let put = Contract::put("SPY").strike(450.0).expires_on(2024, 1, 19).build();
assert_eq!(put.symbol, Symbol::from("SPY"));
assert_eq!(put.right, Some(OptionRight::Put));
assert_eq!(put.strike, 450.0);
let btc = Contract::crypto("BTC").build();
assert_eq!(btc.symbol, Symbol::from("BTC"));
assert_eq!(btc.security_type, SecurityType::Crypto);
assert_eq!(btc.currency, Currency::from("USD"));
assert_eq!(btc.exchange, Exchange::from("PAXOS"));
let spx = Contract::index("SPX");
assert_eq!(spx.symbol, Symbol::from("SPX"));
assert_eq!(spx.security_type, SecurityType::Index);
assert_eq!(spx.exchange, Exchange::from("CBOE"));
assert_eq!(spx.currency, Currency::from("USD"));
let news = Contract::news("BZ");
assert_eq!(news.symbol, Symbol::from("BZ:BZ_ALL"));
assert_eq!(news.security_type, SecurityType::News);
assert_eq!(news.exchange, Exchange::from("BZ"));
let option = Contract::option("AAPL", "20231215", 150.0, OptionRight::Call);
assert_eq!(option.symbol, Symbol::from("AAPL"));
assert_eq!(option.security_type, SecurityType::Option);
assert_eq!(option.strike, 150.0);
assert_eq!(option.right, Some(OptionRight::Call));
}
const ALL_SECURITY_TYPES: [(SecurityType, &str); 15] = [
(SecurityType::Stock, "STK"),
(SecurityType::Option, "OPT"),
(SecurityType::Future, "FUT"),
(SecurityType::ContinuousFuture, "CONTFUT"),
(SecurityType::Index, "IND"),
(SecurityType::FuturesOption, "FOP"),
(SecurityType::ForexPair, "CASH"),
(SecurityType::Spread, "BAG"),
(SecurityType::Warrant, "WAR"),
(SecurityType::Bond, "BOND"),
(SecurityType::Commodity, "CMDTY"),
(SecurityType::News, "NEWS"),
(SecurityType::MutualFund, "FUND"),
(SecurityType::Crypto, "CRYPTO"),
(SecurityType::CFD, "CFD"),
];
#[test]
fn security_type_round_trip() {
check_wire_enum_round_trip(&ALL_SECURITY_TYPES);
check_wire_enum_round_trip(&[(SecurityType::Other("XYZ".into()), "XYZ"), (SecurityType::Other("stk".into()), "stk")]);
}
#[test]
fn security_type_from_str_rejects_empty() {
check_wire_enum_rejects_unknown::<SecurityType>(&[""]);
}
#[test]
fn all_security_types_covers_every_variant() {
fn modeled_index(security_type: &SecurityType) -> Option<usize> {
match security_type {
SecurityType::Stock => Some(0),
SecurityType::Option => Some(1),
SecurityType::Future => Some(2),
SecurityType::ContinuousFuture => Some(3),
SecurityType::Index => Some(4),
SecurityType::FuturesOption => Some(5),
SecurityType::ForexPair => Some(6),
SecurityType::Spread => Some(7),
SecurityType::Warrant => Some(8),
SecurityType::Bond => Some(9),
SecurityType::Commodity => Some(10),
SecurityType::News => Some(11),
SecurityType::MutualFund => Some(12),
SecurityType::Crypto => Some(13),
SecurityType::CFD => Some(14),
SecurityType::Other(_) => None,
}
}
let mut seen = [false; 15];
for (variant, _) in &ALL_SECURITY_TYPES {
if let Some(index) = modeled_index(variant) {
seen[index] = true;
}
}
assert!(seen.iter().all(|&s| s), "ALL_SECURITY_TYPES is missing a SecurityType variant");
}
#[test]
fn security_type_display_all_variants() {
let cases = [
(SecurityType::Stock, "STK"),
(SecurityType::Option, "OPT"),
(SecurityType::Future, "FUT"),
(SecurityType::ContinuousFuture, "CONTFUT"),
(SecurityType::Index, "IND"),
(SecurityType::FuturesOption, "FOP"),
(SecurityType::ForexPair, "CASH"),
(SecurityType::Spread, "BAG"),
(SecurityType::Warrant, "WAR"),
(SecurityType::Bond, "BOND"),
(SecurityType::Commodity, "CMDTY"),
(SecurityType::News, "NEWS"),
(SecurityType::MutualFund, "FUND"),
(SecurityType::Crypto, "CRYPTO"),
(SecurityType::CFD, "CFD"),
(SecurityType::Other("XYZ".to_string()), "XYZ"),
];
for (variant, expected) in &cases {
assert_eq!(format!("{variant}"), *expected, "Display mismatch for {variant:?}");
assert_eq!(variant.to_field(), *expected, "ToField mismatch for {variant:?}");
}
}
#[test]
fn option_security_type_to_field() {
let some: Option<SecurityType> = Some(SecurityType::Option);
assert_eq!(some.to_field(), "OPT");
let none: Option<SecurityType> = None;
assert_eq!(none.to_field(), "");
}
fn assert_cusip_bond(bond: Contract, cusip: &str) {
assert_eq!(bond.symbol, Symbol::from(cusip));
assert_eq!(bond.security_type, SecurityType::Bond);
assert_eq!(bond.security_id_type, Some(SecurityIdType::Cusip));
assert_eq!(bond.security_id, cusip);
assert_eq!(bond.exchange, Exchange::from("SMART"));
assert_eq!(bond.currency, Currency::from("USD"));
}
fn assert_isin_currency_mapping(make_bond: impl Fn(&str) -> Contract) {
let cases = [
("US0378331005", "USD"),
("CA1234567890", "USD"),
("GB0002374006", "GBP"),
("JP3902900004", "JPY"),
("CH0012032048", "CHF"),
("AU000000BHP4", "AUD"),
("DE0001102309", "EUR"),
("FR0000131104", "EUR"),
("IT0001234567", "EUR"),
("ES0123456789", "EUR"),
("NL0011794037", "EUR"),
("BE0974293251", "EUR"),
("XX0000000000", "USD"),
("X", "USD"),
];
for (isin, expected_currency) in cases {
let bond = make_bond(isin);
assert_eq!(bond.symbol, Symbol::from(isin), "symbol for {isin}");
assert_eq!(bond.security_type, SecurityType::Bond);
assert_eq!(bond.security_id_type, Some(SecurityIdType::Isin));
assert_eq!(bond.security_id, isin);
assert_eq!(bond.exchange, Exchange::from("SMART"));
assert_eq!(bond.currency, Currency::from(expected_currency), "currency for {isin}");
}
}
#[test]
fn bond_cusip_constructor() {
assert_cusip_bond(Contract::bond_cusip("912810RN0"), "912810RN0");
}
#[test]
fn bond_constructor_cusip_variant() {
assert_cusip_bond(Contract::bond(BondIdentifier::Cusip(Cusip::new("912810RN0"))), "912810RN0");
}
#[test]
fn bond_isin_constructor_currency_mapping() {
assert_isin_currency_mapping(|isin| Contract::bond_isin(isin));
}
#[test]
fn bond_constructor_isin_currency_mapping() {
assert_isin_currency_mapping(|isin| Contract::bond(BondIdentifier::Isin(Isin::new(isin))));
}
#[test]
fn combo_leg_open_close() {
assert_eq!(ComboLegOpenClose::from(0), ComboLegOpenClose::Same, "0 should be Same");
assert_eq!(ComboLegOpenClose::from(1), ComboLegOpenClose::Open, "1 should be Open");
assert_eq!(ComboLegOpenClose::from(2), ComboLegOpenClose::Close, "2 should be Close");
assert_eq!(ComboLegOpenClose::from(3), ComboLegOpenClose::Unknown, "3 should be Unknown");
assert_eq!(ComboLegOpenClose::Same.to_field(), "0", "Same should be 0");
assert_eq!(ComboLegOpenClose::Open.to_field(), "1", "Open should be 1");
assert_eq!(ComboLegOpenClose::Close.to_field(), "2", "Close should be 2");
assert_eq!(ComboLegOpenClose::Unknown.to_field(), "3", "Unknown should be 3");
assert_eq!(ComboLegOpenClose::default(), ComboLegOpenClose::Same, "Default should be Same");
}
#[test]
fn combo_leg_open_close_unknown_value() {
assert_eq!(ComboLegOpenClose::from(4), ComboLegOpenClose::Unknown);
assert_eq!(ComboLegOpenClose::from(-1), ComboLegOpenClose::Unknown);
}
#[test]
fn tag_value_to_field() {
let tag_values = vec![
TagValue {
tag: "TAG1".to_string(),
value: "VALUE1".to_string(),
},
TagValue {
tag: "TAG2".to_string(),
value: "VALUE2".to_string(),
},
TagValue {
tag: "TAG3".to_string(),
value: "VALUE3".to_string(),
},
];
assert_eq!(
tag_values.to_field(),
"TAG1=VALUE1;TAG2=VALUE2;TAG3=VALUE3;",
"Tag values should be formatted as TAG=VALUE; pairs"
);
let single_tag_value = vec![TagValue {
tag: "SINGLE_TAG".to_string(),
value: "SINGLE_VALUE".to_string(),
}];
assert_eq!(
single_tag_value.to_field(),
"SINGLE_TAG=SINGLE_VALUE;",
"Single tag value should be formatted as TAG=VALUE;"
);
let empty: Vec<TagValue> = vec![];
assert_eq!(empty.to_field(), "", "Empty vec should result in empty string");
let empty_fields = vec![TagValue {
tag: "".to_string(),
value: "".to_string(),
}];
assert_eq!(empty_fields.to_field(), "=;", "Empty tag/value should be formatted as =;");
}
#[test]
fn is_bag() {
let stock_contract = Contract::stock("AAPL").build();
assert!(!stock_contract.is_bag(), "Stock contract should not be a bag");
let option_contract = Contract::option("AAPL", "20231215", 150.0, OptionRight::Call);
assert!(!option_contract.is_bag(), "Option contract should not be a bag");
let futures_contract = Contract {
symbol: Symbol::from("ES"),
security_type: SecurityType::Future,
..Default::default()
};
assert!(!futures_contract.is_bag(), "Futures contract should not be a bag");
let spread_contract = Contract {
security_type: SecurityType::Spread,
..Default::default()
};
assert!(spread_contract.is_bag(), "Spread contract should be a bag");
let bag_contract = Contract {
security_type: "BAG".parse().unwrap(),
..Default::default()
};
assert!(bag_contract.is_bag(), "BAG contract should be a bag");
let combo_contract = Contract {
security_type: SecurityType::Spread,
combo_legs: vec![
ComboLeg {
contract_id: 12345,
ratio: 1,
action: LegAction::Buy,
exchange: "SMART".to_string(),
..Default::default()
},
ComboLeg {
contract_id: 67890,
ratio: 1,
action: LegAction::Sell,
exchange: "SMART".to_string(),
..Default::default()
},
],
..Default::default()
};
assert!(combo_contract.is_bag(), "Contract with combo legs should be a bag");
}
#[cfg(feature = "utoipa")]
#[test]
fn schema_derives_work() {
fn assert_schema<T: utoipa::ToSchema>() {}
assert_schema::<Contract>();
assert_schema::<ContractDetails>();
assert_schema::<SecurityType>();
assert_schema::<TagValue>();
}