Skip to main content

nautilus_testkit/
cache.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Stateful cache database test double.
17
18use 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/// Shared control and observation handle for [`TestCacheDatabase`].
64#[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    /// Creates an adapter and its shared control handle.
75    #[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    /// Records an event in the shared lifecycle log.
87    pub fn record(&self, event: impl Into<String>) {
88        self.state.lock().unwrap().events.push(event.into());
89    }
90
91    /// Returns the recorded lifecycle events.
92    #[must_use]
93    pub fn events(&self) -> Vec<String> {
94        self.state.lock().unwrap().events.clone()
95    }
96
97    /// Seeds actor state for a later load.
98    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    /// Seeds strategy state for a later load.
107    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    /// Returns persisted actor state.
116    #[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    /// Returns persisted strategy state.
128    #[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    /// Configures actor loads to fail.
140    pub fn set_fail_load_actor(&self, fail: bool) {
141        self.state.lock().unwrap().fail_load_actor = fail;
142    }
143
144    /// Configures strategy loads to fail.
145    pub fn set_fail_load_strategy(&self, fail: bool) {
146        self.state.lock().unwrap().fail_load_strategy = fail;
147    }
148
149    /// Configures actor updates to fail.
150    pub fn set_fail_update_actor(&self, fail: bool) {
151        self.state.lock().unwrap().fail_update_actor = fail;
152    }
153
154    /// Configures strategy updates to fail.
155    pub fn set_fail_update_strategy(&self, fail: bool) {
156        self.state.lock().unwrap().fail_update_strategy = fail;
157    }
158
159    /// Configures position updates to fail.
160    pub fn set_fail_update_position(&self, fail: bool) {
161        self.state.lock().unwrap().fail_update_position = fail;
162    }
163}
164
165/// Stateful cache database adapter for lifecycle tests.
166#[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}