1use std::sync::Arc;
19
20use ahash::AHashMap;
21use bytes::Bytes;
22use indexmap::IndexMap;
23use nautilus_common::{
24 cache::database::{CacheDatabaseAdapter, CacheMap},
25 signal::Signal,
26};
27use nautilus_core::UnixNanos;
28use nautilus_model::{
29 accounts::AccountAny,
30 data::{
31 Bar, CustomData, DataType, FundingRateUpdate, InstrumentClose, QuoteTick, TradeTick,
32 greeks::{GreeksData, YieldCurve},
33 },
34 events::{OrderEventAny, OrderSnapshot, position::snapshot::PositionSnapshot},
35 identifiers::{
36 AccountId, ActorId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId,
37 VenueOrderId,
38 },
39 instruments::{InstrumentAny, SyntheticInstrument},
40 orderbook::OrderBook,
41 orders::OrderAny,
42 position::Position,
43 types::{Currency, Money},
44};
45use parking_lot::Mutex;
46use ustr::Ustr;
47
48#[expect(
49 clippy::struct_excessive_bools,
50 reason = "independent switches cover lifecycle persistence failure modes"
51)]
52#[derive(Debug, Default)]
53struct TestCacheDatabaseState {
54 actors: AHashMap<ActorId, AHashMap<String, Bytes>>,
55 strategies: AHashMap<StrategyId, AHashMap<String, Bytes>>,
56 instrument_closes: AHashMap<InstrumentId, InstrumentClose>,
57 events: Vec<String>,
58 fail_load_actor: bool,
59 fail_load_strategy: bool,
60 fail_update_actor: bool,
61 fail_update_strategy: bool,
62 fail_update_position: bool,
63 fail_update_position_id: Option<PositionId>,
64 updated_positions: Vec<Position>,
65}
66
67#[derive(Clone, Debug, Default)]
69pub struct TestCacheDatabaseControl {
70 state: Arc<Mutex<TestCacheDatabaseState>>,
71}
72
73impl TestCacheDatabaseControl {
74 #[must_use]
76 pub fn create() -> (TestCacheDatabase, Self) {
77 let control = Self::default();
78 (
79 TestCacheDatabase {
80 control: control.clone(),
81 },
82 control,
83 )
84 }
85
86 pub fn record(&self, event: impl Into<String>) {
88 self.state.lock().events.push(event.into());
89 }
90
91 #[must_use]
93 pub fn events(&self) -> Vec<String> {
94 self.state.lock().events.clone()
95 }
96
97 pub fn set_actor_state(&self, actor_id: ActorId, state: &IndexMap<String, Vec<u8>>) {
99 self.state
100 .lock()
101 .actors
102 .insert(actor_id, encode_state(state));
103 }
104
105 pub fn set_strategy_state(&self, strategy_id: StrategyId, state: &IndexMap<String, Vec<u8>>) {
107 self.state
108 .lock()
109 .strategies
110 .insert(strategy_id, encode_state(state));
111 }
112
113 #[must_use]
115 pub fn actor_state(&self, actor_id: &ActorId) -> Option<IndexMap<String, Vec<u8>>> {
116 self.state
117 .lock()
118 .actors
119 .get(actor_id)
120 .cloned()
121 .map(decode_state)
122 }
123
124 #[must_use]
126 pub fn strategy_state(&self, strategy_id: &StrategyId) -> Option<IndexMap<String, Vec<u8>>> {
127 self.state
128 .lock()
129 .strategies
130 .get(strategy_id)
131 .cloned()
132 .map(decode_state)
133 }
134
135 pub fn set_fail_load_actor(&self, fail: bool) {
137 self.state.lock().fail_load_actor = fail;
138 }
139
140 pub fn set_fail_load_strategy(&self, fail: bool) {
142 self.state.lock().fail_load_strategy = fail;
143 }
144
145 pub fn set_fail_update_actor(&self, fail: bool) {
147 self.state.lock().fail_update_actor = fail;
148 }
149
150 pub fn set_fail_update_strategy(&self, fail: bool) {
152 self.state.lock().fail_update_strategy = fail;
153 }
154
155 pub fn set_fail_update_position(&self, fail: bool) {
157 self.state.lock().fail_update_position = fail;
158 }
159
160 pub fn set_fail_update_position_id(&self, position_id: Option<PositionId>) {
162 self.state.lock().fail_update_position_id = position_id;
163 }
164
165 #[must_use]
167 pub fn updated_positions(&self) -> Vec<Position> {
168 self.state.lock().updated_positions.clone()
169 }
170}
171
172#[derive(Debug)]
174pub struct TestCacheDatabase {
175 control: TestCacheDatabaseControl,
176}
177
178#[async_trait::async_trait]
179impl CacheDatabaseAdapter for TestCacheDatabase {
180 fn close(&mut self) -> anyhow::Result<()> {
181 self.control.record("database.close");
182 Ok(())
183 }
184
185 fn flush(&mut self) -> anyhow::Result<()> {
186 Ok(())
187 }
188
189 async fn load_all(&self) -> anyhow::Result<CacheMap> {
190 Ok(CacheMap {
191 instrument_closes: self.control.state.lock().instrument_closes.clone(),
192 ..Default::default()
193 })
194 }
195
196 fn load(&self) -> anyhow::Result<AHashMap<String, Bytes>> {
197 Ok(AHashMap::new())
198 }
199
200 async fn load_currencies(&self) -> anyhow::Result<AHashMap<Ustr, Currency>> {
201 Ok(AHashMap::new())
202 }
203
204 async fn load_instruments(&self) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
205 Ok(AHashMap::new())
206 }
207
208 async fn load_instrument_closes(
209 &self,
210 ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentClose>> {
211 Ok(self.control.state.lock().instrument_closes.clone())
212 }
213
214 async fn load_synthetics(&self) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
215 Ok(AHashMap::new())
216 }
217
218 async fn load_accounts(&self) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
219 Ok(AHashMap::new())
220 }
221
222 async fn load_orders(&self) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
223 Ok(AHashMap::new())
224 }
225
226 async fn load_positions(&self) -> anyhow::Result<AHashMap<PositionId, Position>> {
227 Ok(AHashMap::new())
228 }
229
230 fn load_index_order_position(&self) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
231 Ok(AHashMap::new())
232 }
233
234 fn load_index_order_client(&self) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
235 Ok(AHashMap::new())
236 }
237
238 async fn load_currency(&self, _code: &Ustr) -> anyhow::Result<Option<Currency>> {
239 Ok(None)
240 }
241
242 async fn load_instrument(
243 &self,
244 _instrument_id: &InstrumentId,
245 ) -> anyhow::Result<Option<InstrumentAny>> {
246 Ok(None)
247 }
248
249 async fn load_synthetic(
250 &self,
251 _instrument_id: &InstrumentId,
252 ) -> anyhow::Result<Option<SyntheticInstrument>> {
253 Ok(None)
254 }
255
256 async fn load_account(&self, _account_id: &AccountId) -> anyhow::Result<Option<AccountAny>> {
257 Ok(None)
258 }
259
260 async fn load_order(
261 &self,
262 _client_order_id: &ClientOrderId,
263 ) -> anyhow::Result<Option<OrderAny>> {
264 Ok(None)
265 }
266
267 async fn load_position(&self, _position_id: &PositionId) -> anyhow::Result<Option<Position>> {
268 Ok(None)
269 }
270
271 fn load_actor(&self, actor_id: &ActorId) -> anyhow::Result<AHashMap<String, Bytes>> {
272 self.control.record(format!("actor.load:{actor_id}"));
273 let state = self.control.state.lock();
274 if state.fail_load_actor {
275 anyhow::bail!("test actor load failure");
276 }
277 Ok(state.actors.get(actor_id).cloned().unwrap_or_default())
278 }
279
280 fn load_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<AHashMap<String, Bytes>> {
281 self.control.record(format!("strategy.load:{strategy_id}"));
282 let state = self.control.state.lock();
283 if state.fail_load_strategy {
284 anyhow::bail!("test strategy load failure");
285 }
286 Ok(state
287 .strategies
288 .get(strategy_id)
289 .cloned()
290 .unwrap_or_default())
291 }
292
293 fn load_signals(&self, _name: &str) -> anyhow::Result<Vec<Signal>> {
294 Ok(Vec::new())
295 }
296
297 fn load_custom_data(&self, _data_type: &DataType) -> anyhow::Result<Vec<CustomData>> {
298 Ok(Vec::new())
299 }
300
301 fn load_order_snapshot(
302 &self,
303 _client_order_id: &ClientOrderId,
304 ) -> anyhow::Result<Option<OrderSnapshot>> {
305 Ok(None)
306 }
307
308 fn load_position_snapshot(
309 &self,
310 _position_id: &PositionId,
311 ) -> anyhow::Result<Option<PositionSnapshot>> {
312 Ok(None)
313 }
314
315 fn load_quotes(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<QuoteTick>> {
316 Ok(Vec::new())
317 }
318
319 fn load_trades(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<TradeTick>> {
320 Ok(Vec::new())
321 }
322
323 fn load_funding_rates(
324 &self,
325 _instrument_id: &InstrumentId,
326 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
327 Ok(Vec::new())
328 }
329
330 fn load_bars(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<Bar>> {
331 Ok(Vec::new())
332 }
333
334 fn add(&self, _key: String, _value: Bytes) -> anyhow::Result<()> {
335 Ok(())
336 }
337
338 fn add_currency(&self, _currency: &Currency) -> anyhow::Result<()> {
339 Ok(())
340 }
341
342 fn add_instrument(&self, _instrument: &InstrumentAny) -> anyhow::Result<()> {
343 Ok(())
344 }
345
346 fn add_instrument_close(&self, close: &InstrumentClose) -> anyhow::Result<()> {
347 let mut state = self.control.state.lock();
348 state.instrument_closes.insert(close.instrument_id, *close);
349 Ok(())
350 }
351
352 fn add_synthetic(&self, _synthetic: &SyntheticInstrument) -> anyhow::Result<()> {
353 Ok(())
354 }
355
356 fn add_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
357 Ok(())
358 }
359
360 fn add_order(&self, _order: &OrderAny, _client_id: Option<ClientId>) -> anyhow::Result<()> {
361 Ok(())
362 }
363
364 fn add_order_snapshot(&self, _snapshot: &OrderSnapshot) -> anyhow::Result<()> {
365 Ok(())
366 }
367
368 fn add_position(&self, _position: &Position) -> anyhow::Result<()> {
369 Ok(())
370 }
371
372 fn add_position_snapshot(&self, _snapshot: &PositionSnapshot) -> anyhow::Result<()> {
373 Ok(())
374 }
375
376 fn add_order_book(&self, _order_book: &OrderBook) -> anyhow::Result<()> {
377 Ok(())
378 }
379
380 fn add_signal(&self, _signal: &Signal) -> anyhow::Result<()> {
381 Ok(())
382 }
383
384 fn add_custom_data(&self, _data: &CustomData) -> anyhow::Result<()> {
385 Ok(())
386 }
387
388 fn add_quote(&self, _quote: &QuoteTick) -> anyhow::Result<()> {
389 Ok(())
390 }
391
392 fn add_trade(&self, _trade: &TradeTick) -> anyhow::Result<()> {
393 Ok(())
394 }
395
396 fn add_funding_rate(&self, _funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
397 Ok(())
398 }
399
400 fn add_bar(&self, _bar: &Bar) -> anyhow::Result<()> {
401 Ok(())
402 }
403
404 fn add_greeks(&self, _greeks: &GreeksData) -> anyhow::Result<()> {
405 Ok(())
406 }
407
408 fn add_yield_curve(&self, _yield_curve: &YieldCurve) -> anyhow::Result<()> {
409 Ok(())
410 }
411
412 fn delete_actor(&self, _actor_id: &ActorId) -> anyhow::Result<()> {
413 Ok(())
414 }
415
416 fn delete_strategy(&self, _component_id: &StrategyId) -> anyhow::Result<()> {
417 Ok(())
418 }
419
420 fn delete_order(&self, _client_order_id: &ClientOrderId) -> anyhow::Result<()> {
421 Ok(())
422 }
423
424 fn delete_position(&self, _position_id: &PositionId) -> anyhow::Result<()> {
425 Ok(())
426 }
427
428 fn delete_account_event(&self, _account_id: &AccountId, _event_id: &str) -> anyhow::Result<()> {
429 Ok(())
430 }
431
432 fn index_venue_order_id(
433 &self,
434 _client_order_id: ClientOrderId,
435 _venue_order_id: VenueOrderId,
436 ) -> anyhow::Result<()> {
437 Ok(())
438 }
439
440 fn index_order_position(
441 &self,
442 _client_order_id: ClientOrderId,
443 _position_id: PositionId,
444 ) -> anyhow::Result<()> {
445 Ok(())
446 }
447
448 fn update_actor(
449 &self,
450 actor_id: &ActorId,
451 actor_state: &AHashMap<String, Bytes>,
452 ) -> anyhow::Result<()> {
453 self.control.record(format!("actor.update:{actor_id}"));
454 let mut state = self.control.state.lock();
455 if state.fail_update_actor {
456 anyhow::bail!("test actor update failure");
457 }
458 state.actors.insert(*actor_id, actor_state.clone());
459 Ok(())
460 }
461
462 fn update_strategy(
463 &self,
464 strategy_id: &StrategyId,
465 strategy_state: &AHashMap<String, Bytes>,
466 ) -> anyhow::Result<()> {
467 self.control
468 .record(format!("strategy.update:{strategy_id}"));
469 let mut state = self.control.state.lock();
470 if state.fail_update_strategy {
471 anyhow::bail!("test strategy update failure");
472 }
473 state
474 .strategies
475 .insert(*strategy_id, strategy_state.clone());
476 Ok(())
477 }
478
479 fn update_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
480 Ok(())
481 }
482
483 fn update_order(&self, _order_event: &OrderEventAny) -> anyhow::Result<()> {
484 Ok(())
485 }
486
487 fn update_position(&self, position: &Position) -> anyhow::Result<()> {
488 let mut state = self.control.state.lock();
489 if state.fail_update_position || state.fail_update_position_id == Some(position.id) {
490 anyhow::bail!("test position update failure");
491 }
492 state.updated_positions.push(position.clone());
493 Ok(())
494 }
495
496 fn snapshot_order_state(&self, _order: &OrderAny) -> anyhow::Result<()> {
497 Ok(())
498 }
499
500 fn snapshot_position_state(
501 &self,
502 _position: &Position,
503 _ts_snapshot: UnixNanos,
504 _unrealized_pnl: Option<Money>,
505 ) -> anyhow::Result<()> {
506 Ok(())
507 }
508
509 fn heartbeat(&self, _timestamp: UnixNanos) -> anyhow::Result<()> {
510 Ok(())
511 }
512}
513
514fn decode_state(state: AHashMap<String, Bytes>) -> IndexMap<String, Vec<u8>> {
515 state
516 .into_iter()
517 .map(|(key, value)| (key, value.to_vec()))
518 .collect()
519}
520
521fn encode_state(state: &IndexMap<String, Vec<u8>>) -> AHashMap<String, Bytes> {
522 state
523 .iter()
524 .map(|(key, value)| (key.clone(), Bytes::copy_from_slice(value)))
525 .collect()
526}
527
528#[cfg(test)]
529mod tests {
530 use nautilus_model::{data::InstrumentClose, enums::InstrumentCloseType, types::Price};
531 use rstest::rstest;
532
533 use super::*;
534
535 #[rstest]
536 #[tokio::test]
537 async fn test_instrument_close_persistence_stores_latest_value() {
538 let instrument_id = InstrumentId::from("BINARY-1.POLYMARKET");
539 let first = InstrumentClose::new(
540 instrument_id,
541 Price::from("1.00000"),
542 InstrumentCloseType::ContractExpired,
543 UnixNanos::from(10),
544 UnixNanos::from(11),
545 );
546 let replacement = InstrumentClose::new(
547 instrument_id,
548 Price::from("0.00000"),
549 InstrumentCloseType::EndOfSession,
550 UnixNanos::from(20),
551 UnixNanos::from(21),
552 );
553 let (database, _) = TestCacheDatabaseControl::create();
554
555 database.add_instrument_close(&first).unwrap();
556 database.add_instrument_close(&replacement).unwrap();
557 let loaded = database.load_instrument_closes().await.unwrap();
558 let loaded_all = database.load_all().await.unwrap();
559
560 assert_eq!(loaded, AHashMap::from([(instrument_id, replacement)]));
561 assert_eq!(loaded_all.instrument_closes, loaded);
562 }
563}