#[cfg(feature = "defi")]
use std::sync::Arc;
use ahash::AHashSet;
use bytes::Bytes;
use nautilus_core::{UUID4, UnixNanos};
#[cfg(feature = "defi")]
use nautilus_model::defi::{
AmmType, Dex, DexType, Pool, PoolIdentifier, PoolProfiler, Token, chain::chains,
};
use nautilus_model::{
accounts::AccountAny,
data::{
Bar, BarType, FundingRateUpdate, InstrumentStatus, MarkPriceUpdate, QuoteTick, TradeTick,
},
enums::{
AggressorSide, BookType, ContingencyType, MarketStatusAction, OmsType, OrderSide,
OrderStatus, OrderType, PositionSide, PriceType, TriggerType,
},
events::{
OrderAccepted, OrderCanceled, OrderEmulated, OrderEventAny, OrderFilled, OrderRejected,
OrderReleased, OrderSubmitted,
},
identifiers::{
AccountId, ClientOrderId, InstrumentId, OrderListId, PositionId, StrategyId, Symbol,
TradeId, Venue, VenueOrderId,
},
instruments::{CurrencyPair, Instrument, InstrumentAny, SyntheticInstrument, stubs::*},
orderbook::OrderBook,
orders::{
Order, OrderList,
builder::OrderTestBuilder,
stubs::{TestOrderEventStubs, TestOrdersGenerator},
},
position::Position,
stubs::TestDefault,
types::{Currency, Price, Quantity},
};
use rstest::{fixture, rstest};
use crate::cache::{Cache, CacheConfig};
#[fixture]
fn cache() -> Cache {
Cache::default()
}
#[rstest]
fn test_build_index_when_empty(mut cache: Cache) {
cache.build_index();
}
#[rstest]
fn test_check_integrity_when_empty(mut cache: Cache) {
let result = cache.check_integrity();
assert!(result);
}
#[rstest]
fn test_check_residuals_when_empty(cache: Cache) {
let result = cache.check_residuals();
assert!(!result);
}
#[rstest]
fn test_clear_index_when_empty(mut cache: Cache) {
cache.clear_index();
}
#[rstest]
fn test_reset_when_empty(mut cache: Cache) {
cache.reset();
}
#[rstest]
#[case(true, false)]
#[case(false, true)]
fn test_reset_honors_drop_instruments_on_reset(
audusd_sim: CurrencyPair,
#[case] drop_on_reset: bool,
#[case] retained: bool,
) {
let config = CacheConfig::builder()
.drop_instruments_on_reset(drop_on_reset)
.build();
let mut cache = Cache::new(Some(config), None);
let instrument = InstrumentAny::CurrencyPair(audusd_sim.clone());
cache.add_instrument(instrument).unwrap();
assert!(cache.instrument(&audusd_sim.id).is_some());
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
cache.add_order(order, None, None, false).unwrap();
assert_eq!(cache.orders_total_count(None, None, None, None, None), 1);
cache.reset();
assert_eq!(cache.orders_total_count(None, None, None, None, None), 0);
assert_eq!(cache.positions_total_count(None, None, None, None, None), 0);
assert_eq!(cache.instrument(&audusd_sim.id).is_some(), retained);
}
#[rstest]
fn test_dispose_when_empty(mut cache: Cache) {
cache.dispose();
}
#[rstest]
fn test_flush_db_when_empty(mut cache: Cache) {
cache.flush_db();
}
#[rstest]
fn test_cache_general_when_no_database(mut cache: Cache) {
assert!(cache.cache_general().is_ok());
}
#[rstest]
fn test_cache_orders_when_no_database(mut cache: Cache) {
assert!(futures::executor::block_on(cache.cache_orders()).is_ok());
}
#[rstest]
fn test_assign_position_ids_to_contingencies_propagates_parent_to_children(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let position_id = PositionId::new("P-1");
let parent_id = ClientOrderId::from("PARENT-1");
let child_a_id = ClientOrderId::from("CHILD-A");
let child_b_id = ClientOrderId::from("CHILD-B");
let mut parent = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.client_order_id(parent_id)
.contingency_type(ContingencyType::Oto)
.linked_order_ids(vec![child_a_id, child_b_id])
.build();
parent.set_position_id(Some(position_id));
let parent_strategy_id = parent.strategy_id();
cache.add_order(parent, None, None, false).unwrap();
let child_a = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Sell)
.price(Price::from("1.10000"))
.quantity(Quantity::from(100_000))
.client_order_id(child_a_id)
.build();
cache.add_order(child_a, None, None, false).unwrap();
let child_b = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Sell)
.price(Price::from("0.90000"))
.quantity(Quantity::from(100_000))
.client_order_id(child_b_id)
.build();
cache.add_order(child_b, None, None, false).unwrap();
cache.assign_position_ids_to_contingencies();
assert_eq!(
cache.order(&child_a_id).unwrap().position_id(),
Some(position_id),
);
assert_eq!(
cache.order(&child_b_id).unwrap().position_id(),
Some(position_id),
);
assert_eq!(cache.position_id(&child_a_id), Some(&position_id));
assert_eq!(cache.position_id(&child_b_id), Some(&position_id));
assert_eq!(
cache.strategy_id_for_position(&position_id),
Some(&parent_strategy_id),
);
let position_order_ids: AHashSet<ClientOrderId> = cache
.orders_for_position(&position_id)
.iter()
.map(|o| o.client_order_id())
.collect();
assert!(position_order_ids.contains(&child_a_id));
assert!(position_order_ids.contains(&child_b_id));
}
#[rstest]
fn test_assign_position_ids_to_contingencies_skips_non_oto_parent(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let position_id = PositionId::new("P-1");
let parent_id = ClientOrderId::from("PARENT-1");
let child_id = ClientOrderId::from("CHILD-1");
let mut parent = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.client_order_id(parent_id)
.contingency_type(ContingencyType::Oco)
.linked_order_ids(vec![child_id])
.build();
parent.set_position_id(Some(position_id));
cache.add_order(parent, None, None, false).unwrap();
let child = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Sell)
.price(Price::from("1.10000"))
.quantity(Quantity::from(100_000))
.client_order_id(child_id)
.build();
cache.add_order(child, None, None, false).unwrap();
cache.assign_position_ids_to_contingencies();
assert_eq!(cache.order(&child_id).unwrap().position_id(), None);
assert_eq!(cache.position_id(&child_id), None);
}
#[rstest]
fn test_assign_position_ids_to_contingencies_skips_when_parent_has_no_position_id(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let parent_id = ClientOrderId::from("PARENT-1");
let child_id = ClientOrderId::from("CHILD-1");
let parent = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.client_order_id(parent_id)
.contingency_type(ContingencyType::Oto)
.linked_order_ids(vec![child_id])
.build();
cache.add_order(parent, None, None, false).unwrap();
let child = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Sell)
.price(Price::from("1.10000"))
.quantity(Quantity::from(100_000))
.client_order_id(child_id)
.build();
cache.add_order(child, None, None, false).unwrap();
cache.assign_position_ids_to_contingencies();
assert_eq!(cache.order(&child_id).unwrap().position_id(), None);
assert_eq!(cache.position_id(&child_id), None);
}
#[rstest]
fn test_assign_position_ids_to_contingencies_does_not_overwrite_existing_child_position_id(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let parent_position_id = PositionId::new("P-PARENT");
let child_position_id = PositionId::new("P-CHILD");
let parent_id = ClientOrderId::from("PARENT-1");
let child_id = ClientOrderId::from("CHILD-1");
let mut parent = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.client_order_id(parent_id)
.contingency_type(ContingencyType::Oto)
.linked_order_ids(vec![child_id])
.build();
parent.set_position_id(Some(parent_position_id));
cache.add_order(parent, None, None, false).unwrap();
let mut child = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Sell)
.price(Price::from("1.10000"))
.quantity(Quantity::from(100_000))
.client_order_id(child_id)
.build();
child.set_position_id(Some(child_position_id));
cache.add_order(child, None, None, false).unwrap();
cache.assign_position_ids_to_contingencies();
assert_eq!(
cache.order(&child_id).unwrap().position_id(),
Some(child_position_id),
);
}
#[rstest]
fn test_assign_position_ids_to_contingencies_handles_missing_child(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let position_id = PositionId::new("P-1");
let parent_id = ClientOrderId::from("PARENT-1");
let absent_child_id = ClientOrderId::from("CHILD-ABSENT");
let mut parent = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.client_order_id(parent_id)
.contingency_type(ContingencyType::Oto)
.linked_order_ids(vec![absent_child_id])
.build();
parent.set_position_id(Some(position_id));
cache.add_order(parent, None, None, false).unwrap();
cache.assign_position_ids_to_contingencies();
assert_eq!(cache.position_id(&absent_child_id), None);
}
#[rstest]
fn test_assign_position_ids_to_contingencies_is_idempotent(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let position_id = PositionId::new("P-1");
let parent_id = ClientOrderId::from("PARENT-1");
let child_id = ClientOrderId::from("CHILD-1");
let mut parent = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.client_order_id(parent_id)
.contingency_type(ContingencyType::Oto)
.linked_order_ids(vec![child_id])
.build();
parent.set_position_id(Some(position_id));
cache.add_order(parent, None, None, false).unwrap();
let child = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Sell)
.price(Price::from("1.10000"))
.quantity(Quantity::from(100_000))
.client_order_id(child_id)
.build();
cache.add_order(child, None, None, false).unwrap();
cache.assign_position_ids_to_contingencies();
cache.assign_position_ids_to_contingencies();
assert_eq!(
cache.order(&child_id).unwrap().position_id(),
Some(position_id),
);
assert_eq!(cache.position_id(&child_id), Some(&position_id));
assert_eq!(cache.orders_for_position(&position_id).len(), 1);
}
#[rstest]
fn test_order_when_empty(cache: Cache) {
let client_order_id = ClientOrderId::test_default();
let result = cache.order(&client_order_id);
assert!(result.is_none());
}
#[rstest]
fn test_order_when_initialized(mut cache: Cache, audusd_sim: CurrencyPair) {
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
let client_order_id = order.client_order_id();
cache.add_order(order, None, None, false).unwrap();
let order = cache.order(&client_order_id).unwrap();
assert_eq!(cache.orders(None, None, None, None, None), vec![order]);
assert!(cache.orders_open(None, None, None, None, None).is_empty());
assert!(cache.orders_closed(None, None, None, None, None).is_empty());
assert_eq!(
cache.orders_active_local(None, None, None, None, None),
vec![order]
);
assert!(
cache
.orders_emulated(None, None, None, None, None)
.is_empty()
);
assert!(
cache
.orders_inflight(None, None, None, None, None)
.is_empty()
);
assert!(cache.order_exists(&order.client_order_id()));
assert!(!cache.is_order_open(&order.client_order_id()));
assert!(!cache.is_order_closed(&order.client_order_id()));
assert!(cache.is_order_active_local(&order.client_order_id()));
assert!(!cache.is_order_emulated(&order.client_order_id()));
assert!(!cache.is_order_inflight(&order.client_order_id()));
assert!(!cache.is_order_pending_cancel_local(&order.client_order_id()));
assert_eq!(cache.orders_open_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_closed_count(None, None, None, None, None), 0);
assert_eq!(
cache.orders_active_local_count(None, None, None, None, None),
1
);
assert_eq!(cache.orders_emulated_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_inflight_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_total_count(None, None, None, None, None), 1);
assert_eq!(cache.venue_order_id(&order.client_order_id()), None);
}
#[rstest]
fn test_order_when_submitted(mut cache: Cache, audusd_sim: CurrencyPair) {
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
let client_order_id = order.client_order_id();
cache.add_order(order.clone(), None, None, false).unwrap();
let submitted = OrderSubmitted::default();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
cache.update_order(&order).unwrap();
let cached_order = cache.order(&client_order_id).unwrap();
assert_eq!(cached_order.status(), OrderStatus::Submitted);
let result = cache.order(&order.client_order_id()).unwrap();
assert_eq!(order.status(), OrderStatus::Submitted);
assert_eq!(result, &order);
assert_eq!(cache.orders(None, None, None, None, None), vec![&order]);
assert!(cache.orders_open(None, None, None, None, None).is_empty());
assert!(cache.orders_closed(None, None, None, None, None).is_empty());
assert!(
cache
.orders_active_local(None, None, None, None, None)
.is_empty()
);
assert!(
cache
.orders_emulated(None, None, None, None, None)
.is_empty()
);
assert!(
!cache
.orders_inflight(None, None, None, None, None)
.is_empty()
);
assert!(cache.order_exists(&order.client_order_id()));
assert!(!cache.is_order_open(&order.client_order_id()));
assert!(!cache.is_order_closed(&order.client_order_id()));
assert!(!cache.is_order_active_local(&order.client_order_id()));
assert!(!cache.is_order_emulated(&order.client_order_id()));
assert!(cache.is_order_inflight(&order.client_order_id()));
assert!(!cache.is_order_pending_cancel_local(&order.client_order_id()));
assert_eq!(cache.orders_open_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_closed_count(None, None, None, None, None), 0);
assert_eq!(
cache.orders_active_local_count(None, None, None, None, None),
0
);
assert_eq!(cache.orders_emulated_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_inflight_count(None, None, None, None, None), 1);
assert_eq!(cache.orders_total_count(None, None, None, None, None), 1);
assert_eq!(cache.venue_order_id(&order.client_order_id()), None);
}
#[ignore = "Production bug: rejected orders incorrectly showing in emulated list"]
#[rstest]
fn test_order_when_rejected(mut cache: Cache, audusd_sim: CurrencyPair) {
let mut order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
let submitted = OrderSubmitted::default();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
cache.update_order(&order).unwrap();
let rejected = OrderRejected::default();
order.apply(OrderEventAny::Rejected(rejected)).unwrap();
cache.update_order(&order).unwrap();
let cached_order = cache.order(&order.client_order_id()).unwrap();
assert_eq!(cached_order.status(), OrderStatus::Rejected);
let result = cache.order(&order.client_order_id()).unwrap();
assert!(order.is_closed());
assert_eq!(result, &order);
assert_eq!(cache.orders(None, None, None, None, None), vec![&order]);
assert!(cache.orders_open(None, None, None, None, None).is_empty());
assert_eq!(
cache.orders_closed(None, None, None, None, None),
vec![&order]
);
assert!(
cache
.orders_emulated(None, None, None, None, None)
.is_empty()
);
assert!(
cache
.orders_inflight(None, None, None, None, None)
.is_empty()
);
assert!(cache.order_exists(&order.client_order_id()));
assert!(!cache.is_order_open(&order.client_order_id()));
assert!(cache.is_order_closed(&order.client_order_id()));
assert!(!cache.is_order_emulated(&order.client_order_id()));
assert!(!cache.is_order_inflight(&order.client_order_id()));
assert!(!cache.is_order_pending_cancel_local(&order.client_order_id()));
assert_eq!(cache.orders_open_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_closed_count(None, None, None, None, None), 1);
assert_eq!(cache.orders_emulated_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_inflight_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_total_count(None, None, None, None, None), 1);
}
#[rstest]
fn test_order_when_accepted(mut cache: Cache, audusd_sim: CurrencyPair) {
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
let submitted = OrderSubmitted::default();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
cache.update_order(&order).unwrap();
let accepted = OrderAccepted::default();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
cache.update_order(&order).unwrap();
let result = cache.order(&order.client_order_id()).unwrap();
assert!(order.is_open());
assert_eq!(result, &order);
assert_eq!(cache.orders(None, None, None, None, None), vec![&order]);
assert_eq!(
cache.orders_open(None, None, None, None, None),
vec![&order]
);
assert!(cache.orders_closed(None, None, None, None, None).is_empty());
assert!(
cache
.orders_emulated(None, None, None, None, None)
.is_empty()
);
assert!(
cache
.orders_inflight(None, None, None, None, None)
.is_empty()
);
assert!(cache.order_exists(&order.client_order_id()));
assert!(cache.is_order_open(&order.client_order_id()));
assert!(!cache.is_order_closed(&order.client_order_id()));
assert!(!cache.is_order_emulated(&order.client_order_id()));
assert!(!cache.is_order_inflight(&order.client_order_id()));
assert!(!cache.is_order_pending_cancel_local(&order.client_order_id()));
assert_eq!(cache.orders_open_count(None, None, None, None, None), 1);
assert_eq!(cache.orders_closed_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_emulated_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_inflight_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_total_count(None, None, None, None, None), 1);
assert_eq!(
cache.client_order_id(&order.venue_order_id().unwrap()),
Some(&order.client_order_id())
);
assert_eq!(
cache.venue_order_id(&order.client_order_id()),
Some(&order.venue_order_id().unwrap())
);
}
#[rstest]
fn test_client_order_ids_filtering(mut cache: Cache) {
let venue_a = Venue::from("VENUE-A");
let _venue_b = Venue::from("VENUE-B");
let mut generator = TestOrdersGenerator::new(OrderType::Limit);
generator.add_venue_and_total_instruments(venue_a, 3);
generator.add_venue_and_total_instruments(_venue_b, 3);
generator.set_orders_per_instrument(2);
let orders = generator.build();
let _instrument_a0 = InstrumentId::from("SYMBOL-0.VENUE-A");
assert_eq!(orders.len(), 12);
for order in &orders {
cache.add_order(order.clone(), None, None, false).unwrap();
}
assert_eq!(
cache.client_order_ids(None, None, None, None).len(),
orders.len()
);
let expected_venue_a = orders
.iter()
.filter(|o| o.instrument_id().venue == venue_a)
.count();
assert_eq!(
cache
.client_order_ids(Some(&venue_a), None, None, None)
.len(),
expected_venue_a
);
let instrument_a0 = InstrumentId::from("SYMBOL-0.VENUE-A");
assert_eq!(
cache
.client_order_ids(Some(&venue_a), Some(&instrument_a0), None, None)
.len(),
orders
.iter()
.filter(|o| o.instrument_id() == instrument_a0)
.count()
);
}
#[rstest]
fn test_position_ids_filtering(mut cache: Cache) {
fn make_pair(id_str: &str) -> CurrencyPair {
CurrencyPair::new(
InstrumentId::from(id_str),
Symbol::from(id_str),
Currency::USD(),
Currency::EUR(),
2,
4,
Price::from("0.01"),
Quantity::from("0.0001"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
UnixNanos::default(),
UnixNanos::default(),
)
}
let venue_a = Venue::from("VENUE-A");
let _venue_b = Venue::from("VENUE-B");
let instr_a0 = make_pair("PAIR-0.VENUE-A");
let instr_b0 = make_pair("PAIR-0.VENUE-B");
let base_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instr_a0.id)
.side(OrderSide::Buy)
.quantity(Quantity::from("1"))
.build();
let fill_a_event = TestOrderEventStubs::filled(
&base_order,
&InstrumentAny::CurrencyPair(instr_a0.clone()),
None,
Some(PositionId::new("POS-A")),
None,
None,
None,
None,
None,
None,
);
let fill_a = match fill_a_event {
OrderEventAny::Filled(f) => f,
_ => unreachable!(),
};
let pos_a = Position::new(&InstrumentAny::CurrencyPair(instr_a0.clone()), fill_a);
let order_b = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instr_b0.id)
.side(OrderSide::Buy)
.quantity(Quantity::from("1"))
.build();
let fill_b_event = TestOrderEventStubs::filled(
&order_b,
&InstrumentAny::CurrencyPair(instr_b0.clone()),
None,
Some(PositionId::new("POS-B")),
None,
None,
None,
None,
None,
None,
);
let fill_b = match fill_b_event {
OrderEventAny::Filled(f) => f,
_ => unreachable!(),
};
let pos_b = Position::new(&InstrumentAny::CurrencyPair(instr_b0), fill_b);
let mut pos_closed = pos_a.clone();
pos_closed.id = PositionId::new("POS-C");
pos_closed.side = PositionSide::Flat;
pos_closed.ts_closed = Some(UnixNanos::from(1));
cache.add_position(&pos_a, OmsType::Netting).unwrap();
cache.add_position(&pos_b, OmsType::Netting).unwrap();
cache.add_position(&pos_closed, OmsType::Netting).unwrap();
assert_eq!(cache.position_ids(None, None, None, None).len(), 3);
assert_eq!(
cache.position_ids(Some(&venue_a), None, None, None).len(),
2
);
assert_eq!(
cache
.position_ids(Some(&venue_a), Some(&instr_a0.id), None, None)
.len(),
2 );
assert!(
cache
.position_open_ids(None, None, None, None)
.contains(&pos_a.id)
);
}
#[ignore = "Production bug: cache state management during order lifecycle"]
#[rstest]
fn test_order_when_filled(mut cache: Cache, audusd_sim: CurrencyPair) {
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let mut order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
let submitted = OrderSubmitted::default();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
cache.update_order(&order).unwrap();
let accepted = OrderAccepted::default();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
cache.update_order(&order).unwrap();
let filled = TestOrderEventStubs::filled(
&order,
&audusd_sim,
None,
None,
None,
None,
None,
None,
None,
None,
);
order.apply(filled).unwrap();
cache.update_order(&order).unwrap();
let result = cache.order(&order.client_order_id()).unwrap();
assert!(order.is_closed());
assert_eq!(result, &order);
assert_eq!(cache.orders(None, None, None, None, None), vec![&order]);
assert_eq!(
cache.orders_closed(None, None, None, None, None),
vec![&order]
);
assert!(cache.orders_open(None, None, None, None, None).is_empty());
assert!(
cache
.orders_emulated(None, None, None, None, None)
.is_empty()
);
assert!(
cache
.orders_inflight(None, None, None, None, None)
.is_empty()
);
assert!(cache.order_exists(&order.client_order_id()));
assert!(!cache.is_order_open(&order.client_order_id()));
assert!(cache.is_order_closed(&order.client_order_id()));
assert!(!cache.is_order_emulated(&order.client_order_id()));
assert!(!cache.is_order_inflight(&order.client_order_id()));
assert!(!cache.is_order_pending_cancel_local(&order.client_order_id()));
assert_eq!(cache.orders_open_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_closed_count(None, None, None, None, None), 1);
assert_eq!(cache.orders_emulated_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_inflight_count(None, None, None, None, None), 0);
assert_eq!(cache.orders_total_count(None, None, None, None, None), 1);
assert_eq!(
cache.client_order_id(&order.venue_order_id().unwrap()),
Some(&order.client_order_id())
);
assert_eq!(
cache.venue_order_id(&order.client_order_id()),
Some(&order.venue_order_id().unwrap())
);
}
#[rstest]
fn test_get_general_when_empty(cache: Cache) {
let result = cache.get("A").unwrap();
assert!(result.is_none());
}
#[rstest]
fn test_add_general_when_value(mut cache: Cache) {
let key = "A";
let value = Bytes::from_static(&[0_u8]);
cache.add(key, value.clone()).unwrap();
let result = cache.get(key).unwrap();
assert_eq!(result, Some(&value));
}
#[rstest]
fn test_orders_for_position(mut cache: Cache, audusd_sim: CurrencyPair) {
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
let position_id = PositionId::test_default();
cache
.add_order(order.clone(), Some(position_id), None, false)
.unwrap();
let result = cache.order(&order.client_order_id()).unwrap();
assert_eq!(result, &order);
assert_eq!(cache.orders_for_position(&position_id), vec![&order]);
}
#[rstest]
fn test_correct_order_indexing(mut cache: Cache) {
let binance = Venue::from("BINANCE");
let bybit = Venue::from("BYBIT");
let mut orders_generator = TestOrdersGenerator::new(OrderType::Limit);
orders_generator.add_venue_and_total_instruments(bybit, 10);
orders_generator.add_venue_and_total_instruments(binance, 10);
orders_generator.set_orders_per_instrument(2);
let orders = orders_generator.build();
assert_eq!(orders.len(), 40);
for order in orders {
cache.add_order(order, None, None, false).unwrap();
}
assert_eq!(cache.orders(None, None, None, None, None).len(), 40);
assert_eq!(cache.orders(Some(&bybit), None, None, None, None).len(), 20);
assert_eq!(
cache.orders(Some(&binance), None, None, None, None).len(),
20
);
assert_eq!(
cache
.orders(
Some(&bybit),
Some(&InstrumentId::from("SYMBOL-0.BYBIT")),
None,
None,
None,
)
.len(),
2
);
assert_eq!(
cache
.orders(
Some(&binance),
Some(&InstrumentId::from("SYMBOL-0.BINANCE")),
None,
None,
None,
)
.len(),
2
);
}
#[rstest]
fn test_cache_orders_returned_sorted_by_client_order_id(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let instrument = InstrumentAny::CurrencyPair(audusd_sim);
for raw in ["O-303", "O-101", "O-202"] {
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.client_order_id(ClientOrderId::from(raw))
.build();
cache.add_order(order, None, None, false).unwrap();
}
let returned: Vec<ClientOrderId> = cache
.orders(None, None, None, None, None)
.iter()
.map(|o| o.client_order_id())
.collect();
assert_eq!(
returned,
vec![
ClientOrderId::from("O-101"),
ClientOrderId::from("O-202"),
ClientOrderId::from("O-303"),
],
);
}
#[rstest]
fn test_cache_positions_returned_sorted_by_position_id(mut cache: Cache, audusd_sim: CurrencyPair) {
let instrument = InstrumentAny::CurrencyPair(audusd_sim);
for raw in ["POS-303", "POS-101", "POS-202"] {
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let fill_event = TestOrderEventStubs::filled(
&order,
&instrument,
None,
Some(PositionId::new(raw)),
None,
None,
None,
None,
None,
None,
);
let fill = match fill_event {
OrderEventAny::Filled(f) => f,
_ => unreachable!(),
};
let position = Position::new(&instrument, fill);
cache.add_position(&position, OmsType::Hedging).unwrap();
}
let returned: Vec<PositionId> = cache
.positions(None, None, None, None, None)
.iter()
.map(|p| p.id)
.collect();
assert_eq!(
returned,
vec![
PositionId::new("POS-101"),
PositionId::new("POS-202"),
PositionId::new("POS-303"),
],
);
}
#[rstest]
fn test_add_order_with_account_id_populates_account_index() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let instrument = InstrumentAny::CurrencyPair(audusd_sim);
let account_id = AccountId::new("SIM-001");
let mut order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let submitted = TestOrderEventStubs::submitted(&order, account_id);
order.apply(submitted).unwrap();
let client_order_id = order.client_order_id();
cache.add_order(order.clone(), None, None, false).unwrap();
assert!(cache.index.account_orders.contains_key(&account_id));
assert!(
cache
.index
.account_orders
.get(&account_id)
.unwrap()
.contains(&client_order_id)
);
let orders_for_account = cache.orders(None, None, None, Some(&account_id), None);
assert_eq!(orders_for_account.len(), 1);
assert!(orders_for_account.contains(&&order));
}
#[rstest]
fn test_add_order_list() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let instrument = InstrumentAny::CurrencyPair(audusd_sim);
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
let order_list_id = OrderListId::new("OL-001");
let order_list = OrderList::new(
order_list_id,
instrument.id(),
order.strategy_id(),
vec![order.client_order_id()],
UnixNanos::default(),
);
cache.add_order_list(order_list.clone()).unwrap();
assert!(cache.order_list_exists(&order_list_id));
assert_eq!(cache.order_list(&order_list_id), Some(&order_list));
assert!(
cache
.order_lists(None, None, None, None)
.contains(&&order_list)
);
}
#[rstest]
fn test_add_order_list_when_already_exists_errors() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let instrument = InstrumentAny::CurrencyPair(audusd_sim);
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
let order_list_id = OrderListId::new("OL-001");
let order_list = OrderList::new(
order_list_id,
instrument.id(),
order.strategy_id(),
vec![order.client_order_id()],
UnixNanos::default(),
);
cache.add_order_list(order_list.clone()).unwrap();
let result = cache.add_order_list(order_list);
assert!(result.is_err());
}
#[rstest]
fn test_cache_positions_when_no_database(mut cache: Cache) {
assert!(futures::executor::block_on(cache.cache_positions()).is_ok());
}
#[rstest]
fn test_position_when_empty(cache: Cache) {
let position_id = PositionId::from("1");
let result = cache.position(&position_id);
assert!(result.is_none());
assert!(!cache.position_exists(&position_id));
}
#[rstest]
fn test_position_when_some(mut cache: Cache, audusd_sim: CurrencyPair) {
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let filled = TestOrderEventStubs::filled(
&order,
&audusd_sim,
None,
Some(PositionId::new("P-123456")),
None,
None,
None,
None,
None,
None,
);
let position = Position::new(&audusd_sim, filled.into());
cache.add_position(&position, OmsType::Netting).unwrap();
let result = cache.position(&position.id);
assert_eq!(result, Some(&position));
assert!(cache.position_exists(&position.id));
assert_eq!(
cache.position_id(&order.client_order_id()),
Some(&position.id)
);
assert_eq!(
cache.positions_open(None, None, None, None, None),
vec![&position]
);
assert_eq!(
cache.positions_closed(None, None, None, None, None),
Vec::<&Position>::new()
);
assert_eq!(cache.positions_open_count(None, None, None, None, None), 1);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
0
);
}
#[rstest]
fn test_cache_currencies_when_no_database(mut cache: Cache) {
assert!(futures::executor::block_on(cache.cache_currencies()).is_ok());
}
#[rstest]
fn test_cache_instruments_when_no_database(mut cache: Cache) {
assert!(futures::executor::block_on(cache.cache_instruments()).is_ok());
}
#[rstest]
fn test_instrument_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.instrument(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_instrument_when_some(mut cache: Cache, audusd_sim: CurrencyPair) {
cache
.add_instrument(InstrumentAny::CurrencyPair(audusd_sim.clone()))
.unwrap();
let result = cache.instrument(&audusd_sim.id);
assert_eq!(result, Some(&InstrumentAny::CurrencyPair(audusd_sim)));
}
#[rstest]
fn test_instruments_when_empty(cache: Cache) {
let esz1 = futures_contract_es(None, None);
let result = cache.instruments(&esz1.id.venue, None);
assert!(result.is_empty());
}
#[rstest]
fn test_instruments_when_some(mut cache: Cache) {
let esz1 = futures_contract_es(None, None);
cache
.add_instrument(InstrumentAny::FuturesContract(esz1.clone()))
.unwrap();
let result1 = cache.instruments(&esz1.id.venue, None);
let result2 = cache.instruments(&esz1.id.venue, Some(&esz1.underlying));
assert_eq!(result1, vec![&InstrumentAny::FuturesContract(esz1.clone())]);
assert_eq!(result2, vec![&InstrumentAny::FuturesContract(esz1.clone())]);
}
#[rstest]
fn test_cache_synthetics_when_no_database(mut cache: Cache) {
assert!(futures::executor::block_on(cache.cache_synthetics()).is_ok());
}
#[rstest]
fn test_synthetic_when_empty(cache: Cache) {
let synth = SyntheticInstrument::default();
let result = cache.synthetic(&synth.id);
assert!(result.is_none());
}
#[rstest]
fn test_synthetic_when_some(mut cache: Cache) {
let synth = SyntheticInstrument::default();
cache.add_synthetic(synth.clone()).unwrap();
let result = cache.synthetic(&synth.id);
assert_eq!(result, Some(&synth));
}
#[rstest]
fn test_order_book_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.order_book(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_order_book_when_some(mut cache: Cache, audusd_sim: CurrencyPair) {
let book = OrderBook::new(audusd_sim.id, BookType::L2_MBP);
cache.add_order_book(book.clone()).unwrap();
let result = cache.order_book(&audusd_sim.id);
assert_eq!(result, Some(&book));
}
#[rstest]
fn test_order_book_mut_when_empty(mut cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.order_book_mut(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_order_book_mut_when_some(mut cache: Cache, audusd_sim: CurrencyPair) {
let mut book = OrderBook::new(audusd_sim.id, BookType::L2_MBP);
cache.add_order_book(book.clone()).unwrap();
let result = cache.order_book_mut(&audusd_sim.id);
assert_eq!(result, Some(&mut book));
}
#[cfg(feature = "defi")]
#[fixture]
fn test_pool() -> Pool {
let chain = Arc::new(chains::ETHEREUM.clone());
let dex = Dex::new(
chains::ETHEREUM.clone(),
DexType::UniswapV3,
"0x1F98431c8aD98523631AE4a59f267346ea31F984",
0,
AmmType::CLAMM,
"PoolCreated(address,address,uint24,int24,address)",
"Swap(address,address,int256,int256,uint160,uint128,int24)",
"Mint(address,address,int24,int24,uint128,uint256,uint256)",
"Burn(address,int24,int24,uint128,uint256,uint256)",
"Collect(address,address,int24,int24,uint128,uint128)",
);
let token0 = Token::new(
chain.clone(),
"0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D"
.parse()
.unwrap(),
"Wrapped Ether".to_string(),
"WETH".to_string(),
18,
);
let token1 = Token::new(
chain.clone(),
"0xdAC17F958D2ee523a2206206994597C13D831ec7"
.parse()
.unwrap(),
"Tether USD".to_string(),
"USDT".to_string(),
6,
);
let pool_address = "0x11b815efB8f581194ae79006d24E0d814B7697F6"
.parse()
.unwrap();
let pool_identifier: PoolIdentifier = "0x11b815efB8f581194ae79006d24E0d814B7697F6"
.parse()
.unwrap();
Pool::new(
chain,
Arc::new(dex),
pool_address,
pool_identifier,
12345678,
token0,
token1,
Some(3000),
Some(60),
UnixNanos::from(1_234_567_890_000_000_000u64),
)
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_when_empty(cache: Cache, test_pool: Pool) {
let instrument_id = test_pool.instrument_id;
let result = cache.pool(&instrument_id);
assert!(result.is_none());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_when_some(mut cache: Cache, test_pool: Pool) {
let instrument_id = test_pool.instrument_id;
cache.add_pool(test_pool.clone()).unwrap();
let result = cache.pool(&instrument_id);
assert_eq!(result, Some(&test_pool));
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_mut_when_empty(mut cache: Cache, test_pool: Pool) {
let instrument_id = test_pool.instrument_id;
let result = cache.pool_mut(&instrument_id);
assert!(result.is_none());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_mut_when_some(mut cache: Cache, test_pool: Pool) {
let instrument_id = test_pool.instrument_id;
cache.add_pool(test_pool).unwrap();
let result = cache.pool_mut(&instrument_id);
assert!(result.is_some());
if let Some(pool_ref) = result {
assert_eq!(pool_ref.fee.unwrap(), 3000);
}
}
#[cfg(feature = "defi")]
#[rstest]
fn test_add_pool(mut cache: Cache, test_pool: Pool) {
let instrument_id = test_pool.instrument_id;
cache.add_pool(test_pool.clone()).unwrap();
let cached_pool = cache.pool(&instrument_id);
assert!(cached_pool.is_some());
assert_eq!(cached_pool.unwrap(), &test_pool);
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_ids_when_empty(cache: Cache, test_pool: Pool) {
let result = cache.pool_ids(Some(&test_pool.instrument_id.venue));
assert!(result.is_empty());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_ids_when_some(mut cache: Cache, test_pool: Pool) {
let venue = test_pool.instrument_id.venue;
cache.add_pool(test_pool.clone()).unwrap();
let result1 = cache.pool_ids(None);
let result2 = cache.pool_ids(Some(&venue));
assert_eq!(result1, vec![test_pool.instrument_id]);
assert_eq!(result2, vec![test_pool.instrument_id]);
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pools_when_empty(cache: Cache, test_pool: Pool) {
let result = cache.pools(Some(&test_pool.instrument_id.venue));
assert!(result.is_empty());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pools_when_some(mut cache: Cache, test_pool: Pool) {
let venue = test_pool.instrument_id.venue;
cache.add_pool(test_pool.clone()).unwrap();
let result1 = cache.pools(None);
let result2 = cache.pools(Some(&venue));
assert_eq!(result1, vec![&test_pool]);
assert_eq!(result2, vec![&test_pool]);
}
#[cfg(feature = "defi")]
#[fixture]
fn test_pool_profiler(test_pool: Pool) -> PoolProfiler {
PoolProfiler::new(Arc::new(test_pool))
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_profiler_when_empty(cache: Cache, test_pool_profiler: PoolProfiler) {
let instrument_id = test_pool_profiler.pool.instrument_id;
let result = cache.pool_profiler(&instrument_id);
assert!(result.is_none());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_profiler_when_some(mut cache: Cache, test_pool_profiler: PoolProfiler) {
let instrument_id = test_pool_profiler.pool.instrument_id;
cache.add_pool_profiler(test_pool_profiler).unwrap();
let result = cache.pool_profiler(&instrument_id);
assert!(result.is_some());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_profiler_mut_when_empty(mut cache: Cache, test_pool_profiler: PoolProfiler) {
let instrument_id = test_pool_profiler.pool.instrument_id;
let result = cache.pool_profiler_mut(&instrument_id);
assert!(result.is_none());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_profiler_mut_when_some(mut cache: Cache, test_pool_profiler: PoolProfiler) {
let instrument_id = test_pool_profiler.pool.instrument_id;
cache.add_pool_profiler(test_pool_profiler).unwrap();
let result = cache.pool_profiler_mut(&instrument_id);
assert!(result.is_some());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_add_pool_profiler(mut cache: Cache, test_pool_profiler: PoolProfiler) {
let instrument_id = test_pool_profiler.pool.instrument_id;
cache.add_pool_profiler(test_pool_profiler).unwrap();
let cached_profiler = cache.pool_profiler(&instrument_id);
assert!(cached_profiler.is_some());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_profiler_ids_when_empty(cache: Cache, test_pool_profiler: PoolProfiler) {
let result = cache.pool_profiler_ids(Some(&test_pool_profiler.pool.instrument_id.venue));
assert!(result.is_empty());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_profiler_ids_when_some(mut cache: Cache, test_pool_profiler: PoolProfiler) {
let venue = test_pool_profiler.pool.instrument_id.venue;
cache.add_pool_profiler(test_pool_profiler.clone()).unwrap();
let result1 = cache.pool_profiler_ids(None);
let result2 = cache.pool_profiler_ids(Some(&venue));
assert_eq!(result1, vec![test_pool_profiler.pool.instrument_id]);
assert_eq!(result2, vec![test_pool_profiler.pool.instrument_id]);
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_profilers_when_empty(cache: Cache, test_pool_profiler: PoolProfiler) {
let result = cache.pool_profilers(Some(&test_pool_profiler.pool.instrument_id.venue));
assert!(result.is_empty());
}
#[cfg(feature = "defi")]
#[rstest]
fn test_pool_profilers_when_some(mut cache: Cache, test_pool_profiler: PoolProfiler) {
let venue = test_pool_profiler.pool.instrument_id.venue;
cache.add_pool_profiler(test_pool_profiler).unwrap();
let result1 = cache.pool_profilers(None);
let result2 = cache.pool_profilers(Some(&venue));
assert_eq!(result1.len(), 1);
assert_eq!(result2.len(), 1);
}
#[rstest]
#[case(PriceType::Bid)]
#[case(PriceType::Ask)]
#[case(PriceType::Mid)]
#[case(PriceType::Last)]
#[case(PriceType::Mark)]
fn test_price_when_empty(cache: Cache, audusd_sim: CurrencyPair, #[case] price_type: PriceType) {
let result = cache.price(&audusd_sim.id, price_type);
assert!(result.is_none());
}
#[rstest]
fn test_price_when_some(mut cache: Cache, audusd_sim: CurrencyPair) {
let mark_price = MarkPriceUpdate::new(
audusd_sim.id,
Price::from("1.00000"),
UnixNanos::from(5),
UnixNanos::from(10),
);
cache.add_mark_price(mark_price).unwrap();
let result = cache.price(&audusd_sim.id, PriceType::Mark);
assert_eq!(result, Some(mark_price.value));
}
#[rstest]
fn test_quote_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.quote(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_quote_tick_when_some(mut cache: Cache) {
let quote = QuoteTick::default();
cache.add_quote(quote).unwrap();
let result = cache.quote("e.instrument_id);
assert_eq!(result, Some("e));
}
#[rstest]
fn test_quote_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.quotes(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_quote_ticks_when_some(mut cache: Cache) {
let quotes = vec![
QuoteTick::default(),
QuoteTick::default(),
QuoteTick::default(),
];
cache.add_quotes("es).unwrap();
let result = cache.quotes("es[0].instrument_id);
assert_eq!(result, Some(quotes));
}
#[rstest]
fn test_trade_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.trade(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_trade_tick_when_some(mut cache: Cache) {
let trade = TradeTick::default();
cache.add_trade(trade).unwrap();
let result = cache.trade(&trade.instrument_id);
assert_eq!(result, Some(&trade));
}
#[rstest]
fn test_trade_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.trades(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_trade_ticks_when_some(mut cache: Cache) {
let trades = vec![
TradeTick::default(),
TradeTick::default(),
TradeTick::default(),
];
cache.add_trades(&trades).unwrap();
let result = cache.trades(&trades[0].instrument_id);
assert_eq!(result, Some(trades));
}
#[rstest]
fn test_mark_price_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.mark_price(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_mark_prices_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.mark_prices(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_index_price_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.index_price(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_index_prices_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.index_prices(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_funding_rate_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
let result = cache.funding_rate(&audusd_sim.id);
assert!(result.is_none());
}
#[rstest]
fn test_add_funding_rate(mut cache: Cache, audusd_sim: CurrencyPair) {
let funding_rate = FundingRateUpdate::new(
audusd_sim.id,
"0.0001".parse().unwrap(),
None,
None,
UnixNanos::from(5),
UnixNanos::from(10),
);
cache.add_funding_rate(funding_rate).unwrap();
let result = cache.funding_rate(&audusd_sim.id);
assert_eq!(result, Some(&funding_rate));
}
#[rstest]
fn test_add_funding_rate_updates_existing(mut cache: Cache, audusd_sim: CurrencyPair) {
let funding_rate1 = FundingRateUpdate::new(
audusd_sim.id,
"0.0001".parse().unwrap(),
None,
None,
UnixNanos::from(5),
UnixNanos::from(10),
);
let funding_rate2 = FundingRateUpdate::new(
audusd_sim.id,
"0.0002".parse().unwrap(),
None,
None,
UnixNanos::from(15),
UnixNanos::from(20),
);
cache.add_funding_rate(funding_rate1).unwrap();
cache.add_funding_rate(funding_rate2).unwrap();
let result = cache.funding_rate(&audusd_sim.id);
assert_eq!(result, Some(&funding_rate2));
}
#[rstest]
fn test_instrument_status_when_empty(cache: Cache, audusd_sim: CurrencyPair) {
assert!(cache.instrument_status(&audusd_sim.id).is_none());
assert!(cache.instrument_statuses(&audusd_sim.id).is_none());
}
#[rstest]
fn test_add_instrument_status(mut cache: Cache, audusd_sim: CurrencyPair) {
let status = InstrumentStatus::new(
audusd_sim.id,
MarketStatusAction::Trading,
UnixNanos::from(5),
UnixNanos::from(10),
None,
None,
Some(true),
Some(true),
None,
);
cache.add_instrument_status(status).unwrap();
assert_eq!(cache.instrument_status(&audusd_sim.id), Some(&status));
assert_eq!(
cache.instrument_statuses(&audusd_sim.id),
Some(vec![status])
);
}
#[rstest]
fn test_add_instrument_status_keeps_time_series(mut cache: Cache, audusd_sim: CurrencyPair) {
let status1 = InstrumentStatus::new(
audusd_sim.id,
MarketStatusAction::PreOpen,
UnixNanos::from(5),
UnixNanos::from(10),
None,
None,
Some(false),
Some(false),
None,
);
let status2 = InstrumentStatus::new(
audusd_sim.id,
MarketStatusAction::Trading,
UnixNanos::from(15),
UnixNanos::from(20),
None,
None,
Some(true),
Some(true),
None,
);
cache.add_instrument_status(status1).unwrap();
cache.add_instrument_status(status2).unwrap();
assert_eq!(cache.instrument_status(&audusd_sim.id), Some(&status2));
assert_eq!(
cache.instrument_statuses(&audusd_sim.id),
Some(vec![status2, status1]),
);
}
#[rstest]
fn test_bar_when_empty(cache: Cache) {
let bar = Bar::default();
let result = cache.bar(&bar.bar_type);
assert!(result.is_none());
}
#[rstest]
fn test_bar_when_some(mut cache: Cache) {
let bar = Bar::default();
cache.add_bar(bar).unwrap();
let result = cache.bar(&bar.bar_type);
assert_eq!(result, Some(&bar));
}
#[rstest]
fn test_bars_when_empty(cache: Cache) {
let bar = Bar::default();
let result = cache.bars(&bar.bar_type);
assert!(result.is_none());
}
#[rstest]
fn test_bars_when_some(mut cache: Cache) {
let bars = vec![Bar::default(), Bar::default(), Bar::default()];
cache.add_bars(&bars).unwrap();
let result = cache.bars(&bars[0].bar_type);
assert_eq!(result, Some(bars));
}
#[rstest]
fn test_cache_accounts_when_no_database(mut cache: Cache) {
assert!(futures::executor::block_on(cache.cache_accounts()).is_ok());
}
#[rstest]
fn test_cache_add_account(mut cache: Cache) {
let account = AccountAny::default();
cache.add_account(account.clone()).unwrap();
let result = cache.account(&account.id());
assert!(result.is_some());
assert_eq!(*result.unwrap(), account);
}
#[rstest]
fn test_cache_accounts_when_no_accounts_returns_empty(cache: Cache) {
let result = cache.accounts(&AccountId::test_default());
assert!(result.is_empty());
}
#[rstest]
fn test_cache_account_for_venue_returns_empty(cache: Cache) {
let venue = Venue::test_default();
let result = cache.account_for_venue(&venue);
assert!(result.is_none());
}
#[rstest]
fn test_cache_account_for_venue_return_correct(mut cache: Cache) {
let account = AccountAny::default();
let venue = account.last_event().unwrap().account_id.get_issuer();
cache.add_account(account.clone()).unwrap();
let result = cache.account_for_venue(&venue);
assert!(result.is_some());
assert_eq!(*result.unwrap(), account);
}
#[rstest]
fn test_get_mark_xrate_returns_none(cache: Cache) {
assert!(
cache
.get_mark_xrate(Currency::USD(), Currency::EUR())
.is_none()
);
}
#[rstest]
fn test_set_and_get_mark_xrate(mut cache: Cache) {
let xrate = 1.25;
cache.set_mark_xrate(Currency::USD(), Currency::EUR(), xrate);
assert_eq!(
cache.get_mark_xrate(Currency::USD(), Currency::EUR()),
Some(xrate)
);
assert_eq!(
cache.get_mark_xrate(Currency::EUR(), Currency::USD()),
Some(1.0 / xrate)
);
}
#[rstest]
fn test_clear_mark_xrate(mut cache: Cache) {
let xrate = 1.25;
cache.set_mark_xrate(Currency::USD(), Currency::EUR(), xrate);
assert!(
cache
.get_mark_xrate(Currency::USD(), Currency::EUR())
.is_some()
);
cache.clear_mark_xrate(Currency::USD(), Currency::EUR());
assert!(
cache
.get_mark_xrate(Currency::USD(), Currency::EUR())
.is_none()
);
assert_eq!(
cache.get_mark_xrate(Currency::EUR(), Currency::USD()),
Some(1.0 / xrate)
);
}
#[rstest]
fn test_clear_mark_xrates(mut cache: Cache) {
cache.set_mark_xrate(Currency::USD(), Currency::EUR(), 1.25);
cache.set_mark_xrate(Currency::AUD(), Currency::USD(), 0.75);
cache.clear_mark_xrates();
assert!(
cache
.get_mark_xrate(Currency::USD(), Currency::EUR())
.is_none()
);
assert!(
cache
.get_mark_xrate(Currency::EUR(), Currency::USD())
.is_none()
);
assert!(
cache
.get_mark_xrate(Currency::AUD(), Currency::USD())
.is_none()
);
assert!(
cache
.get_mark_xrate(Currency::USD(), Currency::AUD())
.is_none()
);
}
#[rstest]
#[should_panic(expected = "xrate was zero")]
fn test_set_mark_xrate_panics_on_zero(mut cache: Cache) {
cache.set_mark_xrate(Currency::USD(), Currency::EUR(), 0.0);
}
#[rstest]
fn test_purge_order() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
let client_order_id = order.client_order_id();
let filled = TestOrderEventStubs::filled(
&order,
&audusd_sim,
Some(TradeId::new("T-1")),
Some(PositionId::new("P-123456")),
Some(Price::from("1.00001")),
None,
None,
None,
None,
None,
);
cache.add_order(order, None, None, false).unwrap();
let mut position = Position::new(&audusd_sim, filled.into());
let position_id = position.id;
cache.add_position(&position, OmsType::Netting).unwrap();
let order_close = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.client_order_id(ClientOrderId::new("O-19700101-000000-001-001-2"))
.build();
let filled_close = TestOrderEventStubs::filled(
&order_close,
&audusd_sim,
Some(TradeId::new("T-2")),
Some(position_id),
Some(Price::from("1.00010")),
None,
None,
None,
None,
None,
);
position.apply(&filled_close.into());
cache.update_position(&position).unwrap();
assert!(position.is_closed());
assert!(cache.order_exists(&client_order_id));
assert_eq!(cache.orders_total_count(None, None, None, None, None), 1);
let client_order_id_close = order_close.client_order_id();
cache
.add_order(order_close, Some(position_id), None, false)
.unwrap();
cache.purge_order(client_order_id);
cache.purge_order(client_order_id_close);
assert!(!cache.order_exists(&client_order_id));
assert!(!cache.order_exists(&client_order_id_close));
assert_eq!(cache.orders_total_count(None, None, None, None, None), 0);
assert_eq!(cache.position(&position_id).unwrap().event_count(), 2);
}
#[rstest]
fn test_purge_open_order_skips_purge() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
let client_order_id = order.client_order_id();
cache.add_order(order.clone(), None, None, false).unwrap();
let submitted = OrderSubmitted::default();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
cache.update_order(&order).unwrap();
let accepted = OrderAccepted::default();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
cache.update_order(&order).unwrap();
assert!(order.is_open());
assert!(cache.order_exists(&client_order_id));
assert_eq!(cache.orders_total_count(None, None, None, None, None), 1);
cache.purge_order(client_order_id);
assert!(cache.order_exists(&client_order_id));
assert_eq!(cache.orders_total_count(None, None, None, None, None), 1);
assert!(cache.order(&client_order_id).is_some());
}
#[rstest]
fn test_purge_position() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let filled = TestOrderEventStubs::filled(
&order,
&audusd_sim,
None,
Some(PositionId::new("P-123456")),
Some(Price::from("1.00001")),
None,
None,
None,
None,
None,
);
let mut position = Position::new(&audusd_sim, filled.into());
let position_id = position.id;
cache.add_position(&position, OmsType::Netting).unwrap();
assert!(cache.position_exists(&position_id));
assert!(position.is_open());
assert_eq!(cache.positions_total_count(None, None, None, None, None), 1);
let order_close = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.client_order_id(ClientOrderId::new("O-19700101-000000-001-001-2"))
.build();
let filled_close = TestOrderEventStubs::filled(
&order_close,
&audusd_sim,
Some(TradeId::new("T-2")),
Some(position_id),
Some(Price::from("1.00010")),
None,
None,
None,
None,
None,
);
position.apply(&filled_close.into());
cache.update_position(&position).unwrap();
assert!(position.is_closed());
cache.purge_position(position_id);
assert!(!cache.position_exists(&position_id));
assert_eq!(cache.positions_total_count(None, None, None, None, None), 0);
}
#[rstest]
fn test_purge_open_position_skips_purge() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let filled = TestOrderEventStubs::filled(
&order,
&audusd_sim,
None,
Some(PositionId::new("P-123456")),
Some(Price::from("1.00001")),
None,
None,
None,
None,
None,
);
let position = Position::new(&audusd_sim, filled.into());
let position_id = position.id;
cache.add_position(&position, OmsType::Netting).unwrap();
assert!(position.is_open());
assert!(cache.position_exists(&position_id));
assert_eq!(cache.positions_total_count(None, None, None, None, None), 1);
assert_eq!(position.event_count(), 1);
cache.purge_position(position_id);
assert!(cache.position_exists(&position_id));
assert_eq!(cache.positions_total_count(None, None, None, None, None), 1);
assert!(cache.position(&position_id).is_some());
assert_eq!(cache.position(&position_id).unwrap().event_count(), 1);
}
#[rstest]
fn test_purge_closed_positions_does_not_purge_reopened_position() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let fill1 = TestOrderEventStubs::filled(
&order1,
&audusd_sim,
Some(TradeId::new("T-1")), Some(PositionId::new("P-1")), Some(Price::from("1.00000")), None, None, None, Some(UnixNanos::from(1_000_000_000)), None, );
let mut position = Position::new(&audusd_sim, fill1.into());
let position_id = position.id;
cache.add_position(&position, OmsType::Netting).unwrap();
cache.update_position(&position).unwrap();
assert!(position.is_long());
assert!(!position.is_closed());
assert!(cache.is_position_open(&position_id));
let order2 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.build();
let fill2 = TestOrderEventStubs::filled(
&order2,
&audusd_sim,
Some(TradeId::new("T-2")), Some(position_id), Some(Price::from("1.00010")), None, None, None, Some(UnixNanos::from(2_000_000_000)), None, );
position.apply(&fill2.into());
cache.update_position(&position).unwrap();
assert_eq!(position.side, PositionSide::Flat);
assert!(position.is_closed());
assert!(position.ts_closed.is_some());
let ts_closed_original = position.ts_closed.unwrap();
assert!(cache.is_position_closed(&position_id));
let order3 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(50_000))
.build();
let fill3 = TestOrderEventStubs::filled(
&order3,
&audusd_sim,
Some(TradeId::new("T-3")), Some(position_id), Some(Price::from("1.00020")), None, None, None, Some(UnixNanos::from(3_000_000_000)), None, );
position.apply(&fill3.into());
cache.update_position(&position).unwrap();
assert!(position.is_long());
assert!(!position.is_closed());
assert_eq!(position.ts_closed, None); assert!(cache.is_position_open(&position_id));
cache.purge_closed_positions(
UnixNanos::from(ts_closed_original.as_u64() + 1_000_000_000_000),
0, );
assert!(cache.position_exists(&position_id));
assert!(cache.position(&position_id).is_some());
assert!(cache.is_position_open(&position_id));
assert!(!cache.is_position_closed(&position_id));
assert_eq!(cache.positions_total_count(None, None, None, None, None), 1);
assert_eq!(cache.positions_open_count(None, None, None, None, None), 1);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
0
);
}
#[rstest]
fn test_purge_order_cleans_up_strategy_orders_index() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let mut order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let strategy_id = order.strategy_id();
let client_order_id = order.client_order_id();
cache.add_order(order.clone(), None, None, false).unwrap();
let submitted = OrderSubmitted::default();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
cache.update_order(&order).unwrap();
let accepted = OrderAccepted::default();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
cache.update_order(&order).unwrap();
let filled = TestOrderEventStubs::filled(
&order,
&audusd_sim,
None,
None,
Some(Price::from("1.00001")),
None,
None,
None,
None,
None,
);
order.apply(filled).unwrap();
cache.update_order(&order).unwrap();
assert!(cache.index.strategy_orders.contains_key(&strategy_id));
assert!(
cache
.index
.strategy_orders
.get(&strategy_id)
.unwrap()
.contains(&client_order_id)
);
cache.purge_order(client_order_id);
if let Some(strategy_orders) = cache.index.strategy_orders.get(&strategy_id) {
assert!(!strategy_orders.contains(&client_order_id));
assert!(
!strategy_orders.is_empty(),
"Empty strategy_orders set should have been removed"
);
}
let orders_for_strategy = cache.orders(None, None, Some(&strategy_id), None, None);
assert!(!orders_for_strategy.contains(&&order));
}
#[rstest]
fn test_purge_order_cleans_up_exec_spawn_orders_index() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let parent_id = ClientOrderId::new("PARENT-001");
let mut child_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.exec_spawn_id(parent_id)
.build();
let child_id = child_order.client_order_id();
cache
.add_order(child_order.clone(), None, None, false)
.unwrap();
let submitted = OrderSubmitted::default();
child_order
.apply(OrderEventAny::Submitted(submitted))
.unwrap();
cache.update_order(&child_order).unwrap();
let accepted = OrderAccepted::default();
child_order
.apply(OrderEventAny::Accepted(accepted))
.unwrap();
cache.update_order(&child_order).unwrap();
let filled = TestOrderEventStubs::filled(
&child_order,
&audusd_sim,
None,
None,
Some(Price::from("1.00001")),
None,
None,
None,
None,
None,
);
child_order.apply(filled).unwrap();
cache.update_order(&child_order).unwrap();
assert!(cache.index.exec_spawn_orders.contains_key(&parent_id));
assert!(
cache
.index
.exec_spawn_orders
.get(&parent_id)
.unwrap()
.contains(&child_id)
);
cache.purge_order(child_id);
if let Some(spawn_orders) = cache.index.exec_spawn_orders.get(&parent_id) {
assert!(!spawn_orders.contains(&child_id));
}
let orders_for_spawn = cache.orders_for_exec_spawn(&parent_id);
assert!(!orders_for_spawn.contains(&&child_order));
}
#[rstest]
fn test_purge_order_when_order_not_in_cache_still_cleans_up_indices() {
let mut cache = Cache::default();
let client_order_id = ClientOrderId::new("O-NOT-IN-CACHE");
let strategy_id = StrategyId::test_default();
cache
.index
.order_strategy
.insert(client_order_id, strategy_id);
cache
.index
.strategy_orders
.entry(strategy_id)
.or_default()
.insert(client_order_id);
assert!(cache.index.order_strategy.contains_key(&client_order_id));
assert!(
cache
.index
.strategy_orders
.get(&strategy_id)
.unwrap()
.contains(&client_order_id)
);
cache.purge_order(client_order_id);
assert!(!cache.index.order_strategy.contains_key(&client_order_id));
if let Some(strategy_orders) = cache.index.strategy_orders.get(&strategy_id) {
assert!(!strategy_orders.contains(&client_order_id));
}
}
#[rstest]
fn test_purge_order_cleans_up_account_orders_index() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let mut order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let client_order_id = order.client_order_id();
let account_id = AccountId::new("SIM-001");
cache.add_order(order.clone(), None, None, false).unwrap();
let submitted = TestOrderEventStubs::submitted(&order, account_id);
order.apply(submitted).unwrap();
cache.update_order(&order).unwrap();
let accepted = TestOrderEventStubs::accepted(&order, account_id, VenueOrderId::new("V-001"));
order.apply(accepted).unwrap();
cache.update_order(&order).unwrap();
let filled = TestOrderEventStubs::filled(
&order,
&audusd_sim,
None,
None,
Some(Price::from("1.00001")),
None,
None,
None,
None,
None,
);
order.apply(filled).unwrap();
cache.update_order(&order).unwrap();
assert!(cache.index.account_orders.contains_key(&account_id));
assert!(
cache
.index
.account_orders
.get(&account_id)
.unwrap()
.contains(&client_order_id)
);
cache.purge_order(client_order_id);
assert!(!cache.index.account_orders.contains_key(&account_id));
let orders_for_account = cache.orders(None, None, None, Some(&account_id), None);
assert!(!orders_for_account.contains(&&order));
}
#[rstest]
fn test_purge_position_cleans_up_account_positions_index() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let instrument = InstrumentAny::CurrencyPair(audusd_sim);
let mut order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let account_id = AccountId::new("SIM-001");
let trade_id = TradeId::new("T-001");
cache.add_order(order.clone(), None, None, false).unwrap();
let submitted = TestOrderEventStubs::submitted(&order, account_id);
order.apply(submitted).unwrap();
cache.update_order(&order).unwrap();
let accepted = TestOrderEventStubs::accepted(&order, account_id, VenueOrderId::new("V-001"));
order.apply(accepted).unwrap();
cache.update_order(&order).unwrap();
let filled = TestOrderEventStubs::filled(
&order,
&instrument,
Some(trade_id),
None,
Some(Price::from("1.00001")),
None,
None,
None,
None,
None,
);
order.apply(filled.clone()).unwrap();
cache.update_order(&order).unwrap();
let position = Position::new(&instrument, filled.into());
let position_id = position.id;
cache.add_position(&position, OmsType::Hedging).unwrap();
assert!(cache.index.account_positions.contains_key(&account_id));
assert!(
cache
.index
.account_positions
.get(&account_id)
.unwrap()
.contains(&position_id)
);
let mut position = cache.position(&position_id).unwrap().clone();
let close_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.client_order_id(ClientOrderId::new("O-19700101-000000-001-001-2"))
.build();
let close_filled = TestOrderEventStubs::filled(
&close_order,
&instrument,
Some(TradeId::new("T-002")),
Some(position_id),
Some(Price::from("1.00002")),
None,
None,
None,
None,
None,
);
let close_filled: OrderFilled = close_filled.into();
position.apply(&close_filled);
cache.update_position(&position).unwrap();
assert!(position.is_closed());
cache.purge_position(position_id);
assert!(!cache.index.account_positions.contains_key(&account_id));
let positions_for_account = cache.positions(None, None, None, Some(&account_id), None);
assert!(positions_for_account.is_empty());
}
#[rstest]
fn test_update_own_order_book_with_market_order_does_not_panic(mut cache: Cache) {
let audusd_sim = audusd_sim();
cache
.add_instrument(InstrumentAny::CurrencyPair(audusd_sim.clone()))
.unwrap();
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
cache
.add_order(limit_order.clone(), None, None, false)
.unwrap();
cache.update_own_order_book(&limit_order);
assert!(cache.own_order_book(&audusd_sim.id()).is_some());
let market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(50_000))
.client_order_id(ClientOrderId::new("O-19700101-000000-001-001-2"))
.build();
cache
.add_order(market_order.clone(), None, None, false)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&market_order, AccountId::new("SIM-001"));
let mut market_order_mut = market_order;
market_order_mut.apply(submitted).unwrap();
let accepted = TestOrderEventStubs::accepted(
&market_order_mut,
AccountId::new("SIM-001"),
VenueOrderId::new("V-001"),
);
market_order_mut.apply(accepted).unwrap();
let filled = TestOrderEventStubs::filled(
&market_order_mut,
&InstrumentAny::CurrencyPair(audusd_sim.clone()),
Some(TradeId::new("T-001")),
None,
Some(Price::from("1.00010")),
None,
None,
None,
None,
None,
);
market_order_mut.apply(filled).unwrap();
cache.update_own_order_book(&market_order_mut);
assert!(cache.own_order_book(&audusd_sim.id()).is_some());
}
#[rstest]
fn test_purge_closed_orders_also_purges_order_lists() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let instrument = InstrumentAny::CurrencyPair(audusd_sim);
let order_list_id = OrderListId::new("OL-001");
let mut order1 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.client_order_id(ClientOrderId::new("O-001"))
.order_list_id(order_list_id)
.build();
let mut order2 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Sell)
.price(Price::from("1.00100"))
.quantity(Quantity::from(100_000))
.client_order_id(ClientOrderId::new("O-002"))
.order_list_id(order_list_id)
.build();
let order_list = OrderList::new(
order_list_id,
instrument.id(),
order1.strategy_id(),
vec![order1.client_order_id(), order2.client_order_id()],
UnixNanos::default(),
);
let account_id = AccountId::new("SIM-001");
cache.add_order(order1.clone(), None, None, false).unwrap();
cache.add_order(order2.clone(), None, None, false).unwrap();
cache.add_order_list(order_list).unwrap();
assert!(cache.order_list_exists(&order_list_id));
let submitted1 = TestOrderEventStubs::submitted(&order1, account_id);
order1.apply(submitted1).unwrap();
cache.update_order(&order1).unwrap();
let accepted1 = TestOrderEventStubs::accepted(&order1, account_id, VenueOrderId::new("V-001"));
order1.apply(accepted1).unwrap();
cache.update_order(&order1).unwrap();
let filled1 = TestOrderEventStubs::filled(
&order1,
&instrument,
Some(TradeId::new("T-1")),
None,
Some(Price::from("1.00000")),
None,
None,
None,
None,
None,
);
order1.apply(filled1).unwrap();
cache.update_order(&order1).unwrap();
let submitted2 = TestOrderEventStubs::submitted(&order2, account_id);
order2.apply(submitted2).unwrap();
cache.update_order(&order2).unwrap();
let accepted2 = TestOrderEventStubs::accepted(&order2, account_id, VenueOrderId::new("V-002"));
order2.apply(accepted2).unwrap();
cache.update_order(&order2).unwrap();
let canceled2 =
TestOrderEventStubs::canceled(&order2, account_id, Some(VenueOrderId::new("V-002")));
order2.apply(canceled2).unwrap();
cache.update_order(&order2).unwrap();
assert!(order1.is_closed());
assert!(order2.is_closed());
let ts_now = UnixNanos::from(1_000_000_000_000);
cache.purge_closed_orders(ts_now, 0);
assert!(!cache.order_exists(&order1.client_order_id()));
assert!(!cache.order_exists(&order2.client_order_id()));
assert!(!cache.order_list_exists(&order_list_id));
}
#[rstest]
fn test_purge_closed_orders_does_not_purge_order_list_with_open_orders() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let instrument = InstrumentAny::CurrencyPair(audusd_sim);
let order_list_id = OrderListId::new("OL-001");
let mut order1 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.client_order_id(ClientOrderId::new("O-001"))
.order_list_id(order_list_id)
.build();
let mut order2 = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Sell)
.price(Price::from("1.00100"))
.quantity(Quantity::from(100_000))
.client_order_id(ClientOrderId::new("O-002"))
.order_list_id(order_list_id)
.build();
let order_list = OrderList::new(
order_list_id,
instrument.id(),
order1.strategy_id(),
vec![order1.client_order_id(), order2.client_order_id()],
UnixNanos::default(),
);
let account_id = AccountId::new("SIM-001");
cache.add_order(order1.clone(), None, None, false).unwrap();
cache.add_order(order2.clone(), None, None, false).unwrap();
cache.add_order_list(order_list).unwrap();
let submitted1 = TestOrderEventStubs::submitted(&order1, account_id);
order1.apply(submitted1).unwrap();
cache.update_order(&order1).unwrap();
let accepted1 = TestOrderEventStubs::accepted(&order1, account_id, VenueOrderId::new("V-001"));
order1.apply(accepted1).unwrap();
cache.update_order(&order1).unwrap();
let filled1 = TestOrderEventStubs::filled(
&order1,
&instrument,
Some(TradeId::new("T-1")),
None,
Some(Price::from("1.00000")),
None,
None,
None,
None,
None,
);
order1.apply(filled1).unwrap();
cache.update_order(&order1).unwrap();
let submitted2 = TestOrderEventStubs::submitted(&order2, account_id);
order2.apply(submitted2).unwrap();
cache.update_order(&order2).unwrap();
let accepted2 = TestOrderEventStubs::accepted(&order2, account_id, VenueOrderId::new("V-002"));
order2.apply(accepted2).unwrap();
cache.update_order(&order2).unwrap();
assert!(order1.is_closed());
assert!(order2.is_open());
let ts_now = UnixNanos::from(1_000_000_000_000);
cache.purge_closed_orders(ts_now, 0);
assert!(!cache.order_exists(&order1.client_order_id()));
assert!(cache.order_exists(&order2.client_order_id()));
assert!(cache.order_list_exists(&order_list_id));
}
#[rstest]
fn test_force_remove_from_own_order_book(mut cache: Cache) {
let audusd_sim = audusd_sim();
cache
.add_instrument(InstrumentAny::CurrencyPair(audusd_sim.clone()))
.unwrap();
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
cache
.add_order(limit_order.clone(), None, None, false)
.unwrap();
cache.update_own_order_book(&limit_order);
let submitted = TestOrderEventStubs::submitted(&limit_order, AccountId::new("SIM-001"));
let mut limit_order_mut = limit_order;
limit_order_mut.apply(submitted).unwrap();
cache.update_order(&limit_order_mut).unwrap();
assert!(cache.order_exists(&limit_order_mut.client_order_id()));
assert!(
cache
.index
.orders_inflight
.contains(&limit_order_mut.client_order_id())
);
assert!(cache.own_order_book(&audusd_sim.id()).is_some());
cache.force_remove_from_own_order_book(&limit_order_mut.client_order_id());
assert!(
!cache
.index
.orders_open
.contains(&limit_order_mut.client_order_id())
);
assert!(
!cache
.index
.orders_inflight
.contains(&limit_order_mut.client_order_id())
);
assert!(
!cache
.index
.orders_emulated
.contains(&limit_order_mut.client_order_id())
);
assert!(
!cache
.index
.orders_pending_cancel
.contains(&limit_order_mut.client_order_id())
);
assert!(
cache
.index
.orders_closed
.contains(&limit_order_mut.client_order_id())
);
}
#[rstest]
fn test_audit_own_order_books_with_inflight_orders(mut cache: Cache) {
let audusd_sim = audusd_sim();
cache
.add_instrument(InstrumentAny::CurrencyPair(audusd_sim.clone()))
.unwrap();
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
cache
.add_order(limit_order.clone(), None, None, false)
.unwrap();
cache.update_own_order_book(&limit_order);
let submitted = TestOrderEventStubs::submitted(&limit_order, AccountId::new("SIM-001"));
let mut limit_order_mut = limit_order;
limit_order_mut.apply(submitted).unwrap();
cache.update_order(&limit_order_mut).unwrap();
let own_book = cache.own_order_book(&audusd_sim.id()).unwrap();
assert!(own_book.bids().count() > 0);
cache.audit_own_order_books();
let own_book = cache.own_order_book(&audusd_sim.id()).unwrap();
assert!(own_book.bids().count() > 0);
}
#[rstest]
fn test_audit_own_order_books_removes_closed(mut cache: Cache) {
let audusd_sim = audusd_sim();
cache
.add_instrument(InstrumentAny::CurrencyPair(audusd_sim.clone()))
.unwrap();
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
cache
.add_order(limit_order.clone(), None, None, false)
.unwrap();
cache.update_own_order_book(&limit_order);
let submitted = TestOrderEventStubs::submitted(&limit_order, AccountId::new("SIM-001"));
let mut limit_order_mut = limit_order;
limit_order_mut.apply(submitted).unwrap();
cache.update_order(&limit_order_mut).unwrap();
let accepted = TestOrderEventStubs::accepted(
&limit_order_mut,
AccountId::new("SIM-001"),
VenueOrderId::new("V-001"),
);
limit_order_mut.apply(accepted).unwrap();
cache.update_order(&limit_order_mut).unwrap();
let own_book = cache.own_order_book(&audusd_sim.id()).unwrap();
assert!(own_book.bids().count() > 0);
let canceled = TestOrderEventStubs::canceled(
&limit_order_mut,
AccountId::new("SIM-001"),
Some(VenueOrderId::new("V-001")),
);
limit_order_mut.apply(canceled).unwrap();
cache.update_order(&limit_order_mut).unwrap();
cache.update_own_order_book(&limit_order_mut);
cache.audit_own_order_books();
let own_book = cache.own_order_book(&audusd_sim.id()).unwrap();
assert_eq!(own_book.bids().count(), 0);
}
#[rstest]
fn test_own_order_book_lifecycle_sequence(mut cache: Cache) {
let instrument = InstrumentAny::CurrencyPair(audusd_sim());
cache.add_instrument(instrument.clone()).unwrap();
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
cache
.add_order(limit_order.clone(), None, None, false)
.unwrap();
cache.update_own_order_book(&limit_order);
let mut live_order = limit_order;
let submitted = TestOrderEventStubs::submitted(&live_order, AccountId::new("SIM-001"));
live_order.apply(submitted).unwrap();
cache.update_order(&live_order).unwrap();
let venue_order_id = VenueOrderId::new("V-LCYCLE");
let accepted =
TestOrderEventStubs::accepted(&live_order, AccountId::new("SIM-001"), venue_order_id);
live_order.apply(accepted).unwrap();
cache.update_order(&live_order).unwrap();
let own_book = cache.own_order_book(&instrument.id()).unwrap();
assert!(own_book.bids().count() > 0);
let partial_fill = TestOrderEventStubs::filled(
&live_order,
&instrument,
None,
None,
None,
Some(Quantity::from(50_000)),
None,
None,
None,
None,
);
live_order.apply(partial_fill).unwrap();
cache.update_order(&live_order).unwrap();
let own_book = cache.own_order_book(&instrument.id()).unwrap();
assert!(own_book.bids().count() > 0);
let canceled = TestOrderEventStubs::canceled(
&live_order,
AccountId::new("SIM-001"),
Some(VenueOrderId::new("V-LCYCLE")),
);
live_order.apply(canceled).unwrap();
cache.update_order(&live_order).unwrap();
cache.update_own_order_book(&live_order);
let own_book = cache.own_order_book(&instrument.id()).unwrap();
assert_eq!(own_book.bids().count(), 0);
}
#[rstest]
fn test_own_order_book_pending_cancel_persists_until_final(mut cache: Cache) {
let instrument = InstrumentAny::CurrencyPair(audusd_sim());
cache.add_instrument(instrument.clone()).unwrap();
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
cache
.add_order(limit_order.clone(), None, None, false)
.unwrap();
cache.update_own_order_book(&limit_order);
let mut live_order = limit_order;
let accepted = TestOrderEventStubs::accepted(
&live_order,
AccountId::new("SIM-001"),
VenueOrderId::new("V-PENDING"),
);
live_order.apply(accepted).unwrap();
cache.update_order(&live_order).unwrap();
cache.update_order_pending_cancel_local(&live_order);
cache.audit_own_order_books();
let own_book = cache.own_order_book(&instrument.id()).unwrap();
assert!(own_book.bids().count() > 0);
let canceled = TestOrderEventStubs::canceled(
&live_order,
AccountId::new("SIM-001"),
Some(VenueOrderId::new("V-PENDING")),
);
live_order.apply(canceled).unwrap();
cache.update_order(&live_order).unwrap();
cache.update_own_order_book(&live_order);
let own_book = cache.own_order_book(&instrument.id()).unwrap();
assert_eq!(own_book.bids().count(), 0);
}
#[rstest]
fn test_update_own_order_book_reinserts_missing_levels(mut cache: Cache) {
let instrument = InstrumentAny::CurrencyPair(audusd_sim());
cache.add_instrument(instrument.clone()).unwrap();
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
cache
.add_order(limit_order.clone(), None, None, false)
.unwrap();
cache.update_own_order_book(&limit_order);
let mut live_order = limit_order;
let accepted = TestOrderEventStubs::accepted(
&live_order,
AccountId::new("SIM-001"),
VenueOrderId::new("V-REINSERT"),
);
live_order.apply(accepted).unwrap();
cache.update_order(&live_order).unwrap();
{
let own_book = cache
.own_books
.get_mut(&instrument.id())
.expect("own book missing");
own_book.clear();
}
cache.update_own_order_book(&live_order);
let own_book = cache.own_order_book(&instrument.id()).unwrap();
assert!(own_book.bids().count() > 0);
}
#[rstest]
fn test_position_flip_netting_mode_cleans_up_closed_index() {
let mut cache = Cache::default();
let audusd_sim = audusd_sim();
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let order1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let fill1 = TestOrderEventStubs::filled(
&order1,
&audusd_sim,
Some(TradeId::new("T-1")), Some(PositionId::new("P-1")), Some(Price::from("1.00000")), None, None, None, Some(UnixNanos::from(1_000_000_000)), None, );
let mut position = Position::new(&audusd_sim, fill1.into());
let position_id = position.id;
cache.add_position(&position, OmsType::Netting).unwrap();
assert!(position.is_long());
assert!(!position.is_closed());
assert!(cache.is_position_open(&position_id));
assert!(!cache.is_position_closed(&position_id));
let order2 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.build();
let fill2 = TestOrderEventStubs::filled(
&order2,
&audusd_sim,
Some(TradeId::new("T-2")), Some(position_id), Some(Price::from("1.00010")), None, None, None, Some(UnixNanos::from(2_000_000_000)), None, );
position.apply(&fill2.into());
cache.update_position(&position).unwrap();
assert_eq!(position.side, PositionSide::Flat);
assert!(position.is_closed());
assert!(cache.is_position_closed(&position_id));
assert!(!cache.is_position_open(&position_id));
cache.snapshot_position(&position).unwrap();
let order3 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(50_000))
.build();
let fill3 = TestOrderEventStubs::filled(
&order3,
&audusd_sim,
Some(TradeId::new("T-3")), Some(position_id), Some(Price::from("1.00020")), None, None, None, Some(UnixNanos::from(3_000_000_000)), None, );
let position_reopened = Position::new(&audusd_sim, fill3.into());
assert_eq!(position_reopened.id, position_id);
cache
.add_position(&position_reopened, OmsType::Netting)
.unwrap();
assert!(position_reopened.is_long());
assert!(!position_reopened.is_closed());
assert!(
cache.is_position_open(&position_id),
"Position should be in open index"
);
assert!(
!cache.is_position_closed(&position_id),
"Position should NOT be in closed index (bug fixed)"
);
assert_eq!(cache.positions_total_count(None, None, None, None, None), 1);
assert_eq!(cache.positions_open_count(None, None, None, None, None), 1);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
0
);
assert!(cache.position_snapshots.contains_key(&position_id));
let cached_pos = cache.position(&position_id).unwrap();
assert_eq!(cached_pos.side, PositionSide::Long);
assert_eq!(cached_pos.quantity, Quantity::from(50_000));
assert_eq!(cached_pos.event_count(), 1); }
#[rstest]
fn test_position_snapshots_round_trip(mut cache: Cache) {
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim());
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let fill = TestOrderEventStubs::filled(
&order,
&audusd_sim,
Some(TradeId::new("T-1")),
Some(PositionId::new("P-1")),
Some(Price::from("1.00000")),
None,
None,
None,
Some(UnixNanos::from(1_000_000_000)),
None,
);
let position = Position::new(&audusd_sim, fill.into());
let position_id = position.id;
let account_id = position.account_id;
cache.snapshot_position(&position).unwrap();
cache.snapshot_position(&position).unwrap();
cache.snapshot_position(&position).unwrap();
let frames = cache.position_snapshot_bytes(&position_id).unwrap();
assert_eq!(frames.len(), 3);
let snapshots = cache.position_snapshots(Some(&position_id), None);
assert_eq!(snapshots.len(), 3);
let prefix = format!("{}-", position_id.as_str());
for snapshot in &snapshots {
assert!(snapshot.id.as_str().starts_with(&prefix));
assert_ne!(snapshot.id, position_id);
}
let unique_ids: AHashSet<_> = snapshots.iter().map(|p| p.id).collect();
assert_eq!(unique_ids.len(), 3);
assert_eq!(cache.position_snapshots(None, Some(&account_id)).len(), 3,);
assert!(
cache
.position_snapshots(None, Some(&AccountId::new("OTHER-000")))
.is_empty(),
);
}
fn snapshot_test_position() -> Position {
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim());
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let fill = TestOrderEventStubs::filled(
&order,
&audusd_sim,
Some(TradeId::new("T-1")),
Some(PositionId::new("P-1")),
Some(Price::from("1.00000")),
None,
None,
None,
Some(UnixNanos::from(1_000_000_000)),
None,
);
Position::new(&audusd_sim, fill.into())
}
#[rstest]
#[case(0)]
#[case(1)]
#[case(3)]
fn test_position_snapshot_count(mut cache: Cache, #[case] n: usize) {
let position = snapshot_test_position();
let position_id = position.id;
for _ in 0..n {
cache.snapshot_position(&position).unwrap();
}
assert_eq!(cache.position_snapshot_count(&position_id), n);
}
#[rstest]
fn test_position_snapshot_count_unknown_position(cache: Cache) {
assert_eq!(
cache.position_snapshot_count(&PositionId::new("NOT-PRESENT")),
0,
);
}
#[rstest]
fn test_position_snapshots_from_preserves_order_and_skip(mut cache: Cache) {
let position = snapshot_test_position();
let position_id = position.id;
for _ in 0..3 {
cache.snapshot_position(&position).unwrap();
}
let all_from_zero = cache.position_snapshots_from(&position_id, 0);
assert_eq!(all_from_zero.len(), 3);
let all_ids: Vec<_> = all_from_zero.iter().map(|p| p.id).collect();
let from_one = cache.position_snapshots_from(&position_id, 1);
let from_one_ids: Vec<_> = from_one.iter().map(|p| p.id).collect();
assert_eq!(from_one_ids, all_ids[1..]);
assert!(cache.position_snapshots_from(&position_id, 3).is_empty());
assert!(cache.position_snapshots_from(&position_id, 10).is_empty());
assert!(
cache
.position_snapshots_from(&PositionId::new("NOT-PRESENT"), 0)
.is_empty(),
);
}
#[rstest]
fn test_position_snapshots_skip_malformed_frames(mut cache: Cache) {
let position = snapshot_test_position();
let position_id = position.id;
cache.snapshot_position(&position).unwrap();
cache
.position_snapshots
.get_mut(&position_id)
.unwrap()
.push(Bytes::from_static(b"not json"));
cache.snapshot_position(&position).unwrap();
assert_eq!(cache.position_snapshot_count(&position_id), 3);
assert_eq!(
cache.position_snapshot_bytes(&position_id).unwrap().len(),
3
);
assert_eq!(cache.position_snapshots(Some(&position_id), None).len(), 2);
assert_eq!(cache.position_snapshots_from(&position_id, 0).len(), 2);
}
#[rstest]
fn test_add_trades_same_timestamp_adds_all(mut cache: Cache) {
let ts = UnixNanos::from(1000);
let instrument_id = InstrumentId::from("AUDUSD.SIM");
let trade1 = TradeTick::new(
instrument_id,
Price::from("1.00000"),
Quantity::from(100_000),
AggressorSide::Buyer,
TradeId::new("1"),
ts,
ts,
);
let trade2 = TradeTick::new(
instrument_id,
Price::from("1.00001"),
Quantity::from(100_000),
AggressorSide::Buyer,
TradeId::new("2"),
ts,
ts,
);
let trade3 = TradeTick::new(
instrument_id,
Price::from("1.00002"),
Quantity::from(100_000),
AggressorSide::Buyer,
TradeId::new("3"),
ts,
ts,
);
cache.add_trade(trade1).unwrap();
cache.add_trades(&[trade2, trade3]).unwrap();
let result = cache.trades(&instrument_id).unwrap();
assert_eq!(
result.len(),
3,
"All trades with same timestamp should be added"
);
}
#[rstest]
fn test_add_quotes_same_timestamp_adds_all(mut cache: Cache) {
let ts = UnixNanos::from(1000);
let instrument_id = InstrumentId::from("AUDUSD.SIM");
let quote1 = QuoteTick::new(
instrument_id,
Price::from("1.00000"),
Price::from("1.00001"),
Quantity::from(100_000),
Quantity::from(100_000),
ts,
ts,
);
let quote2 = QuoteTick::new(
instrument_id,
Price::from("1.00002"),
Price::from("1.00003"),
Quantity::from(100_000),
Quantity::from(100_000),
ts,
ts,
);
let quote3 = QuoteTick::new(
instrument_id,
Price::from("1.00004"),
Price::from("1.00005"),
Quantity::from(100_000),
Quantity::from(100_000),
ts,
ts,
);
cache.add_quote(quote1).unwrap();
cache.add_quotes(&[quote2, quote3]).unwrap();
let result = cache.quotes(&instrument_id).unwrap();
assert_eq!(
result.len(),
3,
"All quotes with same timestamp should be added"
);
}
#[rstest]
fn test_add_bars_same_timestamp_adds_all(mut cache: Cache) {
let ts = UnixNanos::from(1000);
let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-BID-EXTERNAL");
let bar1 = Bar::new(
bar_type,
Price::from("1.00000"),
Price::from("1.00001"),
Price::from("0.99999"),
Price::from("1.00000"),
Quantity::from(100_000),
ts,
ts,
);
let bar2 = Bar::new(
bar_type,
Price::from("1.00001"),
Price::from("1.00002"),
Price::from("1.00000"),
Price::from("1.00001"),
Quantity::from(100_000),
ts,
ts,
);
let bar3 = Bar::new(
bar_type,
Price::from("1.00002"),
Price::from("1.00003"),
Price::from("1.00001"),
Price::from("1.00002"),
Quantity::from(100_000),
ts,
ts,
);
cache.add_bar(bar1).unwrap();
cache.add_bars(&[bar2, bar3]).unwrap();
let result = cache.bars(&bar_type).unwrap();
assert_eq!(
result.len(),
3,
"All bars with same timestamp should be added"
);
}
#[rstest]
fn test_add_emulated_order_indexes_in_orders_emulated(mut cache: Cache, audusd_sim: CurrencyPair) {
let order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from("1.00010"))
.emulation_trigger(TriggerType::LastPrice)
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
assert!(
cache
.index
.orders_emulated
.contains(&order.client_order_id()),
"Emulated order should be in orders_emulated index after add"
);
assert_eq!(cache.orders_emulated_count(None, None, None, None, None), 1);
}
#[rstest]
fn test_add_non_emulated_order_not_in_orders_emulated(mut cache: Cache, audusd_sim: CurrencyPair) {
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
assert!(
!cache
.index
.orders_emulated
.contains(&order.client_order_id()),
"Non-emulated order should not be in orders_emulated index"
);
assert_eq!(cache.orders_emulated_count(None, None, None, None, None), 0);
}
#[rstest]
fn test_initialized_order_indexes_in_orders_active_local(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
assert!(
cache
.index
.orders_active_local
.contains(&order.client_order_id()),
"Initialized order should be in orders_active_local index after add"
);
assert_eq!(
cache.orders_active_local_count(None, None, None, None, None),
1
);
let submitted = OrderSubmitted::default();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
cache.update_order(&order).unwrap();
assert!(
!cache
.index
.orders_active_local
.contains(&order.client_order_id()),
"Submitted order should be removed from orders_active_local index"
);
assert_eq!(
cache.orders_active_local_count(None, None, None, None, None),
0
);
}
#[rstest]
fn test_released_order_indexes_in_orders_active_local(mut cache: Cache, audusd_sim: CurrencyPair) {
let order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from("1.00010"))
.emulation_trigger(TriggerType::LastPrice)
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
let released = OrderReleased::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
order.client_order_id(),
Price::from("1.00010"),
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
);
let mut order = order;
order.apply(OrderEventAny::Released(released)).unwrap();
cache.update_order(&order).unwrap();
assert!(
cache
.index
.orders_active_local
.contains(&order.client_order_id()),
"Released order should remain in orders_active_local index"
);
assert!(cache.is_order_active_local(&order.client_order_id()));
assert_eq!(
cache.orders_active_local_count(None, None, None, None, None),
1
);
}
#[rstest]
fn test_emulated_order_indexes_in_orders_active_local(mut cache: Cache, audusd_sim: CurrencyPair) {
let order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from("1.00010"))
.emulation_trigger(TriggerType::LastPrice)
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
let emulated = OrderEmulated::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
order.client_order_id(),
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
);
let mut order = order;
order.apply(OrderEventAny::Emulated(emulated)).unwrap();
cache.update_order(&order).unwrap();
assert!(
cache
.index
.orders_active_local
.contains(&order.client_order_id()),
"Emulated order should remain in orders_active_local index"
);
assert!(cache.is_order_active_local(&order.client_order_id()));
assert_eq!(
cache.orders_active_local_count(None, None, None, None, None),
1
);
}
#[rstest]
fn test_update_released_order_removes_from_orders_emulated(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from("1.00010"))
.emulation_trigger(TriggerType::LastPrice)
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
assert!(
cache
.index
.orders_emulated
.contains(&order.client_order_id()),
"Emulated order should be in orders_emulated index after add"
);
let released = OrderReleased::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
order.client_order_id(),
Price::from("1.00010"),
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
);
let mut order = order;
order.apply(OrderEventAny::Released(released)).unwrap();
cache.update_order(&order).unwrap();
assert!(
!cache
.index
.orders_emulated
.contains(&order.client_order_id()),
"Released order should be removed from orders_emulated index"
);
assert_eq!(cache.orders_emulated_count(None, None, None, None, None), 0);
}
#[rstest]
fn test_update_closed_emulated_order_removes_from_orders_emulated(
mut cache: Cache,
audusd_sim: CurrencyPair,
) {
let order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(audusd_sim.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from("1.00010"))
.emulation_trigger(TriggerType::LastPrice)
.build();
cache.add_order(order.clone(), None, None, false).unwrap();
assert!(
cache
.index
.orders_emulated
.contains(&order.client_order_id()),
"Emulated order should be in orders_emulated index after add"
);
let emulated = OrderEmulated::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
order.client_order_id(),
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
);
let mut order = order;
order.apply(OrderEventAny::Emulated(emulated)).unwrap();
cache.update_order(&order).unwrap();
assert!(
cache
.index
.orders_emulated
.contains(&order.client_order_id()),
"Order should still be in orders_emulated after emulated event"
);
let canceled = OrderCanceled::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
order.client_order_id(),
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
None,
None,
);
order.apply(OrderEventAny::Canceled(canceled)).unwrap();
cache.update_order(&order).unwrap();
assert!(
!cache
.index
.orders_emulated
.contains(&order.client_order_id()),
"Closed emulated order should be removed from orders_emulated index"
);
assert_eq!(cache.orders_emulated_count(None, None, None, None, None), 0);
}