1use std::sync::{Arc, Mutex};
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, 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 ustr::Ustr;
46
47#[expect(
48 clippy::struct_excessive_bools,
49 reason = "independent switches cover lifecycle persistence failure modes"
50)]
51#[derive(Debug, Default)]
52struct TestCacheDatabaseState {
53 actors: AHashMap<ActorId, AHashMap<String, Bytes>>,
54 strategies: AHashMap<StrategyId, AHashMap<String, Bytes>>,
55 events: Vec<String>,
56 fail_load_actor: bool,
57 fail_load_strategy: bool,
58 fail_update_actor: bool,
59 fail_update_strategy: bool,
60 fail_update_position: bool,
61}
62
63#[derive(Clone, Debug, Default)]
65pub struct TestCacheDatabaseControl {
66 state: Arc<Mutex<TestCacheDatabaseState>>,
67}
68
69#[allow(
70 clippy::missing_panics_doc,
71 reason = "mutex poisoning is not expected in lifecycle tests"
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().unwrap().events.push(event.into());
89 }
90
91 #[must_use]
93 pub fn events(&self) -> Vec<String> {
94 self.state.lock().unwrap().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 .unwrap()
102 .actors
103 .insert(actor_id, encode_state(state));
104 }
105
106 pub fn set_strategy_state(&self, strategy_id: StrategyId, state: &IndexMap<String, Vec<u8>>) {
108 self.state
109 .lock()
110 .unwrap()
111 .strategies
112 .insert(strategy_id, encode_state(state));
113 }
114
115 #[must_use]
117 pub fn actor_state(&self, actor_id: &ActorId) -> Option<IndexMap<String, Vec<u8>>> {
118 self.state
119 .lock()
120 .unwrap()
121 .actors
122 .get(actor_id)
123 .cloned()
124 .map(decode_state)
125 }
126
127 #[must_use]
129 pub fn strategy_state(&self, strategy_id: &StrategyId) -> Option<IndexMap<String, Vec<u8>>> {
130 self.state
131 .lock()
132 .unwrap()
133 .strategies
134 .get(strategy_id)
135 .cloned()
136 .map(decode_state)
137 }
138
139 pub fn set_fail_load_actor(&self, fail: bool) {
141 self.state.lock().unwrap().fail_load_actor = fail;
142 }
143
144 pub fn set_fail_load_strategy(&self, fail: bool) {
146 self.state.lock().unwrap().fail_load_strategy = fail;
147 }
148
149 pub fn set_fail_update_actor(&self, fail: bool) {
151 self.state.lock().unwrap().fail_update_actor = fail;
152 }
153
154 pub fn set_fail_update_strategy(&self, fail: bool) {
156 self.state.lock().unwrap().fail_update_strategy = fail;
157 }
158
159 pub fn set_fail_update_position(&self, fail: bool) {
161 self.state.lock().unwrap().fail_update_position = fail;
162 }
163}
164
165#[derive(Debug)]
167pub struct TestCacheDatabase {
168 control: TestCacheDatabaseControl,
169}
170
171#[async_trait::async_trait]
172impl CacheDatabaseAdapter for TestCacheDatabase {
173 fn close(&mut self) -> anyhow::Result<()> {
174 self.control.record("database.close");
175 Ok(())
176 }
177
178 fn flush(&mut self) -> anyhow::Result<()> {
179 Ok(())
180 }
181
182 async fn load_all(&self) -> anyhow::Result<CacheMap> {
183 Ok(CacheMap::default())
184 }
185
186 fn load(&self) -> anyhow::Result<AHashMap<String, Bytes>> {
187 Ok(AHashMap::new())
188 }
189
190 async fn load_currencies(&self) -> anyhow::Result<AHashMap<Ustr, Currency>> {
191 Ok(AHashMap::new())
192 }
193
194 async fn load_instruments(&self) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
195 Ok(AHashMap::new())
196 }
197
198 async fn load_synthetics(&self) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
199 Ok(AHashMap::new())
200 }
201
202 async fn load_accounts(&self) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
203 Ok(AHashMap::new())
204 }
205
206 async fn load_orders(&self) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
207 Ok(AHashMap::new())
208 }
209
210 async fn load_positions(&self) -> anyhow::Result<AHashMap<PositionId, Position>> {
211 Ok(AHashMap::new())
212 }
213
214 fn load_index_order_position(&self) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
215 Ok(AHashMap::new())
216 }
217
218 fn load_index_order_client(&self) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
219 Ok(AHashMap::new())
220 }
221
222 async fn load_currency(&self, _code: &Ustr) -> anyhow::Result<Option<Currency>> {
223 Ok(None)
224 }
225
226 async fn load_instrument(
227 &self,
228 _instrument_id: &InstrumentId,
229 ) -> anyhow::Result<Option<InstrumentAny>> {
230 Ok(None)
231 }
232
233 async fn load_synthetic(
234 &self,
235 _instrument_id: &InstrumentId,
236 ) -> anyhow::Result<Option<SyntheticInstrument>> {
237 Ok(None)
238 }
239
240 async fn load_account(&self, _account_id: &AccountId) -> anyhow::Result<Option<AccountAny>> {
241 Ok(None)
242 }
243
244 async fn load_order(
245 &self,
246 _client_order_id: &ClientOrderId,
247 ) -> anyhow::Result<Option<OrderAny>> {
248 Ok(None)
249 }
250
251 async fn load_position(&self, _position_id: &PositionId) -> anyhow::Result<Option<Position>> {
252 Ok(None)
253 }
254
255 fn load_actor(&self, actor_id: &ActorId) -> anyhow::Result<AHashMap<String, Bytes>> {
256 self.control.record(format!("actor.load:{actor_id}"));
257 let state = self.control.state.lock().unwrap();
258 if state.fail_load_actor {
259 anyhow::bail!("test actor load failure");
260 }
261 Ok(state.actors.get(actor_id).cloned().unwrap_or_default())
262 }
263
264 fn load_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<AHashMap<String, Bytes>> {
265 self.control.record(format!("strategy.load:{strategy_id}"));
266 let state = self.control.state.lock().unwrap();
267 if state.fail_load_strategy {
268 anyhow::bail!("test strategy load failure");
269 }
270 Ok(state
271 .strategies
272 .get(strategy_id)
273 .cloned()
274 .unwrap_or_default())
275 }
276
277 fn load_signals(&self, _name: &str) -> anyhow::Result<Vec<Signal>> {
278 Ok(Vec::new())
279 }
280
281 fn load_custom_data(&self, _data_type: &DataType) -> anyhow::Result<Vec<CustomData>> {
282 Ok(Vec::new())
283 }
284
285 fn load_order_snapshot(
286 &self,
287 _client_order_id: &ClientOrderId,
288 ) -> anyhow::Result<Option<OrderSnapshot>> {
289 Ok(None)
290 }
291
292 fn load_position_snapshot(
293 &self,
294 _position_id: &PositionId,
295 ) -> anyhow::Result<Option<PositionSnapshot>> {
296 Ok(None)
297 }
298
299 fn load_quotes(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<QuoteTick>> {
300 Ok(Vec::new())
301 }
302
303 fn load_trades(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<TradeTick>> {
304 Ok(Vec::new())
305 }
306
307 fn load_funding_rates(
308 &self,
309 _instrument_id: &InstrumentId,
310 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
311 Ok(Vec::new())
312 }
313
314 fn load_bars(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<Bar>> {
315 Ok(Vec::new())
316 }
317
318 fn add(&self, _key: String, _value: Bytes) -> anyhow::Result<()> {
319 Ok(())
320 }
321
322 fn add_currency(&self, _currency: &Currency) -> anyhow::Result<()> {
323 Ok(())
324 }
325
326 fn add_instrument(&self, _instrument: &InstrumentAny) -> anyhow::Result<()> {
327 Ok(())
328 }
329
330 fn add_synthetic(&self, _synthetic: &SyntheticInstrument) -> anyhow::Result<()> {
331 Ok(())
332 }
333
334 fn add_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
335 Ok(())
336 }
337
338 fn add_order(&self, _order: &OrderAny, _client_id: Option<ClientId>) -> anyhow::Result<()> {
339 Ok(())
340 }
341
342 fn add_order_snapshot(&self, _snapshot: &OrderSnapshot) -> anyhow::Result<()> {
343 Ok(())
344 }
345
346 fn add_position(&self, _position: &Position) -> anyhow::Result<()> {
347 Ok(())
348 }
349
350 fn add_position_snapshot(&self, _snapshot: &PositionSnapshot) -> anyhow::Result<()> {
351 Ok(())
352 }
353
354 fn add_order_book(&self, _order_book: &OrderBook) -> anyhow::Result<()> {
355 Ok(())
356 }
357
358 fn add_signal(&self, _signal: &Signal) -> anyhow::Result<()> {
359 Ok(())
360 }
361
362 fn add_custom_data(&self, _data: &CustomData) -> anyhow::Result<()> {
363 Ok(())
364 }
365
366 fn add_quote(&self, _quote: &QuoteTick) -> anyhow::Result<()> {
367 Ok(())
368 }
369
370 fn add_trade(&self, _trade: &TradeTick) -> anyhow::Result<()> {
371 Ok(())
372 }
373
374 fn add_funding_rate(&self, _funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
375 Ok(())
376 }
377
378 fn add_bar(&self, _bar: &Bar) -> anyhow::Result<()> {
379 Ok(())
380 }
381
382 fn add_greeks(&self, _greeks: &GreeksData) -> anyhow::Result<()> {
383 Ok(())
384 }
385
386 fn add_yield_curve(&self, _yield_curve: &YieldCurveData) -> anyhow::Result<()> {
387 Ok(())
388 }
389
390 fn delete_actor(&self, _actor_id: &ActorId) -> anyhow::Result<()> {
391 Ok(())
392 }
393
394 fn delete_strategy(&self, _component_id: &StrategyId) -> anyhow::Result<()> {
395 Ok(())
396 }
397
398 fn delete_order(&self, _client_order_id: &ClientOrderId) -> anyhow::Result<()> {
399 Ok(())
400 }
401
402 fn delete_position(&self, _position_id: &PositionId) -> anyhow::Result<()> {
403 Ok(())
404 }
405
406 fn delete_account_event(&self, _account_id: &AccountId, _event_id: &str) -> anyhow::Result<()> {
407 Ok(())
408 }
409
410 fn index_venue_order_id(
411 &self,
412 _client_order_id: ClientOrderId,
413 _venue_order_id: VenueOrderId,
414 ) -> anyhow::Result<()> {
415 Ok(())
416 }
417
418 fn index_order_position(
419 &self,
420 _client_order_id: ClientOrderId,
421 _position_id: PositionId,
422 ) -> anyhow::Result<()> {
423 Ok(())
424 }
425
426 fn update_actor(
427 &self,
428 actor_id: &ActorId,
429 actor_state: &AHashMap<String, Bytes>,
430 ) -> anyhow::Result<()> {
431 self.control.record(format!("actor.update:{actor_id}"));
432 let mut state = self.control.state.lock().unwrap();
433 if state.fail_update_actor {
434 anyhow::bail!("test actor update failure");
435 }
436 state.actors.insert(*actor_id, actor_state.clone());
437 Ok(())
438 }
439
440 fn update_strategy(
441 &self,
442 strategy_id: &StrategyId,
443 strategy_state: &AHashMap<String, Bytes>,
444 ) -> anyhow::Result<()> {
445 self.control
446 .record(format!("strategy.update:{strategy_id}"));
447 let mut state = self.control.state.lock().unwrap();
448 if state.fail_update_strategy {
449 anyhow::bail!("test strategy update failure");
450 }
451 state
452 .strategies
453 .insert(*strategy_id, strategy_state.clone());
454 Ok(())
455 }
456
457 fn update_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
458 Ok(())
459 }
460
461 fn update_order(&self, _order_event: &OrderEventAny) -> anyhow::Result<()> {
462 Ok(())
463 }
464
465 fn update_position(&self, _position: &Position) -> anyhow::Result<()> {
466 if self.control.state.lock().unwrap().fail_update_position {
467 anyhow::bail!("test position update failure");
468 }
469 Ok(())
470 }
471
472 fn snapshot_order_state(&self, _order: &OrderAny) -> anyhow::Result<()> {
473 Ok(())
474 }
475
476 fn snapshot_position_state(
477 &self,
478 _position: &Position,
479 _ts_snapshot: UnixNanos,
480 _unrealized_pnl: Option<Money>,
481 ) -> anyhow::Result<()> {
482 Ok(())
483 }
484
485 fn heartbeat(&self, _timestamp: UnixNanos) -> anyhow::Result<()> {
486 Ok(())
487 }
488}
489
490fn decode_state(state: AHashMap<String, Bytes>) -> IndexMap<String, Vec<u8>> {
491 state
492 .into_iter()
493 .map(|(key, value)| (key, value.to_vec()))
494 .collect()
495}
496
497fn encode_state(state: &IndexMap<String, Vec<u8>>) -> AHashMap<String, Bytes> {
498 state
499 .iter()
500 .map(|(key, value)| (key.clone(), Bytes::copy_from_slice(value)))
501 .collect()
502}