use std::{cell::RefCell, collections::VecDeque, rc::Rc};
use indexmap::{IndexMap, IndexSet};
use nautilus_common::cache::Cache;
use nautilus_model::{
events::OrderUpdated,
identifiers::{AccountId, ClientOrderId, InstrumentId},
orders::Order,
types::Quantity,
};
#[derive(Debug, Clone, Default)]
pub struct MatchingOrders {
pub(super) pending_updates: Rc<RefCell<IndexMap<ClientOrderId, Vec<OrderUpdated>>>>,
pub(super) cached_filled_qty: Rc<RefCell<IndexMap<ClientOrderId, Quantity>>>,
pub(super) pending_oto_order_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
pub(super) released_oto_order_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
pub(super) rejected_oto_parent_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
pub(super) closed_oto_order_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
pub(super) commands: Rc<RefCell<VecDeque<OtoCommand>>>,
}
#[derive(Debug, Clone)]
pub enum OtoCommand {
Release {
instrument_id: InstrumentId,
client_order_id: ClientOrderId,
account_id: AccountId,
parent_instrument_id: InstrumentId,
parent_order_id: ClientOrderId,
parent_quantity: Option<Quantity>,
},
Resize {
instrument_id: InstrumentId,
parent_order_id: ClientOrderId,
quantity: Quantity,
},
Cancel {
instrument_id: InstrumentId,
client_order_id: ClientOrderId,
cancel_contingencies: bool,
excluded: Vec<ClientOrderId>,
},
}
impl OtoCommand {
pub fn instrument_id(&self) -> InstrumentId {
match self {
Self::Release { instrument_id, .. }
| Self::Resize { instrument_id, .. }
| Self::Cancel { instrument_id, .. } => *instrument_id,
}
}
}
impl MatchingOrders {
pub fn pop_command(&self) -> Option<OtoCommand> {
self.commands.borrow_mut().pop_front()
}
pub(super) fn reset(&self, instrument_id: InstrumentId, cache: &Cache) {
let keep = |id: &ClientOrderId| {
cache
.order(id)
.is_some_and(|order| order.instrument_id() != instrument_id)
};
self.pending_updates.borrow_mut().retain(|id, _| keep(id));
self.cached_filled_qty.borrow_mut().retain(|id, _| keep(id));
self.pending_oto_order_ids.borrow_mut().retain(keep);
self.released_oto_order_ids.borrow_mut().retain(keep);
self.rejected_oto_parent_ids.borrow_mut().retain(keep);
self.closed_oto_order_ids.borrow_mut().retain(keep);
self.commands.borrow_mut().retain(|command| {
command.instrument_id() != instrument_id && !matches!(command,
OtoCommand::Release { parent_instrument_id, .. } if *parent_instrument_id == instrument_id)
});
}
}
#[cfg(test)]
mod tests {
use nautilus_model::{
enums::OrderType,
events::order::spec::OrderUpdatedSpec,
instruments::{CryptoPerpetual, Instrument, stubs::crypto_perpetual_ethusdt},
orders::OrderTestBuilder,
};
use rstest::rstest;
use super::*;
#[rstest]
fn test_reset_retains_other_instrument_state(crypto_perpetual_ethusdt: CryptoPerpetual) {
let first = crypto_perpetual_ethusdt.id();
let second = InstrumentId::from("BTCUSDT-PERP.BINANCE");
let first_id = ClientOrderId::from("FIRST");
let second_id = ClientOrderId::from("SECOND");
let orders = MatchingOrders::default();
let mut cache = Cache::default();
let update = OrderUpdatedSpec::builder()
.instrument_id(second)
.client_order_id(second_id)
.quantity(Quantity::from("4.560"))
.build();
for (instrument, id, qty) in [(first, first_id, "1.230"), (second, second_id, "4.560")] {
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument)
.client_order_id(id)
.quantity(Quantity::from(qty))
.build();
cache.add_order(order, None, None, false).unwrap();
let pending = if id == second_id {
update
} else {
OrderUpdatedSpec::builder()
.instrument_id(instrument)
.client_order_id(id)
.quantity(Quantity::from(qty))
.build()
};
orders
.pending_updates
.borrow_mut()
.insert(id, vec![pending]);
orders
.cached_filled_qty
.borrow_mut()
.insert(id, Quantity::from(qty));
orders.pending_oto_order_ids.borrow_mut().insert(id);
orders.released_oto_order_ids.borrow_mut().insert(id);
orders.rejected_oto_parent_ids.borrow_mut().insert(id);
orders.closed_oto_order_ids.borrow_mut().insert(id);
}
orders.commands.borrow_mut().push_back(OtoCommand::Release {
instrument_id: second,
client_order_id: second_id,
account_id: AccountId::from("SIM-001"),
parent_instrument_id: first,
parent_order_id: first_id,
parent_quantity: Some(Quantity::from("1.230")),
});
orders.commands.borrow_mut().push_back(OtoCommand::Cancel {
instrument_id: second,
client_order_id: second_id,
cancel_contingencies: true,
excluded: vec![first_id],
});
for (instrument_id, parent_order_id, quantity) in [
(first, first_id, Quantity::from("2.340")),
(second, second_id, Quantity::from("5.670")),
] {
orders.commands.borrow_mut().push_back(OtoCommand::Resize {
instrument_id,
parent_order_id,
quantity,
});
}
orders.reset(first, &cache);
assert_eq!(
*orders.pending_updates.borrow(),
IndexMap::from([(second_id, vec![update])])
);
assert_eq!(
*orders.cached_filled_qty.borrow(),
IndexMap::from([(second_id, Quantity::from("4.560"))])
);
assert_eq!(
*orders.pending_oto_order_ids.borrow(),
IndexSet::from([second_id])
);
assert_eq!(
*orders.released_oto_order_ids.borrow(),
IndexSet::from([second_id])
);
assert_eq!(
*orders.rejected_oto_parent_ids.borrow(),
IndexSet::from([second_id])
);
assert_eq!(
*orders.closed_oto_order_ids.borrow(),
IndexSet::from([second_id])
);
let OtoCommand::Cancel {
instrument_id,
client_order_id,
cancel_contingencies,
excluded,
} = orders.pop_command().unwrap()
else {
panic!("Expected remaining peer cancellation");
};
assert_eq!(instrument_id, second);
assert_eq!(client_order_id, second_id);
assert!(cancel_contingencies);
assert_eq!(excluded, vec![first_id]);
let OtoCommand::Resize {
instrument_id,
parent_order_id,
quantity,
} = orders.pop_command().unwrap()
else {
panic!("Expected remaining peer resize");
};
assert_eq!(instrument_id, second);
assert_eq!(parent_order_id, second_id);
assert_eq!(quantity, Quantity::from("5.670"));
assert!(orders.pop_command().is_none());
}
}