use crate::instrument::name::InstrumentNameExchange;
use fnv::FnvHashMap;
use ibapi::contracts::Contract;
use parking_lot::RwLock;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum ContractRegistryError {
#[error(
"contract for {name} has no IB contract id; resolve it through IB's contract details first"
)]
Unresolved {
name: InstrumentNameExchange,
},
#[error("IB contract id {contract_id} is already registered as {registered}, not {name}")]
ContractIdTaken {
contract_id: i32,
name: InstrumentNameExchange,
registered: InstrumentNameExchange,
},
}
#[derive(Debug, Clone)]
pub struct ContractRegistry {
inner: Arc<RwLock<ContractRegistryInner>>,
}
#[derive(Debug, Default)]
struct ContractRegistryInner {
by_name: FnvHashMap<InstrumentNameExchange, Contract>,
by_con_id: FnvHashMap<i32, InstrumentNameExchange>,
}
impl ContractRegistryInner {
fn remove_reverse_mapping(&mut self, name: &InstrumentNameExchange) {
let Some(old_con_id) = self.by_name.get(name).map(|contract| contract.contract_id) else {
return;
};
if self.by_con_id.get(&old_con_id) == Some(name) {
self.by_con_id.remove(&old_con_id);
}
}
}
impl ContractRegistry {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(ContractRegistryInner::default())),
}
}
pub fn register(
&self,
name: InstrumentNameExchange,
contract: Contract,
) -> Result<(), ContractRegistryError> {
let con_id = contract.contract_id;
if con_id <= 0 {
return Err(ContractRegistryError::Unresolved { name });
}
let mut inner = self.inner.write();
if let Some(registered) = inner.by_con_id.get(&con_id)
&& *registered != name
{
return Err(ContractRegistryError::ContractIdTaken {
contract_id: con_id,
name,
registered: registered.clone(),
});
}
inner.remove_reverse_mapping(&name);
inner.by_name.insert(name.clone(), contract);
inner.by_con_id.insert(con_id, name);
Ok(())
}
pub fn register_by_name_only(&self, name: InstrumentNameExchange, contract: Contract) {
let mut inner = self.inner.write();
inner.remove_reverse_mapping(&name);
inner.by_name.insert(name, contract);
}
pub fn get_contract(&self, name: &InstrumentNameExchange) -> Option<Contract> {
self.inner.read().by_name.get(name).cloned()
}
pub fn get_name_by_con_id(&self, con_id: i32) -> Option<InstrumentNameExchange> {
self.inner.read().by_con_id.get(&con_id).cloned()
}
pub fn contains(&self, name: &InstrumentNameExchange) -> bool {
self.inner.read().by_name.contains_key(name)
}
pub fn len(&self) -> usize {
self.inner.read().by_name.len()
}
pub fn is_empty(&self) -> bool {
self.inner.read().by_name.is_empty()
}
}
impl Default for ContractRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use ibapi::contracts::Contract;
fn stock_contract(symbol: &str, exchange: &str, currency: &str) -> Contract {
Contract::stock(symbol)
.on_exchange(exchange)
.in_currency(currency)
.build()
}
#[test]
fn test_contract_registry_basic() {
let registry = ContractRegistry::new();
let name = InstrumentNameExchange::from("AAPL");
let mut contract = stock_contract("AAPL", "SMART", "USD");
contract.contract_id = 265598;
registry.register(name.clone(), contract.clone()).unwrap();
assert!(registry.contains(&name));
assert_eq!(registry.len(), 1);
let retrieved = registry.get_contract(&name).unwrap();
assert_eq!(retrieved.symbol.as_str(), "AAPL");
assert_eq!(retrieved.contract_id, 265598);
let name_by_id = registry.get_name_by_con_id(265598).unwrap();
assert_eq!(name_by_id, name);
}
#[test]
fn test_contract_registry_missing() {
let registry = ContractRegistry::new();
let name = InstrumentNameExchange::from("MISSING");
assert!(!registry.contains(&name));
assert!(registry.get_contract(&name).is_none());
assert!(registry.get_name_by_con_id(999999).is_none());
}
#[test]
fn test_contract_registry_reregistration() {
let registry = ContractRegistry::new();
let name = InstrumentNameExchange::from("AAPL");
let mut contract1 = stock_contract("AAPL", "SMART", "USD");
contract1.contract_id = 111111;
registry.register(name.clone(), contract1).unwrap();
assert_eq!(registry.get_name_by_con_id(111111), Some(name.clone()));
let mut contract2 = stock_contract("AAPL", "SMART", "USD");
contract2.contract_id = 222222;
registry.register(name.clone(), contract2).unwrap();
assert_eq!(registry.get_name_by_con_id(222222), Some(name.clone()));
assert!(registry.get_name_by_con_id(111111).is_none());
assert_eq!(registry.len(), 1);
}
fn resolved(symbol: &str, contract_id: i32) -> Contract {
Contract {
contract_id,
..stock_contract(symbol, "SMART", "USD")
}
}
#[test]
fn an_unresolved_contract_is_refused_and_nothing_is_registered() {
let registry = ContractRegistry::new();
let name = InstrumentNameExchange::from("AAPL");
for contract_id in [0, -1] {
assert_eq!(
registry.register(name.clone(), resolved("AAPL", contract_id)),
Err(ContractRegistryError::Unresolved { name: name.clone() })
);
}
assert!(registry.is_empty());
assert!(registry.get_name_by_con_id(0).is_none());
}
#[test]
fn a_contract_id_registered_under_another_name_is_refused() {
let registry = ContractRegistry::new();
let aapl = InstrumentNameExchange::from("AAPL");
let other = InstrumentNameExchange::from("AAPL-2");
registry
.register(aapl.clone(), resolved("AAPL", 265598))
.unwrap();
assert_eq!(
registry.register(other.clone(), resolved("AAPL", 265598)),
Err(ContractRegistryError::ContractIdTaken {
contract_id: 265598,
name: other.clone(),
registered: aapl.clone(),
})
);
assert_eq!(registry.get_name_by_con_id(265598), Some(aapl));
assert!(!registry.contains(&other));
assert_eq!(registry.len(), 1);
}
#[test]
fn registering_the_same_name_and_contract_id_again_is_accepted() {
let registry = ContractRegistry::new();
let name = InstrumentNameExchange::from("AAPL");
registry
.register(name.clone(), resolved("AAPL", 265598))
.unwrap();
registry
.register(name.clone(), resolved("AAPL", 265598))
.unwrap();
assert_eq!(registry.get_name_by_con_id(265598), Some(name));
assert_eq!(registry.len(), 1);
}
#[test]
fn a_contract_id_freed_by_reregistration_can_be_registered_under_another_name() {
let registry = ContractRegistry::new();
let front = InstrumentNameExchange::from("ES-FRONT");
let next = InstrumentNameExchange::from("ES-NEXT");
registry
.register(front.clone(), resolved("ES", 111))
.unwrap();
registry
.register(front.clone(), resolved("ES", 222))
.unwrap();
registry
.register(next.clone(), resolved("ES", 111))
.unwrap();
assert_eq!(registry.get_name_by_con_id(111), Some(next));
assert_eq!(registry.get_name_by_con_id(222), Some(front));
}
#[test]
fn a_name_only_contract_is_found_by_name_and_never_by_contract_id() {
let registry = ContractRegistry::new();
let unresolved = InstrumentNameExchange::from("MSFT");
let with_id = InstrumentNameExchange::from("AAPL");
registry.register_by_name_only(unresolved.clone(), stock_contract("MSFT", "SMART", "USD"));
registry.register_by_name_only(with_id.clone(), resolved("AAPL", 265598));
assert!(registry.get_contract(&unresolved).is_some());
assert!(registry.get_contract(&with_id).is_some());
assert!(registry.get_name_by_con_id(0).is_none());
assert!(registry.get_name_by_con_id(265598).is_none());
assert_eq!(registry.len(), 2);
}
#[test]
fn registering_a_name_by_name_only_clears_its_contract_id() {
let registry = ContractRegistry::new();
let name = InstrumentNameExchange::from("AAPL");
registry
.register(name.clone(), resolved("AAPL", 265598))
.unwrap();
registry.register_by_name_only(name.clone(), stock_contract("AAPL", "SMART", "USD"));
assert!(registry.get_name_by_con_id(265598).is_none());
assert_eq!(registry.get_contract(&name).unwrap().contract_id, 0);
}
#[test]
fn a_name_only_entry_does_not_clear_another_names_contract_id() {
let registry = ContractRegistry::new();
let aapl = InstrumentNameExchange::from("AAPL");
let quotes = InstrumentNameExchange::from("AAPL-QUOTES");
registry
.register(aapl.clone(), resolved("AAPL", 265598))
.unwrap();
registry.register_by_name_only(quotes.clone(), resolved("AAPL", 265598));
registry.register_by_name_only(quotes, stock_contract("AAPL", "SMART", "USD"));
assert_eq!(registry.get_name_by_con_id(265598), Some(aapl));
}
}