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;
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/// Shared control and observation handle for [`TestCacheDatabase`].
66#[derive(Clone, Debug, Default)]
67pub struct TestCacheDatabaseControl {
68    state: Arc<Mutex<TestCacheDatabaseState>>,
69}
70
71impl TestCacheDatabaseControl {
72    /// Creates an adapter and its shared control handle.
73    #[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    /// Records an event in the shared lifecycle log.
85    pub fn record(&self, event: impl Into<String>) {
86        self.state.lock().events.push(event.into());
87    }
88
89    /// Returns the recorded lifecycle events.
90    #[must_use]
91    pub fn events(&self) -> Vec<String> {
92        self.state.lock().events.clone()
93    }
94
95    /// Seeds actor state for a later load.
96    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    /// Seeds strategy state for a later load.
104    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    /// Returns persisted actor state.
112    #[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    /// Returns persisted strategy state.
123    #[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    /// Configures actor loads to fail.
134    pub fn set_fail_load_actor(&self, fail: bool) {
135        self.state.lock().fail_load_actor = fail;
136    }
137
138    /// Configures strategy loads to fail.
139    pub fn set_fail_load_strategy(&self, fail: bool) {
140        self.state.lock().fail_load_strategy = fail;
141    }
142
143    /// Configures actor updates to fail.
144    pub fn set_fail_update_actor(&self, fail: bool) {
145        self.state.lock().fail_update_actor = fail;
146    }
147
148    /// Configures strategy updates to fail.
149    pub fn set_fail_update_strategy(&self, fail: bool) {
150        self.state.lock().fail_update_strategy = fail;
151    }
152
153    /// Configures position updates to fail.
154    pub fn set_fail_update_position(&self, fail: bool) {
155        self.state.lock().fail_update_position = fail;
156    }
157}
158
159/// Stateful cache database adapter for lifecycle tests.
160#[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}