use crate::{
AuthKind, ChainCapability, ChainPollCapability, GreeksCapability, OptionStreamCapability,
ProviderCapabilities, RESERVED_PROVIDER_IDS,
};
struct MatrixRow {
id: &'static str,
chain: ChainCapability,
depth: bool,
greeks: GreeksCapability,
option_stream: OptionStreamCapability,
underlying_stream: bool,
chain_poll: bool,
trades_tape: bool,
auth: AuthKind,
}
#[track_caller]
fn assert_row(caps: ProviderCapabilities, row: &MatrixRow) {
let id = row.id;
assert_eq!(caps.chain, row.chain, "{id}: chain cell drifted from §8");
assert_eq!(caps.depth, row.depth, "{id}: depth cell drifted from §8");
assert_eq!(caps.greeks, row.greeks, "{id}: greeks cell drifted from §8");
assert_eq!(
caps.option_stream, row.option_stream,
"{id}: option-stream cell drifted from §8"
);
assert_eq!(
caps.underlying_stream, row.underlying_stream,
"{id}: underlying-stream cell drifted from §8"
);
let polls = matches!(caps.chain_poll, ChainPollCapability::Poll { .. });
assert_eq!(
polls, row.chain_poll,
"{id}: chain-poll cell drifted from §8"
);
assert_eq!(
caps.trades_tape, row.trades_tape,
"{id}: trades-tape cell drifted from §8"
);
assert_eq!(caps.auth, row.auth, "{id}: auth cell drifted from §8");
}
fn deribit_row() -> MatrixRow {
MatrixRow {
id: "deribit",
chain: ChainCapability::Assemble,
depth: true,
greeks: GreeksCapability::Provided,
option_stream: OptionStreamCapability::ChainQuotes { verified: false },
underlying_stream: true,
chain_poll: true,
trades_tape: false,
auth: AuthKind::None,
}
}
#[test]
fn test_deribit_row_reconciles_with_section_8() {
assert_row(
crate::providers::deribit::deribit_capabilities(),
&deribit_row(),
);
}
#[cfg(feature = "tastytrade")]
#[test]
fn test_tastytrade_row_reconciles_with_section_8() {
let row = MatrixRow {
id: "tastytrade",
chain: ChainCapability::Native,
depth: false,
greeks: GreeksCapability::Provided,
option_stream: OptionStreamCapability::ChainQuotes { verified: false },
underlying_stream: false,
chain_poll: true,
trades_tape: false,
auth: AuthKind::OAuth,
};
assert_row(
crate::providers::tastytrade::tastytrade_capabilities(),
&row,
);
}
#[cfg(feature = "alpaca")]
#[test]
fn test_alpaca_row_reconciles_with_section_8() {
let row = MatrixRow {
id: "alpaca",
chain: ChainCapability::Native,
depth: false,
greeks: GreeksCapability::Provided,
option_stream: OptionStreamCapability::None,
underlying_stream: false,
chain_poll: true,
trades_tape: false,
auth: AuthKind::KeySecret,
};
assert_row(crate::providers::alpaca::alpaca_capabilities(), &row);
}
#[cfg(feature = "dxlink")]
#[test]
fn test_dxlink_row_reconciles_with_section_8() {
let row = MatrixRow {
id: "dxlink",
chain: ChainCapability::None,
depth: false,
greeks: GreeksCapability::Provided,
option_stream: OptionStreamCapability::SymbolOnly { verified: false },
underlying_stream: false,
chain_poll: false,
trades_tape: false,
auth: AuthKind::Token,
};
assert_row(crate::providers::dxlink::dxlink_capabilities(), &row);
}
#[cfg(feature = "ig")]
#[test]
fn test_ig_row_reconciles_with_section_8() {
let row = MatrixRow {
id: "ig",
chain: ChainCapability::Partial,
depth: false,
greeks: GreeksCapability::ComputedLocally,
option_stream: OptionStreamCapability::ChainQuotes { verified: false },
underlying_stream: false,
chain_poll: true,
trades_tape: false,
auth: AuthKind::UserPass,
};
assert_row(crate::providers::ig::ig_capabilities(), &row);
}
#[cfg(feature = "ibkr")]
#[test]
fn test_ibkr_row_reconciles_with_section_8() {
let row = MatrixRow {
id: "ibkr",
chain: ChainCapability::Assemble,
depth: false,
greeks: GreeksCapability::Provided,
option_stream: OptionStreamCapability::ChainQuotes { verified: false },
underlying_stream: false,
chain_poll: true,
trades_tape: false,
auth: AuthKind::None,
};
assert_row(crate::providers::ibkr::ibkr_capabilities(), &row);
}
#[test]
fn test_ibkr_stays_a_reserved_id() {
assert!(
RESERVED_PROVIDER_IDS.contains(&"ibkr"),
"ibkr stays a reserved built-in id"
);
}
#[test]
fn test_ig_stays_a_reserved_id() {
assert!(
RESERVED_PROVIDER_IDS.contains(&"ig"),
"ig stays a reserved built-in id"
);
}
#[test]
fn test_every_reserved_id_is_either_reconciled_or_deferred() {
assert_eq!(RESERVED_PROVIDER_IDS.len(), 6);
for id in ["deribit", "tastytrade", "dxlink", "ig", "alpaca", "ibkr"] {
assert!(
RESERVED_PROVIDER_IDS.contains(&id),
"{id} is one of the six reserved built-in ids"
);
}
}
const IG_OPTION_DEPTH_FIXTURE: &str =
include_str!("../tests/fixtures/ig/depth/option_epic_price_snapshot.json");
const IG_DOM_FIELDS: [&str; 20] = [
"BIDPRICE1",
"BIDPRICE2",
"BIDPRICE3",
"BIDPRICE4",
"BIDPRICE5",
"ASKPRICE1",
"ASKPRICE2",
"ASKPRICE3",
"ASKPRICE4",
"ASKPRICE5",
"BIDSIZE1",
"BIDSIZE2",
"BIDSIZE3",
"BIDSIZE4",
"BIDSIZE5",
"ASKSIZE1",
"ASKSIZE2",
"ASKSIZE3",
"ASKSIZE4",
"ASKSIZE5",
];
#[derive(serde::Deserialize)]
struct IgOptionDepthFixture {
epic: String,
instrument_type: String,
market_details_snapshot: IgMarketDetailsSnapshot,
price_subscription: IgPriceSubscription,
}
#[derive(serde::Deserialize)]
struct IgMarketDetailsSnapshot {
bid: f64,
offer: f64,
}
#[derive(serde::Deserialize)]
struct IgPriceSubscription {
fields: std::collections::BTreeMap<String, Option<String>>,
}
#[test]
fn test_ig_option_epic_depth_fixture_shape_proves_no_populated_ladder() {
let fixture: IgOptionDepthFixture = match serde_json::from_str(IG_OPTION_DEPTH_FIXTURE) {
Ok(fixture) => fixture,
Err(e) => {
panic!("the IG option-epic depth fixture must parse as the documented shape: {e}")
}
};
assert!(
fixture.epic.starts_with("OP."),
"a dated-option epic (OP.*): {}",
fixture.epic,
);
assert!(
fixture.instrument_type.starts_with("OPT"),
"an option instrument type: {}",
fixture.instrument_type,
);
let snapshot = &fixture.market_details_snapshot;
assert!(
snapshot.bid > 0.0 && snapshot.offer > snapshot.bid,
"the option carries a top-of-book bid/offer (it is quoted): bid {} offer {}",
snapshot.bid,
snapshot.offer,
);
let fields = &fixture.price_subscription.fields;
for name in IG_DOM_FIELDS {
match fields.get(name) {
Some(value) => assert!(
value.is_none(),
"DOM field {name} must be unpopulated (null) for a dated-option epic, got {value:?}",
),
None => panic!("the fixture must carry the documented DOM field {name}"),
}
}
for name in ["BIDQUOTEID", "ASKQUOTEID"] {
assert!(
matches!(fields.get(name), Some(None)),
"{name} must be unpopulated (no ladder)",
);
}
assert!(
matches!(fields.get("TIMESTAMP"), Some(Some(_))),
"the venue timestamp is present",
);
}
#[test]
fn test_ig_depth_disposition_is_evidence_on_file() {
assert!(
RESERVED_PROVIDER_IDS.contains(&"ig"),
"ig is a reserved built-in id (#39)",
);
}