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